diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt index 8eb3ecc8d..f4b04d719 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt @@ -21,7 +21,7 @@ import kotlin.time.Instant data class Collection( val id: Long, val name: String, - val ownedBy: String, + val ownedBy: Long, val organism: String, val description: String?, val variants: List, diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/User.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/User.kt new file mode 100644 index 000000000..5b5b2edbe --- /dev/null +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/User.kt @@ -0,0 +1,16 @@ +package org.genspectrum.dashboardsbackend.api + +import kotlin.time.Instant + +data class User( + val id: Long, + val githubId: String?, + val name: String, + val email: String?, + val createdAt: Instant, + val updatedAt: Instant, +) + +data class UserSyncRequest(val githubId: String, val name: String, val email: String?) + +data class PublicUser(val id: Long, val name: String) diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsController.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsController.kt index 997981184..7972ec529 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsController.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsController.kt @@ -26,7 +26,7 @@ class CollectionsController(private val collectionModel: CollectionModel) { description = "Returns collections filtered by optional userId and/or organism parameters.", ) fun getCollections( - @RequestParam(required = false) userId: String?, + @RequestParam(required = false) userId: Long?, @RequestParam(required = false) organism: String?, ): List = collectionModel.getCollections( userId = userId, @@ -50,7 +50,7 @@ class CollectionsController(private val collectionModel: CollectionModel) { ) fun postCollection( @RequestBody collection: CollectionRequest, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): Collection = collectionModel.createCollection( request = collection, userId = userId, @@ -65,7 +65,7 @@ class CollectionsController(private val collectionModel: CollectionModel) { fun putCollection( @RequestBody collection: CollectionUpdate, @Parameter(description = "The ID of the collection", example = "1") @PathVariable id: Long, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): Collection = collectionModel.putCollection(id, collection, userId) @DeleteMapping("/collections/{id}") @@ -76,6 +76,6 @@ class CollectionsController(private val collectionModel: CollectionModel) { ) fun deleteCollection( @Parameter(description = "The ID of the collection", example = "1") @PathVariable id: Long, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ) = collectionModel.deleteCollection(id, userId) } diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsController.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsController.kt index 79bb91efa..74c5bb4e8 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsController.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsController.kt @@ -36,7 +36,7 @@ class SubscriptionsController( ) fun getSubscription( @IdParameter @PathVariable id: String, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): Subscription = subscriptionModel.getSubscription( subscriptionId = id, userId = userId, @@ -47,7 +47,7 @@ class SubscriptionsController( summary = "Get all subscriptions of a user", description = "Returns a list of all subscriptions of a user.", ) - fun getSubscriptions(@UserIdParameter @RequestParam userId: String): List = + fun getSubscriptions(@UserIdParameter @RequestParam userId: Long): List = subscriptionModel.getSubscriptions(userId) @PostMapping("/subscriptions") @@ -58,7 +58,7 @@ class SubscriptionsController( ) fun postSubscriptions( @RequestBody subscription: SubscriptionRequest, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): Subscription = subscriptionModel.postSubscriptions( request = subscription, userId = userId, @@ -70,7 +70,7 @@ class SubscriptionsController( summary = "Delete a subscription", description = "Deletes a specific subscription of a user by its uuid.", ) - fun deleteSubscription(@IdParameter @PathVariable id: String, @UserIdParameter @RequestParam userId: String) { + fun deleteSubscription(@IdParameter @PathVariable id: String, @UserIdParameter @RequestParam userId: Long) { subscriptionModel.deleteSubscription( subscriptionId = id, userId = userId, @@ -85,7 +85,7 @@ class SubscriptionsController( fun putSubscription( @RequestBody subscription: SubscriptionUpdate, @IdParameter @PathVariable id: String, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): Subscription = subscriptionModel.putSubscription( subscriptionId = id, subscriptionUpdate = subscription, @@ -99,7 +99,7 @@ class SubscriptionsController( ) fun evaluateTrigger( @IdParameter @RequestParam id: String, - @UserIdParameter @RequestParam userId: String, + @UserIdParameter @RequestParam userId: Long, ): TriggerEvaluationResponse { val triggerEvaluationResult = triggerEvaluationModel.evaluateSubscriptionTrigger( subscriptionId = id, diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/UsersController.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/UsersController.kt new file mode 100644 index 000000000..e180e1a70 --- /dev/null +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/controller/UsersController.kt @@ -0,0 +1,30 @@ +package org.genspectrum.dashboardsbackend.controller + +import io.swagger.v3.oas.annotations.Operation +import org.genspectrum.dashboardsbackend.api.PublicUser +import org.genspectrum.dashboardsbackend.api.User +import org.genspectrum.dashboardsbackend.api.UserSyncRequest +import org.genspectrum.dashboardsbackend.model.user.UserModel +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class UsersController(private val userModel: UserModel) { + @PostMapping("/users/sync", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Sync user from external auth provider", + description = "Upserts a user record by github_id. Returns the user with their internal ID.", + ) + fun syncUser(@RequestBody request: UserSyncRequest): User = userModel.syncUser(request) + + @GetMapping("/users/{id}", produces = [MediaType.APPLICATION_JSON_VALUE]) + @Operation( + summary = "Get user by internal ID", + description = "Returns public user info by internal UUID.", + ) + fun getUser(@PathVariable id: Long): PublicUser = userModel.getUser(id) +} diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt index d9ee18ddc..55f8c6e55 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt @@ -10,6 +10,7 @@ import org.genspectrum.dashboardsbackend.config.DashboardsConfig import org.genspectrum.dashboardsbackend.controller.BadRequestException import org.genspectrum.dashboardsbackend.controller.ForbiddenException import org.genspectrum.dashboardsbackend.controller.NotFoundException +import org.genspectrum.dashboardsbackend.util.now import org.jetbrains.exposed.v1.core.Op import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq @@ -18,19 +19,12 @@ import org.jetbrains.exposed.v1.core.notInList import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import kotlin.time.Clock import kotlin.time.Instant @Service @Transactional class CollectionModel(private val dashboardsConfig: DashboardsConfig) { - // Truncate to milliseconds to avoid mismatches between the in-memory value - // we return and what is read back from the DB. - private fun now(): Instant = Clock.System.now().run { - Instant.fromEpochMilliseconds(toEpochMilliseconds()) - } - - fun getCollections(userId: String?, organism: String?): List { + fun getCollections(userId: Long?, organism: String?): List { if (organism != null) { dashboardsConfig.validateIsValidOrganism(organism) dashboardsConfig.validateCollectionsEnabled(organism) @@ -81,7 +75,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { return entity.toCollection() } - fun createCollection(request: CollectionRequest, userId: String): Collection { + fun createCollection(request: CollectionRequest, userId: Long): Collection { dashboardsConfig.validateIsValidOrganism(request.organism) dashboardsConfig.validateCollectionsEnabled(request.organism) @@ -113,7 +107,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { ) } - fun deleteCollection(id: Long, userId: String) { + fun deleteCollection(id: Long, userId: Long) { // Find with ownership check val entity = CollectionEntity.findForUser(id, userId) ?: throw ForbiddenException("Collection $id not found or you don't have permission to delete it") @@ -122,7 +116,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { entity.delete() } - fun putCollection(id: Long, update: CollectionUpdate, userId: String): Collection { + fun putCollection(id: Long, update: CollectionUpdate, userId: Long): Collection { val collectionEntity = CollectionEntity.findForUser(id, userId) ?: throw ForbiddenException("Collection $id not found or you don't have permission to update it") dashboardsConfig.validateCollectionsEnabled(collectionEntity.organism) diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionTable.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionTable.kt index cbb212e77..1a12b3366 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionTable.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionTable.kt @@ -11,7 +11,7 @@ const val COLLECTION_TABLE = "collections_table" object CollectionTable : LongIdTable(COLLECTION_TABLE) { val name = text("name") - val ownedBy = varchar("owned_by", 255) + val ownedBy = long("owned_by") val organism = varchar("organism", 255) val description = text("description").nullable() val createdAt = timestamp("created_at") @@ -20,7 +20,7 @@ object CollectionTable : LongIdTable(COLLECTION_TABLE) { class CollectionEntity(id: EntityID) : LongEntity(id) { companion object : LongEntityClass(CollectionTable) { - fun findForUser(id: Long, userId: String) = findById(id) + fun findForUser(id: Long, userId: Long) = findById(id) ?.takeIf { it.ownedBy == userId } } diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionModel.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionModel.kt index de7b9d409..ffba83c76 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionModel.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionModel.kt @@ -13,18 +13,18 @@ import org.springframework.transaction.annotation.Transactional @Service @Transactional class SubscriptionModel(private val dashboardsConfig: DashboardsConfig) { - fun getSubscription(subscriptionId: String, userId: String): Subscription = + fun getSubscription(subscriptionId: String, userId: Long): Subscription = SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId) ?.toSubscription() ?: throw NotFoundException("Subscription $subscriptionId not found") - fun getSubscriptions(userId: String): List = SubscriptionEntity.find { + fun getSubscriptions(userId: Long): List = SubscriptionEntity.find { SubscriptionTable.userId eq userId }.map { it.toSubscription() } - fun postSubscriptions(request: SubscriptionRequest, userId: String): Subscription { + fun postSubscriptions(request: SubscriptionRequest, userId: Long): Subscription { dashboardsConfig.validateIsValidOrganism(request.organism) return SubscriptionEntity @@ -40,14 +40,14 @@ class SubscriptionModel(private val dashboardsConfig: DashboardsConfig) { .toSubscription() } - fun deleteSubscription(subscriptionId: String, userId: String) { + fun deleteSubscription(subscriptionId: String, userId: Long) { val subscription = SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId) ?: throw NotFoundException("Subscription $subscriptionId not found") subscription.delete() } - fun putSubscription(subscriptionId: String, subscriptionUpdate: SubscriptionUpdate, userId: String): Subscription { + fun putSubscription(subscriptionId: String, subscriptionUpdate: SubscriptionUpdate, userId: Long): Subscription { subscriptionUpdate.organism?.also { dashboardsConfig.validateIsValidOrganism(it) } val subscription = SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId) diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionTable.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionTable.kt index 110025834..8af91e9e4 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionTable.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/subscription/SubscriptionTable.kt @@ -17,12 +17,12 @@ object SubscriptionTable : UUIDTable(SUBSCRIPTION_TABLE) { val organism = varchar("organism", 255) val dateWindow = varchar("date_window", 255) val trigger = jacksonSerializableJsonb("trigger") - val userId = varchar("user_id", 255) + val userId = long("user_id") } class SubscriptionEntity(id: EntityID) : UUIDEntity(id) { companion object : UUIDEntityClass(SubscriptionTable) { - fun findForUser(id: UUID, userId: String) = findById(id) + fun findForUser(id: UUID, userId: Long) = findById(id) ?.takeIf { it.userId == userId } } diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/triggerevaluation/TriggerEvaluationModel.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/triggerevaluation/TriggerEvaluationModel.kt index bf1375142..b257faa1d 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/triggerevaluation/TriggerEvaluationModel.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/triggerevaluation/TriggerEvaluationModel.kt @@ -8,7 +8,7 @@ class TriggerEvaluationModel( private val subscriptionModel: SubscriptionModel, private val triggerEvaluator: TriggerEvaluator, ) { - fun evaluateSubscriptionTrigger(subscriptionId: String, userId: String) = triggerEvaluator.evaluate( + fun evaluateSubscriptionTrigger(subscriptionId: String, userId: Long) = triggerEvaluator.evaluate( subscriptionModel.getSubscription( subscriptionId = subscriptionId, userId = userId, diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserModel.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserModel.kt new file mode 100644 index 000000000..d372430b4 --- /dev/null +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserModel.kt @@ -0,0 +1,35 @@ +package org.genspectrum.dashboardsbackend.model.user + +import org.genspectrum.dashboardsbackend.api.PublicUser +import org.genspectrum.dashboardsbackend.api.User +import org.genspectrum.dashboardsbackend.api.UserSyncRequest +import org.genspectrum.dashboardsbackend.controller.NotFoundException +import org.genspectrum.dashboardsbackend.util.now +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class UserModel { + fun syncUser(request: UserSyncRequest): User { + val now = now() + val existing = UserEntity.findByGithubId(request.githubId) + return if (existing != null) { + existing.name = request.name + existing.email = request.email + existing.updatedAt = now + existing.toUser() + } else { + UserEntity.new { + githubId = request.githubId + name = request.name + email = request.email + createdAt = now + updatedAt = now + }.toUser() + } + } + + fun getUser(id: Long): PublicUser = UserEntity.findById(id)?.toPublicUser() + ?: throw NotFoundException("User $id not found") +} diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserTable.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserTable.kt new file mode 100644 index 000000000..4ab112f93 --- /dev/null +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/user/UserTable.kt @@ -0,0 +1,43 @@ +package org.genspectrum.dashboardsbackend.model.user + +import org.genspectrum.dashboardsbackend.api.PublicUser +import org.genspectrum.dashboardsbackend.api.User +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.LongIdTable +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.dao.LongEntity +import org.jetbrains.exposed.v1.dao.LongEntityClass +import org.jetbrains.exposed.v1.datetime.timestamp + +const val USER_TABLE = "users_table" + +object UserTable : LongIdTable(USER_TABLE) { + val githubId = varchar("github_id", 255).nullable().uniqueIndex() + val name = varchar("name", 255) + val email = varchar("email", 255).nullable() + val createdAt = timestamp("created_at") + val updatedAt = timestamp("updated_at") +} + +class UserEntity(id: EntityID) : LongEntity(id) { + companion object : LongEntityClass(UserTable) { + fun findByGithubId(githubId: String) = find { UserTable.githubId eq githubId }.firstOrNull() + } + + var githubId by UserTable.githubId + var name by UserTable.name + var email by UserTable.email + var createdAt by UserTable.createdAt + var updatedAt by UserTable.updatedAt + + fun toPublicUser() = PublicUser(id = id.value, name = name) + + fun toUser() = User( + id = id.value, + githubId = githubId, + name = name, + email = email, + createdAt = createdAt, + updatedAt = updatedAt, + ) +} diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/util/InstantProvider.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/util/InstantProvider.kt new file mode 100644 index 000000000..0724a6f61 --- /dev/null +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/util/InstantProvider.kt @@ -0,0 +1,10 @@ +package org.genspectrum.dashboardsbackend.util + +import kotlin.time.Clock +import kotlin.time.Instant + +// Truncate to milliseconds to avoid mismatches between the in-memory value +// we return and what is read back from the DB. +fun now(): Instant = Clock.System.now().run { + Instant.fromEpochMilliseconds(toEpochMilliseconds()) +} diff --git a/backend/src/main/resources/db/migration/V1.3__add_users_table.sql b/backend/src/main/resources/db/migration/V1.3__add_users_table.sql new file mode 100644 index 000000000..81012d713 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1.3__add_users_table.sql @@ -0,0 +1,38 @@ +create table users_table ( + id bigserial primary key, + github_id varchar(255) unique, + name varchar(255) not null, + email varchar(255), + created_at timestamp without time zone not null default date_trunc('milliseconds', timezone('UTC', current_timestamp)), + updated_at timestamp without time zone not null default date_trunc('milliseconds', timezone('UTC', current_timestamp)) +); + +create index idx_users_github_id on users_table(github_id); + +-- Backfill users from existing collections +insert into users_table (github_id, name) +select distinct owned_by, owned_by +from collections_table +on conflict (github_id) do nothing; + +-- Backfill users from existing subscriptions +insert into users_table (github_id, name) +select distinct user_id, user_id +from subscriptions_table +on conflict (github_id) do nothing; + +-- Migrate collections_table.owned_by to bigint FK +alter table collections_table add column owned_by_id bigint; +update collections_table c set owned_by_id = u.id from users_table u where u.github_id = c.owned_by; +alter table collections_table alter column owned_by_id set not null; +alter table collections_table add constraint fk_collections_user foreign key (owned_by_id) references users_table(id); +alter table collections_table drop column owned_by; +alter table collections_table rename column owned_by_id to owned_by; + +-- Migrate subscriptions_table.user_id to bigint FK +alter table subscriptions_table add column user_id_new bigint; +update subscriptions_table s set user_id_new = u.id from users_table u where u.github_id = s.user_id; +alter table subscriptions_table alter column user_id_new set not null; +alter table subscriptions_table add constraint fk_subscriptions_user foreign key (user_id_new) references users_table(id); +alter table subscriptions_table drop column user_id; +alter table subscriptions_table rename column user_id_new to user_id; diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsClient.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsClient.kt index 2dd8f35b5..9b8ac16d2 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsClient.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsClient.kt @@ -16,18 +16,18 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class CollectionsClient(private val mockMvc: MockMvc, private val objectMapper: ObjectMapper) { - fun postCollectionRaw(collection: CollectionRequest, userId: String): ResultActions = mockMvc.perform( + fun postCollectionRaw(collection: CollectionRequest, userId: Long): ResultActions = mockMvc.perform( post("/collections?userId=$userId") .content(objectMapper.writeValueAsString(collection)) .contentType(MediaType.APPLICATION_JSON), ) - fun postCollection(collection: CollectionRequest, userId: String): Collection = deserializeJsonResponse( + fun postCollection(collection: CollectionRequest, userId: Long): Collection = deserializeJsonResponse( postCollectionRaw(collection, userId) .andExpect(status().isCreated), ) - fun getCollectionsRaw(userId: String? = null, organism: String? = null): ResultActions { + fun getCollectionsRaw(userId: Long? = null, organism: String? = null): ResultActions { val params = buildString { val queryParams = mutableListOf() if (userId != null) queryParams.add("userId=$userId") @@ -40,7 +40,7 @@ class CollectionsClient(private val mockMvc: MockMvc, private val objectMapper: return mockMvc.perform(get("/collections$params")) } - fun getCollections(userId: String? = null, organism: String? = null): List = deserializeJsonResponse( + fun getCollections(userId: Long? = null, organism: String? = null): List = deserializeJsonResponse( getCollectionsRaw(userId, organism) .andExpect(status().isOk), ) @@ -52,21 +52,21 @@ class CollectionsClient(private val mockMvc: MockMvc, private val objectMapper: .andExpect(status().isOk), ) - fun putCollectionRaw(collection: CollectionUpdate, id: Long, userId: String): ResultActions = mockMvc.perform( + fun putCollectionRaw(collection: CollectionUpdate, id: Long, userId: Long): ResultActions = mockMvc.perform( put("/collections/$id?userId=$userId") .content(objectMapper.writeValueAsString(collection)) .contentType(MediaType.APPLICATION_JSON), ) - fun putCollection(collection: CollectionUpdate, id: Long, userId: String): Collection = deserializeJsonResponse( + fun putCollection(collection: CollectionUpdate, id: Long, userId: Long): Collection = deserializeJsonResponse( putCollectionRaw(collection, id, userId) .andExpect(status().isOk), ) - fun deleteCollectionRaw(id: Long, userId: String): ResultActions = + fun deleteCollectionRaw(id: Long, userId: Long): ResultActions = mockMvc.perform(delete("/collections/$id?userId=$userId")) - fun deleteCollection(id: Long, userId: String) { + fun deleteCollection(id: Long, userId: Long) { deleteCollectionRaw(id, userId).andExpect(status().isNoContent) } diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsDeleteTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsDeleteTest.kt index 326846569..f9f7754bf 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsDeleteTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsDeleteTest.kt @@ -14,12 +14,15 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -@Import(CollectionsClient::class) -class CollectionsDeleteTest(@param:Autowired private val collectionsClient: CollectionsClient) { +@Import(CollectionsClient::class, UsersClient::class) +class CollectionsDeleteTest( + @param:Autowired private val collectionsClient: CollectionsClient, + @param:Autowired private val usersClient: UsersClient, +) { @Test fun `WHEN owner deletes collection THEN succeeds and collection is removed`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) collectionsClient.deleteCollection(createdCollection.id, userId) @@ -32,8 +35,8 @@ class CollectionsDeleteTest(@param:Autowired private val collectionsClient: Coll @Test fun `WHEN non-owner deletes collection THEN returns 403 forbidden`() { - val owner = getNewUserId() - val nonOwner = getNewUserId() + val owner = usersClient.createUser() + val nonOwner = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, owner) collectionsClient.deleteCollectionRaw(createdCollection.id, nonOwner) @@ -44,7 +47,7 @@ class CollectionsDeleteTest(@param:Autowired private val collectionsClient: Coll @Test fun `WHEN deleting non-existent collection THEN returns 403`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val nonExistentId = 999999L collectionsClient.deleteCollectionRaw(nonExistentId, userId) diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt index d813ea6b7..e95b3fda1 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt @@ -23,16 +23,17 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -@Import(CollectionsClient::class) +@Import(CollectionsClient::class, UsersClient::class) class CollectionsGetTest( @param:Autowired private val collectionsClient: CollectionsClient, @param:Autowired private val mockMvc: MockMvc, + @param:Autowired private val usersClient: UsersClient, ) { @Test fun `GIVEN collections for multiple users WHEN getting collections THEN users get separate collections`() { - val userA = getNewUserId() - val userB = getNewUserId() + val userA = usersClient.createUser() + val userB = usersClient.createUser() val collectionA = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "User A Collection"), @@ -55,8 +56,8 @@ class CollectionsGetTest( @Test fun `GIVEN multiple collections WHEN getting all THEN returns all`() { - val userA = getNewUserId() - val userB = getNewUserId() + val userA = usersClient.createUser() + val userB = usersClient.createUser() val covidCollectionA = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "Covid A", organism = KnownTestOrganisms.Covid.name), @@ -80,8 +81,8 @@ class CollectionsGetTest( @Test fun `GIVEN collections for multiple users WHEN getting by userId THEN returns only that user's collections`() { - val userA = getNewUserId() - val userB = getNewUserId() + val userA = usersClient.createUser() + val userB = usersClient.createUser() val collectionA = collectionsClient.postCollection(dummyCollectionRequest.copy(name = "User A"), userA) val collectionB = collectionsClient.postCollection(dummyCollectionRequest.copy(name = "User B"), userB) @@ -94,7 +95,7 @@ class CollectionsGetTest( @Test fun `GIVEN collection with both variant types WHEN getting collections THEN type field is present on variants`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) collectionsClient.getCollectionsRaw(userId = userId) @@ -106,7 +107,7 @@ class CollectionsGetTest( @Test fun `GIVEN user has no collections WHEN getting collections for user THEN returns empty array`() { - val nonexistentUserId = getNewUserId() + val nonexistentUserId = usersClient.createUser() val collections = collectionsClient.getCollections(userId = nonexistentUserId) @@ -115,7 +116,7 @@ class CollectionsGetTest( @Test fun `GIVEN covid and mpox collections WHEN getting by organism THEN returns only that organism's collections`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val covidCollection = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "Covid", organism = KnownTestOrganisms.Covid.name), @@ -141,8 +142,8 @@ class CollectionsGetTest( @Test fun `GIVEN multiple collections WHEN filtering by userId AND organism THEN returns subset`() { - val userA = getNewUserId() - val userB = getNewUserId() + val userA = usersClient.createUser() + val userB = usersClient.createUser() val covidCollectionA = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "Covid A", organism = KnownTestOrganisms.Covid.name), @@ -169,7 +170,7 @@ class CollectionsGetTest( @Test fun `GIVEN collections WHEN filtering by userId and organism with no matches THEN returns empty array`() { - val userA = getNewUserId() + val userA = usersClient.createUser() collectionsClient.postCollection( dummyCollectionRequest.copy(organism = KnownTestOrganisms.Covid.name), userA, @@ -182,7 +183,7 @@ class CollectionsGetTest( @Test fun `GIVEN collection with variants WHEN getting collection THEN all variant fields are present`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val collections = collectionsClient.getCollections(userId = userId) @@ -208,7 +209,7 @@ class CollectionsGetTest( @Test fun `GIVEN both variant types WHEN getting collection THEN types are discriminated`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val collections = collectionsClient.getCollections(userId = userId) @@ -226,7 +227,7 @@ class CollectionsGetTest( @Test fun `GIVEN collection exists WHEN getting collection by ID THEN returns collection with all fields and variants`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val retrievedCollection = collectionsClient.getCollection(createdCollection.id) @@ -241,7 +242,7 @@ class CollectionsGetTest( @Test fun `GIVEN different user's collection WHEN getting by ID THEN returns (public access)`() { - val userA = getNewUserId() + val userA = usersClient.createUser() val createdCollection = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "User A Collection"), userA, diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt index d9e286bac..b0876021f 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt @@ -28,15 +28,16 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -@Import(CollectionsClient::class) +@Import(CollectionsClient::class, UsersClient::class) class CollectionsPostTest( @param:Autowired private val collectionsClient: CollectionsClient, @param:Autowired private val mockMvc: MockMvc, + @param:Autowired private val usersClient: UsersClient, ) { @Test fun `WHEN creating collection THEN createdAt and updatedAt are set`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) assertThat(createdCollection.createdAt, notNullValue()) @@ -60,7 +61,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with createdAt in body THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() mockMvc.perform( post("/collections?userId=$userId") .content("""{"name":"Test","organism":"Covid","variants":[],"createdAt":"2000-01-01T00:00:00Z"}""") @@ -70,7 +71,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with updatedAt in body THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() mockMvc.perform( post("/collections?userId=$userId") .content("""{"name":"Test","organism":"Covid","variants":[],"updatedAt":"2000-01-01T00:00:00Z"}""") @@ -80,7 +81,7 @@ class CollectionsPostTest( @Test fun `GIVEN collection with variants WHEN creating THEN returns with generated IDs`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) assertThat(createdCollection.id, notNullValue()) @@ -95,7 +96,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with only query variants THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val request = dummyCollectionRequest.copy( variants = listOf(dummyQueryVariantRequest), ) @@ -108,7 +109,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with only mutation list variants THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val request = dummyCollectionRequest.copy( variants = listOf(dummyFilterObjectVariantRequest), ) @@ -121,7 +122,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with no variants THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val request = dummyCollectionRequest.copy(variants = emptyList()) val createdCollection = collectionsClient.postCollection(request, userId) @@ -131,7 +132,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with unknown organism THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() collectionsClient.postCollectionRaw(dummyCollectionRequest.copy(organism = "unknown organism"), userId) .andExpect(status().isBadRequest) .andExpect(jsonPath("\$.detail").value("Organism 'unknown organism' is not supported")) @@ -139,7 +140,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection for organism with collections disabled THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() collectionsClient.postCollectionRaw( dummyCollectionRequest.copy(organism = ORGANISM_WITHOUT_COLLECTIONS), userId, @@ -150,7 +151,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with lineage filter THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val variantWithLineage = VariantRequest.FilterObjectVariantRequest( name = "BA.2 lineage", description = "BA.2 variant", @@ -171,7 +172,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with invalid lineage field THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val variantWithInvalidLineage = VariantRequest.FilterObjectVariantRequest( name = "Invalid lineage", description = "Has invalid lineage field", @@ -190,7 +191,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with multiple lineage filters THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val variantWithMultipleLineages = VariantRequest.FilterObjectVariantRequest( name = "Multiple lineages", description = "Has multiple lineage filters", @@ -211,7 +212,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with only aminoAcidMutations THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val variantWithOnlyAaMutations = VariantRequest.FilterObjectVariantRequest( name = "Only AA mutations", description = "Only has amino acid mutations", @@ -230,7 +231,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with insertions THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val variantWithInsertions = VariantRequest.FilterObjectVariantRequest( name = "With insertions", description = "Has insertions", @@ -251,7 +252,7 @@ class CollectionsPostTest( @Test fun `WHEN creating variant with non-string extra property value THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val body = """ { "name": "Test", @@ -277,7 +278,7 @@ class CollectionsPostTest( @Test fun `WHEN creating collection with lineage filter in filters field THEN succeeds`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val body = """ { "name": "Test", diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPutTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPutTest.kt index 5131866f9..41bd7f627 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPutTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPutTest.kt @@ -24,15 +24,16 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -@Import(CollectionsClient::class) +@Import(CollectionsClient::class, UsersClient::class) class CollectionsPutTest( @param:Autowired private val collectionsClient: CollectionsClient, @param:Autowired private val mockMvc: MockMvc, + @param:Autowired private val usersClient: UsersClient, ) { @Test fun `WHEN owner updates all fields THEN collection is updated`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val updatedVariants = listOf( @@ -61,7 +62,7 @@ class CollectionsPutTest( @Test fun `WHEN owner updates only name THEN only name changes`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val originalVariantCount = createdCollection.variants.size @@ -76,7 +77,7 @@ class CollectionsPutTest( @Test fun `WHEN owner sends empty update THEN name, description and variants are unchanged`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val updated = collectionsClient.putCollection(CollectionUpdate(), createdCollection.id, userId) @@ -89,7 +90,7 @@ class CollectionsPutTest( @Test fun `WHEN collection is updated THEN updatedAt advances but createdAt stays the same`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val updated = collectionsClient.putCollection( @@ -104,7 +105,7 @@ class CollectionsPutTest( @Test fun `WHEN updating collection with createdAt in body THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) mockMvc.perform( @@ -116,7 +117,7 @@ class CollectionsPutTest( @Test fun `WHEN updating collection with updatedAt in body THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) mockMvc.perform( @@ -128,7 +129,7 @@ class CollectionsPutTest( @Test fun `WHEN owner adds new variant without ID THEN variant is created`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val originalVariantCount = createdCollection.variants.size @@ -176,8 +177,8 @@ class CollectionsPutTest( @Test fun `WHEN non-owner updates collection THEN returns 403 forbidden`() { - val owner = getNewUserId() - val nonOwner = getNewUserId() + val owner = usersClient.createUser() + val nonOwner = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, owner) collectionsClient.putCollectionRaw( @@ -192,7 +193,7 @@ class CollectionsPutTest( @Test fun `WHEN owner updates existing variant with ID THEN variant is updated in place`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val firstVariant = createdCollection.variants[0] as Variant.QueryVariant @@ -217,7 +218,7 @@ class CollectionsPutTest( @Test fun `WHEN owner omits variant from update THEN variant is deleted`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val originalVariants = listOf( VariantRequest.QueryVariantRequest( name = "Variant 1", @@ -248,7 +249,7 @@ class CollectionsPutTest( @Test fun `WHEN updating with invalid lineage fields THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val invalidVariant = VariantUpdate.FilterObjectVariantUpdate( @@ -269,7 +270,7 @@ class CollectionsPutTest( @Test fun `WHEN updating variant type THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdCollection = collectionsClient.postCollection(dummyCollectionRequest, userId) val firstVariant = createdCollection.variants[0] as Variant.QueryVariant @@ -291,7 +292,7 @@ class CollectionsPutTest( @Test fun `WHEN updating non-existent collection THEN returns 403`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val nonExistentId = 999999L collectionsClient.putCollectionRaw(CollectionUpdate(name = "Updated Name"), nonExistentId, userId) @@ -302,7 +303,7 @@ class CollectionsPutTest( @Test fun `WHEN updating with variant ID from different collection THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val collection1 = collectionsClient.postCollection(dummyCollectionRequest, userId) val collection2 = collectionsClient.postCollection( dummyCollectionRequest.copy(name = "Collection 2"), diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsClient.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsClient.kt index 4d672007c..207c94773 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsClient.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsClient.kt @@ -16,53 +16,53 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class SubscriptionsClient(private val mockMvc: MockMvc, private val objectMapper: ObjectMapper) { - fun getSubscriptionRaw(id: String, userId: String) = mockMvc.perform(get("/subscriptions/$id?userId=$userId")) + fun getSubscriptionRaw(id: String, userId: Long) = mockMvc.perform(get("/subscriptions/$id?userId=$userId")) - fun getSubscription(id: String, userId: String): Subscription = deserializeJsonResponse( + fun getSubscription(id: String, userId: Long): Subscription = deserializeJsonResponse( getSubscriptionRaw(id, userId) .andExpect(status().isOk), ) - fun getSubscriptionsRaw(userId: String) = mockMvc.perform(get("/subscriptions?userId=$userId")) + fun getSubscriptionsRaw(userId: Long) = mockMvc.perform(get("/subscriptions?userId=$userId")) - fun getSubscriptions(userId: String): List = deserializeJsonResponse( + fun getSubscriptions(userId: Long): List = deserializeJsonResponse( getSubscriptionsRaw(userId) .andExpect(status().isOk), ) - fun postSubscriptionRaw(subscription: SubscriptionRequest, userId: String) = mockMvc.perform( + fun postSubscriptionRaw(subscription: SubscriptionRequest, userId: Long) = mockMvc.perform( post("/subscriptions?userId=$userId") .content(objectMapper.writeValueAsString(subscription)) .contentType(MediaType.APPLICATION_JSON), ) - fun postSubscription(subscription: SubscriptionRequest, userId: String): Subscription = deserializeJsonResponse( + fun postSubscription(subscription: SubscriptionRequest, userId: Long): Subscription = deserializeJsonResponse( postSubscriptionRaw(subscription, userId) .andExpect(status().isCreated), ) - fun deleteSubscriptionRaw(id: String, userId: String) = mockMvc.perform(delete("/subscriptions/$id?userId=$userId")) + fun deleteSubscriptionRaw(id: String, userId: Long) = mockMvc.perform(delete("/subscriptions/$id?userId=$userId")) - fun deleteSubscription(id: String, userId: String) = deleteSubscriptionRaw( + fun deleteSubscription(id: String, userId: Long) = deleteSubscriptionRaw( id, userId, ).andExpect(status().isNoContent) - fun putSubscriptionRaw(subscription: SubscriptionUpdate, id: String, userId: String) = mockMvc.perform( + fun putSubscriptionRaw(subscription: SubscriptionUpdate, id: String, userId: Long) = mockMvc.perform( put("/subscriptions/$id?userId=$userId") .content(objectMapper.writeValueAsString(subscription)) .contentType(MediaType.APPLICATION_JSON), ) - fun putSubscription(subscription: SubscriptionUpdate, id: String, userId: String): Subscription = + fun putSubscription(subscription: SubscriptionUpdate, id: String, userId: Long): Subscription = deserializeJsonResponse( putSubscriptionRaw(subscription, id, userId) .andExpect(status().isOk), ) - fun evaluateTriggerRaw(userId: String, subscriptionId: String) = mockMvc.perform( + fun evaluateTriggerRaw(userId: Long, subscriptionId: String) = mockMvc.perform( get("/subscriptions/evaluateTrigger") - .queryParam("userId", userId) + .queryParam("userId", userId.toString()) .queryParam("id", subscriptionId), ) diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTest.kt index bf1a7eb6a..333456944 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTest.kt @@ -21,17 +21,17 @@ import org.springframework.http.MediaType import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.util.* - -fun getNewUserId(): String = UUID.randomUUID().toString() @SpringBootTest @AutoConfigureMockMvc -@Import(SubscriptionsClient::class) -class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClient: SubscriptionsClient) { +@Import(SubscriptionsClient::class, UsersClient::class) +class SubscriptionsControllerTest( + @param:Autowired private val subscriptionsClient: SubscriptionsClient, + @param:Autowired private val usersClient: UsersClient, +) { @Test fun `GIVEN I created a subscription WHEN getting subscriptions THEN contains created subscription`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) val subscriptions = subscriptionsClient.getSubscriptions(userId) @@ -41,10 +41,10 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscriptions for multiple users WHEN getting subscriptions THEN contains subscriptions of user`() { - val otherUserId = getNewUserId() + val otherUserId = usersClient.createUser() val otherCreatedSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, otherUserId) - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) val subscriptions = subscriptionsClient.getSubscriptions(userId) @@ -55,7 +55,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN getting subscriptions with invalid UUID THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.getSubscriptionRaw("this-is-not-a-uuid", userId) .andExpect(status().isBadRequest) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -64,7 +64,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN getting subscriptions that does not exist THEN returns 404`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.getSubscriptionRaw("00000000-0000-0000-0000-000000000000", userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -73,7 +73,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists WHEN getting subscription THEN returns its data`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) val subscription = subscriptionsClient.getSubscription(createdSubscription.id, userId) @@ -83,10 +83,10 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists for another user WHEN getting subscription THEN returns 404`() { - val otherUserId = getNewUserId() + val otherUserId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, otherUserId) - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.getSubscriptionRaw(createdSubscription.id, userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -95,7 +95,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN I create a subscription for unknown organism THEN returns bad request`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.postSubscriptionRaw(dummySubscriptionRequest.copy(organism = "unknown organism"), userId) .andExpect(status().isBadRequest) .andExpect(jsonPath("\$.detail").value("Organism 'unknown organism' is not supported")) @@ -103,7 +103,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN I delete a subscription that does not exist THEN returns 404`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.deleteSubscriptionRaw("00000000-0000-0000-0000-000000000000", userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -112,10 +112,10 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN I delete a subscription that exist for other user THEN returns 404`() { - val otherUserId = getNewUserId() + val otherUserId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, otherUserId) - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.deleteSubscriptionRaw(createdSubscription.id, userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -124,7 +124,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `WHEN I delete a subscription with invalid UUID THEN return 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.deleteSubscriptionRaw("this-is-not-a-uuid", userId) .andExpect(status().isBadRequest) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -133,7 +133,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists WHEN I delete it THEN it does not exist anymore`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) subscriptionsClient.deleteSubscriptionRaw(createdSubscription.id, userId) @@ -150,7 +150,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists WHEN I edit all fields THEN it is updated`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) val updatedSubscriptionRequest = SubscriptionUpdate( @@ -184,7 +184,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists WHEN I change organism to unknown organism THEN rejects update`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) subscriptionsClient.putSubscriptionRaw( @@ -202,7 +202,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN subscription exists WHEN I edit one entry THEN it is updated`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) val updatedSubscriptionRequest = SubscriptionUpdate( @@ -224,10 +224,10 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN a subscription from another user WHEN I edit it THEN returns 404`() { - val otherUserId = getNewUserId() + val otherUserId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, otherUserId) - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.putSubscriptionRaw(SubscriptionUpdate(), createdSubscription.id, userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -236,7 +236,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN a subscription with invalid UUID WHEN I edit it THEN returns 400`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.putSubscriptionRaw(SubscriptionUpdate(), "this-is-not-a-uuid", userId) .andExpect(status().isBadRequest) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -245,7 +245,7 @@ class SubscriptionsControllerTest(@param:Autowired private val subscriptionsClie @Test fun `GIVEN no subscription WHEN I edit it THEN returns 404`() { - val userId = getNewUserId() + val userId = usersClient.createUser() subscriptionsClient.putSubscriptionRaw(SubscriptionUpdate(), "00000000-0000-0000-0000-000000000000", userId) .andExpect(status().isNotFound) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTriggerEvaluationTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTriggerEvaluationTest.kt index 5325d5f43..317133c9f 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTriggerEvaluationTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/SubscriptionsControllerTriggerEvaluationTest.kt @@ -28,9 +28,10 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc -@Import(SubscriptionsClient::class) +@Import(SubscriptionsClient::class, UsersClient::class) class SubscriptionsControllerTriggerEvaluationTest( @param:Autowired private val subscriptionsClient: SubscriptionsClient, + @param:Autowired private val usersClient: UsersClient, ) { @MockkBean private lateinit var lapisClientProviderMock: LapisClientProvider @@ -42,7 +43,7 @@ class SubscriptionsControllerTriggerEvaluationTest( @Test fun `GIVEN lapis returns count greater than threshold WHEN evaluating count trigger THEN returns condition met`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription( subscription = dummySubscriptionRequest.copy(trigger = countTrigger), @@ -65,7 +66,7 @@ class SubscriptionsControllerTriggerEvaluationTest( @Test fun `GIVEN lapis returns count less than threshold WHEN evaluating count trigger THEN returns condition not met`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription( subscription = dummySubscriptionRequest.copy(trigger = countTrigger), @@ -88,7 +89,7 @@ class SubscriptionsControllerTriggerEvaluationTest( @Test fun `GIVEN lapis returns error WHEN evaluating count trigger THEN returns evaluation error`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription(dummySubscriptionRequest, userId) @@ -113,7 +114,7 @@ class SubscriptionsControllerTriggerEvaluationTest( @Test fun `GIVEN lapis returns proportion below threshold WHEN evaluating proportion trigger THEN condition is met`() { - val userId = getNewUserId() + val userId = usersClient.createUser() val createdSubscription = subscriptionsClient.postSubscription( dummySubscriptionRequest.copy( trigger = Trigger.ProportionTrigger( diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersClient.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersClient.kt new file mode 100644 index 000000000..7c728b10e --- /dev/null +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersClient.kt @@ -0,0 +1,52 @@ +package org.genspectrum.dashboardsbackend.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.genspectrum.dashboardsbackend.api.PublicUser +import org.genspectrum.dashboardsbackend.api.User +import org.genspectrum.dashboardsbackend.api.UserSyncRequest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.UUID + +class UsersClient(private val mockMvc: MockMvc, private val objectMapper: ObjectMapper) { + fun syncUserRaw(request: UserSyncRequest): ResultActions = mockMvc.perform( + post("/users/sync") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON), + ) + + fun syncUser(request: UserSyncRequest): User = deserializeJsonResponse( + syncUserRaw(request).andExpect(status().isOk), + ) + + fun getUserRaw(id: Long): ResultActions = mockMvc.perform(get("/users/$id")) + + fun getUser(id: Long): PublicUser = deserializeJsonResponse( + getUserRaw(id).andExpect(status().isOk), + ) + + /** Creates a user with a random GitHub ID and returns the internal Long user ID. */ + fun createUser(): Long = syncUser( + UserSyncRequest( + githubId = UUID.randomUUID().toString(), + name = "Test User", + email = null, + ), + ).id + + private inline fun deserializeJsonResponse(resultActions: ResultActions): T { + val content = + resultActions + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn() + .response + .contentAsString + return objectMapper.readValue(content) + } +} diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersControllerTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersControllerTest.kt new file mode 100644 index 000000000..939287468 --- /dev/null +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/UsersControllerTest.kt @@ -0,0 +1,94 @@ +package org.genspectrum.dashboardsbackend.controller + +import org.genspectrum.dashboardsbackend.api.UserSyncRequest +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.UUID + +@SpringBootTest +@AutoConfigureMockMvc +@Import(UsersClient::class) +class UsersControllerTest(@param:Autowired private val usersClient: UsersClient) { + + @Test + fun `WHEN syncing a new user THEN creates user and returns it with internal ID`() { + val githubId = UUID.randomUUID().toString() + val request = UserSyncRequest(githubId = githubId, name = "Alice", email = "alice@example.com") + + val user = usersClient.syncUser(request) + + assertThat(user.id, notNullValue()) + assertThat(user.githubId, equalTo(githubId)) + assertThat(user.name, equalTo("Alice")) + assertThat(user.email, equalTo("alice@example.com")) + } + + @Test + fun `WHEN syncing the same github ID twice THEN returns same internal ID`() { + val githubId = UUID.randomUUID().toString() + val request = UserSyncRequest(githubId = githubId, name = "Bob", email = null) + + val first = usersClient.syncUser(request) + val second = usersClient.syncUser(request) + + assertThat(first.id, equalTo(second.id)) + } + + @Test + fun `WHEN syncing with updated name THEN name is updated`() { + val githubId = UUID.randomUUID().toString() + usersClient.syncUser(UserSyncRequest(githubId = githubId, name = "Old Name", email = null)) + + val updated = usersClient.syncUser(UserSyncRequest(githubId = githubId, name = "New Name", email = null)) + + assertThat(updated.name, equalTo("New Name")) + } + + @Test + fun `WHEN syncing without email THEN email is null`() { + val githubId = UUID.randomUUID().toString() + val user = usersClient.syncUser(UserSyncRequest(githubId = githubId, name = "Charlie", email = null)) + + assertThat(user.email, nullValue()) + } + + @Test + fun `WHEN getting user by ID THEN returns public user info`() { + val created = usersClient.syncUser( + UserSyncRequest(githubId = UUID.randomUUID().toString(), name = "Dave", email = null), + ) + + val fetched = usersClient.getUser(created.id) + + assertThat(fetched.id, equalTo(created.id)) + assertThat(fetched.name, equalTo("Dave")) + } + + @Test + fun `WHEN getting user with nonexistent ID THEN returns 404`() { + usersClient.getUserRaw(999999999L) + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.detail").value("User 999999999 not found")) + } + + @Test + fun `WHEN syncing two different github IDs THEN they get different internal IDs`() { + val first = usersClient.syncUser( + UserSyncRequest(githubId = UUID.randomUUID().toString(), name = "User1", email = null), + ) + val second = usersClient.syncUser( + UserSyncRequest(githubId = UUID.randomUUID().toString(), name = "User2", email = null), + ) + + assertThat(first.id == second.id, equalTo(false)) + } +} diff --git a/website/tests/collections/collectionDetail.spec.ts b/website/tests/collections/collectionDetail.spec.ts index 884dbb658..93d14d516 100644 --- a/website/tests/collections/collectionDetail.spec.ts +++ b/website/tests/collections/collectionDetail.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import { test } from '../e2e.fixture.ts'; const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8080'; -const USER_ID = 'e2e-test'; +const GITHUB_ID = 'e2e-test'; const ORGANISM = 'covid'; const ORGANISM_LABEL = 'SARS-CoV-2'; @@ -28,14 +28,27 @@ const TEST_COLLECTION = { }; let collectionId: number | undefined; +let userId: number | undefined; function getCollectionId(): number { if (collectionId === undefined) throw new Error('collectionId was not set in beforeAll'); return collectionId; } +function getUserId(): number { + if (userId === undefined) throw new Error('userId was not set in beforeAll'); + return userId; +} + test.beforeAll(async ({ request }) => { - const response = await request.post(`${BACKEND_URL}/collections?userId=${USER_ID}`, { + const syncResponse = await request.post(`${BACKEND_URL}/users/sync`, { + data: { githubId: GITHUB_ID, name: 'E2E Test User', email: null }, + }); + expect(syncResponse.status()).toBe(200); + const syncBody = (await syncResponse.json()) as { id: number }; + userId = syncBody.id; + + const response = await request.post(`${BACKEND_URL}/collections?userId=${getUserId()}`, { data: TEST_COLLECTION, }); expect(response.status()).toBe(201); @@ -47,7 +60,7 @@ test.afterAll(async ({ request }) => { if (collectionId === undefined) { return; } - const response = await request.delete(`${BACKEND_URL}/collections/${collectionId}?userId=${USER_ID}`); + const response = await request.delete(`${BACKEND_URL}/collections/${collectionId}?userId=${getUserId()}`); expect(response.status()).toBe(204); }); diff --git a/website/tests/collections/collectionForm.spec.ts b/website/tests/collections/collectionForm.spec.ts index 386bdffa0..0d75b3f78 100644 --- a/website/tests/collections/collectionForm.spec.ts +++ b/website/tests/collections/collectionForm.spec.ts @@ -28,14 +28,27 @@ const SEED_COLLECTION = { }; let collectionId: number | undefined; +let userId: number | undefined; function getCollectionId(): number { if (collectionId === undefined) throw new Error('collectionId was not set in beforeAll'); return collectionId; } +function getUserId(): number { + if (userId === undefined) throw new Error('userId was not set in beforeAll'); + return userId; +} + test.beforeAll(async ({ request }) => { - const response = await request.post(`${BACKEND_URL}/collections?userId=${USER_ID}`, { + const syncResponse = await request.post(`${BACKEND_URL}/users/sync`, { + data: { githubId: USER_ID, name: 'E2E Test User', email: null }, + }); + expect(syncResponse.status()).toBe(200); + const syncBody = (await syncResponse.json()) as { id: number }; + userId = syncBody.id; + + const response = await request.post(`${BACKEND_URL}/collections?userId=${getUserId()}`, { data: SEED_COLLECTION, }); expect(response.status()).toBe(201); @@ -47,7 +60,7 @@ test.afterAll(async ({ request }) => { if (collectionId === undefined) { return; } - const response = await request.delete(`${BACKEND_URL}/collections/${collectionId}?userId=${USER_ID}`); + const response = await request.delete(`${BACKEND_URL}/collections/${collectionId}?userId=${getUserId()}`); expect(response.status()).toBe(204); }); @@ -105,7 +118,7 @@ test.describe('New collection page', () => { const url = authenticatedCollectionFormPage.page.url(); const id = /\/collections\/\w+\/(\d+)$/.exec(url)?.[1]; if (id !== undefined) { - await request.delete(`${BACKEND_URL}/collections/${id}?userId=${USER_ID}`); + await request.delete(`${BACKEND_URL}/collections/${id}?userId=${getUserId()}`); } }); });