From f399348a0c5ff584b26cbb69572767a152d37019 Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Sun, 16 Jan 2022 19:43:49 +0100 Subject: [PATCH 1/7] rename --- .idea/chat-server.iml | 9 +++++++++ .idea/codeStyles/Project.xml | 16 ---------------- .idea/inspectionProfiles/Project_Default.xml | 6 ++++++ .idea/modules.xml | 8 ++++++++ src/main/kotlin/Application.kt | 4 ++-- src/main/kotlin/di/{pushKodein.kt => pushDi.kt} | 0 src/main/kotlin/di/{setupDi.kt => setup.kt} | 2 +- src/test/kotlin/testutil/databaseTest.kt | 4 ++-- src/test/kotlin/testutil/serverTest.kt | 4 ++-- 9 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 .idea/chat-server.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml rename src/main/kotlin/di/{pushKodein.kt => pushDi.kt} (100%) rename src/main/kotlin/di/{setupDi.kt => setup.kt} (80%) diff --git a/.idea/chat-server.iml b/.idea/chat-server.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/chat-server.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 31d977a..1bec35e 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,22 +1,6 @@ - - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..c83a369 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1291854 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt index 8e60816..a3b059e 100644 --- a/src/main/kotlin/Application.kt +++ b/src/main/kotlin/Application.kt @@ -3,7 +3,7 @@ import clientapi.installClientApi import clientapi.installClientApiJwtAuthentication import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.ObjectMapper -import di.setupKodein +import di.setupDi import error.PlatformApiException import flyway.FlywayConfig import io.ktor.application.* @@ -33,7 +33,7 @@ import java.util.* import kotlin.reflect.typeOf -fun Application.module(bindDependencies: DI.MainBuilder.() -> Unit = { setupKodein() }) { +fun Application.module(bindDependencies: DI.MainBuilder.() -> Unit = { setupDi() }) { installFeatures(bindDependencies) val flywayConfig: FlywayConfig by closestDI().instance() val flyway: Flyway by closestDI().instance() diff --git a/src/main/kotlin/di/pushKodein.kt b/src/main/kotlin/di/pushDi.kt similarity index 100% rename from src/main/kotlin/di/pushKodein.kt rename to src/main/kotlin/di/pushDi.kt diff --git a/src/main/kotlin/di/setupDi.kt b/src/main/kotlin/di/setup.kt similarity index 80% rename from src/main/kotlin/di/setupDi.kt rename to src/main/kotlin/di/setup.kt index b67d8d9..1e1c1b4 100644 --- a/src/main/kotlin/di/setupDi.kt +++ b/src/main/kotlin/di/setup.kt @@ -2,7 +2,7 @@ package di import org.kodein.di.DI -fun DI.MainBuilder.setupKodein() { +fun DI.MainBuilder.setupDi() { import(jacksonDi) import(utilDi) import(postgresDi) diff --git a/src/test/kotlin/testutil/databaseTest.kt b/src/test/kotlin/testutil/databaseTest.kt index 093c5f3..e24bd01 100644 --- a/src/test/kotlin/testutil/databaseTest.kt +++ b/src/test/kotlin/testutil/databaseTest.kt @@ -4,7 +4,7 @@ import persistence.jooq.tables.Channel.Companion.CHANNEL import persistence.jooq.tables.User.Companion.USER import persistence.jooq.tables.references.CHANNEL_MEMBER import persistence.jooq.tables.references.MESSAGE -import di.setupKodein +import di.setupDi import kotlinx.coroutines.runBlocking import org.kodein.di.DI import org.kodein.di.instance @@ -17,7 +17,7 @@ fun databaseTest( test: suspend DatabaseTestEnvironment.() -> Unit ) { val kodein = DI { - setupKodein() + setupDi() setupTestDependencies() bindDependencies() } diff --git a/src/test/kotlin/testutil/serverTest.kt b/src/test/kotlin/testutil/serverTest.kt index 4892bc6..077bb5d 100644 --- a/src/test/kotlin/testutil/serverTest.kt +++ b/src/test/kotlin/testutil/serverTest.kt @@ -7,7 +7,7 @@ import clientapi.queries.ChannelQuery import clientapi.queries.MessageQuery import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import di.setupKodein +import di.setupDi import error.PlatformApiError import error.PlatformApiException import error.duplicate @@ -38,7 +38,7 @@ fun serverTest( ) { withTestApplication({ module { - setupKodein() + setupDi() setupTestDependencies() bindDependencies() } From c508428fe294b95e7a5512dd770ed6ca2ae04420 Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Tue, 13 Sep 2022 08:03:04 +0200 Subject: [PATCH 2/7] draft --- .idea/chat-server.iml | 9 --- .idea/modules.xml | 8 --- .../resources/db/migration/V3__add_events.sql | 67 +++++++++++++++++++ 3 files changed, 67 insertions(+), 17 deletions(-) delete mode 100644 .idea/chat-server.iml delete mode 100644 .idea/modules.xml create mode 100644 src/main/resources/db/migration/V3__add_events.sql diff --git a/.idea/chat-server.iml b/.idea/chat-server.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/chat-server.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1291854..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__add_events.sql b/src/main/resources/db/migration/V3__add_events.sql new file mode 100644 index 0000000..c37d149 --- /dev/null +++ b/src/main/resources/db/migration/V3__add_events.sql @@ -0,0 +1,67 @@ +CREATE TABLE "set_channel_meta_event" +( + "channel_event_id" bigserial, + "name" varchar(512) NULL, + "description" varchar(4096) NULL, + CONSTRAINT "set_channel_meta_event_channel_event_id_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "set_channel_meta_event_channel_event_id_fkey" + FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE +); + +CREATE TABLE "add_member_event" +( + "channel_event_id" bigserial, + "user_id" varchar NULL, + "role" channel_member_role NOT NULL, + CONSTRAINT "add_member_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "add_member_event_channel_event_id_fkey" + FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "add_member_event_user_id_fkey" + FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE SET NULL +); + +CREATE TABLE "update_member_event" +( + "channel_event_id" bigserial, + "source_id" bigserial NOT NULL, + "role" channel_member_role NOT NULL, + CONSTRAINT "add_member_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "add_member_event_channel_event_id_fkey" + FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "add_member_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "add_member_event" ("channel_event_id") ON DELETE CASCADE +); + +CREATE TABLE "delete_member_event" +( + "channel_event_id" bigserial, + "source_id" bigserial NOT NULL, + CONSTRAINT "delete_member_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "delete_member_event_channel_event_id_fkey" + FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "delete_member_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "add_member_event" ("channel_event_id") ON DELETE CASCADE +); + +CREATE TABLE "create_message_event" +( + "channel_event_id" bigserial, + "text" varchar(4096) NULL, + "replied_event_id" bigserial NULL, + "creator_user_id" varchar NULL, + "role" channel_member_role NOT NULL, + CONSTRAINT "create_message_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "create_message_event_replied_event_id_fkey" + FOREIGN KEY ("replied_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "add_member_event_user_id_fkey" + FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE SET NULL +); + +CREATE TABLE "channel_event" +( + id bigserial, + source_id bigserial NULL, + created_at timestamptz NOT NULL, + CONSTRAINT "channel_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("source_id") ON DELETE CASCADE +); From 5aa9495ac3a90e9248241695c5c223dcab8aa102 Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Thu, 10 Nov 2022 11:20:50 +0100 Subject: [PATCH 3/7] implement sql for new event structure --- .../resources/db/migration/V3__add_events.sql | 96 ++++++++++++------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/src/main/resources/db/migration/V3__add_events.sql b/src/main/resources/db/migration/V3__add_events.sql index c37d149..4822f34 100644 --- a/src/main/resources/db/migration/V3__add_events.sql +++ b/src/main/resources/db/migration/V3__add_events.sql @@ -1,67 +1,95 @@ -CREATE TABLE "set_channel_meta_event" +CREATE TABLE "channel_event" ( - "channel_event_id" bigserial, - "name" varchar(512) NULL, + id bigserial, + source_id bigint NULL, + channel_id uuid, + created_at timestamptz NOT NULL, + CONSTRAINT "channel_event_pkey" PRIMARY KEY ("id"), + CONSTRAINT "channel_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "channel_event_channel_id_fkey" + FOREIGN KEY ("channel_id") REFERENCES "channel" ("id") ON DELETE CASCADE +); + +CREATE TABLE "update_channel_meta_event" +( + "channel_event_id" bigint, + "source_id" bigint NULL, + "name" varchar(512) NULL, "description" varchar(4096) NULL, - CONSTRAINT "set_channel_meta_event_channel_event_id_pkey" PRIMARY KEY ("channel_event_id"), - CONSTRAINT "set_channel_meta_event_channel_event_id_fkey" - FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE + CONSTRAINT "update_channel_meta_event_channel_event_id_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "update_channel_meta_event_channel_event_id_fkey" + FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "update_channel_meta_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE ); -CREATE TABLE "add_member_event" +CREATE TABLE "create_member_event" ( - "channel_event_id" bigserial, - "user_id" varchar NULL, + "channel_event_id" bigint, + "user_id" varchar NULL, "role" channel_member_role NOT NULL, - CONSTRAINT "add_member_event_pkey" PRIMARY KEY ("channel_event_id"), - CONSTRAINT "add_member_event_channel_event_id_fkey" + CONSTRAINT "create_member_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "create_member_event_channel_event_id_fkey" FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, - CONSTRAINT "add_member_event_user_id_fkey" + CONSTRAINT "create_member_event_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE SET NULL ); CREATE TABLE "update_member_event" ( - "channel_event_id" bigserial, - "source_id" bigserial NOT NULL, + "channel_event_id" bigint, + "source_id" bigint NOT NULL, "role" channel_member_role NOT NULL, - CONSTRAINT "add_member_event_pkey" PRIMARY KEY ("channel_event_id"), - CONSTRAINT "add_member_event_channel_event_id_fkey" + CONSTRAINT "update_member_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "update_member_event_channel_event_id_fkey" FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, - CONSTRAINT "add_member_event_source_id_fkey" - FOREIGN KEY ("source_id") REFERENCES "add_member_event" ("channel_event_id") ON DELETE CASCADE + CONSTRAINT "update_member_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE ); CREATE TABLE "delete_member_event" ( - "channel_event_id" bigserial, - "source_id" bigserial NOT NULL, + "channel_event_id" bigint, + "source_id" bigint NOT NULL, CONSTRAINT "delete_member_event_pkey" PRIMARY KEY ("channel_event_id"), CONSTRAINT "delete_member_event_channel_event_id_fkey" FOREIGN KEY ("channel_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, CONSTRAINT "delete_member_event_source_id_fkey" - FOREIGN KEY ("source_id") REFERENCES "add_member_event" ("channel_event_id") ON DELETE CASCADE + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE ); CREATE TABLE "create_message_event" ( - "channel_event_id" bigserial, - "text" varchar(4096) NULL, - "replied_event_id" bigserial NULL, - "creator_user_id" varchar NULL, - "role" channel_member_role NOT NULL, + "channel_event_id" bigint, + "text" text NULL, + "replied_event_id" bigint NULL, + "creator_user_id" varchar NULL, CONSTRAINT "create_message_event_pkey" PRIMARY KEY ("channel_event_id"), CONSTRAINT "create_message_event_replied_event_id_fkey" FOREIGN KEY ("replied_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, - CONSTRAINT "add_member_event_user_id_fkey" - FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE SET NULL + CONSTRAINT "create_message_event_creator_user_id_fkey" + FOREIGN KEY ("creator_user_id") REFERENCES "user" ("id") ON DELETE SET NULL ); -CREATE TABLE "channel_event" +CREATE TABLE "update_message_event" ( - id bigserial, - source_id bigserial NULL, - created_at timestamptz NOT NULL, - CONSTRAINT "channel_event_source_id_fkey" - FOREIGN KEY ("source_id") REFERENCES "channel_event" ("source_id") ON DELETE CASCADE + "channel_event_id" bigint, + "source_id" bigint NOT NULL, + "text" text NULL, + "replied_event_id" bigint NULL, + CONSTRAINT "update_message_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "update_message_event_replied_event_id_fkey" + FOREIGN KEY ("replied_event_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE, + CONSTRAINT "update_message_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE +); + +CREATE TABLE "delete_message_event" +( + "channel_event_id" bigint, + "source_id" bigint NOT NULL, + CONSTRAINT "delete_message_event_pkey" PRIMARY KEY ("channel_event_id"), + CONSTRAINT "delete_message_event_source_id_fkey" + FOREIGN KEY ("source_id") REFERENCES "channel_event" ("id") ON DELETE CASCADE ); From f09644472a52adcb4f311adb2980b237ccc320d5 Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Fri, 11 Nov 2022 10:43:53 +0100 Subject: [PATCH 4/7] upgrade ktor and other dependencies --- .idea/compiler.xml | 2 +- .idea/jarRepositories.xml | 5 + .idea/misc.xml | 2 +- build.gradle.kts | 69 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/kotlin/Application.kt | 171 ++++++------ src/main/kotlin/clientapi/AuthContext.kt | 2 +- .../jwt/installClientApiJwtAuthentication.kt} | 18 +- src/main/kotlin/clientapi/installGraphQl.kt | 17 +- src/main/kotlin/di/graphQlDi.kt | 2 +- src/main/kotlin/di/postgresDi.kt | 6 +- src/main/kotlin/di/pushDi.kt | 5 +- src/main/kotlin/di/setup.kt | 6 +- src/main/kotlin/di/utilDi.kt | 7 +- .../graphql/KtorGraphQLContextFactory.kt | 4 +- .../graphql/KtorGraphQLRequestParser.kt | 3 +- src/main/kotlin/graphql/KtorGraphQLServer.kt | 2 +- src/main/kotlin/logging/LogFeature.kt | 246 ------------------ src/main/kotlin/logging/LoggingPlugin.kt | 98 +++++++ ...formApiAccessTokenAuthenticatonProvider.kt | 38 +++ ...allPlatformApiAccessTokenAuthentication.kt | 9 + src/main/kotlin/platformapi/channelDetails.kt | 6 +- src/main/kotlin/platformapi/channelList.kt | 6 +- .../platformapi/channelMemberDetails.kt | 6 +- .../kotlin/platformapi/channelMemberList.kt | 6 +- src/main/kotlin/platformapi/platformApi.kt | 12 +- .../platformApiAccessTokenAuthentication.kt | 43 --- src/main/kotlin/platformapi/userDetails.kt | 6 +- src/main/kotlin/platformapi/userTokenList.kt | 4 +- .../util/GenericTypeConversionService.kt | 33 --- .../util/HoconApplicationConfigExtensions.kt | 33 --- .../util/applicationConfigExtensions.kt | 36 +++ src/main/resources/application.conf | 13 +- .../ClientApiJwtAuthenticationTests.kt | 25 +- .../mutations/ChannelMutationTests.kt | 5 +- .../mutations/MessageMutationTests.kt | 5 +- .../clientapi/mutations/PushMutationTests.kt | 3 +- .../clientapi/queries/ChannelQueryTests.kt | 14 +- .../clientapi/queries/MessageQueryTests.kt | 5 +- .../graphql/GraphQLInstallationTests.kt | 21 +- .../kotlin/platformapi/ChannelDetailsTests.kt | 5 +- .../kotlin/platformapi/ChannelListTests.kt | 3 +- .../platformapi/ChannelMemberDetailsTests.kt | 7 +- .../platformapi/ChannelMemberListTests.kt | 12 +- .../PlatformApiAccessTokenAuthentication.kt | 22 +- .../kotlin/platformapi/UserDetailsTests.kt | 4 +- .../kotlin/platformapi/UserTokenListTest.kt | 5 +- src/test/kotlin/push/PushServiceTests.kt | 5 + src/test/kotlin/testutil/databaseTest.kt | 50 +++- src/test/kotlin/testutil/serverTest.kt | 244 ----------------- .../kotlin/testutil/servertest/asApiError.kt | 7 + .../testutil/servertest/delete/delete.kt | 17 ++ .../servertest/delete/deleteChannel.kt | 13 + .../servertest/delete/deleteMember.kt | 14 + .../testutil/servertest/delete/deleteUser.kt | 12 + .../kotlin/testutil/servertest/ensurers.kt | 44 ++++ .../kotlin/testutil/servertest/get/get.kt | 19 ++ .../testutil/servertest/post/addMember.kt | 17 ++ .../testutil/servertest/post/createChannel.kt | 16 ++ .../servertest/post/createUserToken.kt | 13 + .../kotlin/testutil/servertest/post/post.kt | 31 +++ .../kotlin/testutil/servertest/put/put.kt | 24 ++ .../testutil/servertest/put/setMembers.kt | 16 ++ .../testutil/servertest/put/updateChannel.kt | 16 ++ .../testutil/servertest/put/updateMember.kt | 16 ++ .../testutil/servertest/put/upsertUser.kt | 17 ++ .../kotlin/testutil/servertest/serverTest.kt | 105 ++++++++ .../kotlin/testutil/setupTestDependencies.kt | 5 +- src/test/kotlin/testutil/test.kt | 19 ++ 69 files changed, 932 insertions(+), 842 deletions(-) rename src/main/kotlin/clientapi/{clientApiJwtAuthentication.kt => authentication/jwt/installClientApiJwtAuthentication.kt} (60%) delete mode 100644 src/main/kotlin/logging/LogFeature.kt create mode 100644 src/main/kotlin/logging/LoggingPlugin.kt create mode 100644 src/main/kotlin/platformapi/authentication/accesstoken/PlatformApiAccessTokenAuthenticatonProvider.kt create mode 100644 src/main/kotlin/platformapi/authentication/accesstoken/installPlatformApiAccessTokenAuthentication.kt delete mode 100644 src/main/kotlin/platformapi/platformApiAccessTokenAuthentication.kt delete mode 100644 src/main/kotlin/util/GenericTypeConversionService.kt delete mode 100644 src/main/kotlin/util/HoconApplicationConfigExtensions.kt create mode 100644 src/main/kotlin/util/applicationConfigExtensions.kt delete mode 100644 src/test/kotlin/testutil/serverTest.kt create mode 100644 src/test/kotlin/testutil/servertest/asApiError.kt create mode 100644 src/test/kotlin/testutil/servertest/delete/delete.kt create mode 100644 src/test/kotlin/testutil/servertest/delete/deleteChannel.kt create mode 100644 src/test/kotlin/testutil/servertest/delete/deleteMember.kt create mode 100644 src/test/kotlin/testutil/servertest/delete/deleteUser.kt create mode 100644 src/test/kotlin/testutil/servertest/ensurers.kt create mode 100644 src/test/kotlin/testutil/servertest/get/get.kt create mode 100644 src/test/kotlin/testutil/servertest/post/addMember.kt create mode 100644 src/test/kotlin/testutil/servertest/post/createChannel.kt create mode 100644 src/test/kotlin/testutil/servertest/post/createUserToken.kt create mode 100644 src/test/kotlin/testutil/servertest/post/post.kt create mode 100644 src/test/kotlin/testutil/servertest/put/put.kt create mode 100644 src/test/kotlin/testutil/servertest/put/setMembers.kt create mode 100644 src/test/kotlin/testutil/servertest/put/updateChannel.kt create mode 100644 src/test/kotlin/testutil/servertest/put/updateMember.kt create mode 100644 src/test/kotlin/testutil/servertest/put/upsertUser.kt create mode 100644 src/test/kotlin/testutil/servertest/serverTest.kt create mode 100644 src/test/kotlin/testutil/test.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index c754d27..a961175 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -31,5 +31,10 @@ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 5d98256..f6589e3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 287bd4e..43ecc45 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - val postgresUser = System.getenv("POSTGRES_USER") ?: "postgres" val postgresPassword = System.getenv("POSTGRES_PASSWORD") ?: "postgres" val postgresServerName = System.getenv("POSTGRES_SERVERNAME") ?: "localhost" @@ -7,18 +5,18 @@ val postgresPort = System.getenv("POSTGRES_PORT") ?: "54324" val postgresDb = System.getenv("POSTGRES_DB") ?: "chat-server" val postgresUrl = "jdbc:postgresql://$postgresServerName:$postgresPort/$postgresDb" -val ktorVersion = "1.6.1" -val kotlinVersion = "1.5.21" -val kotlinxCoroutinesVersion = "1.5.1" +val ktorVersion = "2.1.3" +val kotlinVersion = "1.7.21" val postgreSqlJdbcVersion = "42.2.23" val log4jVersion = "2.14.1" val log4jApiKotlinVersion = "1.0.0" val graphQlJavaVersion = "16.2" val graphQlKotlinVersion = "4.1.1" val firebaseAdminVersion = "6.9.0" -val jooqVersion = "3.15.1" +val jooqVersion = "3.17.4" val flywayCoreVersion = "7.11.4" -val kodeinVersion = "7.6.0" +// TODO(saibotma): Remove me when https://github.com/Kodein-Framework/Kodein-DI/issues/410 is resolved. +val kodeinVersion = "8.0.0-ktor-2-SNAPSHOT" val jacksonDataTypeJsr310Version = "2.12.4" val kotestVersion = "4.6.1" val junitJupiterVersion = "5.7.2" @@ -32,10 +30,10 @@ sourceSets["test"].resources.srcDirs("src/test/resources") plugins { application - kotlin("jvm") version "1.5.21" + kotlin("jvm") version "1.7.21" id("com.github.johnrengelman.shadow") version "6.1.0" id("org.flywaydb.flyway") version "7.11.4" - id("nu.studer.jooq") version "6.0" + id("nu.studer.jooq") version "8.0" } application { @@ -47,21 +45,32 @@ version = "0.1.2" repositories { mavenCentral() - jcenter() - maven { url = uri("https://dl.bintray.com/kodein-framework/Kodein-DI/") } + // TODO(saibotma): Remove me when https://github.com/Kodein-Framework/Kodein-DI/issues/410 is resolved. + maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") } dependencies { implementation("org.jetbrains.kotlin", "kotlin-stdlib", kotlinVersion) - implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", kotlinxCoroutinesVersion) - implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-reactive", kotlinxCoroutinesVersion) implementation("io.ktor", "ktor-server-netty", ktorVersion) - implementation("io.ktor:ktor-server-core:$ktorVersion") - implementation("io.ktor", "ktor-jackson", ktorVersion) - implementation("io.ktor", "ktor-locations", ktorVersion) + implementation("io.ktor:ktor-serialization-jackson:$ktorVersion") + implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-data-conversion:$ktorVersion") + implementation("io.ktor:ktor-server-locations:$ktorVersion") + implementation("io.ktor:ktor-server-call-id:$ktorVersion") + implementation("io.ktor:ktor-server-double-receive:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-server-status-pages:$ktorVersion") implementation("io.ktor", "ktor-server-test-host", ktorVersion) - implementation("io.ktor", "ktor-auth-jwt", ktorVersion) + implementation("io.ktor:ktor-server-auth:$ktorVersion") + implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion") + // Out of some reason the plugin returns 403 in case CORS would not be allowed. Related issues: + // https://youtrack.jetbrains.com/issue/KTOR-4237/CORS-the-plugin-responds-with-403-although-specification-doesnt-contain-such-information + // https://youtrack.jetbrains.com/issue/KTOR-4236/CORS-Plugin-should-log-reason-for-returning-403-Forbidden-errors + implementation("io.ktor:ktor-server-cors:$ktorVersion") + + implementation("io.ktor", "ktor-client-core", ktorVersion) + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("org.apache.logging.log4j", "log4j-api", log4jVersion) implementation("org.apache.logging.log4j", "log4j-core", log4jVersion) @@ -154,15 +163,15 @@ flyway { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } -configurations { +configurations.implementation { // We need to remove the logback-classic package // To avoid: Multiple bindings were found on the class path // From: http://www.slf4j.org/codes.html#multiple_bindings - runtime.get().exclude("ch.qos.logback", "logback-classic") + exclude("ch.qos.logback", "logback-classic") } tasks.withType { @@ -173,19 +182,6 @@ tasks.withType { archiveFileName.set("chat-server.jar") } -tasks.withType() { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - - // Disable Kotlin experimental warnings - kotlinOptions.freeCompilerArgs += listOf( - "-Xuse-experimental=kotlin.Experimental", - "-Xuse-experimental=io.ktor.locations.KtorExperimentalLocationsAPI", - "-Xuse-experimental=kotlin.ExperimentalStdlibApi" - ) -} - tasks.named("generateJooq") { dependsOn(tasks.flywayMigrate) inputs.files(fileTree("src/main/resources/db/migration")) @@ -195,7 +191,10 @@ tasks.named("generateJooq") { outputs.cacheIf { true } } - +// Requireded because of https://github.com/gradle/gradle/issues/17236 +tasks.named("processResources") { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} abstract class PrintVersionTask : DefaultTask() { @TaskAction diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt index a3b059e..d47feb1 100644 --- a/src/main/kotlin/Application.kt +++ b/src/main/kotlin/Application.kt @@ -1,40 +1,69 @@ import clientapi.ClientApiConfig +import clientapi.authentication.jwt.installClientApiJwtAuthentication import clientapi.installClientApi -import clientapi.installClientApiJwtAuthentication import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.ObjectMapper +import com.typesafe.config.ConfigFactory import di.setupDi import error.PlatformApiException import flyway.FlywayConfig -import io.ktor.application.* -import io.ktor.auth.* -import io.ktor.features.* import io.ktor.http.* -import io.ktor.jackson.* -import io.ktor.locations.* -import io.ktor.response.* -import io.ktor.routing.* -import logging.Logging +import io.ktor.serialization.jackson.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.config.* +import io.ktor.server.engine.* +import io.ktor.server.locations.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.* +import io.ktor.server.plugins.callid.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.dataconversion.* +import io.ktor.server.plugins.doublereceive.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import logging.LoggingPlugin +import org.apache.logging.log4j.kotlin.logger import org.flywaydb.core.Flyway -import org.kodein.di.DI -import org.kodein.di.direct -import org.kodein.di.instance -import org.kodein.di.ktor.DIFeature -import org.kodein.di.ktor.closestDI import persistence.jooq.KotlinDslContext import platformapi.PlatformApiConfig import platformapi.installPlatformApi -import platformapi.installPlatformApiAccessTokenAuthentication import push.FirebaseInitializer -import util.GenericTypeConversionService import java.time.Instant import java.time.LocalDate import java.util.* -import kotlin.reflect.typeOf +import org.kodein.di.DI +import org.kodein.di.direct +import org.kodein.di.instance +import org.kodein.di.ktor.closestDI +import org.kodein.di.ktor.di +import platformapi.authentication.accesstoken.installPlatformApiAccessTokenAuthentication +import util.serverPort + +fun main() { + // Execute using embedded server because wih automatic module loading + // a second config file without module loading setting would be required + // for tests. This would be a lot of duplicate config properties. + embeddedServer( + Netty, + environment = applicationEngineEnvironment { + config = HoconApplicationConfig(ConfigFactory.load()) + connector { + port = config.serverPort + // Needs to be "0.0.0.0" otherwise it does not work + // with docker and the current port forwarding configuration + // in the docker compose files. + host = "0.0.0.0" + } + module { chatServer() } + } + ).start(wait = true) +} +fun Application.chatServer(di: DI? = null) { + installFeatures(di ?: DI { setupDi(environment.config) }) -fun Application.module(bindDependencies: DI.MainBuilder.() -> Unit = { setupDi() }) { - installFeatures(bindDependencies) val flywayConfig: FlywayConfig by closestDI().instance() val flyway: Flyway by closestDI().instance() // Need to support this in for databases that are not empty, @@ -57,102 +86,90 @@ fun Application.module(bindDependencies: DI.MainBuilder.() -> Unit = { setupDi() } } -private fun Application.installFeatures(bindDependencies: DI.MainBuilder.() -> Unit) { - install(DIFeature) { bindDependencies() } - install(CORS) { - method(HttpMethod.Get) - method(HttpMethod.Post) - header(HttpHeaders.Accept) - header(HttpHeaders.Authorization) - header(HttpHeaders.ContentType) - allowCredentials = true - // TODO(saibotma): Make more strict - anyHost() - } - // Currently only required for the Logging feature - install(CallId) { - // No special reason why this uses this generator - generate(10) - } +private fun Application.installFeatures(di: DI) { + val log = logger() + + di { extend(di) } + + // Currently, only required for the Logging feature + install(CallId) { generate(10, CALL_ID_DEFAULT_DICTIONARY) } install(DoubleReceive) - install(Logging) { - logRequests = true - logResponses = true - logHeaders = true - logBody = false - logFullUrl = true - } - install(Locations) {} + install(Locations) + install(LoggingPlugin) + install(Authentication) { - val platformApiConfig: PlatformApiConfig by closestDI().instance() - val clientApiConfig: ClientApiConfig by closestDI().instance() - val kotlinDslContext: KotlinDslContext by closestDI().instance() + val platformApiConfig: PlatformApiConfig by di.instance() + val clientApiConfig: ClientApiConfig by di.instance() + val kotlinDslContext: KotlinDslContext by di.instance() - installPlatformApiAccessTokenAuthentication(expectedAccessToken = platformApiConfig.accessToken) + installPlatformApiAccessTokenAuthentication(accessToken = platformApiConfig.accessToken) installClientApiJwtAuthentication(jwtSecret = clientApiConfig.jwtSecret, kotlinDslContext = kotlinDslContext) } + install(ContentNegotiation) { - jackson { closestDI().direct.instance Unit>()() } + jackson { di.direct.instance Unit>()() } } install(DataConversion) { convert { - decode { values, _ -> LocalDate.parse(values.first()) } + decode { values -> LocalDate.parse(values.first()) } } convert { - decode { values, _ -> Instant.parse(values.first()) } + decode { values -> Instant.parse(values.first()) } } convert { - decode { values, _ -> UUID.fromString(values.first()) } + decode { values -> UUID.fromString(values.first()) } } - val type = typeOf>() - convert(type, GenericTypeConversionService(type::class).apply { - decode { values, _ -> values.first().split(",").map { UUID.fromString(it) } } - }) + convert> { + decode { values -> values.first().split(",").map { UUID.fromString(it) } } + } } install(StatusPages) { - exception { t -> - when (t) { + exception { call, cause -> + when (cause) { is ContentTransformationException -> { - call.respond( - HttpStatusCode.BadRequest, """ - Request parameters could not be parsed. - ${t.message} - """.trimIndent() + call.respondText( + "Request parameters could not be parsed. ${cause.message}", status = HttpStatusCode.BadRequest, ) } + is ParameterConversionException -> { - call.respond( - HttpStatusCode.BadRequest, - "Parameter \"${t.parameterName}\" could not be parsed.\nPlease check that the type fulfils the specification." + call.respondText( + "Parameter \"${cause.parameterName}\" could not be parsed.\n" + + "Please check that the type fulfils the specification.", + status = HttpStatusCode.BadRequest, ) } + is JsonMappingException -> { - val locationMessage = t.location?.let { + val locationMessage = cause.location?.let { "Error at line ${it.lineNr} and column ${it.columnNr}" } - call.respond( - HttpStatusCode.BadRequest, - """ - Json body could not be parsed. - Please check that the values fulfil the specification. - ${locationMessage ?: ""} - """.trimIndent() + call.respondText( + "Json body could not be parsed. " + + "Please check that the values fulfil the specification ${locationMessage ?: ""}", + status = HttpStatusCode.BadRequest, ) } + is com.fasterxml.jackson.core.JsonParseException -> { - call.respond(HttpStatusCode.BadRequest, "Json body could not be parsed.\n${t.originalMessage}") + call.respondText( + "Json body could not be parsed.\n${cause.originalMessage}", + status = HttpStatusCode.BadRequest + ) } + is PlatformApiException -> { - call.respond(t.statusCode, t.error) + call.respond(cause.statusCode, cause.error) } + else -> { call.respond(HttpStatusCode.InternalServerError) - throw t + log.error("Unhandled exception", cause) } } } diff --git a/src/main/kotlin/clientapi/AuthContext.kt b/src/main/kotlin/clientapi/AuthContext.kt index 2443582..a2a15ab 100644 --- a/src/main/kotlin/clientapi/AuthContext.kt +++ b/src/main/kotlin/clientapi/AuthContext.kt @@ -1,6 +1,6 @@ package clientapi import com.expediagroup.graphql.generator.execution.GraphQLContext -import io.ktor.auth.* +import io.ktor.server.auth.* class AuthContext(val userId: String) : GraphQLContext, Principal diff --git a/src/main/kotlin/clientapi/clientApiJwtAuthentication.kt b/src/main/kotlin/clientapi/authentication/jwt/installClientApiJwtAuthentication.kt similarity index 60% rename from src/main/kotlin/clientapi/clientApiJwtAuthentication.kt rename to src/main/kotlin/clientapi/authentication/jwt/installClientApiJwtAuthentication.kt index da74ab6..0723e19 100644 --- a/src/main/kotlin/clientapi/clientApiJwtAuthentication.kt +++ b/src/main/kotlin/clientapi/authentication/jwt/installClientApiJwtAuthentication.kt @@ -1,24 +1,20 @@ -package clientapi +package clientapi.authentication.jwt +import clientapi.AuthContext import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.ktor.auth.* -import io.ktor.auth.jwt.* -import io.ktor.routing.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.getUser -private const val configurationName = "ClientApiJwtAuthentication" +const val clientApiJwtAuthentication = "clientApiJwtAuthentication" -fun Route.clientApiJwtAuthenticate(build: Route.() -> Unit) { - authenticate(configurationName, build = build) -} - -fun Authentication.Configuration.installClientApiJwtAuthentication( +fun AuthenticationConfig.installClientApiJwtAuthentication( jwtSecret: String, kotlinDslContext: KotlinDslContext, ) { - jwt(configurationName) { + jwt(clientApiJwtAuthentication) { verifier( JWT .require(Algorithm.HMAC256(jwtSecret)) diff --git a/src/main/kotlin/clientapi/installGraphQl.kt b/src/main/kotlin/clientapi/installGraphQl.kt index 0d02e73..7fcfb28 100644 --- a/src/main/kotlin/clientapi/installGraphQl.kt +++ b/src/main/kotlin/clientapi/installGraphQl.kt @@ -1,17 +1,14 @@ package clientapi +import clientapi.authentication.jwt.clientApiJwtAuthentication import com.expediagroup.graphql.server.execution.GraphQLServer import com.fasterxml.jackson.databind.ObjectMapper -import graphql.ExecutionInput -import graphql.GraphQL -import graphql.KtorGraphQLServer -import io.ktor.application.* -import io.ktor.auth.* import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import org.kodein.di.instance import org.kodein.di.ktor.closestDI @@ -23,7 +20,7 @@ fun Route.installClientApi() { // remove the authentication block. // installGraphQlPlayground() - clientApiJwtAuthenticate { + authenticate(clientApiJwtAuthentication) { post("/graphql") { // Execute the query against the schema val result = graphQLServer.execute(call.request) diff --git a/src/main/kotlin/di/graphQlDi.kt b/src/main/kotlin/di/graphQlDi.kt index 36be2fc..c20e7bc 100644 --- a/src/main/kotlin/di/graphQlDi.kt +++ b/src/main/kotlin/di/graphQlDi.kt @@ -17,7 +17,7 @@ import graphql.* import graphql.GraphQL.newGraphQL import graphql.execution.DataFetcherExceptionHandler import graphql.schema.GraphQLSchema -import io.ktor.request.* +import io.ktor.server.request.* import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.instance diff --git a/src/main/kotlin/di/postgresDi.kt b/src/main/kotlin/di/postgresDi.kt index b50714a..14ded31 100644 --- a/src/main/kotlin/di/postgresDi.kt +++ b/src/main/kotlin/di/postgresDi.kt @@ -1,7 +1,7 @@ package di import flyway.FlywayConfig -import io.ktor.config.* +import io.ktor.server.config.* import org.flywaydb.core.Flyway import org.jooq.DSLContext import org.jooq.SQLDialect @@ -16,12 +16,12 @@ import javax.sql.DataSource val postgresDi = DI.Module("postgres") { bind() with singleton { - val hocon: HoconApplicationConfig by di.instance() + val hocon: ApplicationConfig by di.instance() FlywayConfig(baselineVersion = hocon.flywayBaselineVersion, shouldBaseline = hocon.flywayShouldBaseline) } bind() with singleton { - val hocon: HoconApplicationConfig by di.instance() + val hocon: ApplicationConfig by di.instance() PostgresConfig( user = hocon.postgresUser, password = hocon.postgresPassword, diff --git a/src/main/kotlin/di/pushDi.kt b/src/main/kotlin/di/pushDi.kt index 6a65abb..66b103b 100644 --- a/src/main/kotlin/di/pushDi.kt +++ b/src/main/kotlin/di/pushDi.kt @@ -2,8 +2,7 @@ package di import com.google.auth.oauth2.GoogleCredentials import com.google.firebase.FirebaseOptions -import io.ktor.config.* -import io.ktor.util.* +import io.ktor.server.config.* import org.kodein.di.* import push.FirebaseInitializer import push.PushNotificationSender @@ -25,7 +24,7 @@ val pushDi = DI.Module("push") { } bind>() with singleton { - val config: HoconApplicationConfig by di.instance() + val config: ApplicationConfig by di.instance() val firebaseCredentials = config.firebaseCredentials ?: return@singleton Optional.empty() val credentials = GoogleCredentials.fromStream( diff --git a/src/main/kotlin/di/setup.kt b/src/main/kotlin/di/setup.kt index 1e1c1b4..5f84b95 100644 --- a/src/main/kotlin/di/setup.kt +++ b/src/main/kotlin/di/setup.kt @@ -1,8 +1,12 @@ package di +import io.ktor.server.config.* import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.singleton -fun DI.MainBuilder.setupDi() { +fun DI.MainBuilder.setupDi(config: ApplicationConfig) { + bind() with singleton { config } import(jacksonDi) import(utilDi) import(postgresDi) diff --git a/src/main/kotlin/di/utilDi.kt b/src/main/kotlin/di/utilDi.kt index 6cc7205..b6a7f09 100644 --- a/src/main/kotlin/di/utilDi.kt +++ b/src/main/kotlin/di/utilDi.kt @@ -3,7 +3,7 @@ package di import clientapi.ClientApiConfig import platformapi.PlatformApiConfig import com.typesafe.config.ConfigFactory -import io.ktor.config.* +import io.ktor.server.config.* import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.instance @@ -12,13 +12,12 @@ import util.clientApiJwtSecret import util.platformApiAccessToken val utilDi = DI.Module("util") { - bind() with singleton { HoconApplicationConfig(ConfigFactory.load()) } bind() with singleton { - val config: HoconApplicationConfig = instance() + val config: ApplicationConfig = instance() PlatformApiConfig(accessToken = config.platformApiAccessToken) } bind() with singleton { - val config: HoconApplicationConfig = instance() + val config: ApplicationConfig = instance() ClientApiConfig(jwtSecret = config.clientApiJwtSecret) } } diff --git a/src/main/kotlin/graphql/KtorGraphQLContextFactory.kt b/src/main/kotlin/graphql/KtorGraphQLContextFactory.kt index 23343c1..c2fdafb 100644 --- a/src/main/kotlin/graphql/KtorGraphQLContextFactory.kt +++ b/src/main/kotlin/graphql/KtorGraphQLContextFactory.kt @@ -2,8 +2,8 @@ package graphql import clientapi.AuthContext import com.expediagroup.graphql.server.execution.GraphQLContextFactory -import io.ktor.auth.* -import io.ktor.request.* +import io.ktor.server.auth.* +import io.ktor.server.request.* class KtorGraphQLContextFactory : GraphQLContextFactory { override suspend fun generateContext(request: ApplicationRequest): AuthContext? = request.call.principal() diff --git a/src/main/kotlin/graphql/KtorGraphQLRequestParser.kt b/src/main/kotlin/graphql/KtorGraphQLRequestParser.kt index 06dae7c..c0d842c 100644 --- a/src/main/kotlin/graphql/KtorGraphQLRequestParser.kt +++ b/src/main/kotlin/graphql/KtorGraphQLRequestParser.kt @@ -2,8 +2,7 @@ package graphql import com.expediagroup.graphql.server.execution.GraphQLRequestParser import com.expediagroup.graphql.server.types.GraphQLServerRequest -import com.fasterxml.jackson.databind.ObjectMapper -import io.ktor.request.* +import io.ktor.server.request.* class KtorGraphQLRequestParser : GraphQLRequestParser { diff --git a/src/main/kotlin/graphql/KtorGraphQLServer.kt b/src/main/kotlin/graphql/KtorGraphQLServer.kt index d3825c1..98b9ef6 100644 --- a/src/main/kotlin/graphql/KtorGraphQLServer.kt +++ b/src/main/kotlin/graphql/KtorGraphQLServer.kt @@ -5,7 +5,7 @@ import com.expediagroup.graphql.server.execution.GraphQLContextFactory import com.expediagroup.graphql.server.execution.GraphQLRequestHandler import com.expediagroup.graphql.server.execution.GraphQLRequestParser import com.expediagroup.graphql.server.execution.GraphQLServer -import io.ktor.request.* +import io.ktor.server.request.* class KtorGraphQLServer( requestParser: GraphQLRequestParser, diff --git a/src/main/kotlin/logging/LogFeature.kt b/src/main/kotlin/logging/LogFeature.kt deleted file mode 100644 index cef8bf0..0000000 --- a/src/main/kotlin/logging/LogFeature.kt +++ /dev/null @@ -1,246 +0,0 @@ -package logging - -import io.ktor.application.* -import io.ktor.features.* -import io.ktor.http.content.* -import io.ktor.request.* -import io.ktor.routing.* -import io.ktor.routing.Routing.Feature.RoutingCallStarted -import io.ktor.util.* -import io.ktor.util.pipeline.* -import org.apache.logging.log4j.kotlin.KotlinLogger -import org.apache.logging.log4j.kotlin.logger - -// TODO: This is a slightly adjusted clone of https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt #173 - -/** - * Logging feature. Allows logging performance, requests and responses. - */ -@KtorExperimentalAPI -open class Logging(config: Configuration) { - - protected open val filters: List<(ApplicationCall) -> Boolean> = config.filters - protected open val logRequests = config.logRequests - protected open val logResponses = config.logResponses - protected open val logFullUrl = config.logFullUrl - protected open val logHeaders = config.logHeaders - protected open val logBody = config.logBody - - private val log = config.logger ?: logger() - /** - * Logging feature config. - */ - open class Configuration { - internal val filters = mutableListOf<(ApplicationCall) -> Boolean>() - - /** - * Custom logger object. - */ - var logger: KotlinLogger? = null - - /** - * Whether to log request. - * - * WARN: request logs may contain sensitive data. - */ - var logRequests = false - - /** - * Whether to log responses. - * - * WARN: responses logs may contain sensitive data. - */ - var logResponses = false - - /** - * Whether to log full request/response urls. - * - * WARN: url queries may contain sensitive data. - */ - var logFullUrl = false - - /** - * Whether to log request/response headers. - * - * WARN: headers may contain sensitive data. - */ - var logHeaders = false - - /** - * Whether to log request/response payloads. - * - * WARN: payloads may contain sensitive data. - */ - var logBody = false - - /** - * Custom request filter. Logs only if any filter returns true. - */ - fun filter(predicate: (ApplicationCall) -> Boolean) { - filters.add(predicate) - } - - /** - * Filter requests by path prefixes. Logs only for given paths. - */ - fun filterPath(vararg paths: String) = filter { call -> - val requestPath = call.request.path().removePrefix(call.application.environment.rootPath) - - paths.any { requestPath == it || requestPath.startsWith(it) } - } - } - - protected open fun logPerformance(call: ApplicationCall) { - val duration = System.currentTimeMillis() - call.attributes[startTimeKey] - val status = call.response.status()?.value - val method = call.request.httpMethod.value - val requestURI = if (logFullUrl) call.request.origin.uri else call.request.path() - val url = call.request.origin.run { "$scheme://$host:$port$requestURI" } - - log.info("$duration ms - $status - $method $url") - } - - protected open suspend fun logRequest(call: ApplicationCall) { - log.info(StringBuilder().apply { - appendLine("Received request:") - appendLine("Call id: ${call.callId}") - val requestURI = if (logFullUrl) call.request.origin.uri else call.request.path() - appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" }) - - if (logHeaders) { - call.request.headers.forEach { header, values -> - appendLine("$header: ${values.firstOrNull()}") - } - } - - if (logBody) { - try { - // new line before body as in HTTP request - appendLine("Body:") - // have to receive ByteArray for DoubleReceive to work - // new line after body because in the log there might be additional info after "log message" - appendLine(String(call.receive())) - } catch (e: RequestAlreadyConsumedException) { - log.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e) - } - } - }.toString()) - } - - protected open fun logResponse(call: ApplicationCall, subject: Any) { - log.info(StringBuilder().apply { - appendLine("Sent response:") - appendLine("Call id: ${call.callId}") - appendLine("${call.request.httpVersion} ${call.response.status()}") - if (logHeaders) { - call.response.headers.allValues().forEach { header, values -> - appendLine("$header: ${values.firstOrNull()}") - } - } - if (logBody && subject is OutgoingContent.ByteArrayContent) { - // new line before body as in HTTP response - appendLine("Body:") - // new line after body because in the log there might be additional info after "log message" - appendLine(String(subject.bytes())) - } - // do not log warning if subject is not OutgoingContent.ByteArrayContent - // as we could possibly spam warnings without any option to disable them - }.toString()) - } - - protected open fun shouldLog(call: ApplicationCall): Boolean { - return filters.isEmpty() || filters.any { it(call) } - } - - /** - * Feature installation. - */ - protected open fun install(pipeline: Application) { - pipeline.featureOrNull(CallId) ?: throw IllegalStateException("Logging requires CallId feature to be installed") - - pipeline.environment.monitor.subscribe(RoutingCallStarted) { - it.attributes.computeIfAbsent(routeKey) { it.route } - } - pipeline.environment.monitor.subscribe(ApplicationStopped) { - log.info("Server stopped") - } - - pipeline.insertPhaseBefore(CallId.phase, startTimePhase) - pipeline.intercept(startTimePhase) { - call.attributes.put(startTimeKey, System.currentTimeMillis()) - } - - /*pipeline.intercept(CallId.phase) { - withCorrelation(call.callId!!) { - proceed() - log.debug("Finished call") - } - }*/ - - pipeline.sendPipeline.addPhase(responseLoggingPhase) - pipeline.sendPipeline.intercept(responseLoggingPhase) { - if (shouldLog(call)) { - logPerformance(call) - } - } - - if (logRequests || logResponses) { - if (logBody && pipeline.featureOrNull(DoubleReceive) == null) { - throw IllegalStateException("Logging payloads requires DoubleReceive feature to be installed") - } - if (!logBody && !logHeaders && !logFullUrl) { - log.warn("You have enabled logging of requests/responses but body, full url and headers logging is disabled and there is no information gain") - } - } - - if (logRequests) { - pipeline.intercept(ApplicationCallPipeline.Monitoring) { - if (shouldLog(call)) { - logRequest(call) - } - } - } - - if (logResponses) { - pipeline.sendPipeline.intercept(responseLoggingPhase) { - if (shouldLog(call)) { - logResponse(call, subject) - } - } - } - } - - /** - * Feature installation. - */ - companion object Feature : ApplicationFeature { - - override val key = AttributeKey("Logging Feature") - - /** - * Attribute key mapping to request duration start timestamp. - */ - val startTimeKey = AttributeKey("Start Time") - - /** - * Attribute key mapping to matched [Route]. - */ - val routeKey = AttributeKey("Route") - - /** - * Phase when request duration starts counting. - */ - val startTimePhase = PipelinePhase("StartTime") - - /** - * Phase when response is logged. - */ - val responseLoggingPhase = PipelinePhase("ResponseLogging") - - override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging { - val configuration = Configuration().apply(configure) - - return Logging(configuration).apply { install(pipeline) } - } - } -} diff --git a/src/main/kotlin/logging/LoggingPlugin.kt b/src/main/kotlin/logging/LoggingPlugin.kt new file mode 100644 index 0000000..ad2df4a --- /dev/null +++ b/src/main/kotlin/logging/LoggingPlugin.kt @@ -0,0 +1,98 @@ +package logging + +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.plugins.callid.callId +import io.ktor.server.plugins.origin +import io.ktor.server.request.RequestAlreadyConsumedException +import io.ktor.server.request.httpMethod +import io.ktor.server.request.httpVersion +import io.ktor.server.request.path +import io.ktor.server.request.receive +import io.ktor.util.AttributeKey +import org.apache.logging.log4j.kotlin.logger + +private const val name = "LoggingPlugin" +val LoggingPlugin = createApplicationPlugin(name = name, createConfiguration = ::LoggingPluginConfig) { + val log = logger(name) + val startTimeKey = AttributeKey("startTime") + + val shouldLogHeaders = pluginConfig.shouldLogHeaders + val shouldLogBody = pluginConfig.shouldLogBody + val shouldLogFullUrl = pluginConfig.shouldLogFullUrl + + onCall { call -> + call.attributes.put(startTimeKey, System.currentTimeMillis()) + log.info( + StringBuilder().apply { + appendLine("Received request:") + appendLine("Call id: ${call.callId}") + val requestURI = if (shouldLogFullUrl) call.request.origin.uri else call.request.path() + appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" }) + + if (shouldLogHeaders) { + call.request.headers.forEach { header, values -> + appendLine("$header: ${values.firstOrNull()}") + } + } + + if (shouldLogBody) { + try { + // new line before body as in HTTP request + appendLine() + // have to receive ByteArray for DoubleReceive to work + // new line after body because in the log there might be additional info after "log message" + appendLine(String(call.receive())) + } catch (e: RequestAlreadyConsumedException) { + log.error( + "Logging payloads requires DoubleReceive feature " + + "to be installed with receiveEntireContent=true", + e + ) + } + } + }.toString() + ) + } + + onCallRespond { call -> + val startTime = call.attributes.getOrNull(startTimeKey) + if (startTime != null) { + val duration = System.currentTimeMillis() - startTime + val method = call.request.httpMethod.value + val requestURI = if (shouldLogFullUrl) call.request.origin.uri else call.request.path() + val url = call.request.origin.run { "$scheme://$host:$port$requestURI" } + + log.info("$duration ms - ${call.callId} - $method $url") + } + + transformBody { + log.info( + StringBuilder().apply { + appendLine("Sent response:") + appendLine("Call id: ${call.callId}") + appendLine("${call.request.httpVersion} ${call.response.status()}") + if (shouldLogHeaders) { + call.response.headers.allValues().forEach { header, values -> + appendLine("$header: ${values.firstOrNull()}") + } + } + if (shouldLogBody) { + // new line before body as in HTTP response + appendLine() + // new line after body because in the log there might be additional info after "log message" + appendLine(it.toString()) + } + // do not log warning if subject is not OutgoingContent.ByteArrayContent + // as we could possibly spam warnings without any option to disable them + }.toString() + ) + it + } + } +} + +class LoggingPluginConfig( + var shouldLogHeaders: Boolean = true, + var shouldLogBody: Boolean = true, + var shouldLogFullUrl: Boolean = true, +) diff --git a/src/main/kotlin/platformapi/authentication/accesstoken/PlatformApiAccessTokenAuthenticatonProvider.kt b/src/main/kotlin/platformapi/authentication/accesstoken/PlatformApiAccessTokenAuthenticatonProvider.kt new file mode 100644 index 0000000..297ca2a --- /dev/null +++ b/src/main/kotlin/platformapi/authentication/accesstoken/PlatformApiAccessTokenAuthenticatonProvider.kt @@ -0,0 +1,38 @@ +package platformapi.authentication.accesstoken + +import io.ktor.http.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +const val platformApiAccessTokenAuthentication = "platformApiAccessTokenAuthentication" + + +object PlatformApiAccessTokenPrincipal : Principal + +class PlatformApiAccessTokenAuthenticationProvider(config: Configuration) : AuthenticationProvider(config) { + private val accessToken = config.accessToken + + class Configuration(val accessToken: String) : Config(name = platformApiAccessTokenAuthentication) + + override suspend fun onAuthenticate(context: AuthenticationContext) { + val call = context.call + val actualAccessToken = call.request.headers["X-Chat-Server-Platform-Api-Access-Token"] + val cause = when { + actualAccessToken == null -> AuthenticationFailedCause.NoCredentials + actualAccessToken != accessToken -> AuthenticationFailedCause.InvalidCredentials + else -> null + } + + if (cause != null) { + @Suppress("NAME_SHADOWING") + context.challenge(platformApiAccessTokenAuthentication, cause) { challenge, call -> + call.respond(HttpStatusCode.Unauthorized) + challenge.complete() + } + return + } + + context.principal(PlatformApiAccessTokenPrincipal) + } +} diff --git a/src/main/kotlin/platformapi/authentication/accesstoken/installPlatformApiAccessTokenAuthentication.kt b/src/main/kotlin/platformapi/authentication/accesstoken/installPlatformApiAccessTokenAuthentication.kt new file mode 100644 index 0000000..c8c8cd5 --- /dev/null +++ b/src/main/kotlin/platformapi/authentication/accesstoken/installPlatformApiAccessTokenAuthentication.kt @@ -0,0 +1,9 @@ +package platformapi.authentication.accesstoken + +import io.ktor.server.auth.* + +fun AuthenticationConfig.installPlatformApiAccessTokenAuthentication(accessToken: String) { + val config = PlatformApiAccessTokenAuthenticationProvider.Configuration(accessToken = accessToken) + val provider = PlatformApiAccessTokenAuthenticationProvider(config) + register(provider) +} diff --git a/src/main/kotlin/platformapi/channelDetails.kt b/src/main/kotlin/platformapi/channelDetails.kt index b8f70b2..e214d0b 100644 --- a/src/main/kotlin/platformapi/channelDetails.kt +++ b/src/main/kotlin/platformapi/channelDetails.kt @@ -1,9 +1,9 @@ package platformapi -import io.ktor.application.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.* diff --git a/src/main/kotlin/platformapi/channelList.kt b/src/main/kotlin/platformapi/channelList.kt index 1caa419..4cb759c 100644 --- a/src/main/kotlin/platformapi/channelList.kt +++ b/src/main/kotlin/platformapi/channelList.kt @@ -1,9 +1,9 @@ package platformapi -import io.ktor.application.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.* diff --git a/src/main/kotlin/platformapi/channelMemberDetails.kt b/src/main/kotlin/platformapi/channelMemberDetails.kt index 11b6009..0a7fb01 100644 --- a/src/main/kotlin/platformapi/channelMemberDetails.kt +++ b/src/main/kotlin/platformapi/channelMemberDetails.kt @@ -1,9 +1,9 @@ package platformapi -import io.ktor.application.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.deleteMember diff --git a/src/main/kotlin/platformapi/channelMemberList.kt b/src/main/kotlin/platformapi/channelMemberList.kt index efc33eb..3581818 100644 --- a/src/main/kotlin/platformapi/channelMemberList.kt +++ b/src/main/kotlin/platformapi/channelMemberList.kt @@ -4,10 +4,10 @@ import persistence.jooq.enums.ChannelMemberRole import error.PlatformApiException import error.managedChannelHasAdmin import error.resourceNotFound -import io.ktor.application.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.* diff --git a/src/main/kotlin/platformapi/platformApi.kt b/src/main/kotlin/platformapi/platformApi.kt index e58c690..7f85067 100644 --- a/src/main/kotlin/platformapi/platformApi.kt +++ b/src/main/kotlin/platformapi/platformApi.kt @@ -1,13 +1,15 @@ package platformapi import clientapi.ClientApiConfig -import io.ktor.locations.* -import io.ktor.locations.post -import io.ktor.locations.put -import io.ktor.routing.* +import io.ktor.server.auth.* +import io.ktor.server.locations.* +import io.ktor.server.locations.post +import io.ktor.server.locations.put +import io.ktor.server.routing.* import org.kodein.di.instance import org.kodein.di.ktor.closestDI import persistence.jooq.KotlinDslContext +import platformapi.authentication.accesstoken.platformApiAccessTokenAuthentication import java.util.* @Location("/channels") @@ -32,7 +34,7 @@ object UserList { } fun Route.installPlatformApi() { - platformApiAccessTokenAuthenticate { + authenticate(platformApiAccessTokenAuthentication) { val database: KotlinDslContext by closestDI().instance() val clientApiConfig: ClientApiConfig by closestDI().instance() diff --git a/src/main/kotlin/platformapi/platformApiAccessTokenAuthentication.kt b/src/main/kotlin/platformapi/platformApiAccessTokenAuthentication.kt deleted file mode 100644 index fa88ed3..0000000 --- a/src/main/kotlin/platformapi/platformApiAccessTokenAuthentication.kt +++ /dev/null @@ -1,43 +0,0 @@ -package platformapi - -import io.ktor.application.* -import io.ktor.auth.* -import io.ktor.http.* -import io.ktor.response.* -import io.ktor.routing.* - -private const val configurationName = "PlatformApiAccessTokenAuthentication" - -class PlatformApiAccessTokenAuthenticationProvider(config: Configuration) : AuthenticationProvider(config) { - class Configuration : AuthenticationProvider.Configuration(name = configurationName) -} - -fun Route.platformApiAccessTokenAuthenticate(build: Route.() -> Unit) { - authenticate(configurationName, build = build) -} - -fun Authentication.Configuration.installPlatformApiAccessTokenAuthentication(expectedAccessToken: String) { - val config = PlatformApiAccessTokenAuthenticationProvider.Configuration() - val provider = PlatformApiAccessTokenAuthenticationProvider(config) - - provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context -> - val actualAccessToken = call.request.headers["X-Chat-Server-Platform-Api-Access-Token"] - val validatedApiKey = if (actualAccessToken == expectedAccessToken) actualAccessToken else null - val cause = when { - actualAccessToken == null -> AuthenticationFailedCause.NoCredentials - validatedApiKey == null -> AuthenticationFailedCause.InvalidCredentials - else -> null - } - - if (cause != null) { - context.challenge(configurationName, cause) { - call.respond(HttpStatusCode.Unauthorized) - it.complete() - } - } - } - - register(provider) -} - - diff --git a/src/main/kotlin/platformapi/userDetails.kt b/src/main/kotlin/platformapi/userDetails.kt index d083226..d549f5a 100644 --- a/src/main/kotlin/platformapi/userDetails.kt +++ b/src/main/kotlin/platformapi/userDetails.kt @@ -1,9 +1,9 @@ package platformapi -import io.ktor.application.* import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import persistence.postgres.queries.deleteUser diff --git a/src/main/kotlin/platformapi/userTokenList.kt b/src/main/kotlin/platformapi/userTokenList.kt index 6e4c406..7461011 100644 --- a/src/main/kotlin/platformapi/userTokenList.kt +++ b/src/main/kotlin/platformapi/userTokenList.kt @@ -4,9 +4,9 @@ import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import error.PlatformApiException import error.resourceNotFound -import io.ktor.application.* import io.ktor.http.* -import io.ktor.response.* +import io.ktor.server.application.* +import io.ktor.server.response.* import io.ktor.util.pipeline.* import persistence.jooq.KotlinDslContext import models.UserToken diff --git a/src/main/kotlin/util/GenericTypeConversionService.kt b/src/main/kotlin/util/GenericTypeConversionService.kt deleted file mode 100644 index 08cbede..0000000 --- a/src/main/kotlin/util/GenericTypeConversionService.kt +++ /dev/null @@ -1,33 +0,0 @@ -package util - -import io.ktor.util.* -import java.lang.reflect.Type -import kotlin.reflect.KClass - -/** - * Serves as a workaround because the default conversion services of the conversion feature - * can not parse generic types. - */ -class GenericTypeConversionService(private val klass: KClass<*>) : ConversionService { - private var decoder: ((values: List, type: Type) -> Any?)? = null - private var encoder: ((value: Any?) -> List)? = null - - /** - * Configure decoder function. Only one decoder could be supplied - * @throws IllegalStateException - */ - fun decode(converter: (values: List, type: Type) -> Any?) { - if (decoder != null) throw IllegalStateException("Decoder has already been set for type '$klass'") - decoder = converter - } - - override fun fromValues(values: List, type: Type): Any? { - val decoder = decoder ?: throw DataConversionException("Decoder was not specified for class '$klass'") - return decoder(values, type) - } - - override fun toValues(value: Any?): List { - val encoder = encoder ?: throw DataConversionException("Encoder was not specified for class '$klass'") - return encoder(value) - } -} diff --git a/src/main/kotlin/util/HoconApplicationConfigExtensions.kt b/src/main/kotlin/util/HoconApplicationConfigExtensions.kt deleted file mode 100644 index 0c7b0fc..0000000 --- a/src/main/kotlin/util/HoconApplicationConfigExtensions.kt +++ /dev/null @@ -1,33 +0,0 @@ -package util - -import io.ktor.config.* - -val HoconApplicationConfig.postgresUser: String - get() = property("postgres.user").getString() - -val HoconApplicationConfig.postgresPassword: String - get() = property("postgres.password").getString() - -val HoconApplicationConfig.postgresServerName: String - get() = property("postgres.serverName").getString() - -val HoconApplicationConfig.postgresPort: Int - get() = property("postgres.port").getString().toInt() - -val HoconApplicationConfig.postgresDb: String - get() = property("postgres.db").getString() - -val HoconApplicationConfig.platformApiAccessToken: String - get() = property("platformApi.accessToken").getString() - -val HoconApplicationConfig.clientApiJwtSecret: String - get() = property("clientApi.jwtSecret").getString() - -val HoconApplicationConfig.firebaseCredentials: String? - get() = propertyOrNull("firebase.credentials")?.getString() - -val HoconApplicationConfig.flywayBaselineVersion: String - get() = property("flyway.baselineVersion").getString() - -val HoconApplicationConfig.flywayShouldBaseline: Boolean - get() = property("flyway.shouldBaseline").getString().toBoolean() diff --git a/src/main/kotlin/util/applicationConfigExtensions.kt b/src/main/kotlin/util/applicationConfigExtensions.kt new file mode 100644 index 0000000..c8d7328 --- /dev/null +++ b/src/main/kotlin/util/applicationConfigExtensions.kt @@ -0,0 +1,36 @@ +package util + +import io.ktor.server.config.* + +val ApplicationConfig.serverPort: Int + get() = property("server.port").getString().toInt() + +val ApplicationConfig.postgresUser: String + get() = property("postgres.user").getString() + +val ApplicationConfig.postgresPassword: String + get() = property("postgres.password").getString() + +val ApplicationConfig.postgresServerName: String + get() = property("postgres.serverName").getString() + +val ApplicationConfig.postgresPort: Int + get() = property("postgres.port").getString().toInt() + +val ApplicationConfig.postgresDb: String + get() = property("postgres.db").getString() + +val ApplicationConfig.platformApiAccessToken: String + get() = property("platformApi.accessToken").getString() + +val ApplicationConfig.clientApiJwtSecret: String + get() = property("clientApi.jwtSecret").getString() + +val ApplicationConfig.firebaseCredentials: String? + get() = propertyOrNull("firebase.credentials")?.getString() + +val ApplicationConfig.flywayBaselineVersion: String + get() = property("flyway.baselineVersion").getString() + +val ApplicationConfig.flywayShouldBaseline: Boolean + get() = property("flyway.shouldBaseline").getString().toBoolean() diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 0025bf8..7272da2 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,11 +1,8 @@ -ktor { - deployment { - port = 8080 - port = ${?KTOR_DEPLOYMENT_PORT} - } - application { - modules = [ApplicationKt.module] - } +server { + # The port that the ktor server is running on. + # Ktor interprets 0 as random port. This is only useful for development. + port = "8080" + port = ${?SERVER_PORT} } postgres { diff --git a/src/test/kotlin/clientapi/ClientApiJwtAuthenticationTests.kt b/src/test/kotlin/clientapi/ClientApiJwtAuthenticationTests.kt index bc67777..0a6f0c4 100644 --- a/src/test/kotlin/clientapi/ClientApiJwtAuthenticationTests.kt +++ b/src/test/kotlin/clientapi/ClientApiJwtAuthenticationTests.kt @@ -4,12 +4,15 @@ import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.kotest.matchers.shouldBe +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.server.testing.* import org.junit.jupiter.api.Test import org.kodein.di.instance -import testutil.ServerTestEnvironment -import testutil.serverTest +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.post.createUserToken +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest import java.time.Instant.now import java.util.* @@ -19,7 +22,7 @@ class ClientApiJwtAuthenticationTests { serverTest { val (_, user) = upsertUser() val response = sendRequest(createUserToken(userId = user!!.id).second!!.jwt) - response.status() shouldBe HttpStatusCode.OK + response.status shouldBe HttpStatusCode.OK } } @@ -30,7 +33,7 @@ class ClientApiJwtAuthenticationTests { val response = sendRequest( JWT.create().withSubject("invalid subject").sign(Algorithm.HMAC256(clientApiConfig.jwtSecret)) ) - response.status() shouldBe HttpStatusCode.Unauthorized + response.status shouldBe HttpStatusCode.Unauthorized } } @@ -43,17 +46,17 @@ class ClientApiJwtAuthenticationTests { JWT.create().withSubject(user!!.id).withExpiresAt(Date.from(now().minusSeconds(60))) .sign(Algorithm.HMAC256(clientApiConfig.jwtSecret)) ) - response.status() shouldBe HttpStatusCode.Unauthorized + response.status shouldBe HttpStatusCode.Unauthorized } } - private fun ServerTestEnvironment.sendRequest(jwt: String): TestApplicationResponse { - return testApplicationEngine.handleRequest(HttpMethod.Post, "/client/graphql") { + private suspend fun ServerTestEnvironment.sendRequest(jwt: String): HttpResponse { + return client.post("/client/graphql") { val objectMapper = jacksonObjectMapper() - addHeader("Content-Type", "application/json") - addHeader("Authorization", "Bearer $jwt") + header("Content-Type", "application/json") + header("Authorization", "Bearer $jwt") val body = objectMapper.writeValueAsString(mapOf("query" to "{ channels { id } }")) setBody(body) - }.response + } } } diff --git a/src/test/kotlin/clientapi/mutations/ChannelMutationTests.kt b/src/test/kotlin/clientapi/mutations/ChannelMutationTests.kt index 4f90790..4d68873 100644 --- a/src/test/kotlin/clientapi/mutations/ChannelMutationTests.kt +++ b/src/test/kotlin/clientapi/mutations/ChannelMutationTests.kt @@ -15,7 +15,10 @@ import models.toChannelMemberWrite import testutil.mockedAuthContext import testutil.mockedChannelMember import testutil.mockedChannelWrite -import testutil.serverTest +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class ChannelMutationTests { @Nested diff --git a/src/test/kotlin/clientapi/mutations/MessageMutationTests.kt b/src/test/kotlin/clientapi/mutations/MessageMutationTests.kt index 22b3932..76ed8ea 100644 --- a/src/test/kotlin/clientapi/mutations/MessageMutationTests.kt +++ b/src/test/kotlin/clientapi/mutations/MessageMutationTests.kt @@ -16,7 +16,10 @@ import org.junit.jupiter.api.Test import testutil.mockedAuthContext import testutil.mockedChannelMember import testutil.mockedMessage -import testutil.serverTest +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class MessageMutationTests { @Nested diff --git a/src/test/kotlin/clientapi/mutations/PushMutationTests.kt b/src/test/kotlin/clientapi/mutations/PushMutationTests.kt index 4d149d3..33837cc 100644 --- a/src/test/kotlin/clientapi/mutations/PushMutationTests.kt +++ b/src/test/kotlin/clientapi/mutations/PushMutationTests.kt @@ -5,7 +5,8 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import persistence.jooq.tables.pojos.FirebasePushToken import testutil.mockedAuthContext -import testutil.serverTest +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class PushMutationTests { @Nested diff --git a/src/test/kotlin/clientapi/queries/ChannelQueryTests.kt b/src/test/kotlin/clientapi/queries/ChannelQueryTests.kt index 842642e..a8929fd 100644 --- a/src/test/kotlin/clientapi/queries/ChannelQueryTests.kt +++ b/src/test/kotlin/clientapi/queries/ChannelQueryTests.kt @@ -3,21 +3,15 @@ package clientapi.queries import clientapi.models.DetailedMessageReadPayload import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import models.* -import org.jooq.JSON -import org.jooq.impl.DSL -import org.jooq.impl.DSL.value import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import persistence.jooq.jsonArrayAggNoNull -import persistence.jooq.nowInstant -import persistence.jooq.tables.references.CHANNEL -import persistence.postgres.queries.selectMessagesOf import testutil.mockedAuthContext import testutil.mockedChannelMember import testutil.mockedMessage -import testutil.serverTest -import java.time.Instant -import java.time.Instant.now +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class ChannelQueryTests { @Nested diff --git a/src/test/kotlin/clientapi/queries/MessageQueryTests.kt b/src/test/kotlin/clientapi/queries/MessageQueryTests.kt index ea6aeaf..b54d731 100644 --- a/src/test/kotlin/clientapi/queries/MessageQueryTests.kt +++ b/src/test/kotlin/clientapi/queries/MessageQueryTests.kt @@ -16,7 +16,10 @@ import org.junit.jupiter.api.Test import testutil.mockedAuthContext import testutil.mockedChannelMember import testutil.mockedMessage -import testutil.serverTest +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest import java.time.Instant import java.time.Instant.now import java.util.* diff --git a/src/test/kotlin/graphql/GraphQLInstallationTests.kt b/src/test/kotlin/graphql/GraphQLInstallationTests.kt index f2fbadc..7bdce61 100644 --- a/src/test/kotlin/graphql/GraphQLInstallationTests.kt +++ b/src/test/kotlin/graphql/GraphQLInstallationTests.kt @@ -4,11 +4,16 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import io.kotest.matchers.maps.shouldNotContainKey import io.kotest.matchers.shouldBe +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.server.testing.* import org.junit.jupiter.api.Test import testutil.mockedChannelMember -import testutil.serverTest +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.post.createUserToken +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class GraphQLInstallationTests { @@ -19,15 +24,15 @@ class GraphQLInstallationTests { val (_, user) = upsertUser() val (_, channel) = createChannel() addMember(channelId = channel!!.id, mockedChannelMember(userId = user!!.id)) - val response = testApplicationEngine.handleRequest(HttpMethod.Post, "/client/graphql") { - addHeader("Content-Type", "application/json") - addHeader("Authorization", "Bearer ${createUserToken(user.id).second!!.jwt}") + val response = client.post("/client/graphql") { + header("Content-Type", "application/json") + header("Authorization", "Bearer ${createUserToken(user.id).second!!.jwt}") val body = objectMapper.writeValueAsString(mapOf("query" to "{ channels { id } }")) setBody(body) - }.response + } - response.status() shouldBe HttpStatusCode.OK - val json = objectMapper.readValue>(response.content!!) + response.status shouldBe HttpStatusCode.OK + val json = objectMapper.readValue>(response.bodyAsText()) json shouldNotContainKey "errors" } } diff --git a/src/test/kotlin/platformapi/ChannelDetailsTests.kt b/src/test/kotlin/platformapi/ChannelDetailsTests.kt index aeff0c4..1bcb62b 100644 --- a/src/test/kotlin/platformapi/ChannelDetailsTests.kt +++ b/src/test/kotlin/platformapi/ChannelDetailsTests.kt @@ -6,7 +6,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import models.toChannelRead import testutil.mockedChannelWrite -import testutil.serverTest +import testutil.servertest.delete.deleteChannel +import testutil.servertest.post.createChannel +import testutil.servertest.put.updateChannel +import testutil.servertest.serverTest class ChannelDetailsTests { @Nested diff --git a/src/test/kotlin/platformapi/ChannelListTests.kt b/src/test/kotlin/platformapi/ChannelListTests.kt index 1c09061..5c18f43 100644 --- a/src/test/kotlin/platformapi/ChannelListTests.kt +++ b/src/test/kotlin/platformapi/ChannelListTests.kt @@ -8,7 +8,8 @@ import models.ChannelReadPayload import models.ChannelWritePayload import models.toChannelRead import testutil.mockedChannelWrite -import testutil.serverTest +import testutil.servertest.post.createChannel +import testutil.servertest.serverTest class ChannelListTests { @Nested diff --git a/src/test/kotlin/platformapi/ChannelMemberDetailsTests.kt b/src/test/kotlin/platformapi/ChannelMemberDetailsTests.kt index dfa009e..f4ea854 100644 --- a/src/test/kotlin/platformapi/ChannelMemberDetailsTests.kt +++ b/src/test/kotlin/platformapi/ChannelMemberDetailsTests.kt @@ -8,7 +8,12 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import models.toChannelMemberWrite import testutil.mockedChannelMember -import testutil.serverTest +import testutil.servertest.delete.deleteMember +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.updateMember +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class ChannelMemberDetailsTests { @Nested diff --git a/src/test/kotlin/platformapi/ChannelMemberListTests.kt b/src/test/kotlin/platformapi/ChannelMemberListTests.kt index ccf1bbb..c54be77 100644 --- a/src/test/kotlin/platformapi/ChannelMemberListTests.kt +++ b/src/test/kotlin/platformapi/ChannelMemberListTests.kt @@ -14,7 +14,13 @@ import models.ChannelMemberWritePayload import models.toChannelMemberRead import testutil.mockedChannelMember import testutil.mockedChannelWrite -import testutil.serverTest +import testutil.servertest.asApiError +import testutil.servertest.ensureBadRequestWithDuplicate +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.setMembers +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class ChannelMemberListTests { @Nested @@ -47,7 +53,7 @@ class ChannelMemberListTests { channelId = channel!!.id, member = mockedChannelMember(userId = user!!.id, role = ChannelMemberRole.admin) ) { _, _ -> - status() shouldBe HttpStatusCode.BadRequest + status shouldBe HttpStatusCode.BadRequest asApiError() shouldBe PlatformApiException.managedChannelHasAdmin().error } } @@ -112,7 +118,7 @@ class ChannelMemberListTests { channelId = channel!!.id, members = listOf(mockedChannelMember(userId = user!!.id, role = ChannelMemberRole.admin)) ) { _, _ -> - status() shouldBe HttpStatusCode.BadRequest + status shouldBe HttpStatusCode.BadRequest asApiError() shouldBe PlatformApiException.managedChannelHasAdmin().error } } diff --git a/src/test/kotlin/platformapi/PlatformApiAccessTokenAuthentication.kt b/src/test/kotlin/platformapi/PlatformApiAccessTokenAuthentication.kt index 7bbafb1..307d6fc 100644 --- a/src/test/kotlin/platformapi/PlatformApiAccessTokenAuthentication.kt +++ b/src/test/kotlin/platformapi/PlatformApiAccessTokenAuthentication.kt @@ -2,14 +2,14 @@ package platformapi import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.kotest.matchers.shouldBe +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.server.testing.* import org.junit.jupiter.api.Test import org.kodein.di.instance -import testutil.ServerTestEnvironment import testutil.mockedUser -import testutil.serverTest -import java.util.* +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.serverTest import java.util.UUID.randomUUID class PlatformApiAccessTokenAuthentication { @@ -18,7 +18,7 @@ class PlatformApiAccessTokenAuthentication { serverTest { val platformApiConfig: PlatformApiConfig by di.instance() val response = sendRequest(platformApiConfig.accessToken) - response.status() shouldBe HttpStatusCode.Created + response.status shouldBe HttpStatusCode.Created } } @@ -26,19 +26,19 @@ class PlatformApiAccessTokenAuthentication { fun `returns an error when the token is invalid`() { serverTest { val response = sendRequest("invalid token") - response.status() shouldBe HttpStatusCode.Unauthorized + response.status shouldBe HttpStatusCode.Unauthorized } } - private fun ServerTestEnvironment.sendRequest(token: String): TestApplicationResponse { + private suspend fun ServerTestEnvironment.sendRequest(token: String): HttpResponse { val userId = randomUUID() val user = mockedUser() - return testApplicationEngine.handleRequest(HttpMethod.Put, "/platform/users/$userId") { + return client.put("/platform/users/$userId") { val objectMapper = jacksonObjectMapper() - addHeader("Content-Type", "application/json") - addHeader("X-Chat-Server-Platform-Api-Access-Token", token) + header("Content-Type", "application/json") + header("X-Chat-Server-Platform-Api-Access-Token", token) val body = objectMapper.writeValueAsString(user) setBody(body) - }.response + } } } diff --git a/src/test/kotlin/platformapi/UserDetailsTests.kt b/src/test/kotlin/platformapi/UserDetailsTests.kt index 6378527..78b2cc7 100644 --- a/src/test/kotlin/platformapi/UserDetailsTests.kt +++ b/src/test/kotlin/platformapi/UserDetailsTests.kt @@ -9,7 +9,9 @@ import models.UserReadPayload import models.UserWritePayload import models.toUserRead import testutil.mockedUser -import testutil.serverTest +import testutil.servertest.delete.deleteUser +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class UserDetailsTests { @Nested diff --git a/src/test/kotlin/platformapi/UserTokenListTest.kt b/src/test/kotlin/platformapi/UserTokenListTest.kt index e1b9ffe..e021525 100644 --- a/src/test/kotlin/platformapi/UserTokenListTest.kt +++ b/src/test/kotlin/platformapi/UserTokenListTest.kt @@ -11,7 +11,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.kodein.di.direct import org.kodein.di.instance -import testutil.serverTest +import testutil.servertest.ensureResourceNotFound +import testutil.servertest.post.createUserToken +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest class UserTokenListTest { @Nested diff --git a/src/test/kotlin/push/PushServiceTests.kt b/src/test/kotlin/push/PushServiceTests.kt index 20bf6e5..f921a22 100644 --- a/src/test/kotlin/push/PushServiceTests.kt +++ b/src/test/kotlin/push/PushServiceTests.kt @@ -10,6 +10,11 @@ import org.kodein.di.instance import org.kodein.di.singleton import persistence.jooq.tables.pojos.FirebasePushToken import testutil.* +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.post.addMember +import testutil.servertest.post.createChannel +import testutil.servertest.put.upsertUser +import testutil.servertest.serverTest import java.util.* class PushServiceTests { diff --git a/src/test/kotlin/testutil/databaseTest.kt b/src/test/kotlin/testutil/databaseTest.kt index e24bd01..f223191 100644 --- a/src/test/kotlin/testutil/databaseTest.kt +++ b/src/test/kotlin/testutil/databaseTest.kt @@ -6,27 +6,41 @@ import persistence.jooq.tables.references.CHANNEL_MEMBER import persistence.jooq.tables.references.MESSAGE import di.setupDi import kotlinx.coroutines.runBlocking +import org.flywaydb.core.Flyway +import org.jooq.DSLContext +import org.jooq.exception.DataAccessException +import org.jooq.impl.DSL import org.kodein.di.DI import org.kodein.di.instance import persistence.jooq.KotlinDslContext import persistence.jooq.tables.pojos.* import persistence.jooq.tables.references.FIREBASE_PUSH_TOKEN +import testutil.servertest.BindDependencies fun databaseTest( - bindDependencies: DI.MainBuilder.() -> Unit = {}, + useTransaction: Boolean = true, + bindDependencies: BindDependencies = {}, test: suspend DatabaseTestEnvironment.() -> Unit ) { - val kodein = DI { - setupDi() + val di = DI { setupTestDependencies() bindDependencies() } - val environment = DatabaseTestEnvironment(kodein) - runBlocking { test(environment) } + val flyway: Flyway by di.instance() + val environment = DatabaseTestEnvironment(di) + + handleCleanUp(flyway) + + if (useTransaction) { + environment.scopedTest(test) + } else { + runBlocking { test(environment) } + restoreDatabase(flyway) + } } -open class DatabaseTestEnvironment(private val di: DI) { +open class DatabaseTestEnvironment(val di: DI) { val database: KotlinDslContext by di.instance() suspend fun getChannels(): List { @@ -58,4 +72,28 @@ open class DatabaseTestEnvironment(private val di: DI) { db.selectFrom(FIREBASE_PUSH_TOKEN).fetchInto(FirebasePushToken::class.java) } } + + // region helpers + + @JvmName("scopedTestDatabase") + fun scopedTest(block: suspend DatabaseTestEnvironment.() -> Unit) { + return scopedTest(this, block) + } + + protected fun scopedTest(environment: T, block: suspend T.() -> Unit) { + val dslContext: DSLContext by di.instance() + + try { + dslContext.transaction { config -> + val kotlinDslContext: KotlinDslContext by di.instance() + kotlinDslContext.overrideDSLContext = DSL.using(config) + runBlocking { environment.block() } + throw TestRollbackException() + } + } catch (e: DataAccessException) { + if (e.cause !is TestRollbackException) throw e + } + } + + // endregion } diff --git a/src/test/kotlin/testutil/serverTest.kt b/src/test/kotlin/testutil/serverTest.kt deleted file mode 100644 index 077bb5d..0000000 --- a/src/test/kotlin/testutil/serverTest.kt +++ /dev/null @@ -1,244 +0,0 @@ -package testutil - -import clientapi.mutations.ChannelMutation -import clientapi.mutations.MessageMutation -import clientapi.mutations.PushMutation -import clientapi.queries.ChannelQuery -import clientapi.queries.MessageQuery -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import di.setupDi -import error.PlatformApiError -import error.PlatformApiException -import error.duplicate -import error.resourceNotFound -import io.kotest.matchers.collections.shouldBeIn -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlinx.coroutines.runBlocking -import models.* -import module -import org.jooq.DSLContext -import org.jooq.exception.DataAccessException -import org.jooq.impl.DSL -import org.kodein.di.* -import org.kodein.di.ktor.closestDI -import persistence.jooq.KotlinDslContext -import platformapi.PlatformApiConfig -import java.util.* -import java.util.UUID.randomUUID - -class TestRollbackException : Exception() - -fun serverTest( - bindDependencies: DI.MainBuilder.() -> Unit = {}, - test: suspend ServerTestEnvironment.() -> Unit -) { - withTestApplication({ - module { - setupDi() - setupTestDependencies() - bindDependencies() - } - }) { - val di = application.closestDI() - val database: DSLContext by di.instance() - try { - database.transaction { config -> - val kotlinDslContext: KotlinDslContext by di.instance() - kotlinDslContext.overrideDSLContext = DSL.using(config) - val environment = ServerTestEnvironment(this) - runBlocking { test(environment) } - throw TestRollbackException() - } - } catch (e: DataAccessException) { - if (e.cause !is TestRollbackException) throw e - } - } - -} - -class ServerTestEnvironment(val testApplicationEngine: TestApplicationEngine) : - DatabaseTestEnvironment(testApplicationEngine.application.closestDI()) { - val di = testApplicationEngine.application.closestDI() - val objectMapper: ObjectMapper by di.instance() - - val channelQuery: ChannelQuery by di.instance() - val messageQuery: MessageQuery by di.instance() - val channelMutation: ChannelMutation by di.instance() - val messageMutation: MessageMutation by di.instance() - val pushMutation: PushMutation by di.instance() - - fun createChannel( - channel: ChannelWritePayload = mockedChannelWrite(), - response: TestApplicationResponse.(ChannelWritePayload, ChannelReadPayload?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair = post(channel, "/platform/channels", response) - - fun updateChannel( - id: UUID, - channel: ChannelWritePayload, - response: TestApplicationResponse.(ChannelWritePayload, ChannelReadPayload?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair { - return put(channel, "/platform/channels/$id", response) - } - - fun deleteChannel( - id: UUID, - response: TestApplicationResponse.() -> Unit = { ensureNoContent() } - ) = delete(path = "/platform/channels/$id", response) - - fun addMember( - channelId: UUID, - member: ChannelMemberWritePayload, - response: TestApplicationResponse.(ChannelMemberWritePayload, ChannelMemberReadPayload?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair { - return post(member, "/platform/channels/$channelId/members", response) - } - - fun updateMember( - channelId: UUID, - member: ChannelMemberWritePayload, - response: TestApplicationResponse.(ChannelMemberWritePayload, ChannelMemberReadPayload?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair { - return put(member, "/platform/channels/$channelId/members/${member.userId}", response) - } - - fun deleteMember( - channelId: UUID, - userId: String, - response: TestApplicationResponse.() -> Unit = { ensureNoContent() } - ) = delete(path = "/platform/channels/$channelId/members/$userId", response) - - fun setMembers( - channelId: UUID, - members: List, - response: TestApplicationResponse.(List, List?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair, List?> { - return put(members, "/platform/channels/$channelId/members", response) - } - - fun upsertUser( - id: String = randomUUID().toString(), - user: UserWritePayload = mockedUser(), - response: TestApplicationResponse.(UserWritePayload, UserReadPayload?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair { - return put(user, "/platform/users/$id", response) - } - - fun createUserToken( - userId: String, - response: TestApplicationResponse.(Unit, UserToken?) -> Unit = { _, _ -> ensureSuccess() } - ): Pair { - return post(Unit, "/platform/users/$userId/tokens", response) - } - - fun deleteUser( - id: String, - response: TestApplicationResponse.() -> Unit = { ensureNoContent() } - ) = delete(path = "/platform/users/$id", response) - - inline fun post( - resource: T, - path: String, - response: TestApplicationResponse.(T, R?) -> Unit - ): Pair { - var result: R? - testApplicationEngine.handleRequest(HttpMethod.Post, path) { - addApiKeyAuthentication() - setJsonBody(resource, objectMapper) - }.response.apply { - result = - if (status()?.isSuccessWithContent() == true) content?.let { objectMapper.readValue(it) } else null - response(resource, result) - } - return resource to result - } - - inline fun put( - resource: T, - path: String, - response: TestApplicationResponse.(T, R?) -> Unit - ): Pair { - var result: R? - testApplicationEngine.handleRequest(HttpMethod.Put, path) { - addApiKeyAuthentication() - setJsonBody(resource, objectMapper) - }.response.apply { - result = - if (status()?.isSuccessWithContent() == true) content?.let { objectMapper.readValue(it) } else null - response(resource, result) - } - return resource to result - } - - inline fun get( - path: String, - response: TestApplicationResponse.(T?) -> Unit - ) { - testApplicationEngine.handleRequest(HttpMethod.Get, path) { - addApiKeyAuthentication() - }.response.apply { - response(if (status()?.isSuccess() == true) content?.let { objectMapper.readValue(it) } else null) - } - } - - fun delete( - path: String, - response: TestApplicationResponse.() -> Unit - ) { - testApplicationEngine.handleRequest(HttpMethod.Delete, path) { - addApiKeyAuthentication() - }.response.apply { response() } - } - - fun TestApplicationRequest.addApiKeyAuthentication() { - val platformApiConfig: PlatformApiConfig by closestDI { this.call.application }.instance() - addHeader("X-Chat-Server-Platform-Api-Access-Token", platformApiConfig.accessToken) - } - - fun TestApplicationRequest.setJsonBody(body: T, objectMapper: ObjectMapper) { - addHeader("Content-Type", "application/json") - setBody(body.asString(objectMapper)) - } - - private fun T.asString(objectMapper: ObjectMapper): String { - return objectMapper.writeValueAsString(this) - } - - fun TestApplicationResponse.ensureSuccess() { - status() shouldBeIn listOf(HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created) - } - - fun TestApplicationResponse.ensureNoContent() { - content shouldBe null - status() shouldBe HttpStatusCode.NoContent - } - - fun TestApplicationResponse.ensureBadRequestWithDuplicate( - duplicatePropertyName: String, - duplicatePropertyValue: String - ) { - val expectedError = PlatformApiException.duplicate(duplicatePropertyName, duplicatePropertyValue).error - asApiError() shouldBe expectedError - - status() shouldBe HttpStatusCode.BadRequest - } - - fun TestApplicationResponse.ensureHasContent() { - content shouldNotBe null - status() shouldBe HttpStatusCode.OK - } - - fun TestApplicationResponse.ensureResourceNotFound() { - asApiError() shouldBe PlatformApiException.resourceNotFound().error - status() shouldBe HttpStatusCode.NotFound - } - - fun TestApplicationResponse.asApiError(): PlatformApiError { - return objectMapper.readValue(content!!) - } - - fun HttpStatusCode.isSuccessWithContent() = this == HttpStatusCode.OK || this == HttpStatusCode.Created -} diff --git a/src/test/kotlin/testutil/servertest/asApiError.kt b/src/test/kotlin/testutil/servertest/asApiError.kt new file mode 100644 index 0000000..bd89cfb --- /dev/null +++ b/src/test/kotlin/testutil/servertest/asApiError.kt @@ -0,0 +1,7 @@ +package testutil.servertest + +import error.PlatformApiError +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse + +suspend fun HttpResponse.asApiError(): PlatformApiError = body() diff --git a/src/test/kotlin/testutil/servertest/delete/delete.kt b/src/test/kotlin/testutil/servertest/delete/delete.kt new file mode 100644 index 0000000..2b7b365 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/delete/delete.kt @@ -0,0 +1,17 @@ +package testutil.servertest.delete + +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.delete +import io.ktor.client.statement.HttpResponse +import io.ktor.http.URLProtocol +import testutil.servertest.ServerTestEnvironment + +suspend fun ServerTestEnvironment.delete( + path: String, + handleResponse: suspend HttpResponse.() -> Unit, +) { + val response = client.delete(path) { + addApiKeyAuthentication() + } + response.handleResponse() +} diff --git a/src/test/kotlin/testutil/servertest/delete/deleteChannel.kt b/src/test/kotlin/testutil/servertest/delete/deleteChannel.kt new file mode 100644 index 0000000..c3c1804 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/delete/deleteChannel.kt @@ -0,0 +1,13 @@ +package testutil.servertest.delete + +import io.ktor.client.statement.* +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureNoContent +import java.util.* + +suspend fun ServerTestEnvironment.deleteChannel( + id: UUID, + handleResponse: suspend HttpResponse.() -> Unit = { ensureNoContent() } +) { + delete(path = "/platform/channels/$id", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/delete/deleteMember.kt b/src/test/kotlin/testutil/servertest/delete/deleteMember.kt new file mode 100644 index 0000000..90a512b --- /dev/null +++ b/src/test/kotlin/testutil/servertest/delete/deleteMember.kt @@ -0,0 +1,14 @@ +package testutil.servertest.delete + +import io.ktor.client.statement.* +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureNoContent +import java.util.* + +suspend fun ServerTestEnvironment.deleteMember( + channelId: UUID, + userId: String, + response: suspend HttpResponse.() -> Unit = { ensureNoContent() } +) { + delete(path = "/platform/channels/$channelId/members/$userId", handleResponse = response) +} diff --git a/src/test/kotlin/testutil/servertest/delete/deleteUser.kt b/src/test/kotlin/testutil/servertest/delete/deleteUser.kt new file mode 100644 index 0000000..360ff71 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/delete/deleteUser.kt @@ -0,0 +1,12 @@ +package testutil.servertest.delete + +import io.ktor.client.statement.* +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureNoContent + +suspend fun ServerTestEnvironment.deleteUser( + id: String, + handleResponse: suspend HttpResponse.() -> Unit = { ensureNoContent() }, +) { + delete(path = "/platform/users/$id", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/ensurers.kt b/src/test/kotlin/testutil/servertest/ensurers.kt new file mode 100644 index 0000000..f4ab7ee --- /dev/null +++ b/src/test/kotlin/testutil/servertest/ensurers.kt @@ -0,0 +1,44 @@ +package testutil.servertest + +import error.PlatformApiException +import error.duplicate +import error.resourceNotFound +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.ktor.client.statement.* +import io.ktor.http.* + +fun HttpResponse.ensureSuccess() { + status shouldBeIn listOf(HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created) +} + +fun HttpResponse.ensureRedirect(targetUrl: String) { + status shouldBe HttpStatusCode.Found + headers["Location"] shouldBe targetUrl +} + +suspend fun HttpResponse.ensureNoContent() { + status shouldBe HttpStatusCode.NoContent + bodyAsText() shouldBe "" +} + +suspend fun HttpResponse.ensureBadRequestWithDuplicate( + duplicatePropertyName: String, + duplicatePropertyValue: String, +) { + val expectedError = PlatformApiException.duplicate(duplicatePropertyName, duplicatePropertyValue).error + asApiError() shouldBe expectedError + + status shouldBe HttpStatusCode.BadRequest +} + +suspend fun HttpResponse.ensureHasContent() { + status shouldBe HttpStatusCode.OK + bodyAsText() shouldNotBe "" +} + +suspend fun HttpResponse.ensureResourceNotFound() { + asApiError() shouldBe PlatformApiException.resourceNotFound().error + status shouldBe HttpStatusCode.NotFound +} diff --git a/src/test/kotlin/testutil/servertest/get/get.kt b/src/test/kotlin/testutil/servertest/get/get.kt new file mode 100644 index 0000000..0fc48a3 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/get/get.kt @@ -0,0 +1,19 @@ +package testutil.servertest.get + +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess +import testutil.servertest.ServerTestEnvironment + +suspend inline fun ServerTestEnvironment.get( + path: String, + handleResponse: suspend HttpResponse.(T?) -> Unit, +): T? { + val response = client.get(path) { + addApiKeyAuthentication() + } + val result = if (response.status.isSuccess()) response.body() else null + response.handleResponse(result) + return result +} diff --git a/src/test/kotlin/testutil/servertest/post/addMember.kt b/src/test/kotlin/testutil/servertest/post/addMember.kt new file mode 100644 index 0000000..dda3ed2 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/post/addMember.kt @@ -0,0 +1,17 @@ +package testutil.servertest.post + +import io.ktor.client.statement.* +import io.ktor.server.testing.* +import models.ChannelMemberReadPayload +import models.ChannelMemberWritePayload +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess +import java.util.* + +suspend fun ServerTestEnvironment.addMember( + channelId: UUID, + member: ChannelMemberWritePayload, + handleResponse: suspend HttpResponse.(ChannelMemberWritePayload, ChannelMemberReadPayload?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return post(member, "/platform/channels/$channelId/members", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/post/createChannel.kt b/src/test/kotlin/testutil/servertest/post/createChannel.kt new file mode 100644 index 0000000..ca1d08d --- /dev/null +++ b/src/test/kotlin/testutil/servertest/post/createChannel.kt @@ -0,0 +1,16 @@ +package testutil.servertest.post + +import io.ktor.client.statement.* +import models.ChannelReadPayload +import models.ChannelWritePayload +import testutil.mockedChannelWrite +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess + +suspend fun ServerTestEnvironment.createChannel( + channel: ChannelWritePayload = mockedChannelWrite(), + handleResponse: suspend HttpResponse.(ChannelWritePayload, ChannelReadPayload?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return post(channel, "/platform/channels", handleResponse = handleResponse) +} + diff --git a/src/test/kotlin/testutil/servertest/post/createUserToken.kt b/src/test/kotlin/testutil/servertest/post/createUserToken.kt new file mode 100644 index 0000000..556ea98 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/post/createUserToken.kt @@ -0,0 +1,13 @@ +package testutil.servertest.post + +import io.ktor.client.statement.* +import models.UserToken +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess + +suspend fun ServerTestEnvironment.createUserToken( + userId: String, + handleResponse: suspend HttpResponse.(Unit, UserToken?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return post(Unit, "/platform/users/$userId/tokens", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/post/post.kt b/src/test/kotlin/testutil/servertest/post/post.kt new file mode 100644 index 0000000..41938d1 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/post/post.kt @@ -0,0 +1,31 @@ +package testutil.servertest.post + +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.URLProtocol +import io.ktor.http.contentType +import testutil.servertest.ServerTestEnvironment + +suspend inline fun ServerTestEnvironment.post( + resource: T, + path: String, + urlProtocol: URLProtocol = URLProtocol.HTTP, + handleResponse: suspend HttpResponse.(T, R?) -> Unit, +): Pair { + val response = client.post(path) { + url { + protocol = urlProtocol + } + contentType(ContentType.Application.Json) + addApiKeyAuthentication() + setBody(resource) + } + + val result = + if (response.status.isSuccessWithContent()) response.body() else null + response.handleResponse(resource, result) + return resource to result +} diff --git a/src/test/kotlin/testutil/servertest/put/put.kt b/src/test/kotlin/testutil/servertest/put/put.kt new file mode 100644 index 0000000..13732b4 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/put/put.kt @@ -0,0 +1,24 @@ +package testutil.servertest.put + +import io.ktor.client.call.body +import io.ktor.client.request.put +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.contentType +import testutil.servertest.ServerTestEnvironment + +suspend inline fun ServerTestEnvironment.put( + resource: T, + path: String, + handleResponse: suspend HttpResponse.(T, R?) -> Unit, +): Pair { + val response = client.put(path) { + contentType(ContentType.Application.Json) + addApiKeyAuthentication() + setBody(resource) + } + val result = if (response.status.isSuccessWithContent()) response.body() else null + response.handleResponse(resource, result) + return resource to result +} diff --git a/src/test/kotlin/testutil/servertest/put/setMembers.kt b/src/test/kotlin/testutil/servertest/put/setMembers.kt new file mode 100644 index 0000000..f3a18bc --- /dev/null +++ b/src/test/kotlin/testutil/servertest/put/setMembers.kt @@ -0,0 +1,16 @@ +package testutil.servertest.put + +import io.ktor.client.statement.* +import models.ChannelMemberReadPayload +import models.ChannelMemberWritePayload +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess +import java.util.* + +suspend fun ServerTestEnvironment.setMembers( + channelId: UUID, + members: List, + handleResponse: suspend HttpResponse.(List, List?) -> Unit = { _, _ -> ensureSuccess() } +): Pair, List?> { + return put(members, "/platform/channels/$channelId/members", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/put/updateChannel.kt b/src/test/kotlin/testutil/servertest/put/updateChannel.kt new file mode 100644 index 0000000..642743a --- /dev/null +++ b/src/test/kotlin/testutil/servertest/put/updateChannel.kt @@ -0,0 +1,16 @@ +package testutil.servertest.put + +import io.ktor.client.statement.* +import models.ChannelReadPayload +import models.ChannelWritePayload +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess +import java.util.* + +suspend fun ServerTestEnvironment.updateChannel( + id: UUID, + channel: ChannelWritePayload, + handleResponse: suspend HttpResponse.(ChannelWritePayload, ChannelReadPayload?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return put(channel, "/platform/channels/$id", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/put/updateMember.kt b/src/test/kotlin/testutil/servertest/put/updateMember.kt new file mode 100644 index 0000000..93c1d94 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/put/updateMember.kt @@ -0,0 +1,16 @@ +package testutil.servertest.put + +import io.ktor.client.statement.* +import models.ChannelMemberReadPayload +import models.ChannelMemberWritePayload +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess +import java.util.* + +suspend fun ServerTestEnvironment.updateMember( + channelId: UUID, + member: ChannelMemberWritePayload, + handleResponse: suspend HttpResponse.(ChannelMemberWritePayload, ChannelMemberReadPayload?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return put(member, "/platform/channels/$channelId/members/${member.userId}", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/put/upsertUser.kt b/src/test/kotlin/testutil/servertest/put/upsertUser.kt new file mode 100644 index 0000000..afd7384 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/put/upsertUser.kt @@ -0,0 +1,17 @@ +package testutil.servertest.put + +import io.ktor.client.statement.* +import models.UserReadPayload +import models.UserWritePayload +import testutil.mockedUser +import testutil.servertest.ServerTestEnvironment +import testutil.servertest.ensureSuccess +import java.util.* + +suspend fun ServerTestEnvironment.upsertUser( + id: String = UUID.randomUUID().toString(), + user: UserWritePayload = mockedUser(), + handleResponse: suspend HttpResponse.(UserWritePayload, UserReadPayload?) -> Unit = { _, _ -> ensureSuccess() } +): Pair { + return put(user, "/platform/users/$id", handleResponse = handleResponse) +} diff --git a/src/test/kotlin/testutil/servertest/serverTest.kt b/src/test/kotlin/testutil/servertest/serverTest.kt new file mode 100644 index 0000000..e27b274 --- /dev/null +++ b/src/test/kotlin/testutil/servertest/serverTest.kt @@ -0,0 +1,105 @@ +package testutil.servertest + +import chatServer +import clientapi.mutations.ChannelMutation +import clientapi.mutations.MessageMutation +import clientapi.mutations.PushMutation +import clientapi.queries.ChannelQuery +import clientapi.queries.MessageQuery +import com.fasterxml.jackson.databind.ObjectMapper +import io.ktor.client.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.jackson.* +import io.ktor.server.config.* +import io.ktor.server.testing.* +import kotlinx.coroutines.runBlocking +import models.* +import org.flywaydb.core.Flyway +import org.kodein.di.* +import platformapi.PlatformApiConfig +import testutil.DatabaseTestEnvironment +import testutil.handleCleanUp +import testutil.restoreDatabase +import testutil.setupTestDependencies +import java.util.* + +typealias ServerTestResponse = HttpResponse +typealias BindDependencies = DI.MainBuilder.() -> Unit + +fun serverTest( + shouldUseTransaction: Boolean = true, + bindDependencies: BindDependencies = {}, + test: suspend ServerTestEnvironment.() -> Unit +) { + testApplication { + // Need to use the test configuration because the config should not load modules because we do that + // by hand in application block below. + val config = ApplicationConfig(configPath = null) + val di = DI { + setupTestDependencies(config) + bindDependencies() + } + + environment { this.config = config } + + application { chatServer(di = di) } + + val client = createClient { + // TODO(saibotma): Why does this not work? It works in Appella.. maybe reset ide? + install(ContentNegotiation) { + jackson { di.direct.instance Unit>()() } + } + followRedirects = false + } + + val flyway: Flyway by di.instance() + val environment = ServerTestEnvironment(client = client, di = di) + + handleCleanUp(flyway) + + if (shouldUseTransaction) { + environment.scopedTest(test) + } else { + try { + runBlocking { test(environment) } + } finally { + restoreDatabase(flyway) + } + } + } +} + +class ServerTestEnvironment(val client: HttpClient, di: DI) : + DatabaseTestEnvironment(di = di) { + private val platformApiConfig: PlatformApiConfig by di.instance() + val objectMapper: ObjectMapper by di.instance() + + val channelQuery: ChannelQuery by di.instance() + val messageQuery: MessageQuery by di.instance() + val channelMutation: ChannelMutation by di.instance() + val messageMutation: MessageMutation by di.instance() + val pushMutation: PushMutation by di.instance() + + fun HttpRequestBuilder.addApiKeyAuthentication() { + header("X-Chat-Server-Platform-Api-Access-Token", platformApiConfig.accessToken) + } + + fun TestApplicationRequest.setJsonBody(body: T, objectMapper: ObjectMapper) { + addHeader("Content-Type", "application/json") + setBody(body.asString(objectMapper)) + } + + private fun T.asString(objectMapper: ObjectMapper): String { + return objectMapper.writeValueAsString(this) + } + + fun HttpStatusCode.isSuccessWithContent() = this == HttpStatusCode.OK || this == HttpStatusCode.Created + + @JvmName("scopedTestServer") + fun scopedTest(block: suspend ServerTestEnvironment.() -> Unit) { + return scopedTest(this, block) + } +} diff --git a/src/test/kotlin/testutil/setupTestDependencies.kt b/src/test/kotlin/testutil/setupTestDependencies.kt index 85fb054..db85825 100644 --- a/src/test/kotlin/testutil/setupTestDependencies.kt +++ b/src/test/kotlin/testutil/setupTestDependencies.kt @@ -1,5 +1,7 @@ package testutil +import di.setupDi +import io.ktor.server.config.* import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.singleton @@ -7,7 +9,8 @@ import push.FirebaseInitializer import push.PushNotificationSender import java.util.* -fun DI.MainBuilder.setupTestDependencies() { +fun DI.MainBuilder.setupTestDependencies(config: ApplicationConfig = ApplicationConfig(configPath = null)) { + setupDi(config) bind>(overrides = true) with singleton { Optional.of(mockedFirebaseInitializer()) } bind>(overrides = true) with singleton { Optional.of(mockedPushNotificationSender()) } } diff --git a/src/test/kotlin/testutil/test.kt b/src/test/kotlin/testutil/test.kt new file mode 100644 index 0000000..cf722c8 --- /dev/null +++ b/src/test/kotlin/testutil/test.kt @@ -0,0 +1,19 @@ +package testutil + +import org.flywaydb.core.Flyway + +class TestRollbackException : Exception() + +var didCleanUp = false + +fun handleCleanUp(flyway: Flyway) { + if (!didCleanUp) { + restoreDatabase(flyway) + didCleanUp = true + } +} + +fun restoreDatabase(flyway: Flyway) { + flyway.clean() + flyway.migrate() +} From 62d4e34fc728be4379217284212fa2c0d0bbf80f Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Fri, 11 Nov 2022 10:47:50 +0100 Subject: [PATCH 5/7] remove todo --- src/test/kotlin/testutil/servertest/serverTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/kotlin/testutil/servertest/serverTest.kt b/src/test/kotlin/testutil/servertest/serverTest.kt index e27b274..c68e518 100644 --- a/src/test/kotlin/testutil/servertest/serverTest.kt +++ b/src/test/kotlin/testutil/servertest/serverTest.kt @@ -48,7 +48,6 @@ fun serverTest( application { chatServer(di = di) } val client = createClient { - // TODO(saibotma): Why does this not work? It works in Appella.. maybe reset ide? install(ContentNegotiation) { jackson { di.direct.instance Unit>()() } } From 6df1e52fb2c2ab94fa36e3959e0e624260cfc556 Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Fri, 11 Nov 2022 11:25:31 +0100 Subject: [PATCH 6/7] bump all dependency versions --- build.gradle.kts | 38 ++++++++++--------- src/main/kotlin/di/graphQlDi.kt | 5 +-- src/main/kotlin/di/postgresDi.kt | 20 +++++++--- .../kotlin/push/PushNotificationSender.kt | 3 +- .../kotlin/testutil/setupTestDependencies.kt | 7 ++++ 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 43ecc45..0f25055 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,22 +5,21 @@ val postgresPort = System.getenv("POSTGRES_PORT") ?: "54324" val postgresDb = System.getenv("POSTGRES_DB") ?: "chat-server" val postgresUrl = "jdbc:postgresql://$postgresServerName:$postgresPort/$postgresDb" -val ktorVersion = "2.1.3" val kotlinVersion = "1.7.21" -val postgreSqlJdbcVersion = "42.2.23" -val log4jVersion = "2.14.1" -val log4jApiKotlinVersion = "1.0.0" -val graphQlJavaVersion = "16.2" -val graphQlKotlinVersion = "4.1.1" -val firebaseAdminVersion = "6.9.0" +val ktorVersion = "2.1.3" +val log4jVersion = "2.19.0" +val log4jApiKotlinVersion = "1.2.0" +val graphQlKotlinVersion = "6.3.0" +val firebaseAdminVersion = "9.1.1" +val flywayCoreVersion = "9.7.0" +val postgreSqlJdbcVersion = "42.5.0" val jooqVersion = "3.17.4" -val flywayCoreVersion = "7.11.4" // TODO(saibotma): Remove me when https://github.com/Kodein-Framework/Kodein-DI/issues/410 is resolved. val kodeinVersion = "8.0.0-ktor-2-SNAPSHOT" -val jacksonDataTypeJsr310Version = "2.12.4" -val kotestVersion = "4.6.1" -val junitJupiterVersion = "5.7.2" -val mockkVersion = "1.12.0" +val jacksonDataTypeJsr310Version = "2.14.0" +val kotestVersion = "5.5.4" +val junitJupiterVersion = "5.9.1" +val mockkVersion = "1.13.2" kotlin.sourceSets["main"].kotlin.srcDirs("src/main") kotlin.sourceSets["test"].kotlin.srcDirs("src/test") @@ -61,16 +60,17 @@ dependencies { implementation("io.ktor:ktor-server-double-receive:$ktorVersion") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-server-status-pages:$ktorVersion") - implementation("io.ktor", "ktor-server-test-host", ktorVersion) + implementation("io.ktor:ktor-server-test-host:$ktorVersion") implementation("io.ktor:ktor-server-auth:$ktorVersion") implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion") // Out of some reason the plugin returns 403 in case CORS would not be allowed. Related issues: // https://youtrack.jetbrains.com/issue/KTOR-4237/CORS-the-plugin-responds-with-403-although-specification-doesnt-contain-such-information // https://youtrack.jetbrains.com/issue/KTOR-4236/CORS-Plugin-should-log-reason-for-returning-403-Forbidden-errors implementation("io.ktor:ktor-server-cors:$ktorVersion") - implementation("io.ktor", "ktor-client-core", ktorVersion) implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + testImplementation("io.ktor", "ktor-server-tests", ktorVersion) + implementation("org.apache.logging.log4j", "log4j-api", log4jVersion) implementation("org.apache.logging.log4j", "log4j-core", log4jVersion) @@ -89,15 +89,17 @@ dependencies { runtimeOnly("org.kodein.di", "kodein-di-jvm", kodeinVersion) implementation("org.kodein.di", "kodein-di-framework-ktor-server-jvm", kodeinVersion) + implementation("com.fasterxml.jackson.datatype", "jackson-datatype-jsr310", jacksonDataTypeJsr310Version) - testImplementation(kotlin("test-junit5")) testImplementation("io.kotest", "kotest-assertions-core", kotestVersion) testImplementation("io.kotest", "kotest-property", kotestVersion) - testImplementation("io.ktor", "ktor-server-tests", ktorVersion) + + testImplementation(kotlin("test-junit5")) testImplementation("org.junit.jupiter", "junit-jupiter-api", junitJupiterVersion) testImplementation("org.junit.jupiter", "junit-jupiter-params", junitJupiterVersion) testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", junitJupiterVersion) + testImplementation("io.mockk", "mockk", mockkVersion) } @@ -160,6 +162,8 @@ flyway { url = postgresUrl user = postgresUser password = postgresPassword + // The default is true, and Flyway gradle plugin will not be executed in production environment. + cleanDisabled = false } java { @@ -191,7 +195,7 @@ tasks.named("generateJooq") { outputs.cacheIf { true } } -// Requireded because of https://github.com/gradle/gradle/issues/17236 +// Required because of https://github.com/gradle/gradle/issues/17236 tasks.named("processResources") { duplicatesStrategy = DuplicatesStrategy.INCLUDE } diff --git a/src/main/kotlin/di/graphQlDi.kt b/src/main/kotlin/di/graphQlDi.kt index c20e7bc..aa672f8 100644 --- a/src/main/kotlin/di/graphQlDi.kt +++ b/src/main/kotlin/di/graphQlDi.kt @@ -30,9 +30,6 @@ val graphQlDi = DI.Module("graphql") { bind() with singleton { MessageMutation(instance(), instance()) } bind() with singleton { PushMutation(instance()) } bind() with singleton { - val objectMapper = jacksonObjectMapper().apply { - registerModule(JavaTimeModule()) - } val config = SchemaGeneratorConfig( supportedPackages = listOf( "persistence.jooq.tables.pojos", @@ -40,7 +37,7 @@ val graphQlDi = DI.Module("graphql") { "models", ), hooks = ChatServerSchemaGeneratorHooks(), - dataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider(objectMapper = objectMapper) + dataFetcherFactoryProvider = SimpleKotlinDataFetcherFactoryProvider() ) val queries = listOf( diff --git a/src/main/kotlin/di/postgresDi.kt b/src/main/kotlin/di/postgresDi.kt index 14ded31..da414e0 100644 --- a/src/main/kotlin/di/postgresDi.kt +++ b/src/main/kotlin/di/postgresDi.kt @@ -3,6 +3,8 @@ package di import flyway.FlywayConfig import io.ktor.server.config.* import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.Configuration +import org.flywaydb.core.api.configuration.FluentConfiguration import org.jooq.DSLContext import org.jooq.SQLDialect import org.jooq.impl.DSL @@ -45,12 +47,8 @@ val postgresDi = DI.Module("postgres") { } } bind() with singleton { - val config: FlywayConfig by di.instance() - // ⚠️ Configuration here must be the same as in build.gradle.kts - Flyway.configure() - .dataSource(instance()) - .baselineVersion(config.baselineVersion) - .load() + val configuration = buildFlywayConfiguration(dataSource = instance(), isCleanDisabled = true) + Flyway(configuration) } bind() with singleton { val dataSource: DataSource by di.instance() @@ -61,3 +59,13 @@ val postgresDi = DI.Module("postgres") { } bind() with singleton { KotlinDslContext(instance()) } } + +// ⚠️ Configuration here should be the same as in build.gradle.kts +fun buildFlywayConfiguration(dataSource: DataSource, isCleanDisabled: Boolean): Configuration { + return FluentConfiguration() + .baselineVersion("1") + .baselineOnMigrate(true) + .dataSource(dataSource) + .cleanDisabled(isCleanDisabled) +} + diff --git a/src/main/kotlin/push/PushNotificationSender.kt b/src/main/kotlin/push/PushNotificationSender.kt index f936eca..39aecce 100644 --- a/src/main/kotlin/push/PushNotificationSender.kt +++ b/src/main/kotlin/push/PushNotificationSender.kt @@ -3,6 +3,7 @@ package push import com.fasterxml.jackson.databind.ObjectMapper import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.MessagingErrorCode import org.apache.logging.log4j.kotlin.logger import persistence.jooq.tables.pojos.FirebasePushToken import util.convertToStringMap @@ -31,7 +32,7 @@ class PushNotificationSender(private val objectMapper: ObjectMapper) { FirebaseMessaging.getInstance().send(message) return true } catch (t: FirebaseMessagingException) { - if (t.errorCode == "UNREGISTERED") { + if (t.messagingErrorCode == MessagingErrorCode.UNREGISTERED) { logger().info("Firebase token $firebasePushToken not registered.", t) return false } diff --git a/src/test/kotlin/testutil/setupTestDependencies.kt b/src/test/kotlin/testutil/setupTestDependencies.kt index db85825..8c84440 100644 --- a/src/test/kotlin/testutil/setupTestDependencies.kt +++ b/src/test/kotlin/testutil/setupTestDependencies.kt @@ -1,9 +1,12 @@ package testutil +import di.buildFlywayConfiguration import di.setupDi import io.ktor.server.config.* +import org.flywaydb.core.Flyway import org.kodein.di.DI import org.kodein.di.bind +import org.kodein.di.instance import org.kodein.di.singleton import push.FirebaseInitializer import push.PushNotificationSender @@ -13,4 +16,8 @@ fun DI.MainBuilder.setupTestDependencies(config: ApplicationConfig = Application setupDi(config) bind>(overrides = true) with singleton { Optional.of(mockedFirebaseInitializer()) } bind>(overrides = true) with singleton { Optional.of(mockedPushNotificationSender()) } + bind(overrides = true) with singleton { + val configuration = buildFlywayConfiguration(dataSource = instance(), isCleanDisabled = false) + Flyway(configuration) + } } From 930343bff8865bcec44d075ef0ea54a15ee4621f Mon Sep 17 00:00:00 2001 From: Tobias Marschall Date: Fri, 11 Nov 2022 11:54:15 +0100 Subject: [PATCH 7/7] bump plugin versions --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0f25055..571b12d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,8 +30,8 @@ sourceSets["test"].resources.srcDirs("src/test/resources") plugins { application kotlin("jvm") version "1.7.21" - id("com.github.johnrengelman.shadow") version "6.1.0" - id("org.flywaydb.flyway") version "7.11.4" + id("com.github.johnrengelman.shadow") version "7.1.2" + id("org.flywaydb.flyway") version "9.7.0" id("nu.studer.jooq") version "8.0" }