Compare commits

...

10 commits
master ... port

Author SHA1 Message Date
Augusto Dwenger J. 56da64e6e6 WIP: Implement PlayerTimeServer 2023-07-28 21:09:34 +02:00
Augusto Dwenger J. 51115e1a2a Update dependencies to newst versions 2023-07-28 17:43:34 +02:00
Augusto Dwenger J. a83de958d7 Add calculator for the playtime 2023-07-28 17:07:56 +02:00
Augusto Dwenger J. 04cd9e2ff2 Add new PlayerTimeDB interface to represent the persistence caps 2023-07-28 16:07:05 +02:00
Augusto Dwenger J. 905fc4ba35 Refactor reading a player from a file
Reduce code duplication and simplify error handling.
2022-09-23 20:34:13 +02:00
Augusto Dwenger J. 7e0cad6ddb Update jackson-databind version to 2.13.4 2022-09-23 20:20:49 +02:00
Augusto Dwenger J. 01f62303fc Update mockito to version 4.8.0 2022-09-23 20:19:56 +02:00
Augusto Dwenger J. 7d7cf8148a Reformat header section 2022-09-07 14:57:07 +02:00
Augusto Dwenger J. 306f3c25bc Rework persistence
- Rename exceptions to be more descriptive
- Add tests for the FileSystemDB
- Add Mockito extension JUnit
2022-09-07 14:54:35 +02:00
Augusto Dwenger J. fae88aa262 Add basic structure
The basic structure for the project.
This commit includes the code for the model of the main object
`PlayTimePlayer`, its serialization and file persistence. The
persistence package is not tested.

I am trying to keep the dependency list as small as possible. This is
also why I implemented the serialization without jackson.
2022-09-02 22:11:52 +02:00
31 changed files with 1387 additions and 0 deletions

80
pom.xml Normal file
View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.hhhammer</groupId>
<artifactId>playtime-ng</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>20</maven.compiler.source>
<maven.compiler.target>${maven.compiler.source}</maven.compiler.target>
<junit.jupiter.version>5.10.0</junit.jupiter.version>
<mockito.version>5.4.0</mockito.version>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!--Test-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- to test my json implementation -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,62 @@
package de.hhhammer.playtime.ng;
import de.hhhammer.playtime.ng.persistence.PlayerTimeDB;
import de.hhhammer.playtime.ng.persistence.ReadPlayerException;
import de.hhhammer.playtime.ng.persistence.WritePlayerException;
import de.hhhammer.playtime.ng.player.PlayTimePlayer;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;
public class PlayerTimeService {
private final Clock clock;
private final PlayerTimeDB playerTimeDB;
private final PlaytimeCalculator playtimeCalculator;
public PlayerTimeService(Clock clock, PlayerTimeDB playerTimeDB, PlaytimeCalculator playtimeCalculator) {
this.clock = clock;
this.playerTimeDB = playerTimeDB;
this.playtimeCalculator = playtimeCalculator;
}
public PlayerTimeService(PlayerTimeDB playerTimeDB, PlaytimeCalculator playtimeCalculator) {
this(Clock.systemDefaultZone(), playerTimeDB, playtimeCalculator);
}
public void playerJoin(UUID uuid, String playerName) {
try {
PlayTimePlayer player = playerTimeDB.findById(uuid)
.map(foundPlayer -> new PlayTimePlayer(foundPlayer.uuid(), foundPlayer.name(), foundPlayer.playtime(), LocalDateTime.now(clock)))
.orElse(new PlayTimePlayer(uuid, playerName, Duration.ZERO, LocalDateTime.now(clock)));
playerTimeDB.flush(player);
} catch (ReadPlayerException | WritePlayerException e) {
throw new RuntimeException(e);
}
}
public void updatePlayTime(UUID uuid) {
try {
playerTimeDB.findById(uuid)
.map(player -> {
Duration playtime = playtimeCalculator.calculatePlaytime(player);
return new PlayTimePlayer(player.uuid(), player.name(), playtime, player.joinTime(), player.saveTime());
}).ifPresent(player -> {
try {
playerTimeDB.flush(player);
} catch (WritePlayerException e) {
throw new RuntimeException(e);
}
});
} catch (ReadPlayerException e) {
}
}
/**
* timePlayed(uuid: UUID): Duration
* getTopPlayers(): List<Player>
* timeToString(duration: Duration): String
*/
}

View file

@ -0,0 +1,32 @@
package de.hhhammer.playtime.ng;
import de.hhhammer.playtime.ng.player.Player;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
public class PlaytimeCalculator {
private final Clock clock;
public PlaytimeCalculator(Clock clock) {
this.clock = clock;
}
public PlaytimeCalculator() {
this(Clock.systemDefaultZone());
}
public Duration calculatePlaytime(Player player) {
Duration playTime = timePassedBetweenSaveOrJoin(player);
return Duration.from(player.playtime()).plus(playTime);
}
private Duration timePassedBetweenSaveOrJoin(Player player) {
if (player.saveTime().isPresent() && Duration.between(player.saveTime().get(), player.joinTime()).isNegative()) {
return Duration.between(player.saveTime().get(), LocalDateTime.now(clock));
}
return Duration.between(player.joinTime(), LocalDateTime.now(clock));
}
}

View file

@ -0,0 +1,32 @@
package de.hhhammer.playtime.ng;
import de.hhhammer.playtime.ng.persistence.FileSystemPlayerTimeDB;
import de.hhhammer.playtime.ng.serialization.JsonSerializationFactory;
import de.hhhammer.playtime.ng.serialization.SerializationFactory;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.time.format.DateTimeFormatter;
public final class Plugin extends JavaPlugin {
private final FileSystemPlayerTimeDB persistence;
public Plugin() {
super();
final SerializationFactory serializationFactory = new JsonSerializationFactory(DateTimeFormatter.ISO_DATE_TIME);
this.persistence = new FileSystemPlayerTimeDB(new File("plugins/PlayTimeNG"), serializationFactory);
}
@Override
public void onLoad() {
}
@Override
public void onEnable() {
}
@Override
public void onDisable() {
}
}

View file

@ -0,0 +1,12 @@
package de.hhhammer.playtime.ng.listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class JoinListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
}
}

View file

@ -0,0 +1,97 @@
package de.hhhammer.playtime.ng.persistence;
import de.hhhammer.playtime.ng.player.Player;
import de.hhhammer.playtime.ng.serialization.SerializationFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class FileSystemPlayerTimeDB implements PlayerTimeDB {
private final File saveDirectory;
private final SerializationFactory serializationFactory;
public FileSystemPlayerTimeDB(final File saveDirectory, final SerializationFactory serializationFactory) {
this.saveDirectory = saveDirectory;
this.serializationFactory = serializationFactory;
}
@Override
public void flush(final Player player) throws WritePlayerException {
final var playerFile = new File(saveDirectory, player.uuid().toString());
final String serializedPlayer = serializationFactory.createSerializer(player).serialize();
try (final var writer = new FileWriter(playerFile)) {
writer.write(serializedPlayer);
// ensure everything is written
writer.flush();
} catch (IOException e) {
throw new WritePlayerException("Could not persist player data from player: " + player.uuid(), e);
}
}
@Override
public Optional<Player> findById(final UUID uuid) throws ReadPlayerException {
final File[] files = getFiles();
for (final var file : files) {
if (!file.getName().equals(uuid.toString())) {
continue;
}
final Player player = readPlayerFromFile(file);
return Optional.of(player);
}
return Optional.empty();
}
@Override
public Optional<Player> findByName(final String name) throws ReadPlayerException {
final File[] files = getFiles();
for (final var file : files) {
final Player player = readPlayerFromFile(file);
if (player.name().equals(name)) {
return Optional.of(player);
}
}
return Optional.empty();
}
@Override
public List<Player> findAll() throws ReadPlayerException {
final File[] files = getFiles();
final var playerList = new ArrayList<Player>();
for (final var file : files) {
final Player player = readPlayerFromFile(file);
playerList.add(player);
}
return playerList;
}
private File[] getFiles() {
final File[] files = saveDirectory.listFiles();
if (files == null) {
throw new RuntimeException("Could not find files in: " + saveDirectory.getAbsolutePath());
}
return files;
}
private Player readPlayerFromFile(final File playerFile) throws ReadPlayerException {
try {
final String fileContent = Files.readString(playerFile.toPath());
return serializationFactory.createDeserializer(fileContent);
} catch (IOException e) {
final String fileName = playerFile.getName();
throw new ReadPlayerException("Could not read file: " + fileName, e);
}
}
}

View file

@ -0,0 +1,15 @@
package de.hhhammer.playtime.ng.persistence;
import de.hhhammer.playtime.ng.player.Player;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface FindPlayer {
Optional<Player> findById(UUID uuid) throws ReadPlayerException;
Optional<Player> findByName(String name) throws ReadPlayerException;
List<Player> findAll() throws ReadPlayerException;
}

View file

@ -0,0 +1,7 @@
package de.hhhammer.playtime.ng.persistence;
import de.hhhammer.playtime.ng.player.Player;
public interface PersistPlayer {
void flush(Player player) throws WritePlayerException;
}

View file

@ -0,0 +1,4 @@
package de.hhhammer.playtime.ng.persistence;
public interface PlayerTimeDB extends FindPlayer, PersistPlayer{
}

View file

@ -0,0 +1,11 @@
package de.hhhammer.playtime.ng.persistence;
public final class ReadPlayerException extends Exception {
public ReadPlayerException(String msg) {
super(msg);
}
public ReadPlayerException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,11 @@
package de.hhhammer.playtime.ng.persistence;
import java.io.IOException;
public final class WritePlayerException extends Exception {
public WritePlayerException(final String message, final IOException cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,52 @@
package de.hhhammer.playtime.ng.player;
import de.hhhammer.playtime.ng.serialization.deserializer.Deserializer;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
import java.util.UUID;
public final class DeserializationPlayer implements Player {
private final Deserializer deserializer;
private final DateTimeFormatter dateTimeFormatter;
public DeserializationPlayer(final Deserializer deserializer, final DateTimeFormatter dateTimeFormatter) {
this.deserializer = deserializer;
this.dateTimeFormatter = dateTimeFormatter;
}
@Override
public UUID uuid() {
final String uuid = deserializer.getValueForKey("uuid");
return UUID.fromString(uuid);
}
@Override
public String name() {
return deserializer.getValueForKey("name");
}
@Override
public Duration playtime() {
final String[] time = deserializer.getValueForKey("playtime").split(":");
return Duration.ofHours(Integer.parseInt(time[0]))
.plusMinutes(Integer.parseInt(time[1]))
.plusSeconds(Integer.parseInt(time[2]));
}
@Override
public LocalDateTime joinTime() {
final String joinTime = deserializer.getValueForKey("joinTime");
final TemporalAccessor joinTimeFormatted = dateTimeFormatter.parse(joinTime);
return LocalDateTime.from(joinTimeFormatted);
}
@Override
public Optional<LocalDateTime> saveTime() {
return Optional.empty();
}
}

View file

@ -0,0 +1,60 @@
package de.hhhammer.playtime.ng.player;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
public final class PlayTimePlayer implements SettableSaveTimePlayer {
private final Player player;
public PlayTimePlayer(final Player player) {
this.player = player;
}
public PlayTimePlayer(final UUID uuid, final String name, final Duration playtime, final LocalDateTime joinTime, final Optional<LocalDateTime> saveTime) {
this(new PlayerRecord(uuid, name, playtime, joinTime, saveTime));
}
public PlayTimePlayer(final UUID uuid, final String name, final Duration playtime, final LocalDateTime joinTime, final LocalDateTime saveTime) {
this(new PlayerRecord(uuid, name, playtime, joinTime, Optional.of(saveTime)));
}
public PlayTimePlayer(final UUID uuid, final String name, final Duration playtime, final LocalDateTime joinTime) {
this(new PlayerRecord(uuid, name, playtime, joinTime, Optional.empty()));
}
@Override
public Player withSaveTime(final LocalDateTime saveTime) {
return new PlayerRecord(uuid(), name(), playtime(), joinTime(), Optional.of(saveTime));
}
@Override
public UUID uuid() {
return player.uuid();
}
@Override
public String name() {
return player.name();
}
@Override
public Duration playtime() {
return player.playtime();
}
@Override
public LocalDateTime joinTime() {
return player.joinTime();
}
@Override
public Optional<LocalDateTime> saveTime() {
return player.saveTime();
}
private record PlayerRecord(UUID uuid, String name, Duration playtime, LocalDateTime joinTime,
Optional<LocalDateTime> saveTime) implements Player {
}
}

View file

@ -0,0 +1,18 @@
package de.hhhammer.playtime.ng.player;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
public interface Player {
UUID uuid();
String name();
Duration playtime();
LocalDateTime joinTime();
Optional<LocalDateTime> saveTime();
}

View file

@ -0,0 +1,7 @@
package de.hhhammer.playtime.ng.player;
import java.time.LocalDateTime;
public interface SettableSaveTimePlayer extends Player {
Player withSaveTime(LocalDateTime saveTime);
}

View file

@ -0,0 +1,29 @@
package de.hhhammer.playtime.ng.serialization;
import de.hhhammer.playtime.ng.player.DeserializationPlayer;
import de.hhhammer.playtime.ng.player.Player;
import de.hhhammer.playtime.ng.serialization.deserializer.JsonDeserializer;
import de.hhhammer.playtime.ng.serialization.serializer.JsonSerializer;
import de.hhhammer.playtime.ng.serialization.serializer.Serializer;
import java.time.format.DateTimeFormatter;
public final class JsonSerializationFactory implements SerializationFactory {
private final DateTimeFormatter dateTimeFormatter;
public JsonSerializationFactory(final DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}
@Override
public Player createDeserializer(final String serialized) {
return new DeserializationPlayer(new JsonDeserializer(serialized), dateTimeFormatter);
}
@Override
public Serializer createSerializer(final Player player) {
return new JsonSerializer(player, dateTimeFormatter);
}
}

View file

@ -0,0 +1,10 @@
package de.hhhammer.playtime.ng.serialization;
import de.hhhammer.playtime.ng.player.Player;
import de.hhhammer.playtime.ng.serialization.serializer.Serializer;
public interface SerializationFactory {
Player createDeserializer(String serialized);
Serializer createSerializer(Player player);
}

View file

@ -0,0 +1,5 @@
package de.hhhammer.playtime.ng.serialization.deserializer;
public interface Deserializer {
String getValueForKey(String key);
}

View file

@ -0,0 +1,44 @@
package de.hhhammer.playtime.ng.serialization.deserializer;
import java.util.HashMap;
import java.util.Map;
public final class JsonDeserializer implements Deserializer {
private final String serializedPlayer;
public JsonDeserializer(final String serializedPlayer) {
this.serializedPlayer = serializedPlayer;
}
@Override
public String getValueForKey(final String field) {
final String withoutObjectBraces = removeStartingAndEndingObjectCurlyBraces();
final String withoutControlCharacters = withoutObjectBraces.replaceAll("\n", "")
.replaceAll("\t", "");
final String withoutQuotes = withoutControlCharacters.replaceAll("\"", "");
final String[] keyValuePairs = withoutQuotes.split(",");
final Map<String, String> map = createMap(keyValuePairs);
return map.get(field);
}
private Map<String, String> createMap(final String[] keyValuePairs) {
final var map = new HashMap<String, String>();
for (final var keyValuePair : keyValuePairs) {
final int indexOfKeyValueDelimiter = keyValuePair.indexOf(":");
final String key = keyValuePair.substring(0, indexOfKeyValueDelimiter);
// we need to add 1 to skip delimiter character
final String value = keyValuePair.substring(indexOfKeyValueDelimiter + 1);
map.put(key.trim(), value.trim());
}
return map;
}
private String removeStartingAndEndingObjectCurlyBraces() {
final String withoutOpeningBraces = serializedPlayer.replaceFirst("\\{", "");
final String reverseWithoutClosingBraces = new StringBuffer(withoutOpeningBraces).reverse()
.toString()
.replaceFirst("}", "");
return new StringBuffer(reverseWithoutClosingBraces).reverse().toString();
}
}

View file

@ -0,0 +1,45 @@
package de.hhhammer.playtime.ng.serialization.serializer;
import de.hhhammer.playtime.ng.player.Player;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
public final class JsonSerializer implements Serializer {
private final Player player;
private final DateTimeFormatter dateTimeFormatter;
public JsonSerializer(final Player player, final DateTimeFormatter dateTimeFormatter) {
this.player = player;
this.dateTimeFormatter = dateTimeFormatter;
}
@Override
public String serialize() {
return "{\n"
+ "\t\"uuid\": \"" + player.uuid() + "\",\n"
+ "\t\"name\": \"" + player.name() + "\",\n"
+ "\t\"playtime\": \"" + formattedDuration(player.playtime()) + "\",\n"
+ "\t\"joinTime\": \"" + player.joinTime().format(dateTimeFormatter) + "\"\n"
+ "}\n";
}
@Override
public UUID uuid() {
return player.uuid();
}
private String formattedDuration(Duration duration) {
final long daysInHours = duration.toDays() * 24;
final long unformattedHours = daysInHours + duration.toHoursPart();
final String hours = unformattedHours < 10 ?
"0" + unformattedHours : String.valueOf(unformattedHours);
final String minutes = duration.toMinutesPart() < 10 ?
"0" + duration.toMinutesPart() : String.valueOf(duration.toMinutesPart());
final String seconds = duration.toSecondsPart() < 10 ?
"0" + duration.toSecondsPart() : String.valueOf(duration.toSecondsPart());
return hours + ":" + minutes + ":" + seconds;
}
}

View file

@ -0,0 +1,9 @@
package de.hhhammer.playtime.ng.serialization.serializer;
import java.util.UUID;
public interface Serializer {
String serialize();
UUID uuid();
}

View file

@ -0,0 +1,22 @@
name: "PlayTimeNG"
author: "hamburghammer"
version: 0.1.0
description: "Allows player to see how much time they have played."
website: "https://git.hhhammer.de/hamburghammer/playtime-ng"
main: de.hhhammer.playtime.ng.Plugin
api-version: 1.16
load: POSTWORLD
commands:
playtime:
usage: /<command> [|<player>]
description: "Shows the playtime of the player"
uptime:
usage: /<command>
description: "Shows the up time of the server"
toptime:
usage: /<command>
description: "Shows top players with the highest playtime"
timeranke:
usage: /<command> [|<player>]
description: "Shows the rank of the player regarding the playtime."

View file

@ -0,0 +1,76 @@
package de.hhhammer.playtime.ng;
import de.hhhammer.playtime.ng.persistence.PlayerTimeDB;
import de.hhhammer.playtime.ng.persistence.ReadPlayerException;
import de.hhhammer.playtime.ng.persistence.WritePlayerException;
import de.hhhammer.playtime.ng.player.PlayTimePlayer;
import de.hhhammer.playtime.ng.player.Player;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.*;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PlayerTimeServiceTest {
private final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
@Nested
class PlayerJoinTest {
@Mock
private PlayerTimeDB playerTimeDB;
@Mock
private PlaytimeCalculator playtimeCalculator;
@Captor
ArgumentCaptor<Player> playerCaptor;
private PlayerTimeService playerTimeService;
@BeforeEach
void setup() {
playerTimeService = new PlayerTimeService(clock, playerTimeDB, playtimeCalculator);
}
@Test
void shouldSetJoinTimeOnFoundPlayer() throws ReadPlayerException, WritePlayerException {
var uuid = UUID.randomUUID();
when(playerTimeDB.findById(uuid)).thenReturn(Optional.of(new PlayTimePlayer(uuid, "username", Duration.ZERO, LocalDateTime.now(clock).minusMinutes(5))));
playerTimeService.playerJoin(uuid, "someusername");
verify(playerTimeDB).flush(playerCaptor.capture());
assertEquals(LocalDateTime.now(clock), playerCaptor.getValue().joinTime());
}
@Test
void shouldSetJoinTimeOnNotFoundPlayer() throws ReadPlayerException, WritePlayerException {
var uuid = UUID.randomUUID();
when(playerTimeDB.findById(uuid)).thenReturn(Optional.empty());
playerTimeService.playerJoin(uuid, "someusername");
verify(playerTimeDB).flush(playerCaptor.capture());
assertEquals(LocalDateTime.now(clock), playerCaptor.getValue().joinTime());
}
@Test
void shouldHavePlayTimeOnFoundPlayer() throws ReadPlayerException, WritePlayerException {
var uuid = UUID.randomUUID();
when(playerTimeDB.findById(uuid)).thenReturn(Optional.of(new PlayTimePlayer(uuid, "username", Duration.ofHours(1), LocalDateTime.now(clock).minusMinutes(5))));
playerTimeService.playerJoin(uuid, "someusername");
verify(playerTimeDB).flush(playerCaptor.capture());
assertEquals(1, playerCaptor.getValue().playtime().toHours());
}
}
}

View file

@ -0,0 +1,51 @@
package de.hhhammer.playtime.ng;
import de.hhhammer.playtime.ng.player.PlayTimePlayer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.time.*;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PlaytimeCalculatorTest {
@Nested
class CalculatePlaytimeTest {
private final Clock clock = Clock.fixed(Instant.parse("2018-08-22T10:00:00Z"), ZoneId.systemDefault());
@Test
void shouldReturnTimeBetweenJoinAndNowWhenNotSaved() {
var playtimeCalculator = new PlaytimeCalculator(clock);
var player = new PlayTimePlayer(UUID.randomUUID(), "Name", Duration.ofHours(1), LocalDateTime.now(clock).minusMinutes(5));
Duration playtime = playtimeCalculator.calculatePlaytime(player);
assertEquals(Duration.ofHours(1)
.plus(Duration.ofMinutes(5)),
playtime);
}
@Test
void shouldReturnTimeBetweenJoinAndNowWhenSavedBeforeJoin() {
var playtimeCalculator = new PlaytimeCalculator(clock);
var player = new PlayTimePlayer(UUID.randomUUID(), "Name", Duration.ofHours(1), LocalDateTime.now(clock).minusMinutes(5));
Duration playtime = playtimeCalculator.calculatePlaytime(player);
assertEquals(Duration.ofHours(1)
.plus(Duration.ofMinutes(5)),
playtime);
}
@Test
void shouldReturnTimeBetweenSaveAndNowWhenSavedAfterJoin() {
var playtimeCalculator = new PlaytimeCalculator(clock);
var player = new PlayTimePlayer(UUID.randomUUID(), "Name", Duration.ofHours(1), LocalDateTime.now(clock).minusMinutes(5), LocalDateTime.now(clock).minusMinutes(1));
Duration playtime = playtimeCalculator.calculatePlaytime(player);
assertEquals(Duration.ofHours(1)
.plus(Duration.ofMinutes(1)),
playtime);
}
}
}

View file

@ -0,0 +1,229 @@
package de.hhhammer.playtime.ng.persistence;
import de.hhhammer.playtime.ng.player.Player;
import de.hhhammer.playtime.ng.serialization.SerializationFactory;
import de.hhhammer.playtime.ng.serialization.serializer.Serializer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class FileSystemPlayerTimeDBTest {
@Mock
private SerializationFactory mockSerializationFactory;
@Mock
private Player mockPlayer;
@Mock
private Serializer mockSerializer;
private File testDirectory;
private FileSystemPlayerTimeDB fileSystemDB;
@BeforeEach
void setup() throws IOException {
final Path path = Files.createTempDirectory("playtime").toAbsolutePath();
this.testDirectory = path.toFile();
this.fileSystemDB = new FileSystemPlayerTimeDB(testDirectory, mockSerializationFactory);
}
@AfterEach
void cleanup() {
testDirectory.delete();
}
@Nested
class FlushTest {
@Test
void shouldWriteFile() throws WritePlayerException {
when(mockSerializationFactory.createSerializer(any())).thenReturn(mockSerializer);
when(mockPlayer.uuid()).thenReturn(UUID.randomUUID());
when(mockSerializer.serialize()).thenReturn("");
assertEquals(0, testDirectory.listFiles().length);
fileSystemDB.flush(mockPlayer);
assertEquals(1, testDirectory.listFiles().length);
}
@Test
void shouldNameFileWithPlayerUUID() throws WritePlayerException {
final var uuid = UUID.randomUUID();
when(mockSerializationFactory.createSerializer(any())).thenReturn(mockSerializer);
when(mockPlayer.uuid()).thenReturn(uuid);
when(mockSerializer.serialize()).thenReturn("");
fileSystemDB.flush(mockPlayer);
final File[] foundFiles = testDirectory.listFiles();
assertEquals(1, foundFiles.length);
assertEquals(uuid.toString(), foundFiles[0].getName());
}
@Test
void shouldWriteContentFromSerializer() throws WritePlayerException, IOException {
when(mockSerializationFactory.createSerializer(any())).thenReturn(mockSerializer);
when(mockPlayer.uuid()).thenReturn(UUID.randomUUID());
final String content = "test content from serializer";
when(mockSerializer.serialize()).thenReturn(content);
fileSystemDB.flush(mockPlayer);
final File[] foundFiles = testDirectory.listFiles();
assertEquals(1, foundFiles.length);
final String foundContent = Files.readString(foundFiles[0].toPath());
assertEquals(content, foundContent);
}
}
@Nested
class FindById {
@Test
void shouldThrowIfDirectoryIsAFile() {
final var notADir = new File(testDirectory, "not_a_dir");
final var fileSystemDB = new FileSystemPlayerTimeDB(notADir, mockSerializationFactory);
final Exception exception = assertThrows(RuntimeException.class, () -> fileSystemDB.findById(UUID.randomUUID()));
assertEquals("Could not find files in: " + notADir.getAbsolutePath(), exception.getMessage());
}
@Test
void shouldBeEmptyIfNoFileIsPresent() throws IOException, ReadPlayerException {
assertEquals(0, testDirectory.listFiles().length);
final Optional<Player> foundPlayer = fileSystemDB.findById(UUID.randomUUID());
assertTrue(foundPlayer.isEmpty());
}
@Test
void shouldBeEmptyIfUuidNotFound() throws IOException, ReadPlayerException {
new File(testDirectory, "not_a_uuid").createNewFile();
final Optional<Player> foundPlayer = fileSystemDB.findById(UUID.randomUUID());
assertTrue(foundPlayer.isEmpty());
}
@Test
void shouldDeserializeFileContent() throws IOException, ReadPlayerException {
final var playerUUID = UUID.randomUUID();
final var fakePlayerFile = new File(testDirectory, playerUUID.toString());
final String fileContent = "test content";
Files.write(fakePlayerFile.toPath(), fileContent.getBytes());
when(mockSerializationFactory.createDeserializer(any())).thenReturn(mockPlayer);
fileSystemDB.findById(playerUUID);
verify(mockSerializationFactory).createDeserializer(fileContent);
}
}
@Nested
class FindByName {
@Test
void shouldBeEmptyIfNoFilesExist() throws ReadPlayerException {
final Optional<Player> result = fileSystemDB.findByName("username");
assertTrue(result.isEmpty());
}
@Test
void shouldFindThroughDeserializePlayerWithOnlyOneFile() throws IOException, ReadPlayerException {
final var username = "username";
when(mockPlayer.name()).thenReturn(username);
final var fakePlayerFile = new File(testDirectory, "fake_player_file");
Files.write(fakePlayerFile.toPath(), "".getBytes());
when(mockSerializationFactory.createDeserializer(anyString())).thenReturn(mockPlayer);
final Optional<Player> result = fileSystemDB.findByName(username);
assertTrue(result.isPresent());
}
@Test
void shouldNotFindThroughDeserializePlayerWithOnlyOneFile() throws IOException, ReadPlayerException {
final var username = "username";
when(mockPlayer.name()).thenReturn(username);
final var fakePlayerFile = new File(testDirectory, "fake_player_file");
Files.write(fakePlayerFile.toPath(), "".getBytes());
when(mockSerializationFactory.createDeserializer(anyString())).thenReturn(mockPlayer);
final Optional<Player> result = fileSystemDB.findByName("not_a_username");
assertTrue(result.isEmpty());
}
@Test
void shouldFindThroughDeserializePlayerOnMultipleFiles() throws IOException, ReadPlayerException {
final var wantedUsername = "username";
final var notWantedUsername = "not_wanted_username";
final var rightPlayer = mock(Player.class);
final var wrongPlayer = mock(Player.class);
when(rightPlayer.name()).thenReturn(wantedUsername);
when(wrongPlayer.name()).thenReturn(notWantedUsername);
final var rightPlayerFileContent = "right";
final var wrongPlayerFileContent = "wrong";
final var rightPlayerFile = new File(testDirectory, "right_player_file");
final var wrongPlayerFile = new File(testDirectory, "wrong_player_file");
Files.write(rightPlayerFile.toPath(), rightPlayerFileContent.getBytes());
Files.write(wrongPlayerFile.toPath(), wrongPlayerFileContent.getBytes());
when(mockSerializationFactory.createDeserializer(rightPlayerFileContent)).thenReturn(rightPlayer);
when(mockSerializationFactory.createDeserializer(wrongPlayerFileContent)).thenReturn(wrongPlayer);
final Optional<Player> result = fileSystemDB.findByName(wantedUsername);
assertTrue(result.isPresent());
}
}
@Nested
class FindAllTest {
@Test
void shouldBeEmptyIfNoFilesExist() throws ReadPlayerException {
final List<Player> result = fileSystemDB.findAll();
assertEquals(0, result.size());
}
@Test
void shouldFindOneFile() throws IOException, ReadPlayerException {
final var fakePlayerFile = new File(testDirectory, "fake_player_file");
Files.write(fakePlayerFile.toPath(), "test content".getBytes());
when(mockSerializationFactory.createDeserializer(anyString())).thenReturn(mockPlayer);
final List<Player> result = fileSystemDB.findAll();
assertEquals(1, result.size());
}
@Test
void shouldFindTwoFile() throws IOException, ReadPlayerException {
final var fakePlayerFile = new File(testDirectory, "fake_player_file");
Files.write(fakePlayerFile.toPath(), "test content".getBytes());
final var fakePlayerFile2 = new File(testDirectory, "fake_player_file_2");
Files.write(fakePlayerFile2.toPath(), "test content".getBytes());
when(mockSerializationFactory.createDeserializer(anyString())).thenReturn(mockPlayer);
final List<Player> result = fileSystemDB.findAll();
assertEquals(2, result.size());
}
}
}

View file

@ -0,0 +1,63 @@
package de.hhhammer.playtime.ng.player;
import de.hhhammer.playtime.ng.serialization.deserializer.Deserializer;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class DeserializationPlayerTest {
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
private final Deserializer deserializer = mock(Deserializer.class);
@Test
void shouldParseUUID() {
when(deserializer.getValueForKey(anyString())).thenReturn("08f45958-27cc-4d56-b4ea-016cf66c0502");
final var jsonPlayer = new DeserializationPlayer(deserializer, dateTimeFormatter);
assertEquals(UUID.fromString("08f45958-27cc-4d56-b4ea-016cf66c0502"), jsonPlayer.uuid());
}
@Test
void shouldParseName() {
when(deserializer.getValueForKey(anyString())).thenReturn("name");
final var jsonPlayer = new DeserializationPlayer(deserializer, dateTimeFormatter);
assertEquals("name", jsonPlayer.name());
}
@Test
void shouldParsePlaytime() {
when(deserializer.getValueForKey(anyString())).thenReturn("00:01:00");
final var jsonPlayer = new DeserializationPlayer(deserializer, dateTimeFormatter);
assertEquals(Duration.ofMinutes(1), jsonPlayer.playtime());
}
@Test
void shouldParseJoinTime() {
final var joinTime = LocalDateTime.of(2022, Month.JULY, 29, 12, 17, 27, 668767674);
when(deserializer.getValueForKey(anyString())).thenReturn("2022-07-29T12:17:27.668767674");
final var jsonPlayer = new DeserializationPlayer(deserializer, dateTimeFormatter);
assertEquals(joinTime, jsonPlayer.joinTime());
}
@Test
void shouldReturnEmptySaveTime() {
final var jsonPlayer = new DeserializationPlayer(deserializer, dateTimeFormatter);
assertTrue(jsonPlayer.saveTime().isEmpty());
}
}

View file

@ -0,0 +1,40 @@
package de.hhhammer.playtime.ng.player;
import de.hhhammer.playtime.ng.serialization.serializer.JsonSerializer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PlayTimePlayerTest implements PlayerTestHelper {
@Nested
class SerializeTest {
@Test
void shouldProduceCorrectFormat() {
final var joinTime = LocalDateTime.of(2022, Month.JULY, 29, 12, 17, 27, 668767674);
final Player player = new PlayTimePlayer(UUID.fromString("08f45958-27cc-4d56-b4ea-016cf66c0502"), "name", Duration.of(1, ChronoUnit.MINUTES), joinTime);
final var expected = """
{
\t"uuid": "08f45958-27cc-4d56-b4ea-016cf66c0502",
\t"name": "name",
\t"playtime": "00:01:00",
\t"joinTime": "2022-07-29T12:17:27.668767674"
}
""";
final var actual = new JsonSerializer(player, DateTimeFormatter.ISO_DATE_TIME).serialize();
assertEquals(expected, actual);
}
}
}

View file

@ -0,0 +1,22 @@
package de.hhhammer.playtime.ng.player;
import de.hhhammer.playtime.ng.serialization.deserializer.JsonDeserializer;
import de.hhhammer.playtime.ng.serialization.serializer.JsonSerializer;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
public class PlayerDeserializationPlayerDeserializerSerializerAndTest implements PlayerTestHelper {
@Test
void shouldBeEqualAfterSerializationAndDeserialization() {
final var dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
final var player = new PlayTimePlayer(UUID.randomUUID(), "name", Duration.ofMinutes(1), LocalDateTime.now());
final var serializedPlayer = new JsonSerializer(player, dateTimeFormatter).serialize();
final var deserializedPlayer = new DeserializationPlayer(new JsonDeserializer(serializedPlayer), dateTimeFormatter);
assertEqual(player, deserializedPlayer);
}
}

View file

@ -0,0 +1,13 @@
package de.hhhammer.playtime.ng.player;
import static org.junit.jupiter.api.Assertions.assertEquals;
public interface PlayerTestHelper {
default void assertEqual(Player expected, Player actual) {
assertEquals(expected.uuid(), actual.uuid(), "UUIDs are not equal");
assertEquals(expected.name(), actual.name(), "Names are not equal");
assertEquals(expected.playtime(), actual.playtime(), "Playtimes are not equal");
assertEquals(expected.joinTime(), actual.joinTime(), "Join times are not equal");
assertEquals(expected.saveTime(), actual.saveTime(), "Save times are not equal");
}
}

View file

@ -0,0 +1,61 @@
package de.hhhammer.playtime.ng.serialization.deserializer;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.Month;
import static org.junit.jupiter.api.Assertions.assertEquals;
class JsonDeserializerTest {
@Test
void shouldParseValidJsonWithDashValue() {
final var json = """
{
\t"uuid": "08f45958-27cc-4d56-b4ea-016cf66c0502",
}
""";
final var jsonDeserializer = new JsonDeserializer(json);
assertEquals("08f45958-27cc-4d56-b4ea-016cf66c0502", jsonDeserializer.getValueForKey("uuid"));
}
@Test
void shouldParseValidJsonWithStringValue() {
final var json = """
{
\t"name": "name value",
}
""";
final var jsonDeserializer = new JsonDeserializer(json);
assertEquals("name value", jsonDeserializer.getValueForKey("name"));
}
@Test
void shouldParseValidJsonWithDoublePoints() {
final var json = """
{
\t"playtime": "00:01:00",
}
""";
final var jsonDeserializer = new JsonDeserializer(json);
assertEquals("00:01:00", jsonDeserializer.getValueForKey("playtime"));
}
@Test
void shouldParseValidJsonWithDateValue() {
final var joinTime = LocalDateTime.of(2022, Month.JULY, 29, 12, 17, 27, 668767674);
final var json = """
{
\t"joinTime": "2022-07-29T12:17:27.668767674",
}
""";
final var jsonDeserializer = new JsonDeserializer(json);
assertEquals("2022-07-29T12:17:27.668767674", jsonDeserializer.getValueForKey("joinTime"));
}
}

View file

@ -0,0 +1,168 @@
package de.hhhammer.playtime.ng.serialization.serializer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hhhammer.playtime.ng.player.PlayTimePlayer;
import de.hhhammer.playtime.ng.player.Player;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class JsonSerializerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final Player player = new PlayTimePlayer(UUID.randomUUID(), "username", Duration.ofMinutes(10), LocalDateTime.now());
private JsonSerializer jsonSerializer;
private DateTimeFormatter dateTimeFormatter;
@BeforeEach
void setUp() {
dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
this.jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
}
@Test
void shouldHaveUUID() {
final var result = jsonSerializer.serialize();
final String key = "uuid";
assertTrue(result.contains(key));
assertEquals(player.uuid().toString(), getValue(key, result));
}
@Test
void shouldHaveUsername() {
final var result = jsonSerializer.serialize();
final String key = "name";
assertTrue(result.contains(key));
assertEquals(player.name(), getValue(key, result));
}
@Test
void shouldHaveJoinTime() {
final var result = jsonSerializer.serialize();
final String key = "joinTime";
assertTrue(result.contains(key));
assertEquals(player.joinTime().format(dateTimeFormatter), getValue(key, result));
}
@Test
void shouldReturnPlayerUUID() {
assertEquals(player.uuid(), jsonSerializer.uuid());
}
private String getValue(String key, String json) {
try {
var jsonNode = objectMapper.readTree(json);
return jsonNode.findValue(key).asText();
} catch (JsonProcessingException e) {
fail("Finding field failed", e);
}
// should never be reached.
return "";
}
@Nested
class PlaytimeFormattingTest {
@Test
void shouldNoPlaytime() {
final var player = newPlayerWithPlayTime(Duration.ZERO);
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("00:00:00", getValue(key, result));
}
@Test
void shouldOneSecond() {
final var player = newPlayerWithPlayTime(Duration.ofSeconds(1));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("00:00:01", getValue(key, result));
}
@Test
void shouldTenSeconds() {
final var player = newPlayerWithPlayTime(Duration.ofSeconds(10));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("00:00:10", getValue(key, result));
}
@Test
void shouldOneMinute() {
final var player = newPlayerWithPlayTime(Duration.ofMinutes(1));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("00:01:00", getValue(key, result));
}
@Test
void shouldTenMinutes() {
final var player = newPlayerWithPlayTime(Duration.ofMinutes(10));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("00:10:00", getValue(key, result));
}
@Test
void shouldOneHour() {
final var player = newPlayerWithPlayTime(Duration.ofHours(1));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("01:00:00", getValue(key, result));
}
@Test
void shouldTenHours() {
final var player = newPlayerWithPlayTime(Duration.ofHours(10));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("10:00:00", getValue(key, result));
}
@Test
void shouldHundredHours() {
final var player = newPlayerWithPlayTime(Duration.ofHours(100));
final var jsonSerializer = new JsonSerializer(player, dateTimeFormatter);
final var result = jsonSerializer.serialize();
final String key = "playtime";
assertTrue(result.contains(key));
assertEquals("100:00:00", getValue(key, result));
}
private Player newPlayerWithPlayTime(Duration playTime) {
return new PlayTimePlayer(player.uuid(), player.name(), playTime, player.joinTime());
}
}
}