diff --git a/README.md b/README.md index e539948..329d4d3 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ String html = lettermint.messages().html("message-id"); | `stats()` | `retrieve` | | `suppressions()` | `list`, `iterate`, `create`, `delete` | | `team()` | `retrieve`, `update`, `usage`, `roles`, `retrieveRole`, `createRole`, `updateRole`, `deleteRole`, `duplicateRole` | -| `team().members()` | `list`, `iterate`, `retrieve`, `updateAssignment` | +| `team().members()` | `list`, `iterate`, `retrieve`, `updateAssignment`, `remove` | | `team().invites()` | `list`, `iterate`, `retrieve`, `create`, `update`, `revoke` | | `webhooks()` | `list`, `iterate`, `create`, `retrieve`, `update`, `delete`, `test`, `regenerateSecret` | | `webhooks().deliveries()` | `list(webhookId)`, `iterate(webhookId)`, `retrieve(webhookId, deliveryId)`, `retry(webhookId, deliveryId)` | @@ -307,6 +307,14 @@ lettermint.team().duplicateRole(role.id(), DuplicateTeamRoleData.builder().name( lettermint.team().deleteRole(role.id(), new DeleteTeamRoleQuery(replacementRoleId)); ``` +`team().members().remove(userId)` removes a member from the team; the Owner can't be removed. `ListTeamMembersQuery.filterProjectId(...)` limits the member list to members who can access that project: members with access to all projects and members selected for it. + +```java +for (TeamMemberData member : lettermint.team().members().iterate(ListTeamMembersQuery.builder().filterProjectId(projectId).build())) { + System.out.println(member.email()); +} +``` + Request and response types are immutable records in `co.lettermint.types`. Build requests with their builder (`UpdateRouteData.builder().name("Main").build()`) or, for small ones, the constructor (`new StoreDomainData("acme.com")`). In update requests, a field that is optional and nullable is an `OptionalNullable`: leave it unset to keep the current value, or pass `null` to the builder to clear it: ```java diff --git a/src/main/java/co/lettermint/TeamMembers.java b/src/main/java/co/lettermint/TeamMembers.java index eab51e9..09cd76d 100644 --- a/src/main/java/co/lettermint/TeamMembers.java +++ b/src/main/java/co/lettermint/TeamMembers.java @@ -2,6 +2,7 @@ import co.lettermint.types.CursorPage; import co.lettermint.types.ListTeamMembersQuery; +import co.lettermint.types.MessageResponse; import co.lettermint.types.Operations; import co.lettermint.types.TeamMemberData; import co.lettermint.types.UpdateTeamMemberAssignmentData; @@ -24,7 +25,8 @@ public CursorPage list() { } /** - * Lists team members, one page at a time. + * Lists team members, one page at a time. {@code filterProjectId} limits the list to members who + * can access that project. * * @param query the query parameters, or null * @return the response @@ -118,6 +120,27 @@ public TeamMemberData updateAssignment(String userId, UpdateTeamMemberAssignment return transport.call(Operations.UPDATE_TEAM_MEMBER_ASSIGNMENT, Transport.call("team.members.updateAssignment").path(userId).body(body).options(options)); } + /** + * Removes a member from the team. The Owner can't be removed. + * + * @param userId the user id + * @return the response + */ + public MessageResponse remove(String userId) { + return remove(userId, null); + } + + /** + * Removes a member from the team. The Owner can't be removed. + * + * @param userId the user id + * @param options per-call options, or null + * @return the response + */ + public MessageResponse remove(String userId, RequestOptions options) { + return transport.call(Operations.DELETE_TEAM_MEMBER, Transport.call("team.members.remove").path(userId).options(options)); + } + @Override public String toString() { return "TeamMembers{}"; diff --git a/src/test/java/co/lettermint/TeamApiTest.java b/src/test/java/co/lettermint/TeamApiTest.java index 74448e8..6c78964 100644 --- a/src/test/java/co/lettermint/TeamApiTest.java +++ b/src/test/java/co/lettermint/TeamApiTest.java @@ -25,6 +25,7 @@ import co.lettermint.types.GetStatsQuery; import co.lettermint.types.ListDomainsQuery; import co.lettermint.types.ListTeamInvitesQuery; +import co.lettermint.types.ListTeamMembersQuery; import co.lettermint.types.ListWebhookDeliveriesQuery; import co.lettermint.types.MessageData; import co.lettermint.types.MessageResponse; @@ -72,13 +73,6 @@ import org.junit.jupiter.api.Test; class TeamApiTest { - /** - * Operations that have no method yet: they are added by the next PRs in this stack. Each of those - * PRs removes its operations from this list, and the last one removes the list. - */ - private static final Set PENDING = Set.of( - "DELETE /team/members/{userId}"); - private FakeServer server; private Lettermint client; @@ -203,6 +197,7 @@ void everyOperationIsReachable() { () -> client.team().members().list(), () -> client.team().members().iterate().forEach(item -> { }), () -> client.team().members().retrieve("u1"), + () -> client.team().members().remove("u1"), () -> client.team().members().updateAssignment("u1", new UpdateTeamMemberAssignmentData("role1", new UpdateTeamMemberAssignmentDataProjectAccess(ProjectAccessScope.of("all"), null))), () -> client.team().invites().list(), @@ -232,10 +227,7 @@ void everyOperationIsReachable() { for (FakeServer.Request request : server.requests()) { reached.add(operationKey(request)); } - assertTrue(Operations.ALL.keySet().containsAll(PENDING), "every pending operation is generated"); - Set expected = new TreeSet<>(Operations.ALL.keySet()); - expected.removeAll(PENDING); - assertEquals(expected, reached); + assertEquals(new TreeSet<>(Operations.ALL.keySet()), reached); } @Test @@ -568,6 +560,52 @@ void teamRoleErrorsMapToApiExceptions() { assertEquals(List.of("Choose a replacement role."), invalid.getErrors().get("replacement_role_id")); } + @Test + void removesATeamMember() { + server.enqueue(FakeServer.Response.json(200, "{\"message\":\"Member removed.\"}")); + MessageResponse removed = client.team().members().remove("u 1"); + assertTeamRequest("DELETE", "/v1/team/members/u%201"); + assertEquals("", server.last().body()); + assertEquals("Member removed.", removed.message()); + + server.enqueue(FakeServer.Response.json(200, "{\"message\":\"Member removed.\"}")); + client.team().members().remove("u1", RequestOptions.timeout(Duration.ofSeconds(5))); + assertTeamRequest("DELETE", "/v1/team/members/u1"); + + server.enqueue(FakeServer.Response.json(409, "{\"error\":{\"code\":\"owner_protected\",\"message\":\"The Owner cannot be removed.\"}}")); + ConflictException conflict = assertThrows(ConflictException.class, () -> client.team().members().remove("owner")); + assertEquals(409, conflict.getStatus()); + assertEquals("owner_protected", conflict.getCode()); + assertEquals("The Owner cannot be removed.", conflict.getMessage()); + + server.enqueue(FakeServer.Response.json(404, "{\"message\":\"Not found\"}")); + assertThrows(NotFoundException.class, () -> client.team().members().remove("u2")); + + int before = server.requests().size(); + assertThrows(LettermintConfigException.class, () -> client.team().members().remove("")); + assertThrows(LettermintConfigException.class, () -> client.team().members().remove("..")); + Lettermint sending = Lettermint.builder().sendingToken(ClientTest.SENDING).baseUrl(server.baseUrl()).build(); + assertThrows(LettermintConfigException.class, () -> sending.team().members().remove("u1")); + assertEquals(before, server.requests().size()); + } + + @Test + void filtersTeamMembersByProject() { + server.enqueue(FakeServer.Response.json(200, page(null, "u1"))); + client.team().members().list(ListTeamMembersQuery.builder().filterProjectId("p1").build()); + assertTeamRequest("GET", "/v1/team/members"); + assertEquals("filter%5Bproject_id%5D=p1", server.last().rawQuery()); + + server.enqueue(FakeServer.Response.json(200, page("c2", "u1"))); + server.enqueue(FakeServer.Response.json(200, page(null, "u2"))); + client.team().members().iterate(ListTeamMembersQuery.builder().pageSize(1).filterProjectId("p1").build()).forEach(item -> { }); + assertEquals("page%5Bsize%5D=1&page%5Bcursor%5D=c2&filter%5Bproject_id%5D=p1", server.last().rawQuery()); + + server.enqueue(FakeServer.Response.json(200, page(null))); + assertTrue(client.team().members().list().data().isEmpty()); + assertNull(server.last().rawQuery()); + } + @Test void updatesSendExplicitNulls() { client.routes().update("r1", UpdateRouteData.builder().inboundDomain(null).name("Main").build());