From 0c9643fd7ea1735b819198dd5fe20f9fa775e22b Mon Sep 17 00:00:00 2001 From: P0nk Date: Sun, 15 Sep 2024 09:25:35 +0200 Subject: [PATCH] CharacterSaver integration test with Testcontainers --- config.yaml | 3 +- pom.xml | 20 +++ src/main/java/client/CharacterStats.java | 3 + src/main/java/config/ServerConfig.java | 3 +- src/main/java/database/PgDatabaseConfig.java | 10 +- .../character/CharacterRepository.java | 57 +++++++- .../java/database/migration/FlywayRunner.java | 2 +- src/main/java/net/server/Server.java | 12 +- .../migration/postgresql/V0.3__character.sql | 2 +- .../character/CharacterSaverTest.java | 129 ++++++++++++++++++ src/test/java/testutil/GeneratedIds.java | 4 + src/test/java/testutil/TestData.java | 41 ++++++ 12 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 src/test/java/database/character/CharacterSaverTest.java create mode 100644 src/test/java/testutil/GeneratedIds.java create mode 100644 src/test/java/testutil/TestData.java diff --git a/config.yaml b/config.yaml index 2d252ba255..f1617342ca 100644 --- a/config.yaml +++ b/config.yaml @@ -165,8 +165,7 @@ server: DB_PASS: "" INIT_CONNECTION_POOL_TIMEOUT: 90 # Seconds - PG_DB_NAME: "cosmic" - PG_DB_HOST: "localhost" + PG_DB_URL: "jdbc:postgresql://localhost:5432/cosmic" PG_DB_SCHEMA: "cosmic" PG_DB_ADMIN_USERNAME: "cosmic_admin" PG_DB_ADMIN_PASSWORD: "redsnailshell" diff --git a/pom.xml b/pom.xml index 38c81dacb6..8e6bed13dc 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,7 @@ 3.45.1 5.10.2 5.11.0 + 1.20.1 42.5.4 9.15.1 3.1.4 @@ -213,6 +214,25 @@ ${mockito.version} test + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + org.testcontainers + mysql + ${testcontainers.version} + test + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + diff --git a/src/main/java/client/CharacterStats.java b/src/main/java/client/CharacterStats.java index 23311bfbf3..5369d8227a 100644 --- a/src/main/java/client/CharacterStats.java +++ b/src/main/java/client/CharacterStats.java @@ -4,6 +4,9 @@ import lombok.Builder; @Builder public record CharacterStats( + int account, + int world, + String name, int id, int level, int fame, diff --git a/src/main/java/config/ServerConfig.java b/src/main/java/config/ServerConfig.java index ad550e34d1..3e0bf8d74a 100644 --- a/src/main/java/config/ServerConfig.java +++ b/src/main/java/config/ServerConfig.java @@ -13,8 +13,7 @@ public class ServerConfig { public int INIT_CONNECTION_POOL_TIMEOUT; // PostgreSQL database configuration - public String PG_DB_NAME; - public String PG_DB_HOST; + public String PG_DB_URL; public String PG_DB_SCHEMA; public String PG_DB_ADMIN_USERNAME; public String PG_DB_ADMIN_PASSWORD; diff --git a/src/main/java/database/PgDatabaseConfig.java b/src/main/java/database/PgDatabaseConfig.java index bec5c7a645..3e4a7dfe57 100644 --- a/src/main/java/database/PgDatabaseConfig.java +++ b/src/main/java/database/PgDatabaseConfig.java @@ -6,15 +6,15 @@ import java.time.Duration; @Builder public record PgDatabaseConfig( - String databaseName, String host, String schema, + String url, + String schema, String adminUsername, String adminPassword, String username, String password, Duration poolInitTimeout, boolean clean ) { public PgDatabaseConfig { - verifyNotBlank(databaseName); - verifyNotBlank(host); + verifyNotBlank(url); verifyNotBlank(schema); verifyNotBlank(adminUsername); verifyNotBlank(adminPassword); @@ -27,8 +27,4 @@ public record PgDatabaseConfig( throw new IllegalArgumentException("Missing or blank value in PG database config"); } } - - public String getJdbcUrl() { - return "jdbc:postgresql://%s:5432/%s".formatted(host, databaseName); - } } diff --git a/src/main/java/database/character/CharacterRepository.java b/src/main/java/database/character/CharacterRepository.java index 0d544d4723..505148e1e5 100644 --- a/src/main/java/database/character/CharacterRepository.java +++ b/src/main/java/database/character/CharacterRepository.java @@ -7,6 +7,54 @@ import java.sql.Timestamp; public class CharacterRepository { + public int insert(Handle handle, CharacterStats stats) { + String sql = """ + INSERT INTO chr (account, world, name, level, exp, str, dex, "int", luk, hp, mp, max_hp, max_mp, ap, sp, + job, fame, gender, skin, hair, face, meso, map_id, spawn_portal, gacha_exp, used_hp_mp_ap, gm_level, + party_id, buddy_capacity, equip_slots, use_slots, setup_slots, etc_slots) + VALUES (:account, :world, :name, :level, :exp, :str, :dex, :int, :luk, :hp, :mp, :max_hp, :max_mp, :ap, + :sp, :job, :fame, :gender, :skin, :hair, :face, :meso, :map_id, :spawn_portal, :gacha_exp, + :used_hp_mp_ap, :gm_level, :party_id, :buddy_capacity, :equip_slots, :use_slots, :setup_slots, + :etc_slots)"""; + return handle.createUpdate(sql) + .bind("account", stats.account()) + .bind("world", stats.world()) + .bind("name", stats.name()) + .bind("level", stats.level()) + .bind("exp", stats.exp()) + .bind("str", stats.str()) + .bind("dex", stats.dex()) + .bind("int", stats.int_()) + .bind("luk", stats.luk()) + .bind("hp", stats.hp()) + .bind("mp", stats.mp()) + .bind("max_hp", stats.maxHp()) + .bind("max_mp", stats.maxMp()) + .bind("ap", stats.ap()) + .bind("sp", parseSp(stats.sp())) + .bind("job", stats.job()) + .bind("fame", stats.fame()) + .bind("gender", stats.gender()) + .bind("skin", stats.skin()) + .bind("hair", stats.hair()) + .bind("face", stats.face()) + .bind("meso", stats.meso()) + .bind("map_id", stats.mapId()) + .bind("spawn_portal", stats.spawnPortal()) + .bind("gacha_exp", stats.gachaExp()) + .bind("used_hp_mp_ap", stats.hpMpApUsed()) + .bind("gm_level", stats.gmLevel()) + .bind("party_id", stats.party()) + .bind("buddy_capacity", stats.buddyCapacity()) + .bind("equip_slots", stats.equipSlots()) + .bind("use_slots", stats.useSlots()) + .bind("setup_slots", stats.setupSlots()) + .bind("etc_slots", stats.etcSlots()) + .executeAndReturnGeneratedKeys("id") + .mapTo(Integer.class) + .one(); + } + public boolean update(Handle handle, CharacterStats stats) { String sql = """ UPDATE chr @@ -40,7 +88,7 @@ public class CharacterRepository { .bind("max_hp", stats.maxHp()) .bind("max_mp", stats.maxMp()) .bind("ap", stats.ap()) - .bind("sp", parseMultiSkillbookSp(stats.sp())) + .bind("sp", parseSp(stats.sp())) .bind("job", stats.job()) .bind("fame", stats.fame()) .bind("gender", stats.gender()) @@ -89,11 +137,16 @@ public class CharacterRepository { return updatedRows > 0; } - private int parseMultiSkillbookSp(String sp) { + private int parseSp(String sp) { if (sp == null) { return 0; } + if (!sp.contains(",")) { + return Integer.parseInt(sp); + } + + // Old multi skillbook sp to support Evan skills. To be changed - sp will be simple integer in new db. return Integer.parseInt(sp.split(",")[0]); } } diff --git a/src/main/java/database/migration/FlywayRunner.java b/src/main/java/database/migration/FlywayRunner.java index a846b38c64..81b81764f6 100644 --- a/src/main/java/database/migration/FlywayRunner.java +++ b/src/main/java/database/migration/FlywayRunner.java @@ -15,7 +15,7 @@ public class FlywayRunner { public void migrate() throws FlywayException { Flyway flyway = Flyway.configure() - .dataSource(dbConfig.getJdbcUrl(), dbConfig.adminUsername(), dbConfig.adminPassword()) + .dataSource(dbConfig.url(), dbConfig.adminUsername(), dbConfig.adminPassword()) .schemas(dbConfig.schema()) .createSchemas(true) .connectRetries(10) diff --git a/src/main/java/net/server/Server.java b/src/main/java/net/server/Server.java index c58e0ba23b..b6ad554564 100644 --- a/src/main/java/net/server/Server.java +++ b/src/main/java/net/server/Server.java @@ -108,6 +108,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.SortedMap; @@ -969,13 +970,9 @@ public class Server { private PgDatabaseConfig readPgDbConfig() { final ServerConfig serverConfig = YamlConfig.config.server; - String pgDbHost = System.getenv("PG_DB_HOST"); - if (pgDbHost == null) { - pgDbHost = serverConfig.PG_DB_HOST; - } + String url = Objects.requireNonNullElse(System.getenv("PG_DB_URL"), serverConfig.PG_DB_URL); return PgDatabaseConfig.builder() - .databaseName(serverConfig.PG_DB_NAME) - .host(pgDbHost) + .url(url) .schema(serverConfig.PG_DB_SCHEMA) .adminUsername(serverConfig.PG_DB_ADMIN_USERNAME) .adminPassword(serverConfig.PG_DB_ADMIN_PASSWORD) @@ -999,8 +996,7 @@ public class Server { private HikariConfig createHikariConfig(PgDatabaseConfig config) { final HikariConfig hikariConfig = new HikariConfig(); - hikariConfig.setJdbcUrl(config.getJdbcUrl()); - hikariConfig.setSchema(config.schema()); + hikariConfig.setJdbcUrl(config.url()); hikariConfig.setUsername(config.username()); hikariConfig.setPassword(config.password()); hikariConfig.setInitializationFailTimeout(config.poolInitTimeout().toMillis()); diff --git a/src/main/resources/db/migration/postgresql/V0.3__character.sql b/src/main/resources/db/migration/postgresql/V0.3__character.sql index 7835fba30e..12c9ad7063 100644 --- a/src/main/resources/db/migration/postgresql/V0.3__character.sql +++ b/src/main/resources/db/migration/postgresql/V0.3__character.sql @@ -29,7 +29,7 @@ CREATE TABLE chr used_hp_mp_ap integer NOT NULL, gm_level smallint NOT NULL, party_id integer, - buddy_capacity smallint, + buddy_capacity smallint NOT NULL, "rank" integer, rank_move integer, job_rank integer, diff --git a/src/test/java/database/character/CharacterSaverTest.java b/src/test/java/database/character/CharacterSaverTest.java new file mode 100644 index 0000000000..bc214bde11 --- /dev/null +++ b/src/test/java/database/character/CharacterSaverTest.java @@ -0,0 +1,129 @@ +package database.character; + +import client.Character; +import client.CharacterStats; +import client.MonsterBook; +import config.ServerConfig; +import config.YamlConfig; +import database.PgDatabaseConfig; +import database.PgDatabaseConnection; +import database.migration.FlywayRunner; +import database.monsterbook.MonsterCardRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import testutil.GeneratedIds; +import testutil.TestData; +import tools.DatabaseConnection; + +import java.time.Duration; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@Testcontainers +class CharacterSaverTest { + private static final String MYSQL_VERSION = "8.4"; + private static final String POSTGRES_VERSION = "16.4"; + private static final String SCHEMA_NAME = "cosmic"; + + @Container + static MySQLContainer mySql = new MySQLContainer<>("mysql:%s".formatted(MYSQL_VERSION)); + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:%s".formatted(POSTGRES_VERSION)); + + private PgDatabaseConnection pgConnection; + private CharacterSaver characterSaver; + + @BeforeEach + void setUp() { + prepareMysqlConnection(); + runDbMigrations(); + PgDatabaseConnection pgDatabaseConnection = createPgConnection(); + this.pgConnection = pgDatabaseConnection; + this.characterSaver = new CharacterSaver(pgDatabaseConnection, new CharacterRepository(), + new MonsterCardRepository(pgDatabaseConnection)); + } + + // Not using this, but due to the nature of how the db connections are set up, the application requires + // a real database to connect to. + private void prepareMysqlConnection() { + ServerConfig serverConfig = new ServerConfig(); + serverConfig.DB_URL_FORMAT = "%s"; + serverConfig.DB_HOST = mySql.getJdbcUrl(); + serverConfig.DB_USER = mySql.getUsername(); + serverConfig.DB_PASS = mySql.getPassword(); + serverConfig.INIT_CONNECTION_POOL_TIMEOUT = 60; + YamlConfig.config.server = serverConfig; + DatabaseConnection.initializeConnectionPool(); + } + + private void runDbMigrations() { + PgDatabaseConfig config = PgDatabaseConfig.builder() + .url(postgres.getJdbcUrl()) + .schema(SCHEMA_NAME) + .adminUsername(postgres.getUsername()) + .adminPassword(postgres.getPassword()) + .username(postgres.getUsername()) + .password(postgres.getPassword()) + .poolInitTimeout(Duration.ofSeconds(60)) + .clean(false) + .build(); + new FlywayRunner(config).migrate(); + } + + private PgDatabaseConnection createPgConnection() { + return new PgDatabaseConnection(createDataSource()); + } + + private PGSimpleDataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(postgres.getJdbcUrl()); + dataSource.setCurrentSchema(SCHEMA_NAME); + dataSource.setUser(postgres.getUsername()); + dataSource.setPassword(postgres.getPassword()); + return dataSource; + } + + @Test + void saveCharacter_shouldUpdateChrTable() { + GeneratedIds ids = TestData.create(pgConnection); + Character mockChr = Mockito.mock(Character.class); + when(mockChr.isLoggedin()).thenReturn(true); + addEmptyMonsterBook(mockChr); + when(mockChr.getCharacterStats()).thenReturn(CharacterStats.builder() + .id(ids.chrId()) + .level(200) + .build()); + assertEquals(0, getChrLevel(ids.chrId())); + + characterSaver.save(mockChr); + + assertEquals(200, getChrLevel(ids.chrId())); + } + + private static void addEmptyMonsterBook(Character mockChr) { + MonsterBook mockMonsterBook = Mockito.mock(MonsterBook.class); + when(mockMonsterBook.getCards()).thenReturn(Collections.emptyList()); + when(mockChr.getMonsterBook()).thenReturn(mockMonsterBook); + } + + private int getChrLevel(int chrId) { + String sql = """ + SELECT level + FROM chr + WHERE id = :id"""; + return pgConnection.getHandle().createQuery(sql) + .bind("id", chrId) + .mapTo(Integer.class) + .one(); + } + +} diff --git a/src/test/java/testutil/GeneratedIds.java b/src/test/java/testutil/GeneratedIds.java new file mode 100644 index 0000000000..24bdc67925 --- /dev/null +++ b/src/test/java/testutil/GeneratedIds.java @@ -0,0 +1,4 @@ +package testutil; + +public record GeneratedIds(int accountId, int chrId) { +} diff --git a/src/test/java/testutil/TestData.java b/src/test/java/testutil/TestData.java new file mode 100644 index 0000000000..a8a1bce717 --- /dev/null +++ b/src/test/java/testutil/TestData.java @@ -0,0 +1,41 @@ +package testutil; + +import client.CharacterStats; +import database.PgDatabaseConnection; +import database.character.CharacterRepository; +import org.jdbi.v3.core.Handle; + +import java.time.LocalDate; + +public class TestData { + + public static GeneratedIds create(PgDatabaseConnection connection) { + try (Handle handle = connection.getHandle()) { + int accountId = insertAccount(handle); + int chrId = insertChr(handle, accountId); + return new GeneratedIds(accountId, chrId); + } + } + + private static int insertAccount(Handle handle) { + String sql = """ + INSERT INTO account (name, password, birthday) + VALUES (:name, :password, :birthday)"""; + return handle.createUpdate(sql) + .bind("name", "accountname") + .bind("password", "accountpassword") + .bind("birthday", LocalDate.of(2005, 5, 11)) + .executeAndReturnGeneratedKeys() + .mapTo(Integer.class) + .one(); + } + + private static int insertChr(Handle handle, int accountId) { + CharacterRepository chrRepository = new CharacterRepository(); + CharacterStats stats = CharacterStats.builder() + .account(accountId) + .name("chrname") + .build(); + return chrRepository.insert(handle, stats); + } +}