From 8ad0281003407dad7e58d216574a8c711822ad1a Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Sun, 15 Mar 2020 20:13:10 +0100 Subject: [PATCH] #29 Implement Workgroup members routes (Add, remove, update) --- src/main/kotlin/fr/dcproject/Application.kt | 5 +- .../kotlin/fr/dcproject/entity/Workgroup.kt | 7 +- .../fr/dcproject/repository/Workgroup.kt | 39 ++++++++++- .../kotlin/fr/dcproject/routes/Workgroup.kt | 53 ++++++++++++++ .../kotlin/feature/KtorServerRequestSteps.kt | 7 ++ src/test/kotlin/feature/WorkgroupSteps.kt | 25 ++++--- src/test/resources/feature/workgroup.feature | 69 ++++++++++++++++++- src/test/sql/workgroup.sql | 7 ++ 8 files changed, 195 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 5257d6c..0c429c8 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -151,10 +151,10 @@ fun Application.module(env: Env = PROD) { } } - convert { + convert { decode { values, _ -> values.singleOrNull()?.let { - CitizenRef(UUID.fromString(it)) + WorkgroupRef(UUID.fromString(it)) } ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""") } } @@ -288,7 +288,6 @@ fun Application.module(env: Env = PROD) { install(AutoHeadResponse) install(ContentNegotiation) { - // TODO move to postgresJson lib jackson { propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE diff --git a/src/main/kotlin/fr/dcproject/entity/Workgroup.kt b/src/main/kotlin/fr/dcproject/entity/Workgroup.kt index 135bbe0..8780ecc 100644 --- a/src/main/kotlin/fr/dcproject/entity/Workgroup.kt +++ b/src/main/kotlin/fr/dcproject/entity/Workgroup.kt @@ -43,10 +43,9 @@ open class WorkgroupRef( id: UUID? ) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI -interface WorkgroupWithAuthI : WorkgroupI, EntityCreatedBy, EntityDeletedAt { +interface WorkgroupWithAuthI : WorkgroupWithMembersI, EntityCreatedBy, EntityDeletedAt { val anonymous: Boolean val owner: Z - var members: List fun isMember(user: UserI): Boolean = members.map { it.user.id }.contains(user.id) || owner.user.id == user.id @@ -55,4 +54,8 @@ interface WorkgroupWithAuthI : WorkgroupI, EntityCreatedBy isMember(citizen.user) } +interface WorkgroupWithMembersI : WorkgroupI { + var members: List +} + interface WorkgroupI : UuidEntityI \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/repository/Workgroup.kt b/src/main/kotlin/fr/dcproject/repository/Workgroup.kt index 4cc0977..4a06911 100644 --- a/src/main/kotlin/fr/dcproject/repository/Workgroup.kt +++ b/src/main/kotlin/fr/dcproject/repository/Workgroup.kt @@ -1,12 +1,12 @@ package fr.dcproject.repository -import fr.dcproject.entity.CitizenRef -import fr.dcproject.entity.WorkgroupSimple +import fr.dcproject.entity.* import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Requester import fr.postgresjson.entity.Parameter import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI.Direction +import fr.postgresjson.serializer.serialize import net.pearx.kasechange.toSnakeCase import java.util.* import fr.dcproject.entity.Workgroup as WorkgroupEntity @@ -40,6 +40,41 @@ class Workgroup(override var requester: Requester) : RepositoryI { .getFunction("upsert_workgroup") .selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null") + fun addMember(workgroup: WorkgroupI, member: CitizenI): List = + addMembers(workgroup, listOf(member)) + + fun addMembers(workgroup: WorkgroupI, members: List): List = requester + .getFunction("add_workgroup_members") + .select( + "id" to workgroup.id, + "resource" to members.serialize() + ) + + fun removeMember(workgroup: WorkgroupI, memberToDelete: CitizenI): List = + removeMembers(workgroup, listOf(memberToDelete)) + + fun removeMembers(workgroup: WorkgroupI, membersToDelete: List): List = requester + .getFunction("remove_workgroup_members") + .select( + "id" to workgroup.id, + "resource" to membersToDelete + ) + + fun updateMembers(workgroup: WorkgroupI, members: List): List = requester + .getFunction("update_workgroup_members") + .select( + "id" to workgroup.id, + "resource" to members + ) + + fun > updateMembers(workgroup: W): W { + updateMembers(workgroup, workgroup.members).let { + workgroup.members = it + } + + return workgroup + } + class Filter( val createdById: String? = null ) : Parameter diff --git a/src/main/kotlin/fr/dcproject/routes/Workgroup.kt b/src/main/kotlin/fr/dcproject/routes/Workgroup.kt index 6b3974a..6e6cc0f 100644 --- a/src/main/kotlin/fr/dcproject/routes/Workgroup.kt +++ b/src/main/kotlin/fr/dcproject/routes/Workgroup.kt @@ -8,7 +8,9 @@ import fr.dcproject.entity.request.getContent import fr.dcproject.repository.Workgroup.Filter import fr.dcproject.security.voter.WorkgroupVoter.Action.VIEW import fr.dcproject.security.voter.WorkgroupVoter.Action.CREATE +import fr.dcproject.security.voter.WorkgroupVoter.Action.UPDATE import fr.dcproject.security.voter.assertCan +import fr.dcproject.utils.toUUID import fr.postgresjson.repository.RepositoryI import io.ktor.application.ApplicationCall import io.ktor.application.call @@ -17,6 +19,8 @@ import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.locations.post +import io.ktor.locations.put +import io.ktor.locations.delete import io.ktor.request.receive import io.ktor.response.respond import io.ktor.routing.Route @@ -72,6 +76,22 @@ object WorkgroupsPaths { } } +@KtorExperimentalLocationsAPI +object WorkgroupsMembersPaths { + @Location("/workgroups/members/{workgroup}") + class WorkgroupsMembersRequest(val workgroup: WorkgroupEntity) : RequestBuilder> { + class Content : MutableList by mutableListOf() { + class Item(val id: String) + } + + override suspend fun getContent(call: ApplicationCall): List { + return call.receive().map { + CitizenRef(it.id.toUUID()) + } + } + } +} + @KtorExperimentalLocationsAPI fun Route.workgroup(repo: WorkgroupRepository) { get { @@ -96,4 +116,37 @@ fun Route.workgroup(repo: WorkgroupRepository) { call.respond(HttpStatusCode.Created, it) } } + + /* Add members to workgroup */ + post { + call.getContent(it) + .let { members -> + assertCan(UPDATE, it.workgroup) + repo.addMembers(it.workgroup, members) + }.let { + call.respond(HttpStatusCode.OK, it) + } + } + + /* Delete members of workgroup */ + delete { + call.getContent(it) + .let { members -> + assertCan(UPDATE, it.workgroup) + repo.removeMembers(it.workgroup, members) + }.let { + call.respond(HttpStatusCode.OK, it) + } + } + + /* Update members of workgroup */ + put { + call.getContent(it) + .let { members -> + assertCan(UPDATE, it.workgroup) + repo.updateMembers(it.workgroup, members) + }.let { + call.respond(HttpStatusCode.OK, it) + } + } } diff --git a/src/test/kotlin/feature/KtorServerRequestSteps.kt b/src/test/kotlin/feature/KtorServerRequestSteps.kt index 1fe4513..298c937 100644 --- a/src/test/kotlin/feature/KtorServerRequestSteps.kt +++ b/src/test/kotlin/feature/KtorServerRequestSteps.kt @@ -11,6 +11,7 @@ import io.ktor.server.testing.setBody import io.ktor.util.KtorExperimentalAPI import kotlinx.serialization.ImplicitReflectionSerializer import kotlin.test.assertEquals +import kotlin.test.assertNotEquals @ImplicitReflectionSerializer @KtorExperimentalAPI @@ -50,6 +51,12 @@ class KtorServerRequestSteps : En { } } + Then("the response should not contain object:") { expected: DataTable -> + expected.asMap(String::class.java, String::class.java).forEach { (key, valueExpected) -> + assertNotEquals(valueExpected, JsonPath.read(response, key)?.toString() ?: throw AssertionError("\"$key\" element not found on json response")) + } + } + Then("print last response") { print(KtorServerContext.defaultServer.call?.response?.content) } diff --git a/src/test/kotlin/feature/WorkgroupSteps.kt b/src/test/kotlin/feature/WorkgroupSteps.kt index 46d4e01..8144c30 100644 --- a/src/test/kotlin/feature/WorkgroupSteps.kt +++ b/src/test/kotlin/feature/WorkgroupSteps.kt @@ -1,6 +1,7 @@ package feature import fr.dcproject.entity.* +import fr.dcproject.utils.toUUID import io.cucumber.datatable.DataTable import io.cucumber.java8.En import org.joda.time.DateTime @@ -12,12 +13,19 @@ import fr.dcproject.repository.Workgroup as WorkgroupRepository class WorkgroupSteps : En, KoinTest { init { + When("I have members in workgroup {string}:") { workgroupId: String, members: DataTable -> + val membersRefs = members.asList() + .map { CitizenRef(it.toUUID()) } + + get().addMembers(WorkgroupRef(workgroupId.toUUID()), membersRefs) + } + When("I have workgroup:") { body: DataTable -> val data = body.asMap(String::class.java, String::class.java) - val creator = if (data["created_by"] != null) { - CitizenRef(UUID.fromString(data["created_by"])) - } else { + val creator = data["created_by"]?.let { + get().findByUsername(it.toLowerCase().replace(' ', '-')) + } ?: kotlin.run { val username = "paul-langevin".toLowerCase() + UUID.randomUUID() val user = User( username = username, @@ -32,13 +40,12 @@ class WorkgroupSteps : En, KoinTest { get().insertWithUser(it) } } - val owner = if (data["owner"] != null) { - CitizenRef(UUID.fromString(data["owner"])) - } else { - creator - } - val workgroup = WorkgroupSimple( + val owner = data["owner"]?.let { + get().findByUsername(it.toLowerCase().replace(' ', '-')) + } ?: creator + + val workgroup = WorkgroupSimple( id = UUID.fromString(data["id"] ?: UUID.randomUUID().toString()), name = data["name"] ?: "Les Incoruptible", description = data["description"] ?: "La vie est notre jeux", diff --git a/src/test/resources/feature/workgroup.feature b/src/test/resources/feature/workgroup.feature index 86c2324..b6c5581 100644 --- a/src/test/resources/feature/workgroup.feature +++ b/src/test/resources/feature/workgroup.feature @@ -41,4 +41,71 @@ Feature: Workgroup When I send a GET request to "/workgroups" Then the response status code should be 200 And the response should contain object: - | $.result[0]id | 3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c | \ No newline at end of file + | $.result[0]id | 3fd8edb6-c4b4-4c94-bc75-ddd9b290d32c | + + Scenario: Can add member to workgroup + Given I have citizen Blaise Pascal + And I have citizen Roger Penrose with id "6d883fe7-5fc0-4a50-8858-72230673eba4" + And I have citizen Alessandro Volta with id "b5bac515-45d4-4aeb-9b6d-2627a0bbc419" + And I am authenticated as Blaise Pascal + And I have workgroup: + | id | b0ea1922-3bc6-44e2-aa7c-40158998cfbb | + | name | Les bonobos | + | owner | Blaise Pascal | + When I send a POST request to "/workgroups/members/b0ea1922-3bc6-44e2-aa7c-40158998cfbb" with body: + """ + [ + {"id":"6d883fe7-5fc0-4a50-8858-72230673eba4"}, + {"id":"b5bac515-45d4-4aeb-9b6d-2627a0bbc419"} + ] + """ + Then the response status code should be 200 + + Scenario: Can remove member to workgroup + Given I have citizen Heinrich Hertz + And I have citizen William Thomson with id "87909ba3-2069-431c-9924-219fd8411cf2" + And I have citizen Paul Dirac with id "1baf48bb-02bc-4d8f-ac86-33335354f5e7" + And I am authenticated as Heinrich Hertz + And I have workgroup: + | id | b6c975df-dd44-4e99-adc1-f605746b0e11 | + | name | Les Tacos | + | owner | Heinrich Hertz | + And I have members in workgroup "b6c975df-dd44-4e99-adc1-f605746b0e11": + | 87909ba3-2069-431c-9924-219fd8411cf2 | + | 1baf48bb-02bc-4d8f-ac86-33335354f5e7 | + When I send a DELETE request to "/workgroups/members/b6c975df-dd44-4e99-adc1-f605746b0e11" with body: + """ + [ + {"id":"87909ba3-2069-431c-9924-219fd8411cf2"} + ] + """ + Then the response status code should be 200 + And the response should contain object: + | $.[0]id | 1baf48bb-02bc-4d8f-ac86-33335354f5e7 | + And the JSON should have 1 items + + Scenario: Can update members on workgroup + Given I have citizen John Dalton + And I have citizen Sadi Carnot with id "be3b0926-8628-4426-804a-75188a6eb315" + And I have citizen Joseph Fourier with id "d9671eca-abaf-4b67-9230-3ece700c1ddb" + And I have citizen Georg Ohm with id "b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1" + And I am authenticated as John Dalton + And I have workgroup: + | id | 784fe6bc-7635-4ae2-b080-3a4743b998bf | + | name | Les Tacos | + | owner | John Dalton | + And I have members in workgroup "784fe6bc-7635-4ae2-b080-3a4743b998bf": + | be3b0926-8628-4426-804a-75188a6eb315 | + | d9671eca-abaf-4b67-9230-3ece700c1ddb | + When I send a PUT request to "/workgroups/members/784fe6bc-7635-4ae2-b080-3a4743b998bf" with body: + """ + [ + {"id":"be3b0926-8628-4426-804a-75188a6eb315"}, + {"id":"b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1"} + ] + """ + Then the response status code should be 200 + And the response should contain object: + | $.[0]id | be3b0926-8628-4426-804a-75188a6eb315 | + | $.[1]id | b49e20c1-8393-45d6-a6a0-3fa5c71cbdc1 | + And the JSON should have 2 items \ No newline at end of file diff --git a/src/test/sql/workgroup.sql b/src/test/sql/workgroup.sql index 7895c66..baf6f46 100644 --- a/src/test/sql/workgroup.sql +++ b/src/test/sql/workgroup.sql @@ -76,6 +76,13 @@ begin assert not members::jsonb @> jsonb_build_array(jsonb_build_object('id', _citizen_id2)), 'Members must NOT contain citizen2'; + select m into members from find_workgroup_members((created_workgroup->>'id')::uuid) m; + assert json_array_length(members) = 1, 'The members count must be equal to 1'; + assert members::jsonb @> jsonb_build_array(jsonb_build_object('id', _citizen_id)), + 'Members must contain citizen1'; + assert not members::jsonb @> jsonb_build_array(jsonb_build_object('id', _citizen_id2)), + 'Members must NOT contain citizen2'; + rollback; raise notice 'workgroup test pass'; end