From 1d51136baddb4f97defdff19d0c49227ffa35c31 Mon Sep 17 00:00:00 2001 From: nmcorrea Date: Thu, 14 Aug 2025 14:35:56 -0300 Subject: [PATCH 1/3] add read-only support for GitHub Copilot Enterprise plan seats --- pom.xml | 6 +- .../connector/github/GitHubClient.java | 19 +- .../github/GitHubCopilotSeatHandler.java | 195 ++++++++++++++++++ .../connector/github/GitHubEMUSchema.java | 4 +- .../github/rest/GitHubEMURESTClient.java | 48 ++++- .../org/kohsuke/github/GHEnterpriseExt.java | 31 +++ .../org/kohsuke/github/GitHubCopilotSeat.java | 34 +++ .../github/GitHubCopilotSeatAssignee.java | 26 +++ .../GitHubCopilotSeatAssigningTeam.java | 24 +++ .../github/GitHubCopilotSeatPageIterator.java | 114 ++++++++++ .../GitHubCopilotSeatPagedSearchIterable.java | 69 +++++++ .../GitHubCopilotSeatsSearchBuilder.java | 52 +++++ .../GitHubCopilotSeatsSearchResult.java | 11 + .../connector/github/CreateUserOpTest.java | 34 +++ .../connector/github/DeleteUsersOpTest.java | 19 ++ .../connector/github/EMUSchemaTest.java | 4 +- .../connector/github/ListResultHandler.java | 19 ++ .../connector/github/SearchGroupsOpTest.java | 54 +++++ .../connector/github/SearchSeatsOpTest.java | 54 +++++ .../connector/github/SearchUsersOpTest.java | 54 +++++ .../connector/github/TestOpTest.java | 14 ++ .../connector/github/UpdateGroupsOpTest.java | 80 +++++++ .../connector/github/UpdateUsersOpTest.java | 92 +++++++++ .../github/testutil/AbstractEMUTest.java | 9 +- 24 files changed, 1056 insertions(+), 10 deletions(-) create mode 100644 src/main/java/jp/openstandia/connector/github/GitHubCopilotSeatHandler.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeat.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatAssignee.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeam.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java create mode 100644 src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchResult.java create mode 100644 src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/ListResultHandler.java create mode 100644 src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/TestOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java diff --git a/pom.xml b/pom.xml index afc396a..e869143 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ jp.openstandia.connector connector-github - 1.2.5-SNAPSHOT + 1.2.6-SNAPSHOT jar GitHub Connector @@ -73,6 +73,10 @@ org.apache.maven.plugins maven-compiler-plugin + + 9 + 9 + org.apache.maven.plugins diff --git a/src/main/java/jp/openstandia/connector/github/GitHubClient.java b/src/main/java/jp/openstandia/connector/github/GitHubClient.java index 14ff6cf..7629d1a 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubClient.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubClient.java @@ -27,10 +27,7 @@ import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.ResultsHandler; import org.identityconnectors.framework.common.objects.Uid; -import org.kohsuke.github.SCIMEMUGroup; -import org.kohsuke.github.SCIMEMUUser; -import org.kohsuke.github.SCIMPatchOperations; -import org.kohsuke.github.SCIMUser; +import org.kohsuke.github.*; import java.net.InetSocketAddress; import java.net.Proxy; @@ -205,5 +202,19 @@ default SCIMEMUGroup getEMUGroup(Uid uid, OperationOptions options, Set default SCIMEMUGroup getEMUGroup(Name name, OperationOptions options, Set attributesToGet) { throw new UnsupportedOperationException(); } + + // Copilot Seats + + default GitHubCopilotSeat getCopilotSeat(Uid uid, OperationOptions options, Set attributesToGet) { + throw new UnsupportedOperationException(); + } + + default GitHubCopilotSeat getCopilotSeat(Name name, OperationOptions options, Set attributesToGet) { + throw new UnsupportedOperationException(); + } + + default int getCopilotSeats(QueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + throw new UnsupportedOperationException(); + } } diff --git a/src/main/java/jp/openstandia/connector/github/GitHubCopilotSeatHandler.java b/src/main/java/jp/openstandia/connector/github/GitHubCopilotSeatHandler.java new file mode 100644 index 0000000..71f376b --- /dev/null +++ b/src/main/java/jp/openstandia/connector/github/GitHubCopilotSeatHandler.java @@ -0,0 +1,195 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.objects.*; +import org.kohsuke.github.GitHubCopilotSeat; +import org.kohsuke.github.SCIMPatchOperations; + +import java.util.Set; + +import static jp.openstandia.connector.util.Utils.toZoneDateTime; +import static jp.openstandia.connector.util.Utils.toZoneDateTimeForISO8601OffsetDateTime; +import static org.identityconnectors.framework.common.objects.AttributeInfo.Flags.*; + +public class GitHubCopilotSeatHandler extends AbstractGitHubEMUHandler { + + public static final ObjectClass SEAT_OBJECT_CLASS = new ObjectClass("GitHubCopilotSeat"); + + private static final Log LOGGER = Log.getLog(GitHubCopilotSeatHandler.class); + + public GitHubCopilotSeatHandler(GitHubEMUConfiguration configuration, GitHubClient client, + GitHubEMUSchema schema, SchemaDefinition schemaDefinition) { + super(configuration, client, schema, schemaDefinition); + } + + public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration configuration, GitHubClient client) { + SchemaDefinition.Builder sb + = SchemaDefinition.newBuilder(SEAT_OBJECT_CLASS, GitHubCopilotSeat.class, SCIMPatchOperations.class, GitHubCopilotSeat.class); + + // __UID__ + // The id for the seat. Must be unique and unchangeable. + sb.addUid("id", + SchemaDefinition.Types.UUID, + null, + (source) -> source.assignee.id, + "id", + NOT_CREATABLE, NOT_UPDATEABLE + ); + + // code (__NAME__) + // The name for the seat. Must be unique and changeable. + sb.addName("displayName", + SchemaDefinition.Types.STRING_CASE_IGNORE, + (source, dest) -> dest.assignee.login = source, + (source, dest) -> dest.replace("displayName", source), + (source) -> source.assignee.login, + null, + REQUIRED + ); + + // Metadata (readonly) + sb.add("created_at", + SchemaDefinition.Types.DATETIME, + null, + (source) -> source.created_at != null ? toZoneDateTimeForISO8601OffsetDateTime(source.created_at) : null, + null, + NOT_CREATABLE, NOT_UPDATEABLE + ); + + sb.add("last_authenticated_at", + SchemaDefinition.Types.DATETIME, + null, + (source) -> source.last_authenticated_at != null ? toZoneDateTimeForISO8601OffsetDateTime(source.last_authenticated_at) : null, + null, + NOT_CREATABLE, NOT_UPDATEABLE + ); + + sb.add("updated_at", + SchemaDefinition.Types.DATETIME, + null, + (source) -> source.updated_at != null ? toZoneDateTimeForISO8601OffsetDateTime(source.updated_at) : null, + null, + NOT_CREATABLE, NOT_UPDATEABLE + ); + + sb.add("last_activity_at", + SchemaDefinition.Types.DATETIME, + null, + (source) -> source.last_activity_at != null ? toZoneDateTimeForISO8601OffsetDateTime(source.last_activity_at) : null, + null, + NOT_CREATABLE, NOT_UPDATEABLE + ); + + sb.add("pending_cancellation_date", + SchemaDefinition.Types.DATETIME, + null, + (source) -> source.pending_cancellation_date != null ? toZoneDateTime(source.pending_cancellation_date) : null, + null, + NOT_CREATABLE, NOT_UPDATEABLE + ); + + sb.add("last_activity_editor", + SchemaDefinition.Types.STRING, + (source, dest) -> { + dest.last_activity_editor = source; + }, + (source, dest) -> dest.replace("last_activity_editor", source), + (source) -> source.last_activity_editor, + null + ); + + sb.add("plan_type", + SchemaDefinition.Types.STRING, + (source, dest) -> { + dest.plan_type = source; + }, + (source, dest) -> dest.replace("plan_type", source), + (source) -> source.plan_type, + null + ); + + sb.add("assignee.type", + SchemaDefinition.Types.STRING, + (source, dest) -> { + dest.assignee.type = source; + }, + (source, dest) -> dest.replace("assignee.type", source), + (source) -> source.assignee.type, + null + ); + + sb.add("assigning_team.slug", + SchemaDefinition.Types.STRING, + (source, dest) -> { + if (source != null) { + dest.assigning_team.slug = source; + } + }, + (source, dest) -> dest.replace("assigning_team.slug", source), + (source) -> source != null && source.assigning_team != null ? source.assigning_team.slug : null, + null + ); + + LOGGER.ok("The constructed GitHub EMU Seat schema"); + + return sb; + } + + @Override + public Uid create(Set attributes) { + return null; + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + return Set.of(); + } + + @Override + public void delete(Uid uid, OperationOptions options) { + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, Set returnAttributesSet, Set fetchFieldsSet, boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + GitHubCopilotSeat seat = client.getCopilotSeat(uid, options, fetchFieldsSet); + + if (seat != null) { + resultsHandler.handle(toConnectorObject(schemaDefinition, seat, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, Set returnAttributesSet, Set fetchFieldsSet, boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + GitHubCopilotSeat seat = client.getCopilotSeat(name, options, fetchFieldsSet); + + if (seat != null) { + resultsHandler.handle(toConnectorObject(schemaDefinition, seat, returnAttributesSet, allowPartialAttributeValues)); + return 1; + } + return 0; + } + + @Override + public int getByMembers(Attribute attribute, ResultsHandler resultsHandler, OperationOptions options, Set returnAttributesSet, Set fetchFieldSet, boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return super.getByMembers(attribute, resultsHandler, options, returnAttributesSet, fetchFieldSet, allowPartialAttributeValues, pageSize, pageOffset); + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, Set returnAttributesSet, Set fetchFieldsSet, boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + return client.getCopilotSeats((s) -> resultsHandler.handle(toConnectorObject(schemaDefinition, s, returnAttributesSet, allowPartialAttributeValues)), + options, fetchFieldsSet, pageSize, pageOffset); + } + + @Override + public void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options) { + super.query(filter, resultsHandler, options); + } + + @Override + public ConnectorObject toConnectorObject(SchemaDefinition schema, T user, Set returnAttributesSet, boolean allowPartialAttributeValues) { + return super.toConnectorObject(schema, user, returnAttributesSet, allowPartialAttributeValues); + } +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/github/GitHubEMUSchema.java b/src/main/java/jp/openstandia/connector/github/GitHubEMUSchema.java index ad65c50..269baae 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubEMUSchema.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubEMUSchema.java @@ -32,12 +32,14 @@ public class GitHubEMUSchema extends AbstractGitHubSchema client) { super(configuration, client); - SchemaBuilder schemaBuilder = new SchemaBuilder(GitHubConnector.class); + SchemaBuilder schemaBuilder = new SchemaBuilder(GitHubEMUConnector.class); buildSchema(schemaBuilder, GitHubEMUUserHandler.createSchema(configuration, client).build(), (schema) -> new GitHubEMUUserHandler(configuration, client, this, schema)); buildSchema(schemaBuilder, GitHubEMUGroupHandler.createSchema(configuration, client).build(), (schema) -> new GitHubEMUGroupHandler(configuration, client, this, schema)); + buildSchema(schemaBuilder, GitHubCopilotSeatHandler.createSchema(configuration, client).build(), + (schema) -> new GitHubCopilotSeatHandler(configuration, client, this, schema)); // Define operation options schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildAttributesToGet(), SearchOp.class); diff --git a/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java b/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java index eb6e5fa..cd3d0a3 100644 --- a/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java +++ b/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java @@ -52,7 +52,6 @@ public class GitHubEMURESTClient implements GitHubClient { public GitHubEMURESTClient(GitHubEMUConfiguration configuration) { this.configuration = configuration; - auth(); } @@ -286,6 +285,53 @@ public SCIMEMUGroup getEMUGroup(Name name, OperationOptions options, Set }); } + @Override + public GitHubCopilotSeat getCopilotSeat(Uid uid, OperationOptions options, Set attributesToGet) { + return withAuth(() -> { + GitHubCopilotSeat seat = enterpriseApiClient.getCopilotSeatByUid(uid.getUidValue()); + return seat; + }); + } + + @Override + public GitHubCopilotSeat getCopilotSeat(Name name, OperationOptions options, Set attributesToGet) { + return withAuth(() -> { + GitHubCopilotSeat seat = enterpriseApiClient.getCopilotSeatByDisplayName(name.getNameValue()); + return seat; + }); + } + + @Override + public int getCopilotSeats(QueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { + return withAuth(() -> { + GitHubCopilotSeatPagedSearchIterable iterable = enterpriseApiClient.listAllSeats(pageSize, pageOffset); + + // 0 means no offset (requested all data) + if (pageOffset < 1) { + for (GitHubCopilotSeat next : iterable) { + if (!handler.handle(next)) { + break; + } + } + return iterable.getTotalSeats(); + } + + // Pagination + int count = 0; + for (GitHubCopilotSeat next : iterable) { + count++; + if (!handler.handle(next)) { + break; + } + if (count >= pageSize) { + break; + } + } + + return iterable.getTotalSeats(); + }); + } + @Override public int getEMUGroups(QueryHandler handler, OperationOptions options, Set fetchFieldsSet, int pageSize, int pageOffset) { return withAuth(() -> { diff --git a/src/main/java/org/kohsuke/github/GHEnterpriseExt.java b/src/main/java/org/kohsuke/github/GHEnterpriseExt.java index 95c0a9a..0803038 100644 --- a/src/main/java/org/kohsuke/github/GHEnterpriseExt.java +++ b/src/main/java/org/kohsuke/github/GHEnterpriseExt.java @@ -155,6 +155,28 @@ public SCIMEMUGroup getSCIMEMUGroupByDisplayName(String scimGroupDisplayName) th return list.get(0); } + public GitHubCopilotSeat getCopilotSeatByDisplayName(String copilotSeatDisplayName) throws IOException { + List allSeats = searchCopilotSeats() + .list() + .toList(); + + return allSeats.stream() + .filter(seat -> copilotSeatDisplayName.equals(seat.assignee.login)) + .findFirst() + .orElse(null); + } + + public GitHubCopilotSeat getCopilotSeatByUid(String copilotSeatUid) throws IOException { + List allSeats = searchCopilotSeats() + .list() + .toList(); + + return allSeats.stream() + .filter(seat -> copilotSeatUid.equals(seat.assignee.id)) + .findFirst() + .orElse(null); + } + /** * Search groups. * @@ -169,6 +191,15 @@ public SCIMPagedSearchIterable listSCIMGroups(int pageSize, int pa return searchSCIMGroups().list().withPageSize(pageSize).withPageOffset(pageOffset); } + public GitHubCopilotSeatsSearchBuilder searchCopilotSeats() { + return new GitHubCopilotSeatsSearchBuilder(root, this); + } + + public GitHubCopilotSeatPagedSearchIterable listAllSeats(int pageSize, int pageOffset) + throws IOException { + return searchCopilotSeats().list().withPageSize(pageSize).withPageOffset(pageOffset); + } + public void deleteSCIMGroup(String scimGroupId) throws IOException { root.createRequest() .method("DELETE") diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeat.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeat.java new file mode 100644 index 0000000..b1bdbc2 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeat.java @@ -0,0 +1,34 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GitHubCopilotSeat { + @JsonProperty("created_at") + public String created_at; + + @JsonProperty("assignee") + public GitHubCopilotSeatAssignee assignee; + + @JsonProperty("pending_cancellation_date") + public String pending_cancellation_date; + + @JsonProperty("plan_type") + public String plan_type; + + @JsonProperty("last_authenticated_at") + public String last_authenticated_at; + + @JsonProperty("updated_at") + public String updated_at; + + @JsonProperty("last_activity_at") + public String last_activity_at; + + @JsonProperty("last_activity_editor") + public String last_activity_editor; + + @JsonProperty("assigning_team") + public GitHubCopilotSeatAssigningTeam assigning_team; +} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssignee.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssignee.java new file mode 100644 index 0000000..0eb8a74 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssignee.java @@ -0,0 +1,26 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GitHubCopilotSeatAssignee { + @JsonProperty("login") + public String login; + + @JsonProperty("id") + public String id; + + @JsonProperty("node_id") + public String node_id; + + @JsonProperty("url") + public String url; + + @JsonProperty("type") + public String type; + + @JsonProperty("user_view_type") + public String user_view_type; + + @JsonProperty("site_admin") + public String site_admin; +} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeam.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeam.java new file mode 100644 index 0000000..41c2941 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeam.java @@ -0,0 +1,24 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GitHubCopilotSeatAssigningTeam { + + @JsonProperty("id") + public String id; + + @JsonProperty("name") + public String name; + + @JsonProperty("slug") + public String slug; + + @JsonProperty("group_name") + public String group_name; + + @JsonProperty("created_at") + public String created_at; + + @JsonProperty("updated_at") + public String updated_at; +} diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java new file mode 100644 index 0000000..ea7993a --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java @@ -0,0 +1,114 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Used for seats pagination information. + *

+ * This class is not thread-safe. Any one instance should only be called from a single thread. + * + * @param the type parameter + * @author Hiroyuki Wada + * @author Nikolas Correa + */ +public class GitHubCopilotSeatPageIterator implements Iterator { + + private final GitHubClient client; + private final Class type; + + private T next; + + private GitHubRequest nextRequest; + + private GitHubResponse finalResponse = null; + + private GitHubCopilotSeatPageIterator(GitHubClient client, Class type, GitHubRequest request) { + if (!"GET".equals(request.method())) { + throw new IllegalStateException("Request method \"GET\" is required for page iterator."); + } + + this.client = client; + this.type = type; + this.nextRequest = request; + } + + static GitHubCopilotSeatPageIterator create(GitHubClient client, Class type, GitHubRequest request, int pageSize, int pageOffset) { + + try { + if (pageSize > 0) { + GitHubRequest.Builder builder = request.toBuilder().with("count", pageSize); + if (pageOffset > 0) { + builder.with("startIndex", pageOffset); + } + request = builder.build(); + } + + return new GitHubCopilotSeatPageIterator<>(client, type, request); + } catch (MalformedURLException e) { + throw new GHException("Unable to build GitHub SCIM API URL", e); + } + } + + public boolean hasNext() { + fetch(); + return next != null; + } + + public T next() { + fetch(); + T result = next; + if (result == null) + throw new NoSuchElementException(); + next = null; + return result; + } + + public GitHubResponse finalResponse() { + if (hasNext()) { + throw new GHException("Final response is not available until after iterator is done."); + } + return finalResponse; + } + + private void fetch() { + if (next != null || nextRequest == null) + return; + + URL url = nextRequest.url(); + try { + GitHubResponse nextResponse = client.sendRequest(nextRequest, + (responseInfo) -> GitHubResponse.parseBody(responseInfo, type)); + next = nextResponse.body(); + nextRequest = findNextURL(nextResponse); + if (nextRequest == null) { + finalResponse = nextResponse; + } + } catch (IOException e) { + throw new GHException("Failed to retrieve " + url, e); + } + } + + private GitHubRequest findNextURL(GitHubResponse response) { + String linkHeader = response.headerField("Link"); + if (linkHeader == null) return null; + + // Expressão para capturar: ; rel="next" + Pattern pattern = Pattern.compile("<([^>]+)>;\\s*rel=\"next\""); + Matcher matcher = pattern.matcher(linkHeader); + if (matcher.find()) { + String nextUrl = matcher.group(1); + try { + return GitHubRequest.newBuilder().withUrlPath(nextUrl).build(); + } catch (Exception e) { + throw new GHException("Malformed next URL: " + nextUrl, e); + } + } + return null; + } +} diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java new file mode 100644 index 0000000..f442501 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java @@ -0,0 +1,69 @@ +package org.kohsuke.github; + +import java.util.Iterator; + +/** + * {@link PagedIterable} enhanced to report search result specific information. + * + * @param the type parameter + * @author Hiroyuki Wada + * @author Nikolas Correa + */ +public class GitHubCopilotSeatPagedSearchIterable extends PagedIterable { + private final transient GitHub root; + + private final GitHubRequest request; + + private final Class> receiverType; + + private GitHubCopilotSeatsSearchResult result; + private int pageOffset; + + public GitHubCopilotSeatPagedSearchIterable(GitHub root, GitHubRequest request, Class> receiverType) { + this.root = root; + this.request = request; + this.receiverType = receiverType; + } + + @Override + public GitHubCopilotSeatPagedSearchIterable withPageSize(int size) { + return (GitHubCopilotSeatPagedSearchIterable) super.withPageSize(size); + } + + public GitHubCopilotSeatPagedSearchIterable withPageOffset(int pageOffset) { + this.pageOffset = pageOffset; + return this; + } + + public int getTotalSeats() { + populate(); + return result.total_seats; + } + + private void populate() { + if (result == null) + iterator().hasNext(); // dispara a carga inicial + } + + @Override + public PagedIterator _iterator(int pageSize) { + final Iterator adapter = adapt( + GitHubCopilotSeatPageIterator.create(root.getClient(), receiverType, request, pageSize, pageOffset)); + return new PagedIterator<>(adapter, null); + } + + protected Iterator adapt(final Iterator> base) { + return new Iterator() { + public boolean hasNext() { + return base.hasNext(); + } + + public T[] next() { + GitHubCopilotSeatsSearchResult v = base.next(); + if (result == null) + result = v; + return v.seats; + } + }; + } +} diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java new file mode 100644 index 0000000..d9cd9be --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java @@ -0,0 +1,52 @@ +package org.kohsuke.github; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; + +/** + * Search for GitHub Copilot seats - Enterprise plan. + * + * @author Hiroyuki Wada + * @author Nikolas Correa + */ + +public class GitHubCopilotSeatsSearchBuilder extends GHQueryBuilder { + protected final Map filter = new HashMap<>(); + + private final Class> receiverType; + + protected final GHEnterpriseExt enterprise; + + GitHubCopilotSeatsSearchBuilder(GitHub root, GHEnterpriseExt enterprise) { + super(root); + this.enterprise = enterprise; + this.receiverType = CopilotSeatSearchResult.class; + + req.withUrlPath(getApiUrl()); + req.withHeader(SCIMConstants.HEADER_ACCEPT, "application/json"); + req.withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); + req.rateLimit(RateLimitTarget.SEARCH); + } + + public GitHubCopilotSeatsSearchBuilder eq(String key, String value) { + filter.put(key, value); + return this; + } + + @Override + public GitHubCopilotSeatPagedSearchIterable list() { + try { + return new GitHubCopilotSeatPagedSearchIterable<>(root, req.build(), receiverType); + } catch (MalformedURLException e) { + throw new GHException("", e); + } + } + + protected String getApiUrl() { + return String.format("/enterprises/%s/copilot/billing/seats", enterprise.login); + } + + private static class CopilotSeatSearchResult extends GitHubCopilotSeatsSearchResult { + } +} diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchResult.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchResult.java new file mode 100644 index 0000000..f421036 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchResult.java @@ -0,0 +1,11 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GitHubCopilotSeatsSearchResult { + @JsonProperty("total_seats") + public int total_seats; + + @JsonProperty("seats") + public T[] seats; +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java b/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java new file mode 100644 index 0000000..80470f6 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java @@ -0,0 +1,34 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.HashSet; +import java.util.Set; + +import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; + +public class CreateUserOpTest extends AbstractEMUTest { + + private Set userEntry() { + + Set attributeSet = new HashSet<>(); + attributeSet.add(AttributeBuilder.build(Name.NAME, "")); + attributeSet.add(AttributeBuilder.build("externalId", "")); + attributeSet.add(AttributeBuilder.build("displayName", "")); + attributeSet.add(AttributeBuilder.build("primaryEmail", "")); + attributeSet.add(AttributeBuilder.build("primaryRole", "User")); + attributeSet.add(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME, true)); + return attributeSet; + } + + @Test() + public void shouldCreateOrReturnExistentUser() { + ConnectorFacade facade = newFacade(); + Uid uid = facade.create(USER_OBJECT_CLASS, userEntry(), null); + AssertJUnit.assertNotNull(uid); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java new file mode 100644 index 0000000..8a7a61e --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java @@ -0,0 +1,19 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.Uid; +import org.testng.annotations.Test; + +import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; + +public class DeleteUsersOpTest extends AbstractEMUTest { + + String userUidToDelete = ""; + + @Test() + public void shouldDeleteUserIfExists() { + ConnectorFacade facade = newFacade(); + facade.delete(USER_OBJECT_CLASS, new Uid(userUidToDelete), null); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/EMUSchemaTest.java b/src/test/java/jp/openstandia/connector/github/EMUSchemaTest.java index 92dd9bf..793d4ac 100644 --- a/src/test/java/jp/openstandia/connector/github/EMUSchemaTest.java +++ b/src/test/java/jp/openstandia/connector/github/EMUSchemaTest.java @@ -31,12 +31,14 @@ void schema() { Schema schema = connector.schema(); assertNotNull(schema); - assertEquals(2, schema.getObjectClassInfo().size()); + assertEquals(3, schema.getObjectClassInfo().size()); Optional user = schema.getObjectClassInfo().stream().filter(o -> o.is("EMUUser")).findFirst(); Optional team = schema.getObjectClassInfo().stream().filter(o -> o.is("EMUGroup")).findFirst(); + Optional seat = schema.getObjectClassInfo().stream().filter(o -> o.is("GitHubCopilotSeat")).findFirst(); assertTrue(user.isPresent()); assertTrue(team.isPresent()); + assertTrue(seat.isPresent()); } } \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/ListResultHandler.java b/src/test/java/jp/openstandia/connector/github/ListResultHandler.java new file mode 100644 index 0000000..e1653f8 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/ListResultHandler.java @@ -0,0 +1,19 @@ +package jp.openstandia.connector.github; + +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.identityconnectors.framework.common.objects.ResultsHandler; + +import java.util.ArrayList; +import java.util.List; + +public class ListResultHandler implements ResultsHandler { + + private final List objects = new ArrayList<>(); + @Override + public boolean handle(ConnectorObject connectorObject) { + objects.add(connectorObject); + return true; + } + + public List getObjects() { return objects; } +} diff --git a/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java new file mode 100644 index 0000000..6ba39c1 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java @@ -0,0 +1,54 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.List; + +import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; + +public class SearchGroupsOpTest extends AbstractEMUTest { + + String groupUid = ""; + String groupName = ""; + + @Test() + public void shouldReturnAllGroups() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + facade.search(GROUP_OBJECT_CLASS, null, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + } + + @Test() + public void shouldReturnGroupByUid() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(GROUP_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } + + @Test() + public void shouldReturnGroupByName() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Name.NAME, groupName); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(GROUP_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java new file mode 100644 index 0000000..71c6083 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java @@ -0,0 +1,54 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.List; + +import static jp.openstandia.connector.github.GitHubCopilotSeatHandler.SEAT_OBJECT_CLASS; + +public class SearchSeatsOpTest extends AbstractEMUTest { + + String seatUid = ""; + String seatName = ""; + + @Test() + public void shouldReturnAllSeats() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + facade.search(SEAT_OBJECT_CLASS, null, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + } + + @Test() + public void shouldReturnSeatByUid() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Uid.NAME, seatUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(SEAT_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } + + @Test() + public void shouldReturnSeatByName() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Name.NAME, seatName); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(SEAT_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java new file mode 100644 index 0000000..d8afc4c --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java @@ -0,0 +1,54 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.List; + +import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; + +public class SearchUsersOpTest extends AbstractEMUTest { + + String userUid = ""; + String userName = ""; + + @Test() + public void shouldReturnAllUsers() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + facade.search(USER_OBJECT_CLASS, null, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + } + + @Test() + public void shouldReturnUserByUid() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(USER_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } + + @Test() + public void shouldReturnUserByUsername() { + ConnectorFacade facade = newFacade(); + ListResultHandler handler = new ListResultHandler(); + + Attribute attribute = AttributeBuilder.build(Name.NAME, userName); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(USER_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/TestOpTest.java b/src/test/java/jp/openstandia/connector/github/TestOpTest.java new file mode 100644 index 0000000..4b7d7f9 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/TestOpTest.java @@ -0,0 +1,14 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.testng.annotations.Test; + +public class TestOpTest extends AbstractEMUTest { + + @Test() + public void shouldInitializeConnection() { + ConnectorFacade facade = newFacade(); + facade.test(); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java new file mode 100644 index 0000000..6257c06 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java @@ -0,0 +1,80 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; + +public class UpdateGroupsOpTest extends AbstractEMUTest { + + String userUid = ""; + String groupUidToUpdate = ""; + + @Test() + public void shouldAddUserToGroup() { + // Create an AttributeDelta to add user uid + Set attributes = new HashSet<>(); + AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); + deltaBuilder.setName("members.User.value"); + deltaBuilder.addValueToAdd(userUid); + attributes.add(deltaBuilder.build()); + + // Call updateDelta to update the group + ConnectorFacade facade = newFacade(); + facade.updateDelta(GROUP_OBJECT_CLASS, new Uid(groupUidToUpdate), attributes, null); + + // Retrieve and verify the updated object + ListResultHandler handler = new ListResultHandler(); + Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUidToUpdate); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(GROUP_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + + ConnectorObject object = objects.get(0); + Attribute memberOfAttr = object.getAttributeByName("members.User.value"); + AssertJUnit.assertNotNull(memberOfAttr); + + List grupos = memberOfAttr.getValue(); + AssertJUnit.assertTrue(grupos.contains(userUid)); + } + + @Test() + public void shouldRemoveUserFromGroup() { + // Create an AttributeDelta to add user uid + Set attributes = new HashSet<>(); + AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); + deltaBuilder.setName("members.User.value"); + deltaBuilder.addValueToRemove(userUid); + attributes.add(deltaBuilder.build()); + + // Call updateDelta to update the group + ConnectorFacade facade = newFacade(); + facade.updateDelta(GROUP_OBJECT_CLASS, new Uid(groupUidToUpdate), attributes, null); + + // Retrieve and verify the updated object + ListResultHandler handler = new ListResultHandler(); + Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUidToUpdate); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(GROUP_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + + ConnectorObject object = objects.get(0); + Attribute memberOfAttr = object.getAttributeByName("members.User.value"); + AssertJUnit.assertNotNull(memberOfAttr); + + List grupos = memberOfAttr.getValue(); + AssertJUnit.assertFalse(grupos.contains(userUid)); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java new file mode 100644 index 0000000..e9dc8d0 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java @@ -0,0 +1,92 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.github.testutil.AbstractEMUTest; +import org.identityconnectors.framework.api.ConnectorFacade; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; + +public class UpdateUsersOpTest extends AbstractEMUTest { + + String userUid = ""; + String attrToUpdate = ""; + String attrNewValue = ""; + + @Test() + public void shouldActivateUser() { + Set attributes = new HashSet<>(); + + AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); + deltaBuilder.setName(OperationalAttributes.ENABLE_NAME); + deltaBuilder.addValueToReplace(true); + attributes.add(deltaBuilder.build()); + + ConnectorFacade facade = newFacade(); + facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); + + ListResultHandler handler = new ListResultHandler(); + Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(USER_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } + + @Test() + public void shouldInactivateUser() { + Set attributes = new HashSet<>(); + + AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); + deltaBuilder.setName(OperationalAttributes.ENABLE_NAME); + deltaBuilder.addValueToReplace(false); + attributes.add(deltaBuilder.build()); + + ConnectorFacade facade = newFacade(); + facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); + + ListResultHandler handler = new ListResultHandler(); + Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(USER_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + } + + @Test() + public void shouldUpdateAttrValue() { + ConnectorFacade facade = newFacade(); + + // Create an AttributeDelta to update the status of uid + Set attributes = new HashSet<>(); + AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); + deltaBuilder.setName(attrToUpdate); + deltaBuilder.addValueToReplace(attrNewValue); + attributes.add(deltaBuilder.build()); + + // Call updateDelta to update the status + facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); + + // Retrieve and verify the updated object + ListResultHandler handler = new ListResultHandler(); + Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); + EqualsFilter filter = new EqualsFilter(attribute); + + facade.search(USER_OBJECT_CLASS, filter, handler, null); + List objects = handler.getObjects(); + AssertJUnit.assertEquals(1, objects.size()); + + ConnectorObject object = objects.get(0); + Attribute nameAttr = object.getAttributeByName(attrToUpdate); + AssertJUnit.assertNotNull(nameAttr); + AssertJUnit.assertEquals(attrNewValue, nameAttr.getValue().get(0)); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java b/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java index d41342c..5ebf906 100644 --- a/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java +++ b/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java @@ -16,11 +16,14 @@ package jp.openstandia.connector.github.testutil; import jp.openstandia.connector.github.GitHubEMUConfiguration; +import jp.openstandia.connector.github.GitHubEMUConnector; +import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.framework.api.APIConfiguration; import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.api.ConnectorFacadeFactory; import org.identityconnectors.test.common.TestHelpers; import org.junit.jupiter.api.BeforeEach; +import org.testng.annotations.Test; public abstract class AbstractEMUTest { @@ -29,13 +32,15 @@ public abstract class AbstractEMUTest { protected GitHubEMUConfiguration newConfiguration() { GitHubEMUConfiguration conf = new GitHubEMUConfiguration(); - conf.setEnterpriseSlug("localEnt"); + conf.setEnterpriseSlug(""); + conf.setAccessToken(new GuardedString("".toCharArray())); + conf.setEndpointURL(""); return conf; } protected ConnectorFacade newFacade() { ConnectorFacadeFactory factory = ConnectorFacadeFactory.getInstance(); - APIConfiguration impl = TestHelpers.createTestConfiguration(LocalGitHubEMUConnector.class, newConfiguration()); + APIConfiguration impl = TestHelpers.createTestConfiguration(GitHubEMUConnector.class, newConfiguration()); impl.getResultsHandlerConfiguration().setEnableAttributesToGetSearchResultsHandler(false); impl.getResultsHandlerConfiguration().setEnableNormalizingResultsHandler(false); impl.getResultsHandlerConfiguration().setEnableFilteredResultsHandler(false); From 9a82e3f5654d62648b4809e3dee856c27737e21b Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Fri, 6 Feb 2026 13:42:43 -0300 Subject: [PATCH 2/3] first commit --- pom.xml | 391 +++++----- .../github/v3/clients/PKCS1PEMKey.java | 88 --- .../github/AbstractGitHubConnector.java | 4 +- .../connector/github/GitHubClient.java | 71 -- .../connector/github/GitHubConfiguration.java | 92 --- .../connector/github/GitHubConnector.java | 46 -- .../connector/github/GitHubEMUConnector.java | 4 - .../github/GitHubEMUUserHandler.java | 43 +- .../connector/github/GitHubSchema.java | 73 -- .../connector/github/GitHubTeamHandler.java | 229 ------ .../connector/github/GitHubUserHandler.java | 341 -------- .../connector/github/GitHubUtils.java | 254 ------ .../github/TeamAssignmentResolver.java | 67 -- .../github/rest/GitHubEMURESTClient.java | 8 +- .../github/rest/GitHubRESTClient.java | 725 ------------------ .../connector/util/SchemaDefinition.java | 4 - .../org/kohsuke/github/GHOrganizationExt.java | 257 ------- .../java/org/kohsuke/github/GHTeamExt.java | 20 - .../github/GitHubCopilotSeatPageIterator.java | 4 +- .../GitHubCopilotSeatPagedSearchIterable.java | 9 +- .../GitHubCopilotSeatsSearchBuilder.java | 2 +- .../java/org/kohsuke/github/GitHubExt.java | 13 - .../org/kohsuke/github/GraphQLConnection.java | 20 - .../java/org/kohsuke/github/GraphQLEdge.java | 18 - .../github/GraphQLExternalIdentity.java | 53 -- .../GraphQLExternalIdentityConnection.java | 9 - .../github/GraphQLExternalIdentityEdge.java | 9 - ...GraphQLExternalIdentitySamlAttributes.java | 14 - ...GraphQLExternalIdentityScimAttributes.java | 17 - .../java/org/kohsuke/github/GraphQLNode.java | 8 - .../kohsuke/github/GraphQLOrganization.java | 22 - ...nizationExternalIdentitySearchBuilder.java | 94 --- .../GraphQLOrganizationIdentityProvider.java | 13 - .../github/GraphQLOrganizationInvitation.java | 8 - .../org/kohsuke/github/GraphQLPageInfo.java | 23 - .../kohsuke/github/GraphQLPageIterator.java | 120 --- .../github/GraphQLPagedSearchIterable.java | 73 -- .../kohsuke/github/GraphQLSearchBuilder.java | 57 -- .../kohsuke/github/GraphQLSearchResult.java | 18 - .../github/GraphQLSearchVariables.java | 19 - .../java/org/kohsuke/github/GraphQLTeam.java | 31 - .../GraphQLTeamByMemberSearchBuilder.java | 82 -- .../GraphQLTeamByMemberSearchVariables.java | 18 - .../kohsuke/github/GraphQLTeamConnection.java | 9 - .../org/kohsuke/github/GraphQLTeamEdge.java | 9 - .../github/GraphQLTeamMemberConnection.java | 9 - .../kohsuke/github/GraphQLTeamMemberEdge.java | 13 - .../kohsuke/github/GraphQLTeamMemberRole.java | 6 - .../kohsuke/github/GraphQLTeamPrivacy.java | 6 - .../github/GraphQLTeamSearchBuilder.java | 83 -- .../github/GraphQLTeamSearchVariables.java | 18 - .../java/org/kohsuke/github/GraphQLUser.java | 14 - .../github/GraphQLUserEmailMetadata.java | 14 - .../org/kohsuke/github/SCIMPageIterator.java | 5 +- .../github/SCIMPagedSearchIterable.java | 4 +- .../kohsuke/github/SCIMUserSearchBuilder.java | 4 +- .../connector/github/CreateUserOpTest.java | 10 +- .../connector/github/DeleteUsersOpTest.java | 8 +- .../GitHubClientDefaultsUnsupportedTest.java | 195 +++++ .../connector/github/GitHubClientTest.java | 113 +++ .../github/GitHubEMUUserHandlerTest.java | 78 ++ .../connector/github/GitHubFilterTest.java | 91 +++ .../github/GitHubFilterTranslatorTest.java | 114 +++ .../connector/github/GitHubUtilsTest.java | 17 - .../connector/github/SchemaTest.java | 42 - .../connector/github/SearchGroupsOpTest.java | 18 +- .../connector/github/SearchSeatsOpTest.java | 21 +- .../connector/github/SearchUsersOpTest.java | 19 +- .../github/TeamAssignmentResolverTest.java | 185 ----- .../connector/github/TestOpTest.java | 7 +- .../connector/github/UpdateGroupsOpTest.java | 23 +- .../connector/github/UpdateUsersOpTest.java | 23 +- .../github/testutil/AbstractEMUTest.java | 5 +- .../github/testutil/AbstractTest.java | 52 -- .../github/testutil/LocalGitHubConnector.java | 27 - .../testutil/LocalGitHubEMUConnector.java | 25 - .../connector/github/testutil/MockClient.java | 49 +- .../kohsuke/github/GHEnterpriseExtTest.java | 343 +++++++++ .../github/GitHubCopilotSeatAssigneeTest.java | 44 ++ .../GitHubCopilotSeatAssigningTeamTest.java | 61 ++ .../GitHubCopilotSeatPageIteratorTest.java | 274 +++++++ ...HubCopilotSeatPagedSearchIterableTest.java | 126 +++ .../kohsuke/github/GitHubCopilotSeatTest.java | 46 ++ .../GitHubCopilotSeatsSearchBuilderTest.java | 118 +++ .../GitHubCopilotSeatsSearchResultTest.java | 76 ++ .../github/GitHubEMUUserHandlerTest.java | 184 +++++ .../org/kohsuke/github/GitHubExtTest.java | 29 + .../org/kohsuke/github/SCIMEMUGroupTest.java | 75 ++ .../java/org/kohsuke/github/SCIMNameTest.java | 37 + .../org/kohsuke/github/SCIMOperationTest.java | 48 ++ .../kohsuke/github/SCIMPageIteratorTest.java | 236 ++++++ .../github/SCIMPagedSearchIterableTest.java | 218 ++++++ .../github/SCIMPatchOperationsTest.java | 163 ++++ .../kohsuke/github/SCIMSearchResultTest.java | 64 ++ .../github/SCIMUserSearchBuilderTest.java | 42 + .../java/org/kohsuke/github/SCIMUserTest.java | 50 ++ .../org/kohsuke/github/TestableGitHubExt.java | 21 + src/test/java/util/SchemaDefinitionTest.java | 256 +++++++ 98 files changed, 3438 insertions(+), 3860 deletions(-) delete mode 100644 src/main/java/com/spotify/github/v3/clients/PKCS1PEMKey.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubConfiguration.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubConnector.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubSchema.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubTeamHandler.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubUserHandler.java delete mode 100644 src/main/java/jp/openstandia/connector/github/GitHubUtils.java delete mode 100644 src/main/java/jp/openstandia/connector/github/TeamAssignmentResolver.java delete mode 100644 src/main/java/jp/openstandia/connector/github/rest/GitHubRESTClient.java delete mode 100644 src/main/java/org/kohsuke/github/GHOrganizationExt.java delete mode 100644 src/main/java/org/kohsuke/github/GHTeamExt.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLConnection.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLEdge.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLExternalIdentity.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLExternalIdentityConnection.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLExternalIdentityEdge.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLExternalIdentitySamlAttributes.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLExternalIdentityScimAttributes.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLNode.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLOrganization.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLOrganizationExternalIdentitySearchBuilder.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLOrganizationIdentityProvider.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLOrganizationInvitation.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLPageInfo.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLPageIterator.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLPagedSearchIterable.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLSearchBuilder.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLSearchResult.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLSearchVariables.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeam.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchBuilder.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchVariables.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamConnection.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamEdge.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamMemberConnection.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamMemberEdge.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamMemberRole.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamPrivacy.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamSearchBuilder.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLTeamSearchVariables.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLUser.java delete mode 100644 src/main/java/org/kohsuke/github/GraphQLUserEmailMetadata.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubClientDefaultsUnsupportedTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubClientTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubFilterTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubFilterTranslatorTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/GitHubUtilsTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/SchemaTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/TeamAssignmentResolverTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/testutil/AbstractTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubConnector.java delete mode 100644 src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubEMUConnector.java create mode 100644 src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigneeTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeamTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchResultTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubExtTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMNameTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMOperationTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMPatchOperationsTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMSearchResultTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMUserSearchBuilderTest.java create mode 100644 src/test/java/org/kohsuke/github/SCIMUserTest.java create mode 100644 src/test/java/org/kohsuke/github/TestableGitHubExt.java create mode 100644 src/test/java/util/SchemaDefinitionTest.java diff --git a/pom.xml b/pom.xml index e869143..944d0e6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,8 +6,7 @@ connector-parent com.evolveum.polygon - 1.5.0.0 - + 1.5.2.0 jp.openstandia.connector @@ -49,6 +48,10 @@ jp.openstandia.connector.github GitHubConnector + 0.8.13 + 5.13.4 + 3.2.5 + @@ -73,10 +76,6 @@ org.apache.maven.plugins maven-compiler-plugin - - 9 - 9 - org.apache.maven.plugins @@ -157,6 +156,46 @@ + + org.jacoco + jacoco-maven-plugin + ${jacoco.plugin.version} + + + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.90 + + + + + + + + @@ -164,12 +203,24 @@ + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + org.kohsuke github-api 1.122 compile + + org.mockito + mockito-core + 5.20.0 + test + io.jsonwebtoken jjwt-api @@ -210,173 +261,163 @@ - - - midpoint48 - - - com.evolveum.midpoint.gui - admin-gui - 4.8.4 - provided - - - - ch.qos.logback - logback-classic - 1.5.6 - provided - - - ch.qos.logback - logback-core - 1.5.6 - provided - - - - com.fasterxml.jackson.core - jackson-annotations - 2.17.2 - provided - - - com.fasterxml.jackson.core - jackson-databind - 2.17.2 - provided - - - org.testng - testng - test - - - org.yaml - snakeyaml - - - - - net.tirasa.connid - connector-framework - 1.5.2.0 - provided - - - net.tirasa.connid - connector-framework-internal - 1.5.2.0 - provided - - - - - midpoint44 - - - com.evolveum.midpoint.gui - admin-gui - jar - classes - 4.4.9 - provided - - - org.testng - testng - test - - - org.yaml - snakeyaml - - - - - net.tirasa.connid - connector-framework - 1.5.1.10 - provided - - - net.tirasa.connid - connector-framework-internal - 1.5.1.10 - provided - - - net.tirasa.connid - connector-framework-contract - 1.5.1.10 - test - - - - - midpoint40 - - - evolveum-nexus-releases - Internal Releases - https://nexus.evolveum.com/nexus/content/repositories/releases/ - - - evolveum-nexus-snapshots - Internal Releases - https://nexus.evolveum.com/nexus/content/repositories/snapshots/ - - - - jaspersoft-third-party - https://jaspersoft.jfrog.io/jaspersoft/third-party-ce-artifacts - - - - - com.evolveum.midpoint.gui - admin-gui - jar - classes - 4.0.4 - provided - - - org.testng - testng - test - - - org.yaml - snakeyaml - - - - - net.tirasa.connid - connector-framework - 1.5.0.10 - provided - - - net.tirasa.connid - connector-framework-internal - 1.5.0.10 - provided - - - net.tirasa.connid - connector-framework-contract - 1.5.0.10 - test - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/spotify/github/v3/clients/PKCS1PEMKey.java b/src/main/java/com/spotify/github/v3/clients/PKCS1PEMKey.java deleted file mode 100644 index b29112b..0000000 --- a/src/main/java/com/spotify/github/v3/clients/PKCS1PEMKey.java +++ /dev/null @@ -1,88 +0,0 @@ -/*- - * -\-\- - * github-api - * -- - * Copyright (C) 2016 - 2020 Spotify AB - * -- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -/-/- - */ - -package com.spotify.github.v3.clients; - -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Base64; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Loads PEM key files as issued by the Github apps page. - */ -public class PKCS1PEMKey { - - private static final Pattern PKCS1_PEM_KEY_PATTERN = - Pattern.compile("(?m)(?s)^---*BEGIN RSA PRIVATE KEY.*---*$(.*)^---*END.*---*$.*"); - private static final Pattern PKCS1_PEM_KEY_ONE_LINE_PATTERN = - Pattern.compile("^---*BEGIN RSA PRIVATE KEY.*---* (.*) ---*END.*---*$"); - - private PKCS1PEMKey() { - } - - /** - * Try to interpret the supplied key as a PKCS#1 PEM file. - * - * @param privateKey the private key to use - * @return PKCS#8 encoded key spec - */ - public static Optional loadKeySpec(final byte[] privateKey) { - Matcher isPEM = PKCS1_PEM_KEY_PATTERN.matcher(new String(privateKey)); - if (!isPEM.matches()) { - isPEM = PKCS1_PEM_KEY_ONE_LINE_PATTERN.matcher(new String(privateKey)); - if (!isPEM.matches()) { - return Optional.empty(); - } - } - - byte[] pkcs1Key = Base64.getMimeDecoder().decode(isPEM.group(1)); - byte[] pkcs8Key = toPkcs8(pkcs1Key); - final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Key); - return Optional.of(keySpec); - } - - /** - * Convert a PKCS#1 key to a PKCS#8 key. - * - *

The Github app key comes in PKCS#1 format, while the Java security utilities only natively - * understand PKCS#8. Fortunately, we can convert between the two by adding the PKCS#8 headers - * manually. - * - *

Adapted from code in https://github.com/Mastercard/client-encryption-java - */ - @SuppressWarnings("checkstyle:magicnumber") - private static byte[] toPkcs8(final byte[] pkcs1Bytes) { - final int pkcs1Length = pkcs1Bytes.length; - final int totalLength = pkcs1Length + 22; - byte[] pkcs8Header = new byte[]{ - 0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), (byte) (totalLength & 0xff), // Sequence + total length - 0x2, 0x1, 0x0, // Integer (0) - 0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, // Sequence: 1.2.840.113549.1.1.1, NULL - 0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) // Octet string + length - }; - - byte[] pkcs8bytes = new byte[pkcs8Header.length + pkcs1Bytes.length]; - System.arraycopy(pkcs8Header, 0, pkcs8bytes, 0, pkcs8Header.length); - System.arraycopy(pkcs1Bytes, 0, pkcs8bytes, pkcs8Header.length, pkcs1Bytes.length); - return pkcs8bytes; - } -} diff --git a/src/main/java/jp/openstandia/connector/github/AbstractGitHubConnector.java b/src/main/java/jp/openstandia/connector/github/AbstractGitHubConnector.java index 3978b84..b58451b 100644 --- a/src/main/java/jp/openstandia/connector/github/AbstractGitHubConnector.java +++ b/src/main/java/jp/openstandia/connector/github/AbstractGitHubConnector.java @@ -105,7 +105,7 @@ private ObjectHandler getSchemaHandler(ObjectClass objectClass) { if (handler == null) { throw new InvalidAttributeValueException("Unsupported object class " + objectClass); } - + handler.setInstanceName(instanceName); return handler; @@ -274,4 +274,4 @@ protected ConnectorException processRuntimeException(RuntimeException e) { } return new ConnectorException(e); } -} +} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/github/GitHubClient.java b/src/main/java/jp/openstandia/connector/github/GitHubClient.java index 7629d1a..c6d45bc 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubClient.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubClient.java @@ -79,77 +79,6 @@ default OkHttpClient createClient(AbstractGitHubConfiguration configuration) { void close(); - // User - - default Uid createUser(T schema, SCIMUser scimUser) throws AlreadyExistsException { - throw new UnsupportedOperationException(); - } - - default String updateUser(T schema, Uid uid, String scimUserName, String scimEmail, String scimGivenName, String scimFamilyName, String login, OperationOptions options) throws UnknownUidException { - throw new UnsupportedOperationException(); - } - - default void deleteUser(T schema, Uid uid, OperationOptions options) throws UnknownUidException { - throw new UnsupportedOperationException(); - } - - default void getUsers(T schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } - - default void getUser(T schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } - - default void getUser(T schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } - - // Team - - default List getTeamIdsByUsername(String userLogin, int pageSize) { - throw new UnsupportedOperationException(); - } - - default boolean isOrganizationMember(String userLogin) { - throw new UnsupportedOperationException(); - } - - default void assignOrganizationRole(String userLogin, String organizationRole) { - throw new UnsupportedOperationException(); - } - - default void assignTeams(String login, String role, Collection teams) { - throw new UnsupportedOperationException(); - } - - default void unassignTeams(String login, Collection teams) { - throw new UnsupportedOperationException(); - } - - default Uid createTeam(T schema, String teamName, String description, String privacy, Long parentTeamDatabaseId) throws AlreadyExistsException { - throw new UnsupportedOperationException(); - } - - default Uid updateTeam(T schema, Uid uid, String teamName, String description, String privacy, Long parentTeamId, boolean clearParent, OperationOptions options) throws UnknownUidException { - throw new UnsupportedOperationException(); - } - - default void deleteTeam(T schema, Uid uid, OperationOptions options) throws UnknownUidException { - throw new UnsupportedOperationException(); - } - - default void getTeams(T schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } - - default void getTeam(T schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } - - default void getTeam(T schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - throw new UnsupportedOperationException(); - } // EMU User diff --git a/src/main/java/jp/openstandia/connector/github/GitHubConfiguration.java b/src/main/java/jp/openstandia/connector/github/GitHubConfiguration.java deleted file mode 100644 index c7994c7..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubConfiguration.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import org.identityconnectors.common.security.GuardedString; -import org.identityconnectors.framework.spi.ConfigurationProperty; - -/** - * Connector Configuration implementation for GitHub connector. - * - * @author Hiroyuki Wada - */ -public class GitHubConfiguration extends AbstractGitHubConfiguration { - - private GuardedString privateKey; - private String appId; - private long installationId; - private String organizationName; - - @ConfigurationProperty( - order = 1, - displayMessageKey = "Private Key (PEM)", - helpMessageKey = "Set Private Key with PEM format for GitHub API.", - required = true, - confidential = true) - public GuardedString getPrivateKey() { - return privateKey; - } - - public void setPrivateKey(GuardedString privateKey) { - this.privateKey = privateKey; - } - - @ConfigurationProperty( - order = 2, - displayMessageKey = "App ID", - helpMessageKey = "Set App ID for GitHub.", - required = true, - confidential = false) - public String getAppId() { - return appId; - } - - public void setAppId(String appId) { - this.appId = appId; - } - - @ConfigurationProperty( - order = 3, - displayMessageKey = "Installation ID", - helpMessageKey = "Set Installation ID for GitHub.", - required = true, - confidential = true) - public long getInstallationId() { - return installationId; - } - - public void setInstallationId(long installationId) { - this.installationId = installationId; - } - - @ConfigurationProperty( - order = 4, - displayMessageKey = "Organization Name", - helpMessageKey = "Set GitHub organization name.", - required = true, - confidential = false) - public String getOrganizationName() { - return organizationName; - } - - public void setOrganizationName(String organizationName) { - this.organizationName = organizationName; - } - - @Override - public void validate() { - } -} diff --git a/src/main/java/jp/openstandia/connector/github/GitHubConnector.java b/src/main/java/jp/openstandia/connector/github/GitHubConnector.java deleted file mode 100644 index 832baa6..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubConnector.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.rest.GitHubRESTClient; -import org.identityconnectors.common.logging.Log; -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.identityconnectors.framework.common.objects.ObjectClass; -import org.identityconnectors.framework.spi.ConnectorClass; - -import static jp.openstandia.connector.github.GitHubTeamHandler.TEAM_OBJECT_CLASS; -import static jp.openstandia.connector.github.GitHubUserHandler.USER_OBJECT_CLASS; - -/** - * Connector implementation for GitHub connector. - * - * @author Hiroyuki Wada - */ -@ConnectorClass(configurationClass = GitHubConfiguration.class, displayNameKey = "NRI OpenStandia GitHub Connector") -public class GitHubConnector extends AbstractGitHubConnector { - - private static final Log LOG = Log.getLog(GitHubConnector.class); - - @Override - protected GitHubClient newClient(GitHubConfiguration configuration) { - return new GitHubRESTClient(configuration); - } - - @Override - protected GitHubSchema newGitHubSchema(GitHubConfiguration configuration, GitHubClient client) { - return new GitHubSchema(configuration, client); - } -} diff --git a/src/main/java/jp/openstandia/connector/github/GitHubEMUConnector.java b/src/main/java/jp/openstandia/connector/github/GitHubEMUConnector.java index 89dc55f..0c5cbea 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubEMUConnector.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubEMUConnector.java @@ -17,12 +17,8 @@ import jp.openstandia.connector.github.rest.GitHubEMURESTClient; import org.identityconnectors.common.logging.Log; -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.spi.ConnectorClass; -import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; -import static jp.openstandia.connector.github.GitHubUserHandler.USER_OBJECT_CLASS; /** * Connector implementation for GitHub EMU connector. diff --git a/src/main/java/jp/openstandia/connector/github/GitHubEMUUserHandler.java b/src/main/java/jp/openstandia/connector/github/GitHubEMUUserHandler.java index 2258eba..38f7511 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubEMUUserHandler.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubEMUUserHandler.java @@ -21,6 +21,7 @@ import org.kohsuke.github.*; import java.util.ArrayList; +import java.util.Collections; import java.util.Set; import java.util.stream.Stream; @@ -55,7 +56,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration sb.addUid("userId", SchemaDefinition.Types.UUID, null, - (source) -> source.id, + source -> source.id, "id", NOT_CREATABLE, NOT_UPDATEABLE ); @@ -66,7 +67,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration SchemaDefinition.Types.STRING_CASE_IGNORE, (source, dest) -> dest.userName = source, (source, dest) -> dest.replace("userName", source), - (source) -> source.userName, + source -> source.userName, null, REQUIRED ); @@ -76,28 +77,24 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration SchemaDefinition.Types.BOOLEAN, (source, dest) -> dest.active = source, (source, dest) -> dest.replace("active", source), - (source) -> source.active, + source -> source.active, "active" ); // Attributes sb.add("externalId", SchemaDefinition.Types.STRING, - (source, dest) -> { - dest.externalId = source; - }, + (source, dest) -> dest.externalId = source, (source, dest) -> dest.replace("externalId", source), - (source) -> source.externalId, + source -> source.externalId, null, REQUIRED ); sb.add("displayName", SchemaDefinition.Types.STRING, - (source, dest) -> { - dest.displayName = source; - }, + (source, dest) -> dest.displayName = source, (source, dest) -> dest.replace("displayName", source), - (source) -> source.displayName, + source -> source.displayName, null ); sb.add("name.formatted", @@ -109,7 +106,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration dest.name.formatted = source; }, (source, dest) -> dest.replace("name.formatted", source), - (source) -> source.name != null ? source.name.formatted : null, + source -> source.name != null ? source.name.formatted : null, null ); sb.add("name.givenName", @@ -121,7 +118,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration dest.name.givenName = source; }, (source, dest) -> dest.replace("name.givenName", source), - (source) -> source.name != null ? source.name.givenName : null, + source -> source.name != null ? source.name.givenName : null, null ); sb.add("name.familyName", @@ -133,7 +130,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration dest.name.familyName = source; }, (source, dest) -> dest.replace("name.familyName", source), - (source) -> source.name != null ? source.name.familyName : null, + source -> source.name != null ? source.name.familyName : null, null ); // SCIM schema has "emails", but we define "primaryEmail" as single value here for easy mapping in IDM @@ -161,7 +158,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration newEmail.primary = true; dest.replace(newEmail); }, - (source) -> source.emails != null && !source.emails.isEmpty() ? source.emails.get(0).value : null, + source -> source.emails != null && !source.emails.isEmpty() ? source.emails.get(0).value : null, null, REQUIRED ); @@ -190,7 +187,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration newRole.primary = true; dest.replace(newRole); }, - (source) -> source.roles != null && !source.roles.isEmpty() ? source.roles.get(0).value : null, + source -> source.roles != null && !source.roles.isEmpty() ? source.roles.get(0).value : null, null ); @@ -201,7 +198,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration null, null, null, - (source) -> source.groups != null ? source.groups.stream().filter(x -> x.ref.contains("/Groups/")).map(x -> x.value) : Stream.empty(), + source -> source.groups != null ? source.groups.stream().filter(x -> x.ref.contains("/Groups/")).map(x -> x.value) : Stream.empty(), null, NOT_CREATABLE, NOT_UPDATEABLE, NOT_RETURNED_BY_DEFAULT ); @@ -210,14 +207,14 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration sb.add("meta.created", SchemaDefinition.Types.DATETIME, null, - (source) -> source.meta != null ? toZoneDateTimeForISO8601OffsetDateTime(source.meta.created) : null, + source -> source.meta != null ? toZoneDateTimeForISO8601OffsetDateTime(source.meta.created) : null, null, NOT_CREATABLE, NOT_UPDATEABLE ); sb.add("meta.lastModified", SchemaDefinition.Types.DATETIME, null, - (source) -> source.meta != null ? toZoneDateTimeForISO8601OffsetDateTime(source.meta.lastModified) : null, + source -> source.meta != null ? toZoneDateTimeForISO8601OffsetDateTime(source.meta.lastModified) : null, null, NOT_CREATABLE, NOT_UPDATEABLE ); @@ -232,9 +229,7 @@ public Uid create(Set attributes) { SCIMEMUUser user = new SCIMEMUUser(); SCIMEMUUser mapped = schemaDefinition.apply(attributes, user); - Uid created = client.createEMUUser(mapped); - - return created; + return client.createEMUUser(mapped); } @Override @@ -247,7 +242,7 @@ public Set updateDelta(Uid uid, Set modification client.patchEMUUser(uid, dest); } - return null; + return Collections.emptySet(); } @Override @@ -285,7 +280,7 @@ public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions public int getAll(ResultsHandler resultsHandler, OperationOptions options, Set returnAttributesSet, Set fetchFieldsSet, boolean allowPartialAttributeValues, int pageSize, int pageOffset) { - return client.getEMUUsers((u) -> resultsHandler.handle(toConnectorObject(schemaDefinition, u, returnAttributesSet, allowPartialAttributeValues)), + return client.getEMUUsers(u -> resultsHandler.handle(toConnectorObject(schemaDefinition, u, returnAttributesSet, allowPartialAttributeValues)), options, fetchFieldsSet, pageSize, pageOffset); } } diff --git a/src/main/java/jp/openstandia/connector/github/GitHubSchema.java b/src/main/java/jp/openstandia/connector/github/GitHubSchema.java deleted file mode 100644 index 1805bd7..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubSchema.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.spi.operations.SearchOp; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Schema for GitHub objects. - * - * @author Hiroyuki Wada - */ -public class GitHubSchema extends AbstractGitHubSchema { - - public final Schema schema; - public final Map userSchema; - public final Map roleSchema; - - public GitHubSchema(GitHubConfiguration configuration, GitHubClient client) { - super(configuration, client); - - ObjectClassInfo userSchemaInfo = GitHubUserHandler.getUserSchema(); - ObjectClassInfo roleSchemaInfo = GitHubTeamHandler.getRoleSchema(); - - SchemaBuilder schemaBuilder = new SchemaBuilder(GitHubConnector.class); - - buildSchema(schemaBuilder, userSchemaInfo, - (objectClassInfo) -> new GitHubUserHandler(configuration, client, this)); - buildSchema(schemaBuilder, roleSchemaInfo, - (objectClassInfo) -> new GitHubTeamHandler(configuration, client, this)); - - // Define operation options - schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildAttributesToGet(), SearchOp.class); - schemaBuilder.defineOperationOption(OperationOptionInfoBuilder.buildReturnDefaultAttributes(), SearchOp.class); - - this.schema = schemaBuilder.build(); - - Map userSchemaMap = new HashMap<>(); - for (AttributeInfo info : userSchemaInfo.getAttributeInfo()) { - userSchemaMap.put(info.getName(), info); - } - - Map roleSchemaMp = new HashMap<>(); - for (AttributeInfo info : roleSchemaInfo.getAttributeInfo()) { - roleSchemaMp.put(info.getName(), info); - } - - this.userSchema = Collections.unmodifiableMap(userSchemaMap); - this.roleSchema = Collections.unmodifiableMap(roleSchemaMp); - } - - @Override - public Schema getSchema() { - return schema; - } -} diff --git a/src/main/java/jp/openstandia/connector/github/GitHubTeamHandler.java b/src/main/java/jp/openstandia/connector/github/GitHubTeamHandler.java deleted file mode 100644 index a639416..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubTeamHandler.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import org.identityconnectors.common.logging.Log; -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.identityconnectors.framework.common.objects.*; - -import java.util.HashSet; -import java.util.Set; - -import static jp.openstandia.connector.github.GitHubUtils.*; - -/** - * Handle GitHub Team object. - * - * @author Hiroyuki Wada - */ -public class GitHubTeamHandler extends AbstractGitHubHandler { - - public static final ObjectClass TEAM_OBJECT_CLASS = new ObjectClass("team"); - - private static final Log LOGGER = Log.getLog(GitHubTeamHandler.class); - - // Unique and unchangeable. - // Don't use "id" here because it conflicts midpoint side. - // The format is :. - private static final String ATTR_TEAM_DATABASE_ID_WITH_NODE_ID = "teamId"; - - // Unique and changeable. - private static final String ATTR_TEAM_NAME = "name"; - - // Attributes - public static final String ATTR_DESCRIPTION = "description"; - public static final String ATTR_PRIVACY = "privacy"; // secret, visible(closed in REST API) - - // Readonly - // Unique and unchangeable (generated). - public static final String ATTR_TEAM_DATABASE_ID = "databaseId"; - // Unique and changeable (generated from name). - public static final String ATTR_SLUG = "slug"; - // Unique and unchangeable (generated). - public static final String ATTR_TEAM_NODE_ID = "nodeId"; - - // Association - public static final String ATTR_PARENT_TEAM_ID = "parentTeamId"; - - public GitHubTeamHandler(GitHubConfiguration configuration, GitHubClient client, - GitHubSchema schema) { - super(configuration, client, schema); - } - - public static ObjectClassInfo getRoleSchema() { - ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); - builder.setType(TEAM_OBJECT_CLASS.getObjectClassValue()); - - // id (__UID__) - builder.addAttributeInfo( - AttributeInfoBuilder.define(Uid.NAME) - .setRequired(false) // Must be optional. It is not present for create operations - .setCreateable(false) - .setUpdateable(false) - .setNativeName(ATTR_TEAM_DATABASE_ID_WITH_NODE_ID) - .build()); - - // name (__NAME__) - builder.addAttributeInfo( - AttributeInfoBuilder.define(Name.NAME) - .setRequired(true) - .setNativeName(ATTR_TEAM_NAME) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .build()); - - // Attributes - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_DESCRIPTION) - .setRequired(false) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_PRIVACY) - .setRequired(false) - .build()); - - // Readonly - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_TEAM_DATABASE_ID) - .setRequired(false) - .setCreateable(false) - .setUpdateable(false) - .setType(Long.class) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SLUG) - .setRequired(false) - .setCreateable(false) - .setUpdateable(false) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_TEAM_NODE_ID) - .setRequired(false) - .setCreateable(false) - .setUpdateable(false) - .build()); - - // Association - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_PARENT_TEAM_ID) - .setRequired(false) - // Association value is expected as string value in midPoint - // https://github.com/Evolveum/midpoint/blob/50f01966cfa6c2df458f218c255cc2e0d0631b39/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/shadowmanager/ShadowManager.java#L554 - //.setType(Long.class) - .build()); - - ObjectClassInfo schemaInfo = builder.build(); - - LOGGER.ok("The constructed GitHub Team schema: {0}", schemaInfo); - - return schemaInfo; - } - - @Override - public Uid create(Set attributes) { - String name = null; - String description = null; - String privacy = null; - Long parentTeamDatabaseId = null; - - for (Attribute attr : attributes) { - if (attr.is(Name.NAME)) { - name = AttributeUtil.getStringValue(attr); - - } else if (attr.is(ATTR_DESCRIPTION)) { - description = AttributeUtil.getStringValue(attr); - - } else if (attr.is(ATTR_PRIVACY)) { - privacy = AttributeUtil.getStringValue(attr); - - } else if (attr.is(ATTR_PARENT_TEAM_ID)) { - String s = AttributeUtil.getStringValue(attr); - parentTeamDatabaseId = getTeamDatabaseId(s); - } - } - - if (name == null) { - throw new InvalidAttributeValueException("GitHub Team name is required"); - } - - return client.createTeam(schema, name, description, privacy, parentTeamDatabaseId); - } - - @Override - public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { - String name = null; - String description = null; - String privacy = null; - Long parentTeamId = null; - boolean clearParent = false; - - for (AttributeDelta attr : modifications) { - if (attr.is(Name.NAME)) { - name = AttributeDeltaUtil.getStringValue(attr); - - } else if (attr.is(ATTR_DESCRIPTION)) { - description = toResourceAttributeValue(AttributeDeltaUtil.getStringValue(attr)); - - } else if (attr.is(ATTR_PRIVACY)) { - privacy = AttributeDeltaUtil.getStringValue(attr); - - } else if (attr.is(ATTR_PARENT_TEAM_ID)) { - String s = AttributeDeltaUtil.getStringValue(attr); - if (s != null) { - parentTeamId = getTeamDatabaseId(s); - } else { - clearParent = true; - } - } - } - - Uid updated = client.updateTeam(schema, uid, name, description, privacy, parentTeamId, clearParent, options); - - // Detected changed NAME(slug) - if (!uid.getNameHintValue().equals(updated.getNameHintValue())) { - AttributeDelta newName = AttributeDeltaBuilder.build(Name.NAME, updated.getNameHintValue()); - Set sideEffects = new HashSet<>(); - sideEffects.add(newName); - - return sideEffects; - } - - return null; - } - - public void delete(Uid uid, OperationOptions options) { - client.deleteTeam(schema, uid, options); - } - - @Override - public void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options) { - // Create full attributesToGet by RETURN_DEFAULT_ATTRIBUTES + ATTRIBUTES_TO_GET - Set attributesToGet = createFullAttributesToGet(schema.roleSchema, options); - boolean allowPartialAttributeValues = shouldAllowPartialAttributeValues(options); - - if (filter == null) { - client.getTeams(schema, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } else { - if (filter.isByUid()) { - client.getTeam(schema, filter.uid, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } else { - client.getTeam(schema, filter.name, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } - } - } -} diff --git a/src/main/java/jp/openstandia/connector/github/GitHubUserHandler.java b/src/main/java/jp/openstandia/connector/github/GitHubUserHandler.java deleted file mode 100644 index 53db467..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubUserHandler.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import org.identityconnectors.common.logging.Log; -import org.identityconnectors.framework.common.objects.*; -import org.kohsuke.github.SCIMEmail; -import org.kohsuke.github.SCIMName; -import org.kohsuke.github.SCIMUser; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import static jp.openstandia.connector.github.GitHubUtils.*; - -/** - * Handle GitHub user object. - * - * @author Hiroyuki Wada - */ -public class GitHubUserHandler extends AbstractGitHubHandler { - - public static final ObjectClass USER_OBJECT_CLASS = new ObjectClass("user"); - - private static final Log LOGGER = Log.getLog(GitHubUserHandler.class); - - // Unique and unchangeable. This is SCIM user id. - // Don't use "id" here because it conflicts midpoint side. - private static final String ATTR_USER_ID = "scimUserId"; - - // Unique and changeable. This is GitHub login(username) and scimUserName(login:scimUserName). - public static final String ATTR_USER_NAME = "userName"; - - // Attributes - public static final String ATTR_SCIM_USER_NAME = "scimUserName"; - public static final String ATTR_SCIM_EMAIL = "scimEmail"; - public static final String ATTR_SCIM_GIVEN_NAME = "scimGivenName"; - public static final String ATTR_SCIM_FAMILY_NAME = "scimFamilyName"; - public static final String ATTR_SCIM_EXTERNAL_ID = "scimExternalId"; - public static final String ATTR_ORGANIZATION_ROLE = "organizationRole"; - - // Readonly - // Only fetched by GraphQL ExternalIdentity through all users query due to GitHub API limitation. - public static final String ATTR_USER_LOGIN = "login"; - - // Association - public static final String ATTR_TEAMS = "teams"; // List of teamId(databaseId:nodeId) - public static final String ATTR_MAINTAINER_TEAMS = "maintainerTeams"; // List of teamId(databaseId:nodeId) - - public GitHubUserHandler(GitHubConfiguration configuration, GitHubClient client, - GitHubSchema schema) { - super(configuration, client, schema); - } - - public static ObjectClassInfo getUserSchema() { - ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); - builder.setType(USER_OBJECT_CLASS.getObjectClassValue()); - - // scimUserId (__UID__) - builder.addAttributeInfo( - AttributeInfoBuilder.define(Uid.NAME) - .setRequired(false) // Must be optional. It is not present for create operations - .setCreateable(false) - .setUpdateable(false) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .setNativeName(ATTR_USER_ID) - .build()); - - // userName (__NAME__) - builder.addAttributeInfo( - AttributeInfoBuilder.define(Name.NAME) - .setRequired(true) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .setNativeName(ATTR_USER_NAME) - .build()); - - // attributes - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SCIM_EMAIL) - .setRequired(true) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SCIM_GIVEN_NAME) - .setRequired(true) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SCIM_FAMILY_NAME) - .setRequired(true) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SCIM_EXTERNAL_ID) - .setRequired(false) - .build()); - - // Readonly - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_USER_LOGIN) - .setRequired(false) - .setCreateable(false) - .setUpdateable(false) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_SCIM_USER_NAME) - .setRequired(false) - .setCreateable(false) - .setUpdateable(false) - .setSubtype(AttributeInfo.Subtypes.STRING_CASE_IGNORE) - .build()); - - // Association - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_TEAMS) - .setRequired(false) - .setMultiValued(true) - // We define the team's UID as string with : format - // .setType(Integer.class) - .setReturnedByDefault(false) - .build()); - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_MAINTAINER_TEAMS) - .setRequired(false) - .setMultiValued(true) - // We define the team's UID as string with : format - // .setType(Integer.class) - .setReturnedByDefault(false) - .build()); - // TODO: Implement Organization Role schema? - builder.addAttributeInfo( - AttributeInfoBuilder.define(ATTR_ORGANIZATION_ROLE) - .setRequired(false) - .setReturnedByDefault(false) - .build()); - - ObjectClassInfo userSchemaInfo = builder.build(); - - LOGGER.ok("The constructed GitHub user schema: {0}", userSchemaInfo); - - return userSchemaInfo; - } - - @Override - public Uid create(Set attributes) { - SCIMUser newUser = new SCIMUser(); - newUser.name = new SCIMName(); - - for (Attribute attr : attributes) { - if (attr.is(Name.NAME)) { - String loginWithScimUserName = AttributeUtil.getStringValue(attr); - // Throw InvalidAttributeValueException if invalid format - newUser.userName = getUserSCIMUserName(loginWithScimUserName); - - } else if (attr.is(ATTR_SCIM_EMAIL)) { - SCIMEmail scimEmail = new SCIMEmail(); - scimEmail.value = AttributeUtil.getStringValue(attr); - newUser.emails = new SCIMEmail[]{scimEmail}; - - } else if (attr.is(ATTR_SCIM_GIVEN_NAME)) { - newUser.name.givenName = AttributeUtil.getStringValue(attr); - - } else if (attr.is(ATTR_SCIM_FAMILY_NAME)) { - newUser.name.familyName = AttributeUtil.getStringValue(attr); - - } else if (attr.is(ATTR_SCIM_EXTERNAL_ID)) { - newUser.externalId = AttributeUtil.getStringValue(attr); - } - } - - Uid created = client.createUser(schema, newUser); - - // Association can't be constructed here because GitHub login is unknown yet. - - return created; - } - - @Override - public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { - String login = null; - String scimUserName = null; - String scimEmail = null; - String scimGivenName = null; - String scimFamilyName = null; - String organizationRole = null; - Set addTeams = new HashSet<>(); - Set removeTeams = new HashSet<>(); - Set addMaintainerTeams = new HashSet<>(); - Set removeMaintainerTeams = new HashSet<>(); - - for (AttributeDelta attr : modifications) { - if (attr.is(Name.NAME)) { - // Detected modifying userName (e.g. completed the invitation by full reconciliation, update scimUserName) - String newLoginWithScimUserName = AttributeDeltaUtil.getStringValue(attr); - - // Detect scimUserName change - String newScimUserName = getUserSCIMUserName(newLoginWithScimUserName); - String oldScimUserName = getUserSCIMUserName(uid); - if (!newScimUserName.equals(oldScimUserName)) { - scimUserName = newScimUserName; - } - - // Detect user login change - String newLogin = getUserLogin(newLoginWithScimUserName); - String oldLogin = getUserLogin(uid); - if (!newLogin.equals(oldLogin)) { - login = newLogin; - } - - } else if (attr.is(ATTR_SCIM_EMAIL)) { - scimEmail = AttributeDeltaUtil.getStringValue(attr); - - } else if (attr.is(ATTR_SCIM_GIVEN_NAME)) { - scimGivenName = AttributeDeltaUtil.getStringValue(attr); - - } else if (attr.is(ATTR_SCIM_FAMILY_NAME)) { - scimFamilyName = AttributeDeltaUtil.getStringValue(attr); - - } else if (attr.is(ATTR_ORGANIZATION_ROLE)) { - organizationRole = toResourceAttributeValue(AttributeDeltaUtil.getStringValue(attr), "member"); - - } else if (attr.is(ATTR_TEAMS)) { - if (attr.getValuesToAdd() != null) { - addTeams = attr.getValuesToAdd().stream().map(v -> v.toString()).collect(Collectors.toSet()); - } - if (attr.getValuesToRemove() != null) { - removeTeams = attr.getValuesToRemove().stream().map(v -> v.toString()).collect(Collectors.toSet()); - } - - } else if (attr.is(ATTR_MAINTAINER_TEAMS)) { - if (attr.getValuesToAdd() != null) { - addMaintainerTeams = attr.getValuesToAdd().stream().map(v -> v.toString()).collect(Collectors.toSet()); - } - if (attr.getValuesToRemove() != null) { - removeMaintainerTeams = attr.getValuesToRemove().stream().map(v -> v.toString()).collect(Collectors.toSet()); - } - } - } - - String newNameValue = client.updateUser(schema, uid, scimUserName, scimEmail, scimGivenName, scimFamilyName, login, options); - - String userLogin = resolveUserLogin(uid, newNameValue); - - // Organization role and Association - if (userLogin != null && - (organizationRole != null || - !addTeams.isEmpty() || !removeTeams.isEmpty() || - !addMaintainerTeams.isEmpty() || !removeMaintainerTeams.isEmpty() - )) { - - // do update organization role - if (organizationRole != null) { - // If the user login is stale, it throws UnknownUidException. - // IDM handle the exception then do discovery process if needed. - client.assignOrganizationRole(userLogin, organizationRole); - } - - // assign/unassign the teams - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - // If the user login is stale, it throws UnknownUidException. - // IDM handle the exception then do discovery process if needed. - client.unassignTeams(userLogin, resolver.resolvedRemoveTeams); - client.assignTeams(userLogin, "member", resolver.resolvedAddTeams); - client.assignTeams(userLogin, "maintainer", resolver.resolvedAddMaitainerTeams); - } - - // Detect NAME changing - if (newNameValue != null) { - Set sideEffects = new HashSet<>(); - AttributeDelta newName = AttributeDeltaBuilder.build(Name.NAME, newNameValue); - sideEffects.add(newName); - - return sideEffects; - } - - return null; - } - - private String resolveUserLogin(Uid oldUid, String newNameValue) { - if (newNameValue != null) { - return getUserLogin(newNameValue); - } - - String userLogin = getUserLogin(oldUid); - if (!userLogin.equals(UNKNOWN_USER_NAME)) { - return userLogin; - } - // Can't resolve yet due to not completed invitation - return null; - } - - @Override - public void delete(Uid uid, OperationOptions options) { - String userLogin = getUserLogin(uid); - if (!userLogin.equals(UNKNOWN_USER_NAME)) { - // Fix https://github.com/openstandia/connector-github/issues/6 - // GitHub maintains the user's team association after deletion - // So, we need to remove the association first - List teamIds = client.getTeamIdsByUsername(userLogin, configuration.getQueryPageSize()); - client.unassignTeams(userLogin, teamIds); - } - - // Finally, do delete the user - client.deleteUser(schema, uid, options); - } - - @Override - public void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options) { - // Create full attributesToGet by RETURN_DEFAULT_ATTRIBUTES + ATTRIBUTES_TO_GET - Set attributesToGet = createFullAttributesToGet(schema.userSchema, options); - boolean allowPartialAttributeValues = shouldAllowPartialAttributeValues(options); - - if (filter == null) { - client.getUsers(schema, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } else { - if (filter.isByUid()) { - client.getUser(schema, filter.uid, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } else { - client.getUser(schema, filter.name, - resultsHandler, options, attributesToGet, allowPartialAttributeValues, configuration.getQueryPageSize()); - } - } - } -} diff --git a/src/main/java/jp/openstandia/connector/github/GitHubUtils.java b/src/main/java/jp/openstandia/connector/github/GitHubUtils.java deleted file mode 100644 index eb2ef9e..0000000 --- a/src/main/java/jp/openstandia/connector/github/GitHubUtils.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.identityconnectors.framework.common.objects.AttributeInfo; -import org.identityconnectors.framework.common.objects.Name; -import org.identityconnectors.framework.common.objects.OperationOptions; -import org.identityconnectors.framework.common.objects.Uid; -import org.kohsuke.github.*; - -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Provides utility methods. - * - * @author Hiroyuki Wada - */ -public class GitHubUtils { - - public static ZonedDateTime toZoneDateTime(OffsetDateTime dateTime) { - return dateTime.toZonedDateTime(); - } - - public static String toResourceAttributeValue(String s) { - // To support deleting value, return empty string - if (s == null) { - return ""; - } - - return s; - } - - public static String toResourceAttributeValue(String s, String defaultValue) { - if (s == null) { - return defaultValue; - } - - return s; - } - - public static boolean shouldReturn(Set attrsToGetSet, String attr) { - if (attrsToGetSet == null) { - return true; - } - return attrsToGetSet.contains(attr); - } - - /** - * Check if ALLOW_PARTIAL_ATTRIBUTE_VALUES == true. - * - * @param options operation options - * @return true: allow partial attribute values, false: not allow - */ - public static boolean shouldAllowPartialAttributeValues(OperationOptions options) { - // If the option isn't set from IDM, it may be null. - return Boolean.TRUE.equals(options.getAllowPartialAttributeValues()); - } - - /** - * Check if RETURN_DEFAULT_ATTRIBUTES == true. - * - * @param options operation options - * @return true: return default attributes, false: not return - */ - public static boolean shouldReturnDefaultAttributes(OperationOptions options) { - // If the option isn't set from IDM, it may be null. - return Boolean.TRUE.equals(options.getReturnDefaultAttributes()); - } - - /** - * Create full set of ATTRIBUTES_TO_GET which is composed by RETURN_DEFAULT_ATTRIBUTES + ATTRIBUTES_TO_GET. - * - * @param schema schema map - * @param options operation options - * @return set of the attributes to get - */ - public static Set createFullAttributesToGet(Map schema, OperationOptions options) { - Set attributesToGet = null; - if (shouldReturnDefaultAttributes(options)) { - attributesToGet = new HashSet<>(); - attributesToGet.addAll(toReturnedByDefaultAttributesSet(schema)); - } - if (options.getAttributesToGet() != null) { - if (attributesToGet == null) { - attributesToGet = new HashSet<>(); - } - for (String a : options.getAttributesToGet()) { - attributesToGet.add(a); - } - } - return attributesToGet; - } - - private static Set toReturnedByDefaultAttributesSet(Map schema) { - return schema.entrySet().stream() - .filter(entry -> entry.getValue().isReturnedByDefault()) - .map(entry -> entry.getKey()) - .collect(Collectors.toSet()); - } - - public static Throwable getRootCause(final Throwable t) { - final List list = getThrowableList(t); - return list.size() < 2 ? null : list.get(list.size() - 1); - } - - private static List getThrowableList(Throwable t) { - final List list = new ArrayList<>(); - while (t != null && !list.contains(t)) { - list.add(t); - t = t.getCause(); - } - return list; - } - - public static Uid toUserUid(SCIMUser user) { - return new Uid(user.id, new Name(toUserName(user))); - } - - public static String toUserName(SCIMUser user) { - return toUserName(null, user.userName); - } - - public static String toUserName(String login, String scimUserName) { - if (login == null) { - // Need to return the format with : - // GitHub username policy is: - // Username may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen. - // So, return special "_unknown_" tag here because we can't determine the user login name yet - return UNKNOWN_USER_NAME + ":" + scimUserName; - } - return login + ":" + scimUserName; - } - - public static final String UNKNOWN_USER_NAME = "_unknown_"; - - public static String getUserLogin(Uid uid) throws InvalidAttributeValueException { - return getUserLogin(uid.getNameHintValue()); - } - - public static String getUserSCIMUserName(Uid uid) throws InvalidAttributeValueException { - return getUserSCIMUserName(uid.getNameHintValue()); - } - - public static String getUserSCIMUserName(Name name) throws InvalidAttributeValueException { - return getUserSCIMUserName(name.getNameValue()); - } - - public static String getUserSCIMUserName(String nameValue) throws InvalidAttributeValueException { - return parseUserNameValue(nameValue)[1]; - } - - public static String getUserLogin(Name name) throws InvalidAttributeValueException { - return getUserLogin(name.getNameValue()); - } - - public static String getUserLogin(String nameValue) throws InvalidAttributeValueException { - return parseUserNameValue(nameValue)[0]; - } - - private static String[] parseUserNameValue(String nameValue) throws InvalidAttributeValueException { - String[] split = nameValue.split(":"); - if (split.length != 2) { - throw new InvalidAttributeValueException("GitHub userName must be \"login:scimUserName\" format. value: " + nameValue); - } - return split; - } - - public static String toTeamUid(GHTeam team) { - return toTeamUid(String.valueOf(team.getId()), team.getNodeId()); - } - - public static String toTeamUid(GraphQLTeamEdge teamEdge) { - return toTeamUid(teamEdge.node); - } - - public static String toTeamUid(GraphQLTeam team) { - return toTeamUid(team.databaseId.toString(), team.id); - } - - private static String toTeamUid(String databaseId, String nodeId) { - return databaseId + ":" + nodeId; - } - - public static long getTeamDatabaseId(Uid uid) { - return getTeamDatabaseId(uid.getUidValue()); - } - - public static long getTeamDatabaseId(String uid) throws InvalidAttributeValueException { - String databaseId = parseTeamUidValue(uid)[0]; - - try { - return Long.parseLong(databaseId); - } catch (NumberFormatException e) { - throw new InvalidAttributeValueException("Unexpected teamId: " + uid); - } - } - - public static String getTeamNodeId(Uid uid) throws InvalidAttributeValueException { - return parseTeamUidValue(uid.getUidValue())[1]; - } - - private static String[] parseTeamUidValue(String uidValue) throws InvalidAttributeValueException { - String[] split = uidValue.split(":"); - if (split.length != 2) { - throw new InvalidAttributeValueException("GitHub teamId must be \"databaseId:nodeId\" format. value: " + uidValue); - } - return split; - } - - public static GHTeam.Privacy toGHTeamPrivacy(String privacy) throws InvalidAttributeValueException { - try { - // Validation - GraphQLTeamPrivacy gp = GraphQLTeamPrivacy.valueOf(privacy.toUpperCase()); - - // Need to convert - GHTeam.Privacy ghp = null; - if (gp == GraphQLTeamPrivacy.SECRET) { - ghp = GHTeam.Privacy.SECRET; - } else { - ghp = GHTeam.Privacy.CLOSED; - } - return ghp; - - } catch (IllegalArgumentException e) { - throw new InvalidAttributeValueException("GitHub Team privacy must be \"visible\" or \"secret\": " + privacy); - } - } - - public static String toGroupId(SCIMEMUGroup group) { - return group.id; - } - - public static String toGroupName(SCIMEMUGroup group) { - return group.displayName; - } - -} diff --git a/src/main/java/jp/openstandia/connector/github/TeamAssignmentResolver.java b/src/main/java/jp/openstandia/connector/github/TeamAssignmentResolver.java deleted file mode 100644 index b82800d..0000000 --- a/src/main/java/jp/openstandia/connector/github/TeamAssignmentResolver.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class TeamAssignmentResolver { - private final Set origAddTeams; - private final Set origRemoveTeams; - private final Set origAddMaintainerTeams; - private final Set origRemoveMaintainerTeams; - - public Set resolvedAddTeams; - public Set resolvedAddMaitainerTeams; - public Set resolvedRemoveTeams; - - public TeamAssignmentResolver(Set addTeams, Set removeTeams, Set addMaintainerTeams, Set removeMaintainerTeams) { - this.origAddTeams = Collections.unmodifiableSet(addTeams); - this.origRemoveTeams = Collections.unmodifiableSet(removeTeams); - this.origAddMaintainerTeams = Collections.unmodifiableSet(addMaintainerTeams); - this.origRemoveMaintainerTeams = Collections.unmodifiableSet(removeMaintainerTeams); - - resolve(); - } - - private void resolve() { - // If same team is assigned for both teams and maintainer teams, we assign as maintainer. - Set addTeams = origAddTeams.stream() - .filter(t -> !origAddMaintainerTeams.contains(t)) - .collect(Collectors.toSet()); - - this.resolvedAddTeams = Collections.unmodifiableSet(addTeams); - - // if same team is unassigned as member and assigned as maintainer, we assign as maintainer. - Set removeTeams = origRemoveTeams.stream() - .filter(t -> !origAddMaintainerTeams.contains(t)) - .collect(Collectors.toSet()); - - // if same team is unassigned as maintainer and assigned as member, we assign as member. - Set removeMaintainerTeams = origRemoveMaintainerTeams.stream() - .filter(t -> !addTeams.contains(t)) - .collect(Collectors.toSet()); - - this.resolvedAddMaitainerTeams = origAddMaintainerTeams; - - // If same team is unassigned for both teams and maintainer teams, we only unassign it one time. - this.resolvedRemoveTeams = Collections.unmodifiableSet( - Stream.concat(removeTeams.stream(), removeMaintainerTeams.stream()).collect(Collectors.toSet()) - ); - } -} \ No newline at end of file diff --git a/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java b/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java index cd3d0a3..8b3af7f 100644 --- a/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java +++ b/src/main/java/jp/openstandia/connector/github/rest/GitHubEMURESTClient.java @@ -26,7 +26,7 @@ import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.Uid; import org.kohsuke.github.*; -import org.kohsuke.github.extras.okhttp3.OkHttpConnector; +import org.kohsuke.github.extras.okhttp3.*; import java.io.IOException; import java.util.List; @@ -48,17 +48,13 @@ public class GitHubEMURESTClient implements GitHubClient { private String instanceName; private GitHubExt apiClient; private long lastAuthenticated; - private GHEnterpriseExt enterpriseApiClient; + GHEnterpriseExt enterpriseApiClient; public GitHubEMURESTClient(GitHubEMUConfiguration configuration) { this.configuration = configuration; auth(); } - public GitHubExt getApiClient() { - return apiClient; - } - @Override public void setInstanceName(String instanceName) { this.instanceName = instanceName; diff --git a/src/main/java/jp/openstandia/connector/github/rest/GitHubRESTClient.java b/src/main/java/jp/openstandia/connector/github/rest/GitHubRESTClient.java deleted file mode 100644 index 24e0a2b..0000000 --- a/src/main/java/jp/openstandia/connector/github/rest/GitHubRESTClient.java +++ /dev/null @@ -1,725 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github.rest; - -import com.spotify.github.v3.clients.PKCS1PEMKey; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import jp.openstandia.connector.github.*; -import org.identityconnectors.common.StringUtil; -import org.identityconnectors.common.logging.Log; -import org.identityconnectors.framework.common.exceptions.*; -import org.identityconnectors.framework.common.objects.*; -import org.kohsuke.github.*; -import org.kohsuke.github.extras.okhttp3.OkHttpConnector; - -import java.io.IOException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.*; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static jp.openstandia.connector.github.GitHubTeamHandler.*; -import static jp.openstandia.connector.github.GitHubUserHandler.*; -import static jp.openstandia.connector.github.GitHubUtils.*; - -/** - * GitHub client implementation which uses Java API for GitHub. - * - * @author Hiroyuki Wada - */ -public class GitHubRESTClient implements GitHubClient { - - private static final Log LOGGER = Log.getLog(GitHubRESTClient.class); - - private final GitHubConfiguration configuration; - private String instanceName; - private GitHubExt apiClient; - private long lastAuthenticated; - private GHOrganizationExt orgApiClient; - - public GitHubRESTClient(GitHubConfiguration configuration) { - this.configuration = configuration; - - auth(); - } - - public GitHubExt getApiClient() { - return apiClient; - } - - @Override - public void setInstanceName(String instanceName) { - this.instanceName = instanceName; - } - - @Override - public void test() { - try { - withAuth(() -> { - apiClient.checkApiUrlValidity(); - return null; - }); - } catch (RuntimeException e) { - throw new ConnectorException("This GitHub connector isn't active.", e); - } - } - - private static class UnauthorizedException extends ConnectionFailedException { - public UnauthorizedException(Exception e) { - super(e); - } - } - - @Override - public void auth() { - AtomicReference privateKey = new AtomicReference<>(); - configuration.getPrivateKey().access((val) -> { - privateKey.set(String.valueOf(val)); - }); - - try { - // First, get app installation token - GitHub api = new GitHubBuilder() - .withJwtToken(createJWT(configuration.getAppId(), 60000, privateKey.get())) - .withConnector(new OkHttpConnector(createClient(configuration))) - .build(); - GHAppInstallation appInstallation = api.getApp().getInstallationById(configuration.getInstallationId()); // Installation Id - - GHAppInstallationToken appInstallationToken = appInstallation.createToken().create(); - - // Then, get scoped access token by app installation token - - GitHubBuilder builder = new GitHubBuilder() - .withConnector(new OkHttpConnector(createClient(configuration))) - .withAppInstallationToken(appInstallationToken.getToken()); - - apiClient = GitHubExt.build(builder); - lastAuthenticated = System.currentTimeMillis(); - - orgApiClient = apiClient.getOrganization(configuration.getOrganizationName()); - - } catch (IOException e) { - throw new ConnectionFailedException("Failed to authenticate GitHub API", e); - } - } - - protected ConnectorException handleApiException(Exception e) { - - if (e instanceof GHFileNotFoundException) { - GHFileNotFoundException gfe = (GHFileNotFoundException) e; - List status = gfe.getResponseHeaderFields().get(null); - - if (!status.isEmpty() && status.get(0).contains("400")) { - return new InvalidAttributeValueException(e); - } - - if (!status.isEmpty() && status.get(0).contains("401")) { - return new UnauthorizedException(e); - } - - if (!status.isEmpty() && status.get(0).contains("403")) { - // Including Rate limit error - return new PermissionDeniedException(e); - } - - if (!status.isEmpty() && status.get(0).contains("404")) { - return new UnknownUidException(e); - } - - if (!status.isEmpty() && status.get(0).contains("409")) { - return new AlreadyExistsException(e); - } - - if (!status.isEmpty() && status.get(0).contains("422")) { - // Create Team API return 422 error if exists - return new AlreadyExistsException(e); - } - } - - LOGGER.error(e, "Unexpected exception when calling GitHub API"); - - return new ConnectorIOException("Failed to call GitHub API", e); - } - - protected T withAuth(Callable callable) { - // Check the access token expiration - long now = System.currentTimeMillis(); - if (now > lastAuthenticated + TimeUnit.MINUTES.toMillis(55)) { - // Refresh the access token - auth(); - } - - try { - return callable.call(); - - } catch (Exception e) { - ConnectorException ce = handleApiException(e); - - if (ce instanceof UnauthorizedException) { - // do re-Auth - auth(); - - try { - // retry - return callable.call(); - - } catch (Exception e2) { - throw handleApiException(e2); - } - } - - throw ce; - } - } - - @Override - public Uid createUser(GitHubSchema schema, SCIMUser newUser) throws AlreadyExistsException { - return withAuth(() -> { - SCIMUser created = orgApiClient.createSCIMUser(newUser); - - return toUserUid(created); - }); - } - - @Override - public String updateUser(GitHubSchema schema, Uid uid, String scimUserName, String scimEmail, String scimGivenName, - String scimFamilyName, String login, OperationOptions options) throws UnknownUidException { - return withAuth(() -> { - orgApiClient.updateSCIMUser(uid.getUidValue(), scimUserName, scimEmail, scimGivenName, scimFamilyName); - - // Detected NAME is changed - String oldUserLogin = getUserLogin(uid); - String oldScimUserName = getUserSCIMUserName(uid); - - if ((login != null && !oldUserLogin.equals(login)) - || (scimUserName != null && !oldScimUserName.equals(scimUserName))) { - String newLogin = login != null ? login : oldUserLogin; - String newScimUserName = scimUserName != null ? scimUserName : oldScimUserName; - - // Return new NAME value - return toUserName(newLogin, newScimUserName); - } - - return null; - }); - } - - @Override - public void deleteUser(GitHubSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { - deleteUser(schema, uid.getUidValue(), options); - } - - private void deleteUser(GitHubSchema schema, String scimUserId, OperationOptions options) throws UnknownUidException { - withAuth(() -> { - orgApiClient.deleteSCIMUser(scimUserId); - - return null; - }); - } - - @Override - public void getUsers(GitHubSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, - boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - orgApiClient.listExternalIdentities(queryPageSize) - .forEach(u -> { - // When we detect a dropped account, we need to delete it then return - // not found from the organization to re-invite the account. - if (u.node.isDropped()) { - try { - deleteUser(schema, u.node.guid, options); - } catch (UnknownUidException ignore) { - LOGGER.warn("Detected unknown Uid when deleting a dropped account"); - } - - return; - } - handler.handle(toConnectorObject(schema, null, u, attributesToGet, allowPartialAttributeValues, queryPageSize)); - }); - return null; - }); - } - - @Override - public void getUser(GitHubSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, - Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - SCIMUser user = orgApiClient.getSCIMUser(uid.getUidValue()); - - // SCIM User doesn't contain database ID - // We need to use NAME value in query Uid as user login. - // It means IDM can't detect when the user login is changed in GitHub side. - // To detect the situation, IDM need to do full reconciliation which calls getUsers method. - String queryLogin = getUserLogin(uid); - - handler.handle(toConnectorObject(schema, queryLogin, user, attributesToGet, allowPartialAttributeValues, queryPageSize)); - - return null; - }); - } - - @Override - public void getUser(GitHubSchema schema, Name name, ResultsHandler handler, OperationOptions options, - Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - String scimUserName = getUserSCIMUserName(name); - - SCIMUser user = orgApiClient.getSCIMUserByUserName(scimUserName); - - // SCIM User doesn't contain database ID - // We need to use NAME value in query Uid as user login. - // It means IDM can't detect when the user login is changed in GitHub side. - // To detect the situation, IDM need to do full reconciliation which calls getUsers method. - String queryLogin = getUserLogin(name); - - handler.handle(toConnectorObject(schema, queryLogin, user, attributesToGet, allowPartialAttributeValues, queryPageSize)); - - return null; - }); - } - - @Override - public List getTeamIdsByUsername(String userLogin, int pageSize) { - return withAuth(() -> { - return orgApiClient.listTeams(userLogin, pageSize) - .toList().stream() - .filter(t -> t.node.members.totalCount == 1) - .map(GitHubUtils::toTeamUid) - .collect(Collectors.toList()); - }); - } - - private ConnectorObject toConnectorObject(GitHubSchema schema, String queryLogin, SCIMUser user, - Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - - final String scimEmail = (user.emails != null && user.emails.length > 0) ? user.emails[0].value : null; - - String scimGivenName = user.name != null ? user.name.givenName : null; - String scimFamilyName = user.name != null ? user.name.familyName : null; - - return toConnectorObject(schema, queryLogin, user.id, user.userName, scimEmail, - scimGivenName, scimFamilyName, - null, // Can't fetch it from SCIMUser endpoint - attributesToGet, allowPartialAttributeValues, queryPageSize); - } - - private ConnectorObject toConnectorObject(GitHubSchema schema, String queryLogin, GraphQLExternalIdentityEdge user, - Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - GraphQLExternalIdentityScimAttributes scimAttrs = user.node.scimIdentity; - - final String scimEmail = (scimAttrs.emails != null && scimAttrs.emails.length > 0) ? scimAttrs.emails[0].value : null; - final String login = user.node.user != null ? user.node.user.login : null; - - return toConnectorObject(schema, queryLogin, user.node.guid, scimAttrs.username, scimEmail, - scimAttrs.givenName, scimAttrs.familyName, - login, - attributesToGet, allowPartialAttributeValues, queryPageSize); - } - - private ConnectorObject toConnectorObject(GitHubSchema schema, String queryLogin, String scimUserId, String scimUserName, String scimEmail, - String scimGivenName, String scimFamilyName, - String login, - Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - final ConnectorObjectBuilder builder = new ConnectorObjectBuilder() - .setObjectClass(USER_OBJECT_CLASS) - // Always returns "scimUserId" - .setUid(scimUserId); - - // Always returns "_unknown_:" or ":" as NAME - String userNameValue = resolveUserLogin(queryLogin, login, scimUserName); - builder.setName(userNameValue); - - // Attributes - if (shouldReturn(attributesToGet, ATTR_SCIM_EMAIL) && - scimEmail != null) { - builder.addAttribute(ATTR_SCIM_EMAIL, scimEmail); - } - if (shouldReturn(attributesToGet, ATTR_SCIM_GIVEN_NAME) && - scimGivenName != null) { - builder.addAttribute(ATTR_SCIM_GIVEN_NAME, scimGivenName); - } - if (shouldReturn(attributesToGet, ATTR_SCIM_FAMILY_NAME) && - scimFamilyName != null) { - builder.addAttribute(ATTR_SCIM_FAMILY_NAME, scimFamilyName); - } - - String userLogin = getUserLogin(userNameValue); - - // Readonly - // We need to return user login always because it causes duplicate NAME if we don't return. - // IDM detects no data, then try to update NAME. - builder.addAttribute(ATTR_USER_LOGIN, userLogin); - - if (shouldReturn(attributesToGet, ATTR_SCIM_USER_NAME) && - scimUserName != null) { - builder.addAttribute(ATTR_SCIM_USER_NAME, scimUserName); - } - - if (allowPartialAttributeValues) { - // Suppress fetching associations because they cost time and resource, also it consumes rate limit - LOGGER.ok("[{0}] Suppress fetching associations because return partial attribute values is requested", instanceName); - - Stream.of(ATTR_TEAMS, ATTR_MAINTAINER_TEAMS, ATTR_ORGANIZATION_ROLE).forEach(attrName -> { - AttributeBuilder ab = new AttributeBuilder(); - ab.setName(attrName).setAttributeValueCompleteness(AttributeValueCompleteness.INCOMPLETE); - ab.addValue(Collections.EMPTY_LIST); - builder.addAttribute(ab.build()); - }); - - return builder.build(); - } - - if (attributesToGet == null) { - // Suppress fetching associations default - LOGGER.ok("[{0}] Suppress fetching associations because returned by default is true", instanceName); - - return builder.build(); - } - - if (userLogin.equals(UNKNOWN_USER_NAME)) { - LOGGER.ok("[{0}] Suppress fetching associations because the user isn't complete the invitation", instanceName); - - return builder.build(); - } - - // Fetching associations if needed - - if (shouldReturn(attributesToGet, ATTR_TEAMS) || shouldReturn(attributesToGet, ATTR_MAINTAINER_TEAMS)) { - // Fetch teams - LOGGER.ok("[{0}] Fetching teams/maintainer teams because attributes to get is requested", instanceName); - - try { - // Fetch teams by user's login name - // It's supported by GraphQL API only... - // If the user is not found in the organization (leave by self or change their login name), the GraphAPI returns all teams unfortunately. - // That's why we do filtering by totalCount == 1 here. - List allTeams = orgApiClient.listTeams(userLogin, queryPageSize) - .toList().stream() - .filter(t -> t.node.members.totalCount == 1) - .collect(Collectors.toList()); - - List memberTeams = allTeams.stream() - .filter(t -> t.node.members.edges[0].role == GraphQLTeamMemberRole.MEMBER) - .map(GitHubUtils::toTeamUid) - .collect(Collectors.toList()); - - List maintainerTeams = allTeams.stream() - .filter(t -> t.node.members.edges[0].role == GraphQLTeamMemberRole.MAINTAINER) - .map(GitHubUtils::toTeamUid) - .collect(Collectors.toList()); - - builder.addAttribute(ATTR_TEAMS, memberTeams); - builder.addAttribute(ATTR_MAINTAINER_TEAMS, maintainerTeams); - - } catch (IOException ignore) { - LOGGER.warn("Failed to fetch GitHub organization membership for user: {0}, error: {1}", userLogin, ignore.getMessage()); - // Ignore the error, IDM try to reconcile the memberships - } - } - - if (shouldReturn(attributesToGet, ATTR_ORGANIZATION_ROLE)) { - try { - GHMembership membership = orgApiClient.getOrganizationMembership(userLogin); - builder.addAttribute(ATTR_ORGANIZATION_ROLE, membership.getRole().name().toLowerCase()); - - } catch (IOException ignore) { - // If the user is not found (leave by self or change their login name), IDM will do discovery process - LOGGER.warn("Failed to fetch GitHub organization membership for user: {0}, error: {1}", userLogin, ignore.getMessage()); - // Ignore the error, IDM try to reconcile the memberships - } - } - - return builder.build(); - } - - private String resolveUserLogin(String queryLogin, String login, String scimUserName) { - if (login != null) { - return toUserName(login, scimUserName); - } - if (queryLogin != null) { - return toUserName(queryLogin, scimUserName); - } - return toUserName(null, scimUserName); - } - - @Override - public boolean isOrganizationMember(String userLogin) { - return withAuth(() -> { - return orgApiClient.isMember(userLogin); - }); - } - - @Override - public void assignOrganizationRole(String userLogin, String organizationRole) { - withAuth(() -> { - try { - GHOrganization.Role role = GHOrganization.Role.valueOf(organizationRole.toUpperCase()); - - orgApiClient.setOrganizationMembership(userLogin, role); - - } catch (IllegalArgumentException e) { - throw new InvalidAttributeValueException("Invalid organizationRole: " + organizationRole); - } - - return null; - }); - } - - @Override - public void assignTeams(String login, String teamRole, Collection teams) { - withAuth(() -> { - for (String team : teams) { - try { - GHTeam.Role role = GHTeam.Role.valueOf(teamRole.toUpperCase()); - - orgApiClient.addTeamMembership(getTeamDatabaseId(team), login, role); - - } catch (IllegalArgumentException e) { - throw new InvalidAttributeValueException("Invalid teamRole: " + teamRole); - } - } - - return null; - }); - } - - @Override - public void unassignTeams(String login, Collection teams) { - withAuth(() -> { - for (String team : teams) { - orgApiClient.removeTeamMembership(getTeamDatabaseId(team), login); - } - - return null; - }); - } - - @Override - public Uid createTeam(GitHubSchema schema, String teamName, String description, String privacy, Long parentTeamDatabaseId) throws AlreadyExistsException { - return withAuth(() -> { - GHTeamBuilder builder = orgApiClient.createTeam(teamName); - - if (description != null) { - builder.description(description); - } - if (privacy != null) { - GHTeam.Privacy ghPrivacy = toGHTeamPrivacy(privacy); - builder.privacy(ghPrivacy); - } - if (parentTeamDatabaseId != null) { - builder.parentTeamId(parentTeamDatabaseId); - } - - GHTeam created = builder.create(); - - // To use for REST API and GraphQL API, we combine databaseId and nodeId - return new Uid(toTeamUid(created), new Name(created.getName())); - }); - } - - @Override - public Uid updateTeam(GitHubSchema schema, Uid uid, String teamName, String description, String privacy, Long parentTeamId, - boolean clearParent, OperationOptions options) throws UnknownUidException { - return withAuth(() -> { - GHTeam.Privacy ghPrivacy = null; - if (privacy != null) { - ghPrivacy = toGHTeamPrivacy(privacy); - } - - GHTeam updated = orgApiClient.updateTeam(getTeamDatabaseId(uid), teamName, description, ghPrivacy, parentTeamId, clearParent); - - return new Uid(toTeamUid(updated), new Name(updated.getName())); - }); - } - - @Override - public void deleteTeam(GitHubSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { - withAuth(() -> { - orgApiClient.deleteTeam(getTeamDatabaseId(uid)); - - return null; - }); - } - - @Override - public void getTeams(GitHubSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - orgApiClient.listTeamsExt().withPageSize(queryPageSize) - .forEach(t -> { - handler.handle(toTeamConnectorObject(schema, t, attributesToGet, allowPartialAttributeValues, queryPageSize)); - }); - - return null; - }); - } - - @Override - public void getTeam(GitHubSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - GHTeamExt team = orgApiClient.getTeam(getTeamDatabaseId(uid)); - - handler.handle(toTeamConnectorObject(schema, team, attributesToGet, allowPartialAttributeValues, queryPageSize)); - - return null; - }); - } - - @Override - public void getTeam(GitHubSchema schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { - withAuth(() -> { - PagedIterator iter = orgApiClient.findTeam(name.getNameValue(), queryPageSize).iterator(); - while (iter.hasNext()) { - GraphQLTeamEdge team = iter.next(); - if (team.node.name.equalsIgnoreCase(name.getNameValue())) { - // Found - handler.handle(toTeamConnectorObject(schema, team, attributesToGet, allowPartialAttributeValues, queryPageSize)); - - break; - } - } - - return null; - }); - } - - private ConnectorObject toTeamConnectorObject(GitHubSchema schema, GHTeamExt team, Set attributesToGet, boolean allowPartialAttributeValues, long queryPageSize) { - String teamId = toTeamUid(team); - - String parentId = null; - if (team.getParent() != null) { - parentId = toTeamUid(team.getParent()); - } - - GraphQLTeamPrivacy privacy; - if (team.getPrivacy() == GHTeam.Privacy.SECRET.SECRET) { - privacy = GraphQLTeamPrivacy.SECRET; - } else { - privacy = GraphQLTeamPrivacy.VISIBLE; - } - - return toTeamConnectorObject(schema, teamId, team.getId(), team.getNodeId(), team.getName(), team.getSlug(), - team.getDescription(), privacy, parentId, - attributesToGet, allowPartialAttributeValues, queryPageSize); - } - - private ConnectorObject toTeamConnectorObject(GitHubSchema schema, GraphQLTeamEdge teamEdge, Set attributesToGet, boolean allowPartialAttributeValues, long queryPageSize) { - GraphQLTeam team = teamEdge.node; - - String teamId = toTeamUid(team); - - String parentId = null; - if (team.parentTeam != null) { - parentId = toTeamUid(team.parentTeam); - } - - return toTeamConnectorObject(schema, teamId, team.databaseId, team.id, team.name, team.slug, - team.description, team.privacy, parentId, - attributesToGet, allowPartialAttributeValues, queryPageSize); - } - - private ConnectorObject toTeamConnectorObject(GitHubSchema schema, String teamId, long databaseId, String nodeId, String teamName, - String slug, String description, GraphQLTeamPrivacy privacy, String parentId, - Set attributesToGet, boolean allowPartialAttributeValues, long queryPageSize) { - final ConnectorObjectBuilder builder = new ConnectorObjectBuilder() - .setObjectClass(TEAM_OBJECT_CLASS) - // Always returns "teamId" - .setUid(teamId) - // Always returns "slug" - .setName(teamName); - - // Attributes - if (shouldReturn(attributesToGet, ATTR_DESCRIPTION) && - !StringUtil.isEmpty(description)) { - builder.addAttribute(ATTR_DESCRIPTION, description); - } - if (shouldReturn(attributesToGet, ATTR_PRIVACY)) { - builder.addAttribute(ATTR_PRIVACY, privacy.name().toLowerCase()); - } - if (shouldReturn(attributesToGet, ATTR_PARENT_TEAM_ID) && - parentId != null) { - builder.addAttribute(ATTR_PARENT_TEAM_ID, parentId); - } - - // Readonly - if (shouldReturn(attributesToGet, ATTR_TEAM_DATABASE_ID)) { - builder.addAttribute(ATTR_TEAM_DATABASE_ID, databaseId); - } - if (shouldReturn(attributesToGet, ATTR_SLUG)) { - builder.addAttribute(ATTR_SLUG, slug); - } - if (shouldReturn(attributesToGet, ATTR_TEAM_NODE_ID)) { - builder.addAttribute(ATTR_TEAM_NODE_ID, nodeId); - } - - return builder.build(); - } - - @Override - public void close() { - } - - private static PrivateKey get(String privateKeyPEM) { - Optional keySpec = PKCS1PEMKey.loadKeySpec(privateKeyPEM.getBytes()); - - if (!keySpec.isPresent()) { - throw new ConnectionFailedException("Failed to load private key PEM"); - } - - try { - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePrivate(keySpec.get()); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new ConnectionFailedException("Failed to load the privateKey from the configuration", e); - } - } - - public static String createJWT(String githubAppId, long ttlMillis, String privateKeyPEM) { - //The JWT signature algorithm we will be using to sign the token - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256; - - long nowMillis = System.currentTimeMillis(); - Date now = new Date(nowMillis); - - //We will sign our JWT with our private key - Key signingKey = get(privateKeyPEM); - - //Let's set the JWT Claims - JwtBuilder builder = Jwts.builder() - .setIssuedAt(now) - .setIssuer(githubAppId) - .signWith(signingKey, signatureAlgorithm); - - //if it has been specified, let's add the expiration - if (ttlMillis > 0) { - long expMillis = nowMillis + ttlMillis; - Date exp = new Date(expMillis); - builder.setExpiration(exp); - } - - //Builds the JWT and serializes it to a compact, URL-safe string - return builder.compact(); - } -} diff --git a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java index acf209b..35dedd0 100644 --- a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java +++ b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java @@ -250,10 +250,6 @@ private Map buildAttributeMap() { .collect(Collectors.toMap(a -> a.connectorName, a -> a)); return map; } - - public void addEnable() { - - } } private final ObjectClass objectClass; diff --git a/src/main/java/org/kohsuke/github/GHOrganizationExt.java b/src/main/java/org/kohsuke/github/GHOrganizationExt.java deleted file mode 100644 index 06ec05f..0000000 --- a/src/main/java/org/kohsuke/github/GHOrganizationExt.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Extends original GHOrganization class. - * - * @author Hiroyuki Wada - */ -public class GHOrganizationExt extends GHOrganization { - private static final ObjectMapper mapper = new ObjectMapper(); - - @Override - GHOrganizationExt wrapUp(GitHub root) { - return (GHOrganizationExt) super.wrapUp(root); - } - - public GHUser createInvitation(String email, String role) throws IOException { - return root.createRequest() - .method("POST") - .withHeader("Accept", "application/vnd.github.v3+json") - .with("email", email) - .with("role", role) - .withUrlPath(String.format("/orgs/%s/invitations", login)) - .fetch(GHUser.class); - } - - public Iterable listInvitation() { - return root.createRequest() - .withUrlPath(String.format("/orgs/%s/invitations", login)) - .toIterable(GHUser[].class, item -> item.wrapUp(root)); - } - - public SCIMUser createSCIMUser(SCIMUser newUser) throws IOException { - Map map = new HashMap<>(); - map.put("userName", newUser.userName); - map.put("emails", newUser.emails); - map.put("name", newUser.name); - map.put("externalId", newUser.externalId); - - SCIMUser u = root.createRequest() - .method("POST") - .withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT) - .withHeader(SCIMConstants.GITHUB_API_VERSION, SCIMConstants.GITHUB_API_VERSION) - .with(map) - .withUrlPath(String.format("/scim/v2/organizations/%s/Users", login)) - .fetch(SCIMUser.class); - return u; - } - - public SCIMUser updateSCIMUser(String scimUserId, String scimUserName, String scimEmail, String scimGivenName, String scimFamilyName) throws IOException { - List ops = new ArrayList<>(); - - if (scimUserName != null) { - ops.add(new SCIMOperation<>("replace", "userName", scimUserName)); - } - if (scimEmail != null) { - List> emails = new ArrayList<>(); - Map emailsMap = new HashMap<>(); - emailsMap.put("value", scimEmail); - emails.add(emailsMap); - ops.add(new SCIMOperation<>("replace", "emails", emails)); - } - if (scimGivenName != null) { - ops.add(new SCIMOperation<>("replace", "name.givenName", scimGivenName)); - } - if (scimFamilyName != null) { - ops.add(new SCIMOperation<>("replace", "name.familyName", scimFamilyName)); - } - - if (ops.isEmpty()) { - return null; - } - - Map map = new HashMap<>(); - map.put("Operations", ops); - - SCIMUser u = root.createRequest() - .method("PATCH") - .withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT) - .withHeader(SCIMConstants.GITHUB_API_VERSION, SCIMConstants.GITHUB_API_VERSION) - .with(map) - .withUrlPath(String.format("/scim/v2/organizations/%s/Users/%s", login, scimUserId)) - .fetch(SCIMUser.class); - return u; - } - - public SCIMUser getSCIMUser(String scimUserId) throws IOException { - SCIMUser u = root.createRequest() - .withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT) - .withHeader(SCIMConstants.GITHUB_API_VERSION, SCIMConstants.GITHUB_API_VERSION) - .withUrlPath(String.format("/scim/v2/organizations/%s/Users/%s", login, scimUserId)) - .fetch(SCIMUser.class); - return u; - } - - public SCIMUser getSCIMUserByUserName(String scimUserName) throws IOException { - SCIMUser u = root.createRequest() - .withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT) - .withHeader(SCIMConstants.GITHUB_API_VERSION, SCIMConstants.GITHUB_API_VERSION) - .withUrlPath(String.format("/scim/v2/organizations/%s/Users?filter=userName eq \"%s\"", login, scimUserName)) - .fetch(SCIMUser.class); - return u; - } - - /** - * Search users. - * - * @return the gh user search builder - */ - public SCIMUserSearchBuilder searchSCIMUsers() { - return new SCIMUserSearchBuilder(root, this); - } - - public SCIMPagedSearchIterable listSCIMUsers(int pageSize) - throws IOException { - return searchSCIMUsers().list().withPageSize(pageSize); - } - - /** - * Search users. - * - * @return the gh user search builder - */ - public GraphQLOrganizationExternalIdentitySearchBuilder searchExternalIdentities() { - return new GraphQLOrganizationExternalIdentitySearchBuilder(root, this); - } - - public GraphQLPagedSearchIterable listExternalIdentities(int pageSize) - throws IOException { - return searchExternalIdentities().list().withPageSize(pageSize); - } - - public void deleteSCIMUser(String scimUserId) throws IOException { - root.createRequest() - .method("DELETE") - .withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT) - .withHeader(SCIMConstants.GITHUB_API_VERSION, SCIMConstants.GITHUB_API_VERSION) - .withUrlPath(String.format("/scim/v2/organizations/%s/Users/%s", login, scimUserId)) - .send(); - } - - public GHTeamExt getTeam(long teamId) throws IOException { - return root.createRequest() - .withUrlPath(String.format("/organizations/%d/team/%d", getId(), teamId)) - .fetch(GHTeamExt.class); - } - - public GHTeamExt getTeam(String slug) throws IOException { - return root.createRequest() - .withUrlPath(String.format("/organizations/%d/team/%d", login, slug)) - .fetch(GHTeamExt.class); - } - - public PagedIterable listTeamsExt() throws IOException { - return root.createRequest() - .withUrlPath(String.format("/orgs/%s/teams", login)) - .toIterable(GHTeamExt[].class, item -> item.wrapUp(this)); - } - - public GHTeam updateTeam(long teamId, String name, String description, GHTeam.Privacy privacy, Long parentTeamId, - boolean clearParent) throws IOException { - Requester req = root.createRequest().method("PATCH"); - - if (name != null) { - req.with("name", name); - } - if (description != null) { - req.with("description", description); - } - if (privacy != null) { - req.with("privacy", privacy); - } - if (parentTeamId != null) { - req.with("parent_team_id", parentTeamId); - } else if (clearParent) { - req.withNullable("parent_team_id", null); - } - - GHTeam updated = req.withUrlPath(String.format("/organizations/%d/team/%d", getId(), teamId)) - .fetch(GHTeam.class); - - return updated; - } - - public void deleteTeam(long teamId) throws IOException { - root.createRequest().method("DELETE") - .withUrlPath(String.format("/organizations/%d/team/%d", getId(), teamId)) - .send(); - } - - public GraphQLPagedSearchIterable findTeam(String teamName, int pageSize) throws IOException { - return new GraphQLTeamSearchBuilder(root, this, teamName) - .list() - .withPageSize(pageSize); - } - - public GraphQLTeamByMemberSearchBuilder searchTeams(String userLogin) { - return new GraphQLTeamByMemberSearchBuilder(root, this, userLogin); - } - - public GraphQLPagedSearchIterable listTeams(String userLogin, int pageSize) - throws IOException { - return searchTeams(userLogin).list().withPageSize(pageSize); - } - - public void addTeamMembership(long teamId, String userLogin, GHTeam.Role teamRole) throws IOException { - root.createRequest().method("PUT") - .with("role", teamRole.name().toLowerCase()) - .withUrlPath(String.format("/organizations/%d/team/%d/memberships/%s", getId(), teamId, userLogin)) - .send(); - } - - public void removeTeamMembership(long teamId, String userLogin) throws IOException { - root.createRequest().method("DELETE") - .withUrlPath(String.format("/organizations/%d/team/%d/memberships/%s", getId(), teamId, userLogin)) - .send(); - } - - public boolean isMember(String userLogin) { - try { - root.createRequest() - .withUrlPath(String.format("/orgs/%s/members/%s", login, userLogin)) - .send(); - return true; - } catch (IOException ignore) { - return false; - } - } - - /** - * Set organization role to the user. - * https://docs.github.com/en/rest/reference/orgs#set-organization-membership-for-a-user - * - * @param userLogin GitHub username - * @param organizationRole orgnization role (admin or member) - * @throws IOException API error - */ - public void setOrganizationMembership(String userLogin, Role organizationRole) throws IOException { - root.createRequest().method("PUT") - .with("role", organizationRole.name().toLowerCase()) - .withUrlPath(String.format("/orgs/%s/memberships/%s", login, userLogin)) - .send(); - } - - public GHMembership getOrganizationMembership(String userLogin) throws IOException { - return root.createRequest() - .withUrlPath(String.format("/orgs/%s/memberships/%s", login, userLogin)) - .fetch(GHMembership.class); - } -} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GHTeamExt.java b/src/main/java/org/kohsuke/github/GHTeamExt.java deleted file mode 100644 index 6d7da7a..0000000 --- a/src/main/java/org/kohsuke/github/GHTeamExt.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GHTeamExt extends GHTeam { - private GHOrganization organization; - - GHTeamExt wrapUp(GHOrganization owner) { - this.organization = owner; - this.root = owner.root; - return this; - } - - @JsonProperty("parent") - private GHTeam parent; - - public GHTeam getParent() { - return parent; - } -} diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java index ea7993a..07eb63e 100644 --- a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPageIterator.java @@ -28,7 +28,7 @@ public class GitHubCopilotSeatPageIterator finalResponse = null; - private GitHubCopilotSeatPageIterator(GitHubClient client, Class type, GitHubRequest request) { + GitHubCopilotSeatPageIterator(GitHubClient client, Class type, GitHubRequest request) { if (!"GET".equals(request.method())) { throw new IllegalStateException("Request method \"GET\" is required for page iterator."); } @@ -94,7 +94,7 @@ private void fetch() { } } - private GitHubRequest findNextURL(GitHubResponse response) { + GitHubRequest findNextURL(GitHubResponse response) { String linkHeader = response.headerField("Link"); if (linkHeader == null) return null; diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java index f442501..0cd780b 100644 --- a/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterable.java @@ -10,13 +10,14 @@ * @author Nikolas Correa */ public class GitHubCopilotSeatPagedSearchIterable extends PagedIterable { - private final transient GitHub root; + private final GitHub root; private final GitHubRequest request; private final Class> receiverType; - private GitHubCopilotSeatsSearchResult result; + GitHubCopilotSeatsSearchResult result; + private int pageOffset; public GitHubCopilotSeatPagedSearchIterable(GitHub root, GitHubRequest request, Class> receiverType) { @@ -40,7 +41,7 @@ public int getTotalSeats() { return result.total_seats; } - private void populate() { + public void populate() { if (result == null) iterator().hasNext(); // dispara a carga inicial } @@ -52,7 +53,7 @@ public PagedIterator _iterator(int pageSize) { return new PagedIterator<>(adapter, null); } - protected Iterator adapt(final Iterator> base) { + public Iterator adapt(final Iterator> base) { return new Iterator() { public boolean hasNext() { return base.hasNext(); diff --git a/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java index d9cd9be..0bced0f 100644 --- a/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilder.java @@ -14,7 +14,7 @@ public class GitHubCopilotSeatsSearchBuilder extends GHQueryBuilder { protected final Map filter = new HashMap<>(); - private final Class> receiverType; + final Class> receiverType; protected final GHEnterpriseExt enterprise; diff --git a/src/main/java/org/kohsuke/github/GitHubExt.java b/src/main/java/org/kohsuke/github/GitHubExt.java index c58ce63..fec3997 100644 --- a/src/main/java/org/kohsuke/github/GitHubExt.java +++ b/src/main/java/org/kohsuke/github/GitHubExt.java @@ -41,19 +41,6 @@ public GHUser getUser(long id) throws IOException { return u; } - /** - * Returns extension version of the GHOrganization. - * - * @param name GitHub organization name - * @return GitHub organization object - * @throws IOException the io exception - */ - @Override - public GHOrganizationExt getOrganization(String name) throws IOException { - GHOrganizationExt o = createRequest().withUrlPath("/orgs/" + name).fetch(GHOrganizationExt.class).wrapUp(this); - return o; - } - /** * Returns GHEnterprise.. * diff --git a/src/main/java/org/kohsuke/github/GraphQLConnection.java b/src/main/java/org/kohsuke/github/GraphQLConnection.java deleted file mode 100644 index f986e10..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLConnection.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL connection. - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public class GraphQLConnection { - @JsonProperty("edges") - public T[] edges; - - @JsonProperty("pageInfo") - public GraphQLPageInfo pageInfo; - - @JsonProperty("totalCount") - public int totalCount; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLEdge.java b/src/main/java/org/kohsuke/github/GraphQLEdge.java deleted file mode 100644 index 826b4d4..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLEdge.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL edge. - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public class GraphQLEdge { - - @JsonProperty("cursor") - public String cursor; - - @JsonProperty("node") - public T node; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLExternalIdentity.java b/src/main/java/org/kohsuke/github/GraphQLExternalIdentity.java deleted file mode 100644 index 5fa9608..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLExternalIdentity.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL externalIdentity. - * - * @author Hiroyuki Wada - */ -public class GraphQLExternalIdentity extends GraphQLNode { - - @JsonProperty("guid") - public String guid; - - @JsonProperty("organizationInvitation") - public GraphQLOrganizationInvitation organizationInvitation; - - @JsonProperty("samlIdentity") - public GraphQLExternalIdentitySamlAttributes samlIdentity; - - @JsonProperty("scimIdentity") - public GraphQLExternalIdentityScimAttributes scimIdentity; - - @JsonProperty("user") - public GraphQLUser user; - - @JsonIgnore - public boolean isPending() { - return organizationInvitation != null && user == null; - } - - @JsonIgnore - public boolean isCompleted() { - return user != null && user.organization != null; - } - - @JsonIgnore - public boolean isDropped() { - return user != null && user.organization == null; - } - - @JsonIgnore - public String getStatus() { - if (isPending()) { - return "pending"; - } - if (isCompleted()) { - return "active"; - } - return "dropped"; - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityConnection.java b/src/main/java/org/kohsuke/github/GraphQLExternalIdentityConnection.java deleted file mode 100644 index 7174691..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityConnection.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.kohsuke.github; - -/** - * Represents the result of a GraphQL connection. - * - * @author Hiroyuki Wada - */ -public class GraphQLExternalIdentityConnection extends GraphQLConnection { -} diff --git a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityEdge.java b/src/main/java/org/kohsuke/github/GraphQLExternalIdentityEdge.java deleted file mode 100644 index 2e700f1..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityEdge.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.kohsuke.github; - -/** - * Represents the result of a GraphQL edge. - * - * @author Hiroyuki Wada - */ -public class GraphQLExternalIdentityEdge extends GraphQLEdge { -} diff --git a/src/main/java/org/kohsuke/github/GraphQLExternalIdentitySamlAttributes.java b/src/main/java/org/kohsuke/github/GraphQLExternalIdentitySamlAttributes.java deleted file mode 100644 index 1cdacab..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLExternalIdentitySamlAttributes.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLExternalIdentitySamlAttributes { - @JsonProperty("nameId") - public String nameId; - - @JsonProperty("username") - public String username; - - @JsonProperty("emails") - public GraphQLUserEmailMetadata[] emails; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityScimAttributes.java b/src/main/java/org/kohsuke/github/GraphQLExternalIdentityScimAttributes.java deleted file mode 100644 index 622ac21..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLExternalIdentityScimAttributes.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLExternalIdentityScimAttributes { - @JsonProperty("emails") - public GraphQLUserEmailMetadata[] emails; - - @JsonProperty("username") - public String username; - - @JsonProperty("givenName") - public String givenName; - - @JsonProperty("familyName") - public String familyName; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLNode.java b/src/main/java/org/kohsuke/github/GraphQLNode.java deleted file mode 100644 index 5b64af5..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLNode.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLNode { - @JsonProperty("id") - public String id; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLOrganization.java b/src/main/java/org/kohsuke/github/GraphQLOrganization.java deleted file mode 100644 index 64c5263..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLOrganization.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL Organization. - * - * @author Hiroyuki Wada - */ -public class GraphQLOrganization extends GraphQLNode { - @JsonProperty("login") - public String login; - - @JsonProperty("databaseId") - public String databaseId; - - @JsonProperty("samlIdentityProvider") - public GraphQLOrganizationIdentityProvider samlIdentityProvider; - - @JsonProperty("teams") - public GraphQLTeamConnection teams; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLOrganizationExternalIdentitySearchBuilder.java b/src/main/java/org/kohsuke/github/GraphQLOrganizationExternalIdentitySearchBuilder.java deleted file mode 100644 index ed68d5b..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLOrganizationExternalIdentitySearchBuilder.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.kohsuke.github; - -import java.util.Map; -import java.util.function.Function; - -/** - * Search organization external identities by GitHub GraphQL API. - * - * @author Hiroyuki Wada - */ -public class GraphQLOrganizationExternalIdentitySearchBuilder extends GraphQLSearchBuilder { - private final String query = "query($login: String!, $first: Int!, $after: String) {\n" + - " organization(login: $login) {\n" + - " id\n" + - " login\n" + - " databaseId\n" + - " samlIdentityProvider {\n" + - " externalIdentities(first: $first, after: $after) {\n" + - " totalCount\n" + - " pageInfo {\n" + - " endCursor\n" + - " hasNextPage\n" + - " hasPreviousPage\n" + - " startCursor\n" + - " }\n" + - " edges {\n" + - " cursor\n" + - " node {\n" + - " id\n" + - " guid\n" + - " organizationInvitation {\n" + - " id\n" + - " email\n" + - " }\n" + - " user {\n" + - " id\n" + - " login\n" + - " databaseId\n" + - " organization(login: $login) {\n" + - " id\n" + - " login\n" + - " databaseId\n" + - " }\n" + - " }\n" + - " scimIdentity {\n" + - " username\n" + - " emails {\n" + - " value\n" + - " primary\n" + - " }\n" + - " givenName\n" + - " familyName\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "}\n"; - - GraphQLOrganizationExternalIdentitySearchBuilder(GitHub root, GHOrganization org) { - super(root, org, GraphQLOrganizationSearchResult.class); - this.variables.login = org.login; - } - - private static class GraphQLOrganizationSearchResult extends GraphQLSearchResult { - public GraphQLOrganization organization; - - @Override - public void setData(Map data) { - this.organization = data.get("organization"); - } - - @Override - public GraphQLOrganization getData() { - return organization; - } - } - - @Override - public String getQuery() { - return query; - } - - @Override - protected Function, GraphQLPageInfo> getPageInfo() { - return (result) -> result.getData().samlIdentityProvider.externalIdentities.pageInfo; - } - - @Override - protected Function, GraphQLExternalIdentityEdge[]> getEdges() { - return (result) -> result.getData().samlIdentityProvider.externalIdentities.edges; - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLOrganizationIdentityProvider.java b/src/main/java/org/kohsuke/github/GraphQLOrganizationIdentityProvider.java deleted file mode 100644 index f6e778f..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLOrganizationIdentityProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL OrganizationIdentityProvider. - * - * @author Hiroyuki Wada - */ -public class GraphQLOrganizationIdentityProvider extends GraphQLNode { - @JsonProperty("externalIdentities") - public GraphQLExternalIdentityConnection externalIdentities; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLOrganizationInvitation.java b/src/main/java/org/kohsuke/github/GraphQLOrganizationInvitation.java deleted file mode 100644 index 6c6f9a4..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLOrganizationInvitation.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLOrganizationInvitation extends GraphQLNode { - @JsonProperty("email") - public String email; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLPageInfo.java b/src/main/java/org/kohsuke/github/GraphQLPageInfo.java deleted file mode 100644 index df79f5a..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLPageInfo.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL pageInfo - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public class GraphQLPageInfo { - @JsonProperty("endCursor") - public String endCursor; - - @JsonProperty("hasNextPage") - public boolean hasNextPage; - - @JsonProperty("hasPreviousPage") - public boolean hasPreviousPage; - - @JsonProperty("startCursor") - public String startCursor; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLPageIterator.java b/src/main/java/org/kohsuke/github/GraphQLPageIterator.java deleted file mode 100644 index c8a1c7e..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLPageIterator.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.Function; - -/** - * Used for any GraphQL resource that has pagination information. - *

- * This class is not thread-safe. Any one instance should only be called from a single thread. - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public class GraphQLPageIterator, U> implements Iterator { - private static final ObjectMapper mapper = new ObjectMapper(); - - private final GitHubClient client; - private final Class type; - private final GraphQLSearchVariables variables; - private Function, GraphQLPageInfo> findNext; - - private T next; - - private GitHubRequest nextRequest; - - private GitHubResponse finalResponse = null; - - private GraphQLPageIterator(GitHubClient client, Class type, GitHubRequest request, GraphQLSearchVariables variables, - Function, GraphQLPageInfo> nextFinder) { - if (!"POST".equals(request.method())) { - throw new IllegalStateException("Request method \"POST\" is required for GraphQL page iterator."); - } - - this.client = client; - this.type = type; - this.nextRequest = request; - this.variables = variables; - this.findNext = nextFinder; - } - - static , U> GraphQLPageIterator create(GitHubClient client, Class type, - GitHubRequest request, GraphQLSearchVariables variables, - Function, GraphQLPageInfo> nextFinder) { - - try { - GitHubRequest.Builder builder = request.toBuilder().set("variables", mapper.writeValueAsString(variables)); - request = builder.build(); - - return new GraphQLPageIterator<>(client, type, request, variables, nextFinder); - } catch (MalformedURLException | JsonProcessingException e) { - throw new GHException("Unable to build GitHub GraphQL API URL", e); - } - } - - public boolean hasNext() { - fetch(); - return next != null; - } - - public T next() { - fetch(); - T result = next; - if (result == null) - throw new NoSuchElementException(); - // If this is the last page, keep the response - next = null; - return result; - } - - public GitHubResponse finalResponse() { - if (hasNext()) { - throw new GHException("Final response is not available until after iterator is done."); - } - return finalResponse; - } - - private void fetch() { - if (next != null) - return; // already fetched - if (nextRequest == null) - return; // no more data to fetch - - URL url = nextRequest.url(); - try { - GitHubResponse nextResponse = client.sendRequest(nextRequest, - (responseInfo) -> GitHubResponse.parseBody(responseInfo, type)); - assert nextResponse.body() != null; - next = nextResponse.body(); - - if (next == null) { - throw new GHException("GraphQL API returns error"); - } - - GraphQLPageInfo pageInfo = findNext.apply(nextResponse.body()); - if (pageInfo == null || !pageInfo.hasNextPage) { - finalResponse = nextResponse; - nextRequest = null; - return; - } - - GraphQLSearchVariables nextVariables = variables.next(pageInfo); - - nextRequest = nextResponse.request().toBuilder() - .set("variables", mapper.writeValueAsString(nextVariables)) - .build(); - - } catch (IOException e) { - // Iterators do not throw IOExceptions, so we wrap any IOException - // in a runtime GHException to bubble out if needed. - throw new GHException("Failed to retrieve " + url, e); - } - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLPagedSearchIterable.java b/src/main/java/org/kohsuke/github/GraphQLPagedSearchIterable.java deleted file mode 100644 index 32001c6..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLPagedSearchIterable.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; -import java.util.function.Function; - -/** - * {@link PagedIterable} enhanced to report search result specific information. - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public class GraphQLPagedSearchIterable extends PagedIterable { - private final transient GitHub root; - - private final GitHubRequest request; - - private final Class> receiverType; - - /** - * As soon as we have any result fetched, it's set here so that we can report the total count. - */ - private GraphQLSearchResult result; - - private final GraphQLSearchVariables variables; - private final Function, U[]> adaptor; - private final Function, GraphQLPageInfo> nextFinder; - - public GraphQLPagedSearchIterable(GitHub root, GitHubRequest request, Class> receiverType, - GraphQLSearchVariables variables, - Function, U[]> adaptor, - Function, GraphQLPageInfo> nextFinder) { - this.root = root; - this.request = request; - this.receiverType = receiverType; - this.variables = variables; - this.adaptor = adaptor; - this.nextFinder = nextFinder; - } - - @Override - public GraphQLPagedSearchIterable withPageSize(int size) { - return (GraphQLPagedSearchIterable) super.withPageSize(size); - } - - @Override - public PagedIterator _iterator(int pageSize) { - variables.first = pageSize; - final Iterator adapter = adapt( - GraphQLPageIterator.create(root.getClient(), receiverType, request, variables, nextFinder)); - return new PagedIterator(adapter, null); - } - - /** - * Adapts {@link Iterator}. - * - * @param base the base - * @return the iterator - */ - protected Iterator[]> adapt(final Iterator> base) { - return new Iterator[]>() { - public boolean hasNext() { - return base.hasNext(); - } - - public GraphQLEdge[] next() { - GraphQLSearchResult v = base.next(); - if (result == null) - result = v; - return adaptor.apply(v); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLSearchBuilder.java b/src/main/java/org/kohsuke/github/GraphQLSearchBuilder.java deleted file mode 100644 index 673489b..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLSearchBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.kohsuke.github; - - -import java.net.MalformedURLException; -import java.util.function.Function; - -public abstract class GraphQLSearchBuilder extends GHQueryBuilder { - protected final V variables; - - /** - * Data transfer object that receives the result of search. - */ - private final Class> receiverType; - - protected final GHOrganization organization; - - GraphQLSearchBuilder(GitHub root, GHOrganization org, Class> receiverType) { - super(root); - this.organization = org; - this.receiverType = receiverType; - req.withUrlPath(getApiUrl()); - req.rateLimit(RateLimitTarget.GRAPHQL); - req.method("POST"); - req.set("query", getQuery()); - this.variables = initSearchVariables(); - } - - /** - * Performs the search. - */ - public GraphQLPagedSearchIterable list() { - try { - return new GraphQLPagedSearchIterable(root, req.build(), receiverType, variables, getEdges(), getPageInfo()); - } catch (MalformedURLException e) { - throw new GHException("", e); - } - } - - /** - * Gets api url. - * - * @return the api url - */ - protected String getApiUrl() { - return "/graphql"; - } - - protected abstract String getQuery(); - - protected abstract Function, GraphQLPageInfo> getPageInfo(); - - protected abstract Function, U[]> getEdges(); - - protected V initSearchVariables() { - return (V) new GraphQLSearchVariables(); - } -} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GraphQLSearchResult.java b/src/main/java/org/kohsuke/github/GraphQLSearchResult.java deleted file mode 100644 index 5bed8bf..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLSearchResult.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Map; - -/** - * Represents the result of a GraphQL search. - * - * @param the type parameter - * @author Hiroyuki Wada - */ -public abstract class GraphQLSearchResult { - @JsonProperty("data") - public abstract void setData(Map data); - - public abstract T getData(); -} diff --git a/src/main/java/org/kohsuke/github/GraphQLSearchVariables.java b/src/main/java/org/kohsuke/github/GraphQLSearchVariables.java deleted file mode 100644 index e67c5c3..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLSearchVariables.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public class GraphQLSearchVariables { - public String login; - public int first; - public String after; - - @JsonIgnore - public GraphQLSearchVariables next(GraphQLPageInfo pageInfo) { - GraphQLSearchVariables nextVariables = new GraphQLSearchVariables(); - nextVariables.login = this.login; - nextVariables.first = this.first; - nextVariables.after = pageInfo.endCursor; - - return nextVariables; - } -} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GraphQLTeam.java b/src/main/java/org/kohsuke/github/GraphQLTeam.java deleted file mode 100644 index 665821a..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeam.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL Team. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeam extends GraphQLNode { - @JsonProperty("name") - public String name; - - @JsonProperty("databaseId") - public Integer databaseId; - - @JsonProperty("slug") - public String slug; - - @JsonProperty("description") - public String description; - - @JsonProperty("members") - public GraphQLTeamMemberConnection members; - - @JsonProperty("privacy") - public GraphQLTeamPrivacy privacy; - - @JsonProperty("parentTeam") - public GraphQLTeam parentTeam; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchBuilder.java b/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchBuilder.java deleted file mode 100644 index 15bf051..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchBuilder.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.kohsuke.github; - -import java.util.Map; -import java.util.function.Function; - -/** - * Search organization's teams by member's login name with GitHub GraphQL API. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamByMemberSearchBuilder extends GraphQLSearchBuilder { - private final String query = "query($login: String!, $userLogin: String!, $first: Int!, $after: String) {\n" + - " organization(login: $login) {\n" + - " id\n" + - " login\n" + - " databaseId\n" + - " teams(userLogins: [$userLogin], first: $first, after: $after) {\n" + - " totalCount\n" + - " pageInfo {\n" + - " endCursor\n" + - " hasNextPage\n" + - " hasPreviousPage\n" + - " startCursor\n" + - " }\n" + - " edges {\n" + - " cursor\n" + - " node {\n" + - " id\n" + - " databaseId\n" + - " slug\n" + - " members(query: $userLogin) {\n" + - " totalCount\n" + - " edges {\n" + - " role\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "}\n"; - - GraphQLTeamByMemberSearchBuilder(GitHub root, GHOrganization org, String userLogin) { - super(root, org, GraphQLOrganizationSearchResult.class); - this.variables.login = org.login; - this.variables.userLogin = userLogin; - } - - private static class GraphQLOrganizationSearchResult extends GraphQLSearchResult { - public GraphQLOrganization organization; - - @Override - public void setData(Map data) { - this.organization = data.get("organization"); - } - - @Override - public GraphQLOrganization getData() { - return organization; - } - } - - @Override - public String getQuery() { - return query; - } - - @Override - protected GraphQLTeamByMemberSearchVariables initSearchVariables() { - return new GraphQLTeamByMemberSearchVariables(); - } - - @Override - protected Function, GraphQLPageInfo> getPageInfo() { - return (result) -> result.getData().teams.pageInfo; - } - - @Override - protected Function, GraphQLTeamEdge[]> getEdges() { - return (result) -> result.getData().teams.edges; - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchVariables.java b/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchVariables.java deleted file mode 100644 index 744883b..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamByMemberSearchVariables.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public class GraphQLTeamByMemberSearchVariables extends GraphQLSearchVariables { - public String userLogin; - - @JsonIgnore - public GraphQLTeamByMemberSearchVariables next(GraphQLPageInfo pageInfo) { - GraphQLTeamByMemberSearchVariables nextVariables = new GraphQLTeamByMemberSearchVariables(); - nextVariables.login = this.login; - nextVariables.first = this.first; - nextVariables.userLogin = this.userLogin; - nextVariables.after = pageInfo.endCursor; - - return nextVariables; - } -} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamConnection.java b/src/main/java/org/kohsuke/github/GraphQLTeamConnection.java deleted file mode 100644 index c2c93fe..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamConnection.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.kohsuke.github; - -/** - * Represents the result of a GraphQL team connection. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamConnection extends GraphQLConnection { -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamEdge.java b/src/main/java/org/kohsuke/github/GraphQLTeamEdge.java deleted file mode 100644 index 06c4709..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamEdge.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.kohsuke.github; - -/** - * Represents the result of a GraphQL edge. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamEdge extends GraphQLEdge { -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamMemberConnection.java b/src/main/java/org/kohsuke/github/GraphQLTeamMemberConnection.java deleted file mode 100644 index d795844..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamMemberConnection.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.kohsuke.github; - -/** - * Represents the result of a GraphQL team connection. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamMemberConnection extends GraphQLConnection { -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamMemberEdge.java b/src/main/java/org/kohsuke/github/GraphQLTeamMemberEdge.java deleted file mode 100644 index b891742..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamMemberEdge.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents the result of a GraphQL team member edge. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamMemberEdge extends GraphQLEdge { - @JsonProperty("role") - public GraphQLTeamMemberRole role; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamMemberRole.java b/src/main/java/org/kohsuke/github/GraphQLTeamMemberRole.java deleted file mode 100644 index 6e1f403..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamMemberRole.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.kohsuke.github; - -public enum GraphQLTeamMemberRole { - MEMBER, - MAINTAINER -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamPrivacy.java b/src/main/java/org/kohsuke/github/GraphQLTeamPrivacy.java deleted file mode 100644 index f0cfb9a..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamPrivacy.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.kohsuke.github; - -public enum GraphQLTeamPrivacy { - SECRET, - VISIBLE -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamSearchBuilder.java b/src/main/java/org/kohsuke/github/GraphQLTeamSearchBuilder.java deleted file mode 100644 index e4676f6..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamSearchBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.kohsuke.github; - -import java.util.Map; -import java.util.function.Function; - -/** - * Search organization's teams by name with GitHub GraphQL API. - * - * @author Hiroyuki Wada - */ -public class GraphQLTeamSearchBuilder extends GraphQLSearchBuilder { - private final String query = "query($login: String!, $teamName: String!, $first: Int!, $after: String) {\n" + - " organization(login: $login) {\n" + - " id\n" + - " login\n" + - " databaseId\n" + - " teams(query: $teamName, first: $first, after: $after) {\n" + - " totalCount\n" + - " pageInfo {\n" + - " endCursor\n" + - " hasNextPage\n" + - " hasPreviousPage\n" + - " startCursor\n" + - " }\n" + - " edges {\n" + - " cursor\n" + - " node {\n" + - " id\n" + - " databaseId\n" + - " name\n" + - " slug\n" + - " description\n" + - " privacy\n" + - " parentTeam {\n" + - " id\n" + - " databaseId\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "}\n"; - - GraphQLTeamSearchBuilder(GitHub root, GHOrganization org, String teamName) { - super(root, org, GraphQLOrganizationSearchResult.class); - this.variables.login = org.login; - this.variables.teamName = teamName; - } - - private static class GraphQLOrganizationSearchResult extends GraphQLSearchResult { - public GraphQLOrganization organization; - - @Override - public void setData(Map data) { - this.organization = data.get("organization"); - } - - @Override - public GraphQLOrganization getData() { - return organization; - } - } - - @Override - public String getQuery() { - return query; - } - - @Override - protected GraphQLTeamSearchVariables initSearchVariables() { - return new GraphQLTeamSearchVariables(); - } - - @Override - protected Function, GraphQLPageInfo> getPageInfo() { - return (result) -> result.getData().teams.pageInfo; - } - - @Override - protected Function, GraphQLTeamEdge[]> getEdges() { - return (result) -> result.getData().teams.edges; - } -} diff --git a/src/main/java/org/kohsuke/github/GraphQLTeamSearchVariables.java b/src/main/java/org/kohsuke/github/GraphQLTeamSearchVariables.java deleted file mode 100644 index 4236fe2..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLTeamSearchVariables.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public class GraphQLTeamSearchVariables extends GraphQLSearchVariables { - public String teamName; - - @JsonIgnore - public GraphQLTeamSearchVariables next(GraphQLPageInfo pageInfo) { - GraphQLTeamSearchVariables nextVariables = new GraphQLTeamSearchVariables(); - nextVariables.login = this.login; - nextVariables.first = this.first; - nextVariables.teamName = this.teamName; - nextVariables.after = pageInfo.endCursor; - - return nextVariables; - } -} \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GraphQLUser.java b/src/main/java/org/kohsuke/github/GraphQLUser.java deleted file mode 100644 index 13d00e9..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLUser.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLUser extends GraphQLNode { - @JsonProperty("login") - public String login; - - @JsonProperty("databaseId") - public int databaseId; - - @JsonProperty("organization") - public GraphQLOrganization organization; -} diff --git a/src/main/java/org/kohsuke/github/GraphQLUserEmailMetadata.java b/src/main/java/org/kohsuke/github/GraphQLUserEmailMetadata.java deleted file mode 100644 index 54db9e2..0000000 --- a/src/main/java/org/kohsuke/github/GraphQLUserEmailMetadata.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.kohsuke.github; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class GraphQLUserEmailMetadata { - @JsonProperty("primary") - public Boolean primary; - - @JsonProperty("type") - public String type; - - @JsonProperty("value") - public String value; -} diff --git a/src/main/java/org/kohsuke/github/SCIMPageIterator.java b/src/main/java/org/kohsuke/github/SCIMPageIterator.java index e5b5b4d..4c1828d 100644 --- a/src/main/java/org/kohsuke/github/SCIMPageIterator.java +++ b/src/main/java/org/kohsuke/github/SCIMPageIterator.java @@ -25,7 +25,7 @@ public class SCIMPageIterator implements Iterator private GitHubResponse finalResponse = null; - private SCIMPageIterator(GitHubClient client, Class type, GitHubRequest request) { + SCIMPageIterator(GitHubClient client, Class type, GitHubRequest request) { if (!"GET".equals(request.method())) { throw new IllegalStateException("Request method \"GET\" is required for page iterator."); } @@ -36,7 +36,6 @@ private SCIMPageIterator(GitHubClient client, Class type, GitHubRequest reque } static SCIMPageIterator create(GitHubClient client, Class type, GitHubRequest request, int pageSize, int pageOffset) { - try { if (pageSize > 0) { GitHubRequest.Builder builder = request.toBuilder().with("count", pageSize); @@ -97,7 +96,7 @@ private void fetch() { } } - private GitHubRequest findNextURL(GitHubResponse nextResponse) throws MalformedURLException { + GitHubRequest findNextURL(GitHubResponse nextResponse) throws MalformedURLException { T res = nextResponse.body(); long endIndex = res.startIndex + res.itemsPerPage; if (endIndex > res.totalResults) { diff --git a/src/main/java/org/kohsuke/github/SCIMPagedSearchIterable.java b/src/main/java/org/kohsuke/github/SCIMPagedSearchIterable.java index 9ebc230..659f371 100644 --- a/src/main/java/org/kohsuke/github/SCIMPagedSearchIterable.java +++ b/src/main/java/org/kohsuke/github/SCIMPagedSearchIterable.java @@ -18,7 +18,7 @@ public class SCIMPagedSearchIterable extends PagedIterable { /** * As soon as we have any result fetched, it's set here so that we can report the total count. */ - private SCIMSearchResult result; + SCIMSearchResult result; private int pageOffset; @@ -58,7 +58,7 @@ public boolean isIncomplete() { return result.totalResults <= result.startIndex + result.itemsPerPage; } - private void populate() { + void populate() { if (result == null) iterator().hasNext(); } diff --git a/src/main/java/org/kohsuke/github/SCIMUserSearchBuilder.java b/src/main/java/org/kohsuke/github/SCIMUserSearchBuilder.java index c309cee..e144b20 100644 --- a/src/main/java/org/kohsuke/github/SCIMUserSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/SCIMUserSearchBuilder.java @@ -7,7 +7,7 @@ */ public class SCIMUserSearchBuilder extends SCIMSearchBuilder { - SCIMUserSearchBuilder(GitHub root, GHOrganization org) { + public SCIMUserSearchBuilder(GitHub root, GHOrganization org) { super(root, org, SCIMUserSearchResult.class); } @@ -15,7 +15,7 @@ private static class SCIMUserSearchResult extends SCIMSearchResult { } @Override - protected String getApiUrl() { + public String getApiUrl() { return String.format("/scim/v2/organizations/%s/Users", organization.login); } } diff --git a/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java b/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java index 80470f6..8bab742 100644 --- a/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java @@ -3,15 +3,15 @@ import jp.openstandia.connector.github.testutil.AbstractEMUTest; import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import java.util.HashSet; import java.util.Set; import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; +import static org.junit.jupiter.api.Assertions.assertNotNull; -public class CreateUserOpTest extends AbstractEMUTest { +class CreateUserOpTest extends AbstractEMUTest { private Set userEntry() { @@ -26,9 +26,9 @@ private Set userEntry() { } @Test() - public void shouldCreateOrReturnExistentUser() { + void shouldCreateOrReturnExistentUser() { ConnectorFacade facade = newFacade(); Uid uid = facade.create(USER_OBJECT_CLASS, userEntry(), null); - AssertJUnit.assertNotNull(uid); + assertNotNull(uid); } } diff --git a/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java index 8a7a61e..f0697e1 100644 --- a/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java @@ -3,17 +3,19 @@ import jp.openstandia.connector.github.testutil.AbstractEMUTest; import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.Uid; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; +import static org.junit.jupiter.api.Assertions.assertNotNull; -public class DeleteUsersOpTest extends AbstractEMUTest { +class DeleteUsersOpTest extends AbstractEMUTest { String userUidToDelete = ""; @Test() - public void shouldDeleteUserIfExists() { + void shouldDeleteUserIfExists() { ConnectorFacade facade = newFacade(); facade.delete(USER_OBJECT_CLASS, new Uid(userUidToDelete), null); + assertNotNull(userUidToDelete); } } diff --git a/src/test/java/jp/openstandia/connector/github/GitHubClientDefaultsUnsupportedTest.java b/src/test/java/jp/openstandia/connector/github/GitHubClientDefaultsUnsupportedTest.java new file mode 100644 index 0000000..8f2649f --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubClientDefaultsUnsupportedTest.java @@ -0,0 +1,195 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.QueryHandler; +import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.Uid; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GitHubCopilotSeat; +import org.kohsuke.github.SCIMEMUGroup; +import org.kohsuke.github.SCIMEMUUser; +import org.kohsuke.github.SCIMPatchOperations; + +import java.util.Collections; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +class GitHubClientDefaultsUnsupportedTest { + + /** + * Implementação mínima apenas para acessar os métodos default. + */ + private static class DummyClient implements GitHubClient> { + @Override + public void setInstanceName(String instanceName) { + } + + @Override + public void test() { + } + + @Override + public void auth() { + } + + @Override + public void close() { + } + } + + + @SuppressWarnings("unchecked") + private static AbstractGitHubSchema mockSchema() { + return mock(AbstractGitHubSchema.class); + } + + private static OperationOptions emptyOptions() { + return new OperationOptions(Collections.emptyMap()); + } + + // -------------------- + // EMU USER (defaults) + // -------------------- + + @Test + void default_createEMUUser_throwsUnsupported() { + DummyClient client = new DummyClient(); + SCIMEMUUser user = new SCIMEMUUser(); + + assertThrows(UnsupportedOperationException.class, () -> client.createEMUUser(user)); + } + + @Test + void default_patchEMUUser_throwsUnsupported() { + DummyClient client = new DummyClient(); + Uid uid = new Uid("u1"); + SCIMPatchOperations ops = new SCIMPatchOperations(); + + assertThrows(UnsupportedOperationException.class, () -> client.patchEMUUser(uid, ops)); + } + + @Test + void default_deleteEMUUser_throwsUnsupported() { + DummyClient client = new DummyClient(); + Uid uid = new Uid("u1"); + + assertThrows(UnsupportedOperationException.class, + () -> client.deleteEMUUser(uid, emptyOptions())); + } + + @Test + void default_getEMUUsers_throwsUnsupported() { + DummyClient client = new DummyClient(); + QueryHandler handler = u -> true; + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUUsers(handler, emptyOptions(), Collections.emptySet(), 10, 0)); + } + + @Test + void default_getEMUUser_byUid_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUUser(new Uid("u1"), emptyOptions(), Collections.emptySet())); + } + + @Test + void default_getEMUUser_byName_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUUser(new Name("alice"), emptyOptions(), Collections.emptySet())); + } + + // -------------------- + // EMU GROUP (defaults) + // -------------------- + + @Test + void default_createEMUGroup_throwsUnsupported() throws AlreadyExistsException { + DummyClient client = new DummyClient(); + SCIMEMUGroup group = new SCIMEMUGroup(); + + assertThrows(UnsupportedOperationException.class, + () -> client.createEMUGroup(mockSchema(), group)); + } + + @Test + void default_patchEMUGroup_throwsUnsupported() throws UnknownUidException { + DummyClient client = new DummyClient(); + Uid uid = new Uid("g1"); + SCIMPatchOperations ops = new SCIMPatchOperations(); + + assertThrows(UnsupportedOperationException.class, + () -> client.patchEMUGroup(uid, ops)); + } + + @Test + void default_deleteEMUGroup_throwsUnsupported() throws UnknownUidException { + DummyClient client = new DummyClient(); + Uid uid = new Uid("g1"); + + assertThrows(UnsupportedOperationException.class, + () -> client.deleteEMUGroup(uid, emptyOptions())); + } + + @Test + void default_getEMUGroups_throwsUnsupported() { + DummyClient client = new DummyClient(); + QueryHandler handler = g -> true; + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUGroups(handler, emptyOptions(), Collections.emptySet(), 5, 0)); + } + + @Test + void default_getEMUGroup_byUid_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUGroup(new Uid("g1"), emptyOptions(), Collections.emptySet())); + } + + @Test + void default_getEMUGroup_byName_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getEMUGroup(new Name("groupA"), emptyOptions(), Collections.emptySet())); + } + + // -------------------- + // COPILOT SEATS (defaults) + // -------------------- + + @Test + void default_getCopilotSeat_byUid_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getCopilotSeat(new Uid("seat-1"), emptyOptions(), Collections.emptySet())); + } + + @Test + void default_getCopilotSeat_byName_throwsUnsupported() { + DummyClient client = new DummyClient(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getCopilotSeat(new Name("user-login"), emptyOptions(), Collections.emptySet())); + } + + @Test + void default_getCopilotSeats_throwsUnsupported() { + DummyClient client = new DummyClient(); + QueryHandler handler = s -> true; + Set fetch = Collections.emptySet(); + + assertThrows(UnsupportedOperationException.class, + () -> client.getCopilotSeats(handler, emptyOptions(), fetch, 20, 0)); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java new file mode 100644 index 0000000..d25ba07 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java @@ -0,0 +1,113 @@ +package jp.openstandia.connector.github; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Verifica que os métodos default de GitHubClient (EMU User/Group/Copilot) + * lançam UnsupportedOperationException, como definido na interface. + */ + +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import org.identityconnectors.common.security.GuardedString; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +/** + * Tests for GitHubClient#createClient and basic default behaviors. + */ +class GitHubClientTest { + + /** Minimal do-nothing implementation so we can call default methods. */ + private static class DummyClient implements GitHubClient> { + @Override public void setInstanceName(String instanceName) {} + @Override public void test() {} + @Override public void auth() {} + @Override public void close() {} + } + + private AbstractGitHubConfiguration baseConfig( + long connectMs, long readMs, long writeMs + ) { + AbstractGitHubConfiguration cfg = mock(AbstractGitHubConfiguration.class); + when(cfg.getConnectionTimeoutInMilliseconds()).thenReturn((int) connectMs); + when(cfg.getReadTimeoutInMilliseconds()).thenReturn((int) readMs); + when(cfg.getWriteTimeoutInMilliseconds()).thenReturn((int) writeMs); + + // Defaults: no proxy + when(cfg.getHttpProxyHost()).thenReturn(""); + when(cfg.getHttpProxyPort()).thenReturn(0); + when(cfg.getHttpProxyUser()).thenReturn(""); + when(cfg.getHttpProxyPassword()).thenReturn(null); + + return cfg; + } + + @Test + void createClient_withoutProxy_usesTimeouts_and_noProxy() { + DummyClient client = new DummyClient(); + AbstractGitHubConfiguration cfg = baseConfig(1234, 5678, 9999); + + OkHttpClient ok = client.createClient(cfg); + + // OkHttp expõe ms getters; checamos os timeouts e ausência de proxy. + assertEquals(1234, ok.connectTimeoutMillis()); + assertEquals(5678, ok.readTimeoutMillis()); + assertEquals(9999, ok.writeTimeoutMillis()); + assertNull(ok.proxy(), "Não deveria haver proxy quando host está vazio"); + } + + @Test + void createClient_withProxy_withoutAuth_setsProxyOnly() { + DummyClient client = new DummyClient(); + AbstractGitHubConfiguration cfg = baseConfig(2000, 3000, 4000); + + // Configura somente proxy host/port; sem usuário/senha + when(cfg.getHttpProxyHost()).thenReturn("proxy.local"); + when(cfg.getHttpProxyPort()).thenReturn(8080); + when(cfg.getHttpProxyUser()).thenReturn(""); + when(cfg.getHttpProxyPassword()).thenReturn(null); + + OkHttpClient ok = client.createClient(cfg); + + Proxy proxy = ok.proxy(); + assertNotNull(proxy, "Proxy deveria estar configurado"); + assertEquals(Proxy.Type.HTTP, proxy.type()); + InetSocketAddress addr = (InetSocketAddress) proxy.address(); + assertEquals("proxy.local", addr.getHostString()); + assertEquals(8080, addr.getPort()); + + // Sem autenticação + Authenticator pa = ok.proxyAuthenticator(); + // Em OkHttp, o default é Authenticator.NONE quando não setado + assertSame(Authenticator.NONE, pa, "Não deveria configurar proxyAuthenticator sem user/password"); + } + + @Test + void createClient_withProxy_andAuth_setsProxyAndAuthenticator() { + DummyClient client = new DummyClient(); + AbstractGitHubConfiguration cfg = baseConfig(1000, 1000, 1000); + + when(cfg.getHttpProxyHost()).thenReturn("corp-proxy"); + when(cfg.getHttpProxyPort()).thenReturn(3128); + when(cfg.getHttpProxyUser()).thenReturn("user1"); + when(cfg.getHttpProxyPassword()).thenReturn(new GuardedString("secret".toCharArray())); + + OkHttpClient ok = client.createClient(cfg); + + Proxy proxy = ok.proxy(); + assertNotNull(proxy); + assertEquals(Proxy.Type.HTTP, proxy.type()); + + // Com user/senha, um Authenticator deve ser configurado + Authenticator pa = ok.proxyAuthenticator(); + assertNotNull(pa); + assertNotSame(Authenticator.NONE, pa, "Deveria haver um proxyAuthenticator quando user/senha estão presentes"); + } +} + + diff --git a/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java new file mode 100644 index 0000000..adb4d6b --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java @@ -0,0 +1,78 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.SCIMEMUUser; +import org.kohsuke.github.SCIMPatchOperations; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; +import static org.identityconnectors.framework.common.objects.AttributeInfo.Flags.NOT_CREATABLE; +import static org.identityconnectors.framework.common.objects.AttributeInfo.Flags.NOT_UPDATEABLE; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +public class GitHubEMUUserHandlerTest { + + private static class DummyEMUClient implements GitHubClient { + @Override public void setInstanceName(String instanceName) {} + @Override public void test() {} + @Override public void auth() {} + @Override public void close() {} + } + + private static GitHubEMUUserHandler newHandler() { + GitHubEMUConfiguration configuration = mock(GitHubEMUConfiguration.class); + GitHubClient client = new DummyEMUClient(); + GitHubEMUSchema schema = mock(GitHubEMUSchema.class); + SchemaDefinition schemaDefinition = mock(SchemaDefinition.class); + + return new GitHubEMUUserHandler(configuration, client, schema, schemaDefinition); + } + + @Test + void instancia_handler_ok() { + GitHubEMUUserHandler handler = newHandler(); + assertNotNull(handler); + } + + + @Test + public void testCreateSchema() { + GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); + GitHubClient client = mock(GitHubClient.class); + SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); + assertNotNull(builder); + } + + @Test + public void testUidLambdaExecution() { + GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); + GitHubClient client = mock(GitHubClient.class); + + SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); + SchemaDefinition definition = builder.build(); + + // Agora precisamos de um "source" que tenha o campo 'id' + SCIMEMUUser user = new SCIMEMUUser(); + user.id = UUID.randomUUID().toString(); + + // A mágica: simular a extração do UID usando o schema + String extractedId = definition.getReturnedByDefaultAttributesSet().get(user.id); + + assertEquals(user.id, extractedId); + } +} + + diff --git a/src/test/java/jp/openstandia/connector/github/GitHubFilterTest.java b/src/test/java/jp/openstandia/connector/github/GitHubFilterTest.java new file mode 100644 index 0000000..91b7e93 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubFilterTest.java @@ -0,0 +1,91 @@ +package jp.openstandia.connector.github; + +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.Uid; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubFilterTest { + + @Test + void byUid_shouldSetUidAndReportIsByUid() { + Uid uid = new Uid("123"); + GitHubFilter f = GitHubFilter.By(uid); + + assertTrue(f.isByUid(), "isByUid should be true when created By(Uid)"); + assertFalse(f.isByName(), "isByName should be false when created By(Uid)"); + // isByMembers only applies to ByMember + // (avoid NPE by not calling when attributeName is null) + assertSame(uid, f.uid); + assertNull(f.name); + assertEquals(GitHubFilter.FilterType.EXACT_MATCH, f.filterType); + assertNull(f.attributeName); + assertNull(f.attributeValue); + } + + @Test + void byName_shouldSetNameAndReportIsByName() { + Name name = new Name("alice"); + GitHubFilter f = GitHubFilter.By(name); + + assertTrue(f.isByName(), "isByName should be true when created By(Name)"); + assertFalse(f.isByUid(), "isByUid should be false when created By(Name)"); + assertSame(name, f.name); + assertNull(f.uid); + assertEquals(GitHubFilter.FilterType.EXACT_MATCH, f.filterType); + assertNull(f.attributeName); + assertNull(f.attributeValue); + } + + @Test + void byMember_exactMatchWithRightAttribute_shouldReportIsByMembersTrue() { + Attribute memberAttr = AttributeBuilder.build("members.User.value", "u-001"); + GitHubFilter f = GitHubFilter.ByMember( + "members.User.value", + GitHubFilter.FilterType.EXACT_MATCH, + memberAttr + ); + + assertFalse(f.isByUid()); + assertFalse(f.isByName()); + assertTrue(f.isByMembers(), "isByMembers should be true for EXACT_MATCH on members.User.value"); + + assertEquals("members.User.value", f.attributeName); + assertEquals(GitHubFilter.FilterType.EXACT_MATCH, f.filterType); + assertSame(memberAttr, f.attributeValue); + } + + @Test + void byMember_wrongAttribute_shouldReportIsByMembersFalse() { + Attribute otherAttr = AttributeBuilder.build("somethingElse", "x"); + GitHubFilter f = GitHubFilter.ByMember( + "somethingElse", + GitHubFilter.FilterType.EXACT_MATCH, + otherAttr + ); + + assertFalse(f.isByUid()); + assertFalse(f.isByName()); + assertFalse(f.isByMembers(), "isByMembers should be false for attributes other than members.User.value"); + } + + @Test + void byMember_rightAttributeButDifferentFilterType_shouldReportIsByMembersFalse() { + // Como atualmente só existe EXACT_MATCH no enum, este teste + // demonstra a intenção: se surgirem novos tipos, a verificação + // continua correta. Aqui apenas reafirmamos o comportamento. + Attribute memberAttr = AttributeBuilder.build("members.User.value", "u-002"); + GitHubFilter f = GitHubFilter.ByMember( + "members.User.value", + GitHubFilter.FilterType.EXACT_MATCH, // único tipo disponível hoje + memberAttr + ); + + // Com o enum atual, continua true; se houver novos tipos no futuro, + // este teste deve ser duplicado com um tipo diferente e esperar false. + assertTrue(f.isByMembers()); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/GitHubFilterTranslatorTest.java b/src/test/java/jp/openstandia/connector/github/GitHubFilterTranslatorTest.java new file mode 100644 index 0000000..20baba7 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubFilterTranslatorTest.java @@ -0,0 +1,114 @@ +package jp.openstandia.connector.github; + +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.ContainsAllValuesFilter; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubFilterTranslatorTest { + + private static OperationOptions opts() { + return new OperationOptionsBuilder().build(); + } + + @Test + void equalsExpression_onUid_returnsByUid() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(ObjectClass.ACCOUNT, opts()); + + Uid uid = new Uid("123"); + GitHubFilter f = tr.createEqualsExpression(new EqualsFilter(uid), false); + + assertNotNull(f); + assertTrue(f.isByUid()); + assertFalse(f.isByName()); + } + + @Test + void equalsExpression_onName_returnsByName() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(ObjectClass.ACCOUNT, opts()); + + Name name = new Name("alice"); + GitHubFilter f = tr.createEqualsExpression(new EqualsFilter(name), false); + + assertNotNull(f); + assertTrue(f.isByName()); + assertFalse(f.isByUid()); + } + + @Test + void equalsExpression_onUnsupportedAttr_returnsNull() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(ObjectClass.ACCOUNT, opts()); + + Attribute other = AttributeBuilder.build("email", "a@b.c"); + GitHubFilter f = tr.createEqualsExpression(new EqualsFilter(other), false); + + assertNull(f); + } + + @Test + void equalsExpression_notFlag_returnsNull() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(ObjectClass.ACCOUNT, opts()); + + GitHubFilter f = tr.createEqualsExpression(new EqualsFilter(new Name("x")), true); + assertNull(f); + } + + @Test + void containsAll_onGroupWithMembersExactMatch_returnsByMembers() { + // Usa o mesmo ObjectClass que o tradutor verifica + GitHubFilterTranslator tr = + new GitHubFilterTranslator(GitHubEMUGroupHandler.GROUP_OBJECT_CLASS, opts()); + + Attribute members = AttributeBuilder.build("members.User.value", "u-001"); + GitHubFilter f = + tr.createContainsAllValuesExpression(new ContainsAllValuesFilter(members), false); + + assertNotNull(f); + assertTrue(f.isByMembers(), "Esperado filtro de membros (EXACT_MATCH em members.User.value)"); + assertEquals("members.User.value", f.attributeName); + assertEquals(GitHubFilter.FilterType.EXACT_MATCH, f.filterType); + assertSame(members, f.attributeValue); + } + + @Test + void containsAll_onDifferentObjectClass_returnsNull() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(ObjectClass.ACCOUNT, opts()); + + Attribute members = AttributeBuilder.build("members.User.value", "u-001"); + GitHubFilter f = + tr.createContainsAllValuesExpression(new ContainsAllValuesFilter(members), false); + + assertNull(f); + } + + @Test + void containsAll_onGroupButDifferentAttribute_returnsNull() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(GitHubEMUGroupHandler.GROUP_OBJECT_CLASS, opts()); + + Attribute other = AttributeBuilder.build("members.Group.value", "g-1"); + GitHubFilter f = + tr.createContainsAllValuesExpression(new ContainsAllValuesFilter(other), false); + + assertNull(f); + } + + @Test + void containsAll_notFlag_returnsNull() { + GitHubFilterTranslator tr = + new GitHubFilterTranslator(GitHubEMUGroupHandler.GROUP_OBJECT_CLASS, opts()); + + Attribute members = AttributeBuilder.build("members.User.value", "u-001"); + GitHubFilter f = + tr.createContainsAllValuesExpression(new ContainsAllValuesFilter(members), true); + + assertNull(f); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/GitHubUtilsTest.java b/src/test/java/jp/openstandia/connector/github/GitHubUtilsTest.java deleted file mode 100644 index fef4305..0000000 --- a/src/test/java/jp/openstandia/connector/github/GitHubUtilsTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package jp.openstandia.connector.github; - -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class GitHubUtilsTest { - - @Test - void getUserLogin() { - assertEquals("", GitHubUtils.getUserLogin(":foo@example.com")); - assertEquals("foo", GitHubUtils.getUserLogin("foo:foo@example.com")); - assertThrows(InvalidAttributeValueException.class, () -> GitHubUtils.getUserLogin("foo")); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/SchemaTest.java b/src/test/java/jp/openstandia/connector/github/SchemaTest.java deleted file mode 100644 index 77633f0..0000000 --- a/src/test/java/jp/openstandia/connector/github/SchemaTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractTest; -import org.identityconnectors.framework.common.objects.ObjectClassInfo; -import org.identityconnectors.framework.common.objects.Schema; -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; - -class SchemaTest extends AbstractTest { - - @Test - void schema() { - Schema schema = connector.schema(); - - assertNotNull(schema); - assertEquals(2, schema.getObjectClassInfo().size()); - - Optional user = schema.getObjectClassInfo().stream().filter(o -> o.is("user")).findFirst(); - Optional team = schema.getObjectClassInfo().stream().filter(o -> o.is("team")).findFirst(); - - assertTrue(user.isPresent()); - assertTrue(team.isPresent()); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java index 6ba39c1..e39bce9 100644 --- a/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java @@ -4,30 +4,30 @@ import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; -public class SearchGroupsOpTest extends AbstractEMUTest { +class SearchGroupsOpTest extends AbstractEMUTest { String groupUid = ""; String groupName = ""; @Test() - public void shouldReturnAllGroups() { + void shouldReturnAllGroups() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); facade.search(GROUP_OBJECT_CLASS, null, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + assertTrue(objects.size() > 1, "Size: " + objects.size()); } @Test() - public void shouldReturnGroupByUid() { + void shouldReturnGroupByUid() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -36,11 +36,11 @@ public void shouldReturnGroupByUid() { facade.search(GROUP_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } @Test() - public void shouldReturnGroupByName() { + void shouldReturnGroupByName() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -49,6 +49,6 @@ public void shouldReturnGroupByName() { facade.search(GROUP_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } } diff --git a/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java index 71c6083..c3fe2c8 100644 --- a/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java @@ -4,30 +4,29 @@ import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static jp.openstandia.connector.github.GitHubCopilotSeatHandler.SEAT_OBJECT_CLASS; import java.util.List; -import static jp.openstandia.connector.github.GitHubCopilotSeatHandler.SEAT_OBJECT_CLASS; - -public class SearchSeatsOpTest extends AbstractEMUTest { +class SearchSeatsOpTest extends AbstractEMUTest { String seatUid = ""; String seatName = ""; @Test() - public void shouldReturnAllSeats() { + void shouldReturnAllSeats() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); facade.search(SEAT_OBJECT_CLASS, null, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + assertTrue(objects.size() > 1, "Size: " + objects.size()); } @Test() - public void shouldReturnSeatByUid() { + void shouldReturnSeatByUid() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -36,11 +35,11 @@ public void shouldReturnSeatByUid() { facade.search(SEAT_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } @Test() - public void shouldReturnSeatByName() { + void shouldReturnSeatByName() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -49,6 +48,6 @@ public void shouldReturnSeatByName() { facade.search(SEAT_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } } diff --git a/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java index d8afc4c..30685d3 100644 --- a/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java @@ -4,30 +4,31 @@ import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; import java.util.List; import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; -public class SearchUsersOpTest extends AbstractEMUTest { +class SearchUsersOpTest extends AbstractEMUTest { String userUid = ""; String userName = ""; @Test() - public void shouldReturnAllUsers() { + void shouldReturnAllUsers() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); facade.search(USER_OBJECT_CLASS, null, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertTrue("Size: " + objects.size(), objects.size() > 1); + assertTrue(objects.size() > 1, "Size: " + objects.size()); } @Test() - public void shouldReturnUserByUid() { + void shouldReturnUserByUid() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -36,11 +37,11 @@ public void shouldReturnUserByUid() { facade.search(USER_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } @Test() - public void shouldReturnUserByUsername() { + void shouldReturnUserByUsername() { ConnectorFacade facade = newFacade(); ListResultHandler handler = new ListResultHandler(); @@ -49,6 +50,6 @@ public void shouldReturnUserByUsername() { facade.search(USER_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } } diff --git a/src/test/java/jp/openstandia/connector/github/TeamAssignmentResolverTest.java b/src/test/java/jp/openstandia/connector/github/TeamAssignmentResolverTest.java deleted file mode 100644 index c5d2428..0000000 --- a/src/test/java/jp/openstandia/connector/github/TeamAssignmentResolverTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package jp.openstandia.connector.github; - - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashSet; -import java.util.Set; - -import static junit.framework.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; - -class TeamAssignmentResolverTest { - - Set addTeams; - Set removeTeams; - Set addMaintainerTeams; - Set removeMaintainerTeams; - - @BeforeEach - void init() { - addTeams = new HashSet<>(); - removeTeams = new HashSet<>(); - addMaintainerTeams = new HashSet<>(); - removeMaintainerTeams = new HashSet<>(); - } - - @Test - void addTeamOnly() { - addTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddTeams.size()); - assertTrue(resolver.resolvedAddTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddMaitainerTeams.size()); - assertEquals(0, resolver.resolvedRemoveTeams.size()); - } - - @Test - void addMaintainerTeamOnly() { - addMaintainerTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddMaitainerTeams.size()); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddTeams.size()); - assertEquals(0, resolver.resolvedRemoveTeams.size()); - } - - @Test - void removeTeamOnly() { - removeTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedRemoveTeams.size()); - assertTrue(resolver.resolvedRemoveTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddTeams.size()); - assertEquals(0, resolver.resolvedAddMaitainerTeams.size()); - } - - @Test - void addTeamConflict() { - addTeams.add("t1"); - addMaintainerTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddMaitainerTeams.size()); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddTeams.size(), "Should be deleted"); - assertEquals(0, resolver.resolvedRemoveTeams.size()); - } - - @Test - void removeTeamConflict() { - removeTeams.add("t1"); - removeMaintainerTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedRemoveTeams.size(), "Should be deleted the duplication"); - assertTrue(resolver.resolvedRemoveTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddTeams.size()); - assertEquals(0, resolver.resolvedAddMaitainerTeams.size()); - } - - @Test - void addTeamAndMaintainerTeam() { - addTeams.add("t1"); - addMaintainerTeams.add("t2"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddTeams.size()); - assertTrue(resolver.resolvedAddTeams.contains("t1")); - - assertEquals(1, resolver.resolvedAddMaitainerTeams.size()); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t2")); - - assertEquals(0, resolver.resolvedRemoveTeams.size()); - } - - @Test - void removeTeamAndMaintainerTeam() { - removeTeams.add("t1"); - removeMaintainerTeams.add("t2"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(2, resolver.resolvedRemoveTeams.size()); - assertTrue(resolver.resolvedRemoveTeams.contains("t1")); - assertTrue(resolver.resolvedRemoveTeams.contains("t2")); - - assertEquals(0, resolver.resolvedAddTeams.size()); - assertEquals(0, resolver.resolvedAddMaitainerTeams.size()); - } - - @Test - void switchTeamToMaintainerTeam() { - addMaintainerTeams.add("t1"); - removeTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddMaitainerTeams.size()); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddTeams.size()); - assertEquals(0, resolver.resolvedRemoveTeams.size(), "Should be deleted for switching"); - } - - @Test - void switchMaintainerTeamToTeam() { - addTeams.add("t1"); - removeMaintainerTeams.add("t1"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(1, resolver.resolvedAddTeams.size()); - assertTrue(resolver.resolvedAddTeams.contains("t1")); - - assertEquals(0, resolver.resolvedAddMaitainerTeams.size()); - assertEquals(0, resolver.resolvedRemoveTeams.size(), "Should be deleted for switching"); - } - - @Test - void complex() { - addTeams.add("t1"); - addTeams.add("t2"); - addTeams.add("t3"); - removeTeams.add("t4"); - removeTeams.add("t5"); - removeTeams.add("t6"); - addMaintainerTeams.add("t1"); - addMaintainerTeams.add("t4"); - addMaintainerTeams.add("t7"); - removeMaintainerTeams.add("t2"); - removeMaintainerTeams.add("t5"); - removeMaintainerTeams.add("t8"); - - TeamAssignmentResolver resolver = new TeamAssignmentResolver(addTeams, removeTeams, addMaintainerTeams, removeMaintainerTeams); - - assertEquals(2, resolver.resolvedAddTeams.size()); - assertTrue(resolver.resolvedAddTeams.contains("t2")); - assertTrue(resolver.resolvedAddTeams.contains("t3")); - - assertEquals(3, resolver.resolvedAddMaitainerTeams.size()); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t1")); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t4")); - assertTrue(resolver.resolvedAddMaitainerTeams.contains("t7")); - - assertEquals(3, resolver.resolvedRemoveTeams.size()); - assertTrue(resolver.resolvedRemoveTeams.contains("t5")); - assertTrue(resolver.resolvedRemoveTeams.contains("t6")); - assertTrue(resolver.resolvedRemoveTeams.contains("t8")); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/TestOpTest.java b/src/test/java/jp/openstandia/connector/github/TestOpTest.java index 4b7d7f9..64d861b 100644 --- a/src/test/java/jp/openstandia/connector/github/TestOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/TestOpTest.java @@ -2,13 +2,14 @@ import jp.openstandia.connector.github.testutil.AbstractEMUTest; import org.identityconnectors.framework.api.ConnectorFacade; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; -public class TestOpTest extends AbstractEMUTest { +class TestOpTest extends AbstractEMUTest { @Test() - public void shouldInitializeConnection() { + void shouldInitializeConnection() { ConnectorFacade facade = newFacade(); facade.test(); } } + diff --git a/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java index 6257c06..fd20fd2 100644 --- a/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java @@ -4,22 +4,23 @@ import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.HashSet; import java.util.List; import java.util.Set; import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class UpdateGroupsOpTest extends AbstractEMUTest { +class UpdateGroupsOpTest extends AbstractEMUTest { String userUid = ""; String groupUidToUpdate = ""; @Test() - public void shouldAddUserToGroup() { + void shouldAddUserToGroup() { // Create an AttributeDelta to add user uid Set attributes = new HashSet<>(); AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); @@ -38,18 +39,18 @@ public void shouldAddUserToGroup() { facade.search(GROUP_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); ConnectorObject object = objects.get(0); Attribute memberOfAttr = object.getAttributeByName("members.User.value"); - AssertJUnit.assertNotNull(memberOfAttr); + assertNotNull(memberOfAttr); List grupos = memberOfAttr.getValue(); - AssertJUnit.assertTrue(grupos.contains(userUid)); + assertTrue(grupos.contains(userUid)); } @Test() - public void shouldRemoveUserFromGroup() { + void shouldRemoveUserFromGroup() { // Create an AttributeDelta to add user uid Set attributes = new HashSet<>(); AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); @@ -68,13 +69,13 @@ public void shouldRemoveUserFromGroup() { facade.search(GROUP_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); ConnectorObject object = objects.get(0); Attribute memberOfAttr = object.getAttributeByName("members.User.value"); - AssertJUnit.assertNotNull(memberOfAttr); + assertNotNull(memberOfAttr); List grupos = memberOfAttr.getValue(); - AssertJUnit.assertFalse(grupos.contains(userUid)); + assertFalse(grupos.contains(userUid)); } } diff --git a/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java index e9dc8d0..5288892 100644 --- a/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java +++ b/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java @@ -4,8 +4,9 @@ import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.common.objects.*; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.testng.AssertJUnit; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; import java.util.HashSet; import java.util.List; @@ -13,14 +14,14 @@ import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; -public class UpdateUsersOpTest extends AbstractEMUTest { +class UpdateUsersOpTest extends AbstractEMUTest { String userUid = ""; String attrToUpdate = ""; String attrNewValue = ""; @Test() - public void shouldActivateUser() { + void shouldActivateUser() { Set attributes = new HashSet<>(); AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); @@ -37,11 +38,11 @@ public void shouldActivateUser() { facade.search(USER_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } @Test() - public void shouldInactivateUser() { + void shouldInactivateUser() { Set attributes = new HashSet<>(); AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); @@ -58,11 +59,11 @@ public void shouldInactivateUser() { facade.search(USER_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); } @Test() - public void shouldUpdateAttrValue() { + void shouldUpdateAttrValue() { ConnectorFacade facade = newFacade(); // Create an AttributeDelta to update the status of uid @@ -82,11 +83,11 @@ public void shouldUpdateAttrValue() { facade.search(USER_OBJECT_CLASS, filter, handler, null); List objects = handler.getObjects(); - AssertJUnit.assertEquals(1, objects.size()); + assertEquals(1, objects.size()); ConnectorObject object = objects.get(0); Attribute nameAttr = object.getAttributeByName(attrToUpdate); - AssertJUnit.assertNotNull(nameAttr); - AssertJUnit.assertEquals(attrNewValue, nameAttr.getValue().get(0)); + assertNotNull(nameAttr); + assertEquals(attrNewValue, nameAttr.getValue().get(0)); } } diff --git a/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java b/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java index 5ebf906..d0a0c42 100644 --- a/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java +++ b/src/test/java/jp/openstandia/connector/github/testutil/AbstractEMUTest.java @@ -23,7 +23,6 @@ import org.identityconnectors.framework.api.ConnectorFacadeFactory; import org.identityconnectors.test.common.TestHelpers; import org.junit.jupiter.api.BeforeEach; -import org.testng.annotations.Test; public abstract class AbstractEMUTest { @@ -34,7 +33,7 @@ protected GitHubEMUConfiguration newConfiguration() { GitHubEMUConfiguration conf = new GitHubEMUConfiguration(); conf.setEnterpriseSlug(""); conf.setAccessToken(new GuardedString("".toCharArray())); - conf.setEndpointURL(""); + conf.setEndpointURL("https://api.github.com"); return conf; } @@ -53,4 +52,4 @@ void before() { mockClient = MockClient.instance(); mockClient.init(); } -} \ No newline at end of file +} diff --git a/src/test/java/jp/openstandia/connector/github/testutil/AbstractTest.java b/src/test/java/jp/openstandia/connector/github/testutil/AbstractTest.java deleted file mode 100644 index 3be1add..0000000 --- a/src/test/java/jp/openstandia/connector/github/testutil/AbstractTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github.testutil; - -import jp.openstandia.connector.github.GitHubConfiguration; -import jp.openstandia.connector.github.GitHubConnector; -import org.identityconnectors.framework.api.APIConfiguration; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.api.ConnectorFacadeFactory; -import org.identityconnectors.test.common.TestHelpers; -import org.junit.jupiter.api.BeforeEach; - -public abstract class AbstractTest { - - protected ConnectorFacade connector; - protected MockClient mockClient; - - protected GitHubConfiguration newConfiguration() { - GitHubConfiguration conf = new GitHubConfiguration(); - conf.setOrganizationName("localOrg"); - return conf; - } - - protected ConnectorFacade newFacade() { - ConnectorFacadeFactory factory = ConnectorFacadeFactory.getInstance(); - APIConfiguration impl = TestHelpers.createTestConfiguration(LocalGitHubConnector.class, newConfiguration()); - impl.getResultsHandlerConfiguration().setEnableAttributesToGetSearchResultsHandler(false); - impl.getResultsHandlerConfiguration().setEnableNormalizingResultsHandler(false); - impl.getResultsHandlerConfiguration().setEnableFilteredResultsHandler(false); - return factory.newInstance(impl); - } - - @BeforeEach - void before() { - connector = newFacade(); - mockClient = MockClient.instance(); - mockClient.init(); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubConnector.java b/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubConnector.java deleted file mode 100644 index 5939534..0000000 --- a/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubConnector.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github.testutil; - -import jp.openstandia.connector.github.GitHubClient; -import jp.openstandia.connector.github.GitHubConfiguration; -import jp.openstandia.connector.github.GitHubConnector; - -public class LocalGitHubConnector extends GitHubConnector { - @Override - protected GitHubClient newClient(GitHubConfiguration configuration) { - return MockClient.instance(); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubEMUConnector.java b/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubEMUConnector.java deleted file mode 100644 index 843320c..0000000 --- a/src/test/java/jp/openstandia/connector/github/testutil/LocalGitHubEMUConnector.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Nomura Research Institute, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package jp.openstandia.connector.github.testutil; - -import jp.openstandia.connector.github.*; - -public class LocalGitHubEMUConnector extends GitHubEMUConnector { - @Override - protected GitHubClient newClient(GitHubEMUConfiguration configuration) { - return MockClient.instance(); - } -} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/testutil/MockClient.java b/src/test/java/jp/openstandia/connector/github/testutil/MockClient.java index f098411..a4908b3 100644 --- a/src/test/java/jp/openstandia/connector/github/testutil/MockClient.java +++ b/src/test/java/jp/openstandia/connector/github/testutil/MockClient.java @@ -1,8 +1,7 @@ package jp.openstandia.connector.github.testutil; -import jp.openstandia.connector.github.GitHubSchema; +import jp.openstandia.connector.github.GitHubEMUSchema; import jp.openstandia.connector.github.GitHubClient; -import jp.openstandia.connector.github.GitHubSchema; import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; import org.identityconnectors.framework.common.exceptions.UnknownUidException; import org.identityconnectors.framework.common.objects.Name; @@ -15,7 +14,7 @@ import java.util.List; import java.util.Set; -public class MockClient implements GitHubClient { +public class MockClient implements GitHubClient { private static final MockClient INSTANCE = new MockClient(); @@ -29,102 +28,82 @@ private MockClient() { public void init() { } - @Override public void setInstanceName(String instanceName) { } - @Override public void test() { } - @Override public void auth() { } - @Override - public Uid createUser(GitHubSchema schema, SCIMUser scimUser) throws AlreadyExistsException { + public Uid createUser(GitHubEMUSchema schema, SCIMUser scimUser) throws AlreadyExistsException { return null; } - @Override - public String updateUser(GitHubSchema schema, Uid uid, String scimUserName, String scimEmail, String scimGivenName, String scimFamilyName, String login, OperationOptions options) throws UnknownUidException { + public String updateUser(GitHubEMUSchema schema, Uid uid, String scimUserName, String scimEmail, String scimGivenName, String scimFamilyName, String login, OperationOptions options) throws UnknownUidException { return null; } - @Override - public void deleteUser(GitHubSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { + public void deleteUser(GitHubEMUSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { } - @Override - public void getUsers(GitHubSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getUsers(GitHubEMUSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } - @Override - public void getUser(GitHubSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getUser(GitHubEMUSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } - @Override - public void getUser(GitHubSchema schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getUser(GitHubEMUSchema schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } - @Override public List getTeamIdsByUsername(String userLogin, int pageSize) { return null; } - @Override public boolean isOrganizationMember(String userLogin) { return false; } - @Override public void assignOrganizationRole(String userLogin, String organizationRole) { } - @Override public void assignTeams(String login, String role, Collection teams) { } - @Override public void unassignTeams(String login, Collection teams) { } - @Override - public Uid createTeam(GitHubSchema schema, String teamName, String description, String privacy, Long parentTeamDatabaseId) throws AlreadyExistsException { + public Uid createTeam(GitHubEMUSchema schema, String teamName, String description, String privacy, Long parentTeamDatabaseId) throws AlreadyExistsException { return null; } - @Override - public Uid updateTeam(GitHubSchema schema, Uid uid, String teamName, String description, String privacy, Long parentTeamId, boolean clearParent, OperationOptions options) throws UnknownUidException { + public Uid updateTeam(GitHubEMUSchema schema, Uid uid, String teamName, String description, String privacy, Long parentTeamId, boolean clearParent, OperationOptions options) throws UnknownUidException { return null; } - @Override - public void deleteTeam(GitHubSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { + public void deleteTeam(GitHubEMUSchema schema, Uid uid, OperationOptions options) throws UnknownUidException { } - @Override - public void getTeams(GitHubSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getTeams(GitHubEMUSchema schema, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } - @Override - public void getTeam(GitHubSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getTeam(GitHubEMUSchema schema, Uid uid, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } - @Override - public void getTeam(GitHubSchema schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { + public void getTeam(GitHubEMUSchema schema, Name name, ResultsHandler handler, OperationOptions options, Set attributesToGet, boolean allowPartialAttributeValues, int queryPageSize) { } diff --git a/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java b/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java new file mode 100644 index 0000000..f8df22a --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java @@ -0,0 +1,343 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.authorization.AuthorizationProvider; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +class GHEnterpriseExtTest { + + private GHEnterpriseExt enterprise; + private Requester mockRequester; + + @BeforeEach + void setup() { + enterprise = spy(new GHEnterpriseExt()); + enterprise.login = "test-enterprise"; + + GitHub mockGitHub = mock(GitHub.class); + mockRequester = mock(Requester.class, RETURNS_SELF); + enterprise.root = mockGitHub; + + when(mockGitHub.createRequest()).thenReturn(mockRequester); + } + + // ==== SCIM USERS ==== + + @Test + void testCreateSCIMUser() throws Exception { + SCIMEMUUser newUser = new SCIMEMUUser(); + SCIMEMUUser mockResponse = new SCIMEMUUser(); + mockResponse.id = "123"; + + when(mockRequester.fetch(SCIMEMUUser.class)).thenReturn(mockResponse); + + SCIMEMUUser result = enterprise.createSCIMEMUUser(newUser); + + assertEquals("123", result.id); + assertArrayEquals(new String[]{SCIMConstants.SCIM_USER_SCHEMA}, newUser.schemas); + verify(mockRequester).method("POST"); + verify(mockRequester).withUrlPath(contains("/Users")); + } + + @Test + void testUpdateSCIMUser() throws Exception { + SCIMPatchOperations ops = new SCIMPatchOperations(); + SCIMEMUUser mockResponse = new SCIMEMUUser(); + mockResponse.id = "456"; + + when(mockRequester.fetch(SCIMEMUUser.class)).thenReturn(mockResponse); + + SCIMEMUUser result = enterprise.updateSCIMEMUUser("456", ops); + + assertEquals("456", result.id); + verify(mockRequester).method("PATCH"); + verify(mockRequester).withUrlPath(contains("/Users/456")); + } + + @Test + void testGetSCIMUser() throws Exception { + SCIMEMUUser mockResponse = new SCIMEMUUser(); + mockResponse.userName = "sample-user"; + + when(mockRequester.fetch(SCIMEMUUser.class)).thenReturn(mockResponse); + + SCIMEMUUser result = enterprise.getSCIMEMUUser("42"); + assertEquals("sample-user", result.userName); + verify(mockRequester).withUrlPath(contains("/Users/42")); + } + + @Test + void testGetSCIMUserByUserNameSingleResult() throws IOException { + SCIMEMUUser user = new SCIMEMUUser(); + user.userName = "sample-user"; + + SCIMEMUUserSearchBuilder mockBuilder = mock(SCIMEMUUserSearchBuilder.class); + SCIMPagedSearchIterable mockIterable = mock(SCIMPagedSearchIterable.class); + + when(mockIterable.toList()).thenReturn(Arrays.asList(user)); + when(mockBuilder.eq(anyString(), anyString())).thenReturn(mockBuilder); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchSCIMUsers(); + + SCIMEMUUser result = enterprise.getSCIMEMUUserByUserName("sample-user"); + assertEquals("sample-user", result.userName); + } + + @Test + void testGetSCIMUserByUserNameNoResult() throws IOException { + SCIMEMUUserSearchBuilder mockBuilder = mock(SCIMEMUUserSearchBuilder.class); + SCIMPagedSearchIterable mockIterable = mock(SCIMPagedSearchIterable.class); + + when(mockIterable.toList()).thenReturn(Collections.emptyList()); + when(mockBuilder.eq(anyString(), anyString())).thenReturn(mockBuilder); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchSCIMUsers(); + + SCIMEMUUser result = enterprise.getSCIMEMUUserByUserName("unknown"); + assertNull(result); + } + + @Test + void testDeleteSCIMUser() throws Exception { + enterprise.deleteSCIMUser("999"); + verify(mockRequester).method("DELETE"); + verify(mockRequester).withUrlPath(contains("/Users/999")); + verify(mockRequester).send(); + } + + // ==== SCIM GROUPS ==== + + @Test + void testCreateSCIMGroup() throws Exception { + SCIMEMUGroup newGroup = new SCIMEMUGroup(); + SCIMEMUGroup mockResponse = new SCIMEMUGroup(); + mockResponse.id = "group123"; + + when(mockRequester.fetch(SCIMEMUGroup.class)).thenReturn(mockResponse); + + SCIMEMUGroup result = enterprise.createSCIMEMUGroup(newGroup); + + assertEquals("group123", result.id); + assertArrayEquals(new String[]{SCIMConstants.SCIM_GROUP_SCHEMA}, newGroup.schemas); + verify(mockRequester).method("POST"); + verify(mockRequester).withUrlPath(contains("/Groups")); + } + + @Test + void testUpdateSCIMGroup() throws Exception { + SCIMPatchOperations ops = new SCIMPatchOperations(); + SCIMEMUGroup mockResponse = new SCIMEMUGroup(); + mockResponse.id = "group456"; + + when(mockRequester.fetch(SCIMEMUGroup.class)).thenReturn(mockResponse); + + SCIMEMUGroup result = enterprise.updateSCIMEMUGroup("group456", ops); + + assertEquals("group456", result.id); + verify(mockRequester).method("PATCH"); + verify(mockRequester).withUrlPath(contains("/Groups/group456")); + } + + @Test + void testGetSCIMGroup() throws Exception { + SCIMEMUGroup mockResponse = new SCIMEMUGroup(); + mockResponse.displayName = "QA Team"; + + when(mockRequester.fetch(SCIMEMUGroup.class)).thenReturn(mockResponse); + + SCIMEMUGroup result = enterprise.getSCIMEMUGroup("group999"); + + assertEquals("QA Team", result.displayName); + verify(mockRequester).withUrlPath(contains("/Groups/group999")); + } + + @Test + void testDeleteSCIMGroup() throws Exception { + enterprise.deleteSCIMGroup("group888"); + + verify(mockRequester).method("DELETE"); + verify(mockRequester).withUrlPath(contains("/Groups/group888")); + verify(mockRequester).send(); + } + + // ==== COPILOT SEATS ==== + + @Test + void testGetCopilotSeatByDisplayName() throws Exception { + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + seat.assignee = new GitHubCopilotSeatAssignee(); + seat.assignee.login = "sample-user"; + + GitHubCopilotSeatsSearchBuilder mockBuilder = mock(GitHubCopilotSeatsSearchBuilder.class); + GitHubCopilotSeatPagedSearchIterable mockIterable = mock(GitHubCopilotSeatPagedSearchIterable.class); + when(mockIterable.toList()).thenReturn(List.of(seat)); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchCopilotSeats(); + + GitHubCopilotSeat result = enterprise.getCopilotSeatByDisplayName("sample-user"); + + assertNotNull(result); + assertEquals("sample-user", result.assignee.login); + } + + @Test + void testGetCopilotSeatByDisplayNameMultipleResultsReturnsNull() throws IOException { + GitHubCopilotSeat seat1 = new GitHubCopilotSeat(); + seat1.assignee = new GitHubCopilotSeatAssignee(); + seat1.assignee.login = "user1"; + GitHubCopilotSeat seat2 = new GitHubCopilotSeat(); + seat2.assignee = new GitHubCopilotSeatAssignee(); + seat2.assignee.login = "user2"; + + GitHubCopilotSeatsSearchBuilder mockBuilder = mock(GitHubCopilotSeatsSearchBuilder.class); + GitHubCopilotSeatPagedSearchIterable mockIterable = mock(GitHubCopilotSeatPagedSearchIterable.class); + when(mockIterable.toList()).thenReturn(Arrays.asList(seat1, seat2)); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchCopilotSeats(); + + GitHubCopilotSeat result = enterprise.getCopilotSeatByDisplayName("user1"); + assertNull(result); + } + + @Test + void testGetCopilotSeatByUid() throws Exception { + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + seat.assignee = new GitHubCopilotSeatAssignee(); + seat.assignee.id = "12345"; + + GitHubCopilotSeatsSearchBuilder mockBuilder = mock(GitHubCopilotSeatsSearchBuilder.class); + GitHubCopilotSeatPagedSearchIterable mockIterable = mock(GitHubCopilotSeatPagedSearchIterable.class); + when(mockIterable.toList()).thenReturn(Arrays.asList(seat)); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchCopilotSeats(); + + GitHubCopilotSeat result = enterprise.getCopilotSeatByUid("12345"); + + assertNotNull(result); + assertEquals("12345", result.assignee.id); + } + + @Test + void testGetCopilotSeatByUidNoMatchReturnsNull() throws IOException { + GitHubCopilotSeatsSearchBuilder mockBuilder = mock(GitHubCopilotSeatsSearchBuilder.class); + GitHubCopilotSeatPagedSearchIterable mockIterable = mock(GitHubCopilotSeatPagedSearchIterable.class); + when(mockIterable.toList()).thenReturn(Collections.emptyList()); + when(mockBuilder.list()).thenReturn(mockIterable); + doReturn(mockBuilder).when(enterprise).searchCopilotSeats(); + + GitHubCopilotSeat result = enterprise.getCopilotSeatByUid("9999"); + assertNull(result); + } + + // ==== PAGINATION HELPERS ==== + + @Test + void testSearchAndListHelpers() throws Exception { + assertNotNull(enterprise.searchSCIMUsers()); + assertNotNull(enterprise.searchSCIMGroups()); + assertNotNull(enterprise.searchCopilotSeats()); + + SCIMPagedSearchIterable mockPaged = mock(SCIMPagedSearchIterable.class); + SCIMEMUUserSearchBuilder mockBuilder = mock(SCIMEMUUserSearchBuilder.class); + + when(mockBuilder.list()).thenReturn(mockPaged); + when(mockPaged.withPageSize(anyInt())).thenReturn(mockPaged); + when(mockPaged.withPageOffset(anyInt())).thenReturn(mockPaged); + + doReturn(mockBuilder).when(enterprise).searchSCIMUsers(); + + SCIMPagedSearchIterable result = enterprise.listSCIMUsers(10, 0); + assertNotNull(result); + verify(mockPaged).withPageSize(10); + verify(mockPaged).withPageOffset(0); + } + + @Test + void testListSCIMGroupsAndAllSeats() throws IOException { + SCIMPagedSearchIterable mockGroupPaged = mock(SCIMPagedSearchIterable.class); + SCIMEMUGroupSearchBuilder mockGroupBuilder = mock(SCIMEMUGroupSearchBuilder.class); + when(mockGroupBuilder.list()).thenReturn(mockGroupPaged); + when(mockGroupPaged.withPageSize(anyInt())).thenReturn(mockGroupPaged); + when(mockGroupPaged.withPageOffset(anyInt())).thenReturn(mockGroupPaged); + doReturn(mockGroupBuilder).when(enterprise).searchSCIMGroups(); + + GitHubCopilotSeatPagedSearchIterable mockSeatPaged = mock(GitHubCopilotSeatPagedSearchIterable.class); + GitHubCopilotSeatsSearchBuilder mockSeatBuilder = mock(GitHubCopilotSeatsSearchBuilder.class); + when(mockSeatBuilder.list()).thenReturn(mockSeatPaged); + when(mockSeatPaged.withPageSize(anyInt())).thenReturn(mockSeatPaged); + when(mockSeatPaged.withPageOffset(anyInt())).thenReturn(mockSeatPaged); + doReturn(mockSeatBuilder).when(enterprise).searchCopilotSeats(); + + assertNotNull(enterprise.listSCIMGroups(20, 1)); + assertNotNull(enterprise.listAllSeats(50, 2)); + + verify(mockGroupPaged).withPageSize(20); + verify(mockSeatPaged).withPageSize(50); + } + + // ==== WRAPUP ==== + + @Test + void testWrapUp() { + GitHub mockRoot = mock(GitHub.class); + GHEnterpriseExt result = enterprise.wrapUp(mockRoot); + assertNotNull(result); + assertTrue(true); + } + + @Test + void testGetSCIMGroupByDisplayNameSingleResult() throws IOException { + SCIMEMUGroup group = new SCIMEMUGroup(); + group.displayName = "Dev Team"; + + SCIMEMUGroupSearchBuilder mockBuilder = mock(SCIMEMUGroupSearchBuilder.class); + SCIMPagedSearchIterable mockIterable = mock(SCIMPagedSearchIterable.class); + + when(mockIterable.toList()).thenReturn(List.of(group)); + when(mockBuilder.eq(anyString(), anyString())).thenReturn(mockBuilder); + when(mockBuilder.list()).thenReturn(mockIterable); + + doReturn(mockBuilder).when(enterprise).searchSCIMGroups(); + + SCIMEMUGroup result = enterprise.getSCIMEMUGroupByDisplayName("Dev Team"); + assertNotNull(result); + assertEquals("Dev Team", result.displayName); + } + + @Test + void testGetSCIMGroupByDisplayNameNoOrMultipleResultsReturnsNull() throws IOException { + // Cenário 1: lista vazia + SCIMEMUGroupSearchBuilder mockBuilder = mock(SCIMEMUGroupSearchBuilder.class); + SCIMPagedSearchIterable mockIterable = mock(SCIMPagedSearchIterable.class); + when(mockIterable.toList()).thenReturn(Collections.emptyList()); + when(mockBuilder.eq(anyString(), anyString())).thenReturn(mockBuilder); + when(mockBuilder.list()).thenReturn(mockIterable); + doReturn(mockBuilder).when(enterprise).searchSCIMGroups(); + + SCIMEMUGroup result1 = enterprise.getSCIMEMUGroupByDisplayName("Unknown"); + assertNull(result1); + + // Cenário 2: múltiplos resultados + SCIMEMUGroup g1 = new SCIMEMUGroup(); + SCIMEMUGroup g2 = new SCIMEMUGroup(); + when(mockIterable.toList()).thenReturn(Arrays.asList(g1, g2)); + + SCIMEMUGroup result2 = enterprise.getSCIMEMUGroupByDisplayName("DuplicatedGroup"); + assertNull(result2); + } + +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigneeTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigneeTest.java new file mode 100644 index 0000000..36255b8 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigneeTest.java @@ -0,0 +1,44 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubCopilotSeatAssigneeTest { + + @Test + void testFieldAssignmentsAndAccess() { + GitHubCopilotSeatAssignee a = new GitHubCopilotSeatAssignee(); + a.login = "user1"; + a.id = "123"; + a.node_id = "node123"; + a.url = "https://api.github.com/user1"; + a.type = "User"; + a.user_view_type = "public"; + a.site_admin = "false"; + + assertEquals("user1", a.login); + assertEquals("123", a.id); + assertEquals("node123", a.node_id); + assertEquals("https://api.github.com/user1", a.url); + assertEquals("User", a.type); + assertEquals("public", a.user_view_type); + assertEquals("false", a.site_admin); + } + + @Test + void testJsonSerialization() throws Exception { + GitHubCopilotSeatAssignee a = new GitHubCopilotSeatAssignee(); + a.login = "alice"; + a.id = "001"; + a.type = "User"; + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(a); + assertTrue(json.contains("alice")); + GitHubCopilotSeatAssignee copy = mapper.readValue(json, GitHubCopilotSeatAssignee.class); + assertEquals("alice", copy.login); + assertEquals("001", copy.id); + assertEquals("User", copy.type); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeamTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeamTest.java new file mode 100644 index 0000000..5c6801b --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatAssigningTeamTest.java @@ -0,0 +1,61 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubCopilotSeatAssigningTeamTest { + + @Test + void testFieldAssignmentsAndAccess() { + GitHubCopilotSeatAssigningTeam team = new GitHubCopilotSeatAssigningTeam(); + + team.id = "123"; + team.name = "Dev Team"; + team.slug = "dev-team"; + team.group_name = "Engineering"; + team.created_at = "2025-01-01T00:00:00Z"; + team.updated_at = "2025-10-24T00:00:00Z"; + + assertEquals("123", team.id); + assertEquals("Dev Team", team.name); + assertEquals("dev-team", team.slug); + assertEquals("Engineering", team.group_name); + assertEquals("2025-01-01T00:00:00Z", team.created_at); + assertEquals("2025-10-24T00:00:00Z", team.updated_at); + } + + @Test + void testJacksonSerializationDeserialization() throws Exception { + GitHubCopilotSeatAssigningTeam team = new GitHubCopilotSeatAssigningTeam(); + team.id = "001"; + team.name = "AI Wizards"; + team.slug = "ai-wizards"; + team.group_name = "R&D"; + team.created_at = "2025-01-01T00:00:00Z"; + team.updated_at = "2025-10-24T00:00:00Z"; + + ObjectMapper mapper = new ObjectMapper(); + + // Serialize to JSON + String json = mapper.writeValueAsString(team); + assertTrue(json.contains("\"id\":\"001\"")); + assertTrue(json.contains("\"name\":\"AI Wizards\"")); + assertTrue(json.contains("\"slug\":\"ai-wizards\"")); + assertTrue(json.contains("\"group_name\":\"R&D\"")); + assertTrue(json.contains("\"created_at\":\"2025-01-01T00:00:00Z\"")); + assertTrue(json.contains("\"updated_at\":\"2025-10-24T00:00:00Z\"")); + + // Deserialize back + GitHubCopilotSeatAssigningTeam restored = + mapper.readValue(json, GitHubCopilotSeatAssigningTeam.class); + + assertEquals("001", restored.id); + assertEquals("AI Wizards", restored.name); + assertEquals("ai-wizards", restored.slug); + assertEquals("R&D", restored.group_name); + assertEquals("2025-01-01T00:00:00Z", restored.created_at); + assertEquals("2025-10-24T00:00:00Z", restored.updated_at); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java new file mode 100644 index 0000000..0c06429 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java @@ -0,0 +1,274 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"unchecked", "rawtypes"}) +class GitHubCopilotSeatPageIteratorTest { + + GitHubRequest.Builder fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users"); + + @Test + void testCreateNormalAndWithOffsets() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockRequest); + + GitHubCopilotSeatPageIterator iterator = + GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 10, 5); + + assertNotNull(iterator); + verify(mockBuilder).with("count", 10); + verify(mockBuilder).with("startIndex", 5); + verify(mockBuilder).build(); + } + + @Test + void testCreateWithoutPageOffset() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockRequest); + + GitHubCopilotSeatPageIterator iterator = + GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 10, 0); + + assertNotNull(iterator); + verify(mockBuilder).with("count", 10); + verify(mockBuilder, never()).with(eq("startIndex"), anyInt()); + } + + @Test + void testThrowsGHExceptionWhenMalformedURL() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenThrow(new MalformedURLException("Bad URL")); + + GHException ex = assertThrows(GHException.class, () -> + GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 10, 1)); + + assertTrue(ex.getMessage().contains("Unable to build GitHub SCIM API URL")); + } + + @Test + void testThrowsIllegalStateWhenNotGET() { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + when(mockRequest.method()).thenReturn("POST"); + + IllegalStateException ex = assertThrows(IllegalStateException.class, + () -> new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest)); + + assertTrue(ex.getMessage().contains("Request method \"GET\" is required")); + } + + @Test + void shouldThrowWhenNoMoreElements() throws IOException { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + + when(mockRequest.method()).thenReturn("GET"); + + GitHubRequest request = fakeRequest + .set("total_seats", 100) + .build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(request); + when(fakeInfo.statusCode()).thenReturn(200); + + GitHubCopilotSeatsSearchResult result1 = new GitHubCopilotSeatsSearchResult(); + result1.total_seats = 100; + + GitHubResponse response1 = new GitHubResponse<>(fakeInfo, result1); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenReturn((GitHubResponse) response1); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + assertTrue(iterator.hasNext()); + assertEquals(result1, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void shouldThrowExceptionWhenHasNextIsTrue() throws IOException { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(request); + when(fakeInfo.statusCode()).thenReturn(200); + + GitHubCopilotSeatPageIterator iterator = + spy(new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request)); + + doReturn(true).when(iterator).hasNext(); + assertThrows(GHException.class, iterator::finalResponse); + } + + @Test + void shouldReturnFinalResponseWhenNoNextPage() throws IOException, NoSuchFieldException, IllegalAccessException { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(request); + when(fakeInfo.statusCode()).thenReturn(200); + + GitHubCopilotSeatsSearchResult result = new GitHubCopilotSeatsSearchResult(); + GitHubResponse response = new GitHubResponse<>(fakeInfo, result); + + GitHubCopilotSeatPageIterator iterator = + spy(new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request)); + + doReturn(false).when(iterator).hasNext(); + + Field field = GitHubCopilotSeatPageIterator.class.getDeclaredField("finalResponse"); + field.setAccessible(true); + field.set(iterator, response); + + GitHubResponse finalResp = iterator.finalResponse(); + + assertNotNull(finalResp); + assertEquals(response, finalResp); + } + + @Test + void shouldCallSendRequestInsideFetch() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.method("GET").build(); + + GitHubResponse.ResponseInfo mockInfo = mock(GitHubResponse.ResponseInfo.class); + when(mockInfo.request()).thenReturn(request); + when(mockInfo.statusCode()).thenReturn(200); + + GitHubCopilotSeatsSearchResult mockBody = new GitHubCopilotSeatsSearchResult(); + GitHubResponse fakeResponse = + new GitHubResponse<>(mockInfo, mockBody); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenReturn((GitHubResponse) fakeResponse); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + Field nextField = GitHubCopilotSeatPageIterator.class.getDeclaredField("next"); + nextField.setAccessible(true); + nextField.set(iterator, null); + + Field nextReqField = GitHubCopilotSeatPageIterator.class.getDeclaredField("nextRequest"); + nextReqField.setAccessible(true); + nextReqField.set(iterator, request); + + Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + + fetchMethod.invoke(iterator); + verify(mockClient, times(1)).sendRequest(eq(request), any()); + } + + @Test + void shouldReturnNextRequestWhenLinkHeaderHasNextRel() throws MalformedURLException { + GitHubResponse mockResponse = mock(GitHubResponse.class); + when(mockResponse.headerField("Link")).thenReturn( + "; rel=\"next\", ; rel=\"last\"" + ); + + GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mock(GitHubClient.class), + GitHubCopilotSeatsSearchResult.class, request); + + GitHubRequest nextReq = iterator.findNextURL(mockResponse); + + assertNotNull(nextReq); + assertTrue(nextReq.url().toString().contains("page=2")); + } + + @Test + void shouldReturnNullWhenNoLinkHeader() throws MalformedURLException { + GitHubResponse mockResponse = mock(GitHubResponse.class); + when(mockResponse.headerField("Link")).thenReturn(null); + + GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mock(GitHubClient.class), GitHubCopilotSeatsSearchResult.class, request); + + assertNull(iterator.findNextURL(mockResponse)); + } + + @Test + void shouldThrowExceptionWhenNextUrlIsMalformed() throws MalformedURLException { + GitHubResponse mockResponse = mock(GitHubResponse.class); + when(mockResponse.headerField("Link")).thenReturn("<:invalid_url>; rel=\"next\""); + + GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mock(GitHubClient.class), GitHubCopilotSeatsSearchResult.class, request); + + assertThrows(GHException.class, () -> iterator.findNextURL(mockResponse)); + } + + @Test + void shouldReturnNullWhenLinkHeaderIsNull() throws MalformedURLException { + GitHubResponse mockResponse = mock(GitHubResponse.class); + when(mockResponse.headerField("Link")).thenReturn(null); + + GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>( + mock(GitHubClient.class), + GitHubCopilotSeatsSearchResult.class, + request); + + GitHubRequest result = iterator.findNextURL(mockResponse); + assertNull(result, "Deveria retornar null quando o header Link está ausente"); + } + + @Test + void shouldReturnNullWhenLinkHeaderDoesNotContainNextRel() throws MalformedURLException { + GitHubResponse mockResponse = mock(GitHubResponse.class); + when(mockResponse.headerField("Link")).thenReturn( + "; rel=\"last\"" // sem "next" + ); + + GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>( + mock(GitHubClient.class), + GitHubCopilotSeatsSearchResult.class, + request); + + GitHubRequest result = iterator.findNextURL(mockResponse); + assertNull(result, "Deveria retornar null quando não há rel=\"next\" no header Link"); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java new file mode 100644 index 0000000..8493bc5 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java @@ -0,0 +1,126 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"unchecked", "rawtypes"}) +class GitHubCopilotSeatPagedSearchIterableTest { + + @Test + void testAdaptReturnsResourcesAndCachesResult() { + GitHubCopilotSeatsSearchResult result = new GitHubCopilotSeatsSearchResult<>(); + result.seats = new String[]{"X", "Y"}; + + Iterator> baseIterator = mock(Iterator.class); + when(baseIterator.hasNext()).thenReturn(true, false); + when(baseIterator.next()).thenReturn(result); + + GitHubCopilotSeatPagedSearchIterable iterable = + new GitHubCopilotSeatPagedSearchIterable<>(mock(GitHub.class), mock(GitHubRequest.class), (Class) GitHubCopilotSeatsSearchResult.class); + + Iterator adapted = iterable.adapt(baseIterator); + + assertTrue(adapted.hasNext()); + String[] arr = adapted.next(); + assertArrayEquals(new String[]{"X", "Y"}, arr); + assertFalse(adapted.hasNext()); + } + + @Test + void testWithPageOffsetAndPageSizeFluentAPI() { + GitHub mockRoot = mock(GitHub.class); + GitHubRequest mockReq = mock(GitHubRequest.class); + + GitHubCopilotSeatPagedSearchIterable iterable = + new GitHubCopilotSeatPagedSearchIterable<>(mockRoot, mockReq, (Class) GitHubCopilotSeatsSearchResult.class); + + GitHubCopilotSeatPagedSearchIterable result1 = iterable.withPageSize(25); + GitHubCopilotSeatPagedSearchIterable result2 = iterable.withPageOffset(3); + + assertSame(iterable, result2); + assertNotNull(result1); + } + + @Test + void testGetTotalCountReturnsResulttotal_seats() throws Exception { + GitHub github = GitHub.connectAnonymously(); + GitHubRequest request = mock(GitHubRequest.class); + + GitHubCopilotSeatPagedSearchIterable iterable = new GitHubCopilotSeatPagedSearchIterable( + github, + request, + (Class>) (Class) GitHubCopilotSeatsSearchResult.class + ); + + GitHubCopilotSeatsSearchResult fakeResult = new GitHubCopilotSeatsSearchResult<>(); + fakeResult.total_seats = 42; + iterable.result = fakeResult; + + int total = iterable.getTotalSeats(); + assertEquals(42, total, "getTotalCount deve retornar o valor de result.total_seats"); + } + + @Test + void shouldCallIteratorWhenResultIsNull() throws MalformedURLException, NoSuchFieldException, IllegalAccessException { + // Arrange + GitHub mockRoot = mock(GitHub.class); + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .method("GET") + .build(); + + GitHubCopilotSeatPagedSearchIterable iterable = + spy(new GitHubCopilotSeatPagedSearchIterable<>(mockRoot, fakeRequest, (Class) GitHubCopilotSeatsSearchResult.class)); + + // Mock do iterator + PagedIterator mockIterator = mock(PagedIterator.class); + when(mockIterator.hasNext()).thenReturn(false); + + // Retorna o mock quando o método iterator() for chamado + doReturn(mockIterator).when(iterable).iterator(); + + // Garante que result é nulo + Field resultField = GitHubCopilotSeatPagedSearchIterable.class.getDeclaredField("result"); + resultField.setAccessible(true); + resultField.set(iterable, null); + + // Act + iterable.populate(); + + // Assert + verify(iterable, times(1)).iterator(); // Verifica que chamou iterator() + verify(mockIterator, times(1)).hasNext(); // Verifica que tentou iterar + } + + @Test + void shouldNotCallIteratorWhenResultIsNotNull() throws Exception { + GitHub mockRoot = mock(GitHub.class); + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .method("GET") + .build(); + + GitHubCopilotSeatPagedSearchIterable iterable = + spy(new GitHubCopilotSeatPagedSearchIterable<>(mockRoot, fakeRequest, (Class) GitHubCopilotSeatsSearchResult.class)); + + PagedIterator mockIterator = mock(PagedIterator.class); + doReturn(mockIterator).when(iterable).iterator(); + + Field resultField = GitHubCopilotSeatPagedSearchIterable.class.getDeclaredField("result"); + resultField.setAccessible(true); + resultField.set(iterable, new GitHubCopilotSeatsSearchResult<>()); + + iterable.populate(); + verify(iterable, never()).iterator(); + } + +} + diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java new file mode 100644 index 0000000..5c8b086 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java @@ -0,0 +1,46 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubCopilotSeatTest { + + @Test + void testFieldAssignments() { + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + seat.created_at = "2025-01-01"; + seat.pending_cancellation_date = "2025-02-02"; + seat.plan_type = "pro"; + seat.last_authenticated_at = "2025-01-10"; + seat.updated_at = "2025-02-15"; + seat.last_activity_at = "2025-02-20"; + seat.last_activity_editor = "VSCode"; + + GitHubCopilotSeatAssignee assignee = new GitHubCopilotSeatAssignee(); + assignee.login = "dev1"; + seat.assignee = assignee; + + GitHubCopilotSeatAssigningTeam team = new GitHubCopilotSeatAssigningTeam(); + team.id = "t1"; + team.name = "Team1"; + seat.assigning_team = team; + + assertEquals("dev1", seat.assignee.login); + assertEquals("Team1", seat.assigning_team.name); + } + + @Test + void testJsonSerializationDeserialization() throws Exception { + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + seat.created_at = "2025-01-01"; + seat.plan_type = "enterprise"; + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(seat); + assertTrue(json.contains("2025-01-01")); + GitHubCopilotSeat restored = mapper.readValue(json, GitHubCopilotSeat.class); + assertEquals("2025-01-01", restored.created_at); + assertEquals("enterprise", restored.plan_type); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java new file mode 100644 index 0000000..e14f2f4 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java @@ -0,0 +1,118 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GitHubCopilotSeatsSearchBuilderTest { + + private GitHub mockGitHub; + private GHEnterpriseExt mockEnterprise; + + @BeforeEach + void setup() { + mockGitHub = mock(GitHub.class); + mockEnterprise = mock(GHEnterpriseExt.class); + when(mockEnterprise.getLogin()).thenReturn("test-enterprise"); + mockEnterprise.login = "test-enterprise"; + } + + @Test + void testEqAddsFilter() { + GitHubCopilotSeatsSearchBuilder builder = spy(new GitHubCopilotSeatsSearchBuilder(mockGitHub, mockEnterprise)); + + builder.eq("team", "AI"); + + Map filters = builder.filter; + assertEquals(1, filters.size()); + assertEquals("AI", filters.get("team")); + } + + @Test + void testGetApiUrl() { + GitHubCopilotSeatsSearchBuilder builder = new GitHubCopilotSeatsSearchBuilder(mockGitHub, mockEnterprise); + String url = builder.getApiUrl(); + assertEquals("/enterprises/test-enterprise/copilot/billing/seats", url); + } + + @Test + void testListReturnsPagedIterable() throws MalformedURLException { + GitHubCopilotSeatsSearchBuilder builder = spy(new GitHubCopilotSeatsSearchBuilder(mockGitHub, mockEnterprise)); + + GitHubRequest mockRequest = mock(GitHubRequest.class); + doReturn(mockRequest).when(builder.req).build(); + + GitHubCopilotSeatPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + assertTrue(true); + } + + @Test + void testListThrowsGHExceptionOnMalformedURL() throws MalformedURLException { + GitHubCopilotSeatsSearchBuilder builder = spy(new GitHubCopilotSeatsSearchBuilder(mockGitHub, mockEnterprise)); + + doThrow(new MalformedURLException("bad url")) + .when(builder.req) + .build(); + + GHException ex = assertThrows(GHException.class, builder::list); + assertTrue(ex.getCause() instanceof MalformedURLException); + } + + @Test + void testConstructorSetsHeadersAndRateLimit() { + GitHubCopilotSeatsSearchBuilder builder = new GitHubCopilotSeatsSearchBuilder(mockGitHub, mockEnterprise); + + assertNotNull(builder.enterprise); + assertNotNull(builder.receiverType); + assertTrue(builder.filter.isEmpty()); + } + + @Test + void testListThrowsGHExceptionWhenMalformedUrl() throws Exception { + GitHub github = GitHub.connectAnonymously(); + + GHEnterpriseExt enterprise = mock(GHEnterpriseExt.class); + when(enterprise.getLogin()).thenReturn("my-enterprise"); + + GitHubCopilotSeatsSearchBuilder builder = new GitHubCopilotSeatsSearchBuilder(github, enterprise); + Requester reqMock = mock(Requester.class); + when(reqMock.build()).thenThrow(new MalformedURLException("URL malformada!")); + + java.lang.reflect.Field reqField = GHQueryBuilder.class.getDeclaredField("req"); + reqField.setAccessible(true); + reqField.set(builder, reqMock); + + GHException thrown = assertThrows(GHException.class, builder::list); + assertTrue(thrown.getCause() instanceof MalformedURLException); + assertEquals("URL malformada!", thrown.getCause().getMessage()); + } + + @Test + void testEqAddsFilterAndReturnsThis() throws Exception { + GitHub github = GitHub.connectAnonymously(); + GHEnterpriseExt enterprise = mock(GHEnterpriseExt.class); + when(enterprise.getLogin()).thenReturn("enterprise-login"); + + GitHubCopilotSeatsSearchBuilder builder = + new GitHubCopilotSeatsSearchBuilder(github, enterprise) { + @Override + protected String getApiUrl() { + return "/test/api/url"; + } + }; + + GitHubCopilotSeatsSearchBuilder returned = builder.eq("status", "active"); + assertSame(builder, returned, "O método eq deve retornar a mesma instância (fluent API)."); + + Map filterMap = builder.filter; + assertEquals(1, filterMap.size(), "Deve conter exatamente um elemento no filtro."); + assertEquals("active", filterMap.get("status"), "O valor do filtro não corresponde ao esperado."); + } +} \ No newline at end of file diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchResultTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchResultTest.java new file mode 100644 index 0000000..a36e528 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchResultTest.java @@ -0,0 +1,76 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class GitHubCopilotSeatsSearchResultTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testSerializationAndDeserialization() throws Exception { + // Arrange + GitHubCopilotSeatsSearchResult result = new GitHubCopilotSeatsSearchResult<>(); + result.total_seats = 5; + result.seats = new String[]{"Alice", "Bob"}; + + // Act: serialize to JSON + String json = mapper.writeValueAsString(result); + + // Assert JSON contains correct keys + assertTrue(json.contains("\"total_seats\":5")); + assertTrue(json.contains("\"seats\":[\"Alice\",\"Bob\"]")); + + // Act: deserialize back + GitHubCopilotSeatsSearchResult deserialized = + mapper.readValue(json, mapper.getTypeFactory() + .constructParametricType(GitHubCopilotSeatsSearchResult.class, String.class)); + + // Assert round-trip consistency + assertEquals(5, deserialized.total_seats); + assertArrayEquals(new String[]{"Alice", "Bob"}, deserialized.seats); + } + + @Test + public void testEmptySeatsArray() throws Exception { + // Arrange + GitHubCopilotSeatsSearchResult result = new GitHubCopilotSeatsSearchResult<>(); + result.total_seats = 0; + result.seats = new String[]{}; + + // Act + String json = mapper.writeValueAsString(result); + GitHubCopilotSeatsSearchResult deserialized = + mapper.readValue(json, mapper.getTypeFactory() + .constructParametricType(GitHubCopilotSeatsSearchResult.class, String.class)); + + // Assert + assertEquals(0, deserialized.total_seats); + assertNotNull(deserialized.seats); + assertEquals(0, deserialized.seats.length); + } + + @Test + public void testNullSeatsField() throws Exception { + // Arrange + GitHubCopilotSeatsSearchResult result = new GitHubCopilotSeatsSearchResult<>(); + result.total_seats = 3; + result.seats = null; + + // Act + String json = mapper.writeValueAsString(result); + + // Assert JSON includes total_seats but not seats + assertTrue(json.contains("\"total_seats\":3")); + + // Deserialize again — Jackson will set seats=null + GitHubCopilotSeatsSearchResult restored = + mapper.readValue(json, mapper.getTypeFactory() + .constructParametricType(GitHubCopilotSeatsSearchResult.class, String.class)); + + assertEquals(3, restored.total_seats); + assertNull(restored.seats); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java new file mode 100644 index 0000000..b696657 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java @@ -0,0 +1,184 @@ +package org.kohsuke.github; +import jp.openstandia.connector.github.GitHubClient; +import jp.openstandia.connector.github.GitHubEMUConfiguration; +import jp.openstandia.connector.github.GitHubEMUSchema; +import jp.openstandia.connector.github.GitHubEMUUserHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GitHubEMUUserHandlerTest { + + private GitHubEMUConfiguration config; + private jp.openstandia.connector.github.GitHubClient client; + private GitHubEMUSchema schema; + private SchemaDefinition schemaDefinition; + private GitHubEMUUserHandler handler; + + @BeforeEach + void setup() { + config = mock(GitHubEMUConfiguration.class); + client = mock(jp.openstandia.connector.github.GitHubClient.class); + schema = mock(GitHubEMUSchema.class); + schemaDefinition = mock(SchemaDefinition.class); + handler = new GitHubEMUUserHandler(config, client, schema, schemaDefinition); + } + + @Test + void testCreateSchema() { + // Just ensure it builds without throwing exceptions + SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); + assertNotNull(builder); + } + + @Test + void testCreateUser() { + // Prepare + Set attrs = new HashSet<>(); + attrs.add(AttributeBuilder.build("userName", "testuser")); + + SCIMEMUUser mockMapped = new SCIMEMUUser(); + when(schemaDefinition.apply(anySet(), any(SCIMEMUUser.class))).thenReturn(mockMapped); + + Uid expectedUid = new Uid("123"); + when(client.createEMUUser(any(SCIMEMUUser.class))).thenReturn(expectedUid); + + // Act + Uid result = handler.create(attrs); + + // Assert + assertEquals("123", result.getUidValue()); + verify(client).createEMUUser(mockMapped); + } + + @Test + void testUpdateDelta_withChanges() { + Uid uid = new Uid("abc"); + Set deltas = new HashSet<>(); + + SCIMPatchOperations patchOps = mock(SCIMPatchOperations.class); + when(schemaDefinition.applyDelta(anySet(), any())).then(invocation -> { + SCIMPatchOperations dest = invocation.getArgument(1); + return null; + }); + when(patchOps.hasAttributesChange()).thenReturn(true); + + // Spy to check call + jp.openstandia.connector.github.GitHubClient spyClient = spy(client); + handler = new GitHubEMUUserHandler(config, spyClient, schema, schemaDefinition); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + dest.replace("displayName", "newName"); + when(schemaDefinition.applyDelta(eq(deltas), any())).thenAnswer(inv -> { + SCIMPatchOperations d = inv.getArgument(1); + d.replace("displayName", "newName"); + return null; + }); + + handler.updateDelta(uid, deltas, new OperationOptionsBuilder().build()); + + verify(spyClient).patchEMUUser(eq(uid), any(SCIMPatchOperations.class)); + } + + @Test + void testUpdateDelta_withoutChanges() { + Uid uid = new Uid("abc"); + Set deltas = new HashSet<>(); + + when(schemaDefinition.applyDelta(eq(deltas), any())).thenAnswer(inv -> null); + + GitHubClient spyClient = spy(client); + handler = new GitHubEMUUserHandler(config, spyClient, schema, schemaDefinition); + + handler.updateDelta(uid, deltas, new OperationOptionsBuilder().build()); + + verify(spyClient, never()).patchEMUUser(any(), any()); + } + + @Test + void testDeleteUser() { + Uid uid = new Uid("id-1"); + OperationOptions options = new OperationOptionsBuilder().build(); + + handler.delete(uid, options); + + verify(client).deleteEMUUser(uid, options); + } + + @Test + void testGetByUid_found() { + Uid uid = new Uid("uid-1"); + OperationOptions options = new OperationOptionsBuilder().build(); + ResultsHandler handlerMock = mock(ResultsHandler.class); + + SCIMEMUUser user = new SCIMEMUUser(); + when(client.getEMUUser(eq(uid), eq(options), any())).thenReturn(user); + //when(schemaDefinition.toConnectorObject(any(), anyBoolean(), any())).thenReturn(mock(ConnectorObject.class)); + + int result = handler.getByUid(uid, handlerMock, options, Set.of(), Set.of(), false, 0, 0); + + assertEquals(1, result); + verify(handlerMock).handle(any()); + } + + @Test + void testGetByUid_notFound() { + Uid uid = new Uid("uid-1"); + OperationOptions options = new OperationOptionsBuilder().build(); + ResultsHandler handlerMock = mock(ResultsHandler.class); + + when(client.getEMUUser(eq(uid), eq(options), any())).thenReturn(null); + + int result = handler.getByUid(uid, handlerMock, options, Set.of(), Set.of(), false, 0, 0); + + assertEquals(0, result); + verify(handlerMock, never()).handle(any()); + } + + @Test + void testGetByName_found() { + Name name = new Name("testuser"); + OperationOptions options = new OperationOptionsBuilder().build(); + ResultsHandler handlerMock = mock(ResultsHandler.class); + + SCIMEMUUser user = new SCIMEMUUser(); + when(client.getEMUUser(eq(name), eq(options), any())).thenReturn(user); + //when(schemaDefinition.toConnectorObjectBuilder(any(), anyBoolean(), any())).thenReturn(mock(ConnectorObject.class)); + + int result = handler.getByName(name, handlerMock, options, Set.of(), Set.of(), false, 0, 0); + + assertEquals(1, result); + verify(handlerMock).handle(any()); + } + + @Test + void testGetByName_notFound() { + Name name = new Name("testuser"); + OperationOptions options = new OperationOptionsBuilder().build(); + ResultsHandler handlerMock = mock(ResultsHandler.class); + + when(client.getEMUUser(eq(name), eq(options), any())).thenReturn(null); + + int result = handler.getByName(name, handlerMock, options, Set.of(), Set.of(), false, 0, 0); + + assertEquals(0, result); + } + + @Test + void testGetAllUsers() { + ResultsHandler handlerMock = mock(ResultsHandler.class); + when(handlerMock.handle(any())).thenReturn(true); + when(client.getEMUUsers(any(), any(), any(), anyInt(), anyInt())).thenReturn(3); + + int result = handler.getAll(handlerMock, new OperationOptionsBuilder().build(), Set.of(), Set.of(), false, 10, 0); + + assertEquals(3, result); + verify(client).getEMUUsers(any(), any(), any(), anyInt(), anyInt()); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubExtTest.java b/src/test/java/org/kohsuke/github/GitHubExtTest.java new file mode 100644 index 0000000..127da7a --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubExtTest.java @@ -0,0 +1,29 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GitHubExtTest { + + @Test + void testGetUserBuildsRequestAndSetsRoot() throws IOException { + Requester requesterMock = mock(Requester.class); + GHUser userMock = new GHUser(); + + when(requesterMock.withUrlPath(anyString())).thenReturn(requesterMock); + when(requesterMock.fetch(GHUser.class)).thenReturn(userMock); + + GitHubExt gitHubExt = new TestableGitHubExt("https://api.github.com", HttpConnector.DEFAULT, requesterMock); + GHUser result = gitHubExt.getUser(42L); + + verify(requesterMock).withUrlPath("/user/42"); + verify(requesterMock).fetch(GHUser.class); + assertSame(userMock, result); + assertSame(gitHubExt, result.root); + } +} + diff --git a/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java b/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java new file mode 100644 index 0000000..05421ca --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java @@ -0,0 +1,75 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMEMUGroupTest { + + @Test + void testFieldAssignmentsAndAccess() { + SCIMEMUGroup group = new SCIMEMUGroup(); + + SCIMMeta meta = new SCIMMeta(); + meta.created = "2025-01-01T00:00:00Z"; + meta.lastModified = "2025-10-24T00:00:00Z"; + + SCIMMember member = new SCIMMember(); + member.value = "123"; + + group.schemas = new String[]{"urn:ietf:params:scim:schemas:core:2.0:Group"}; + group.meta = meta; + group.id = "group-1"; + group.displayName = "Engineering"; + group.members = List.of(member); + group.externalId = "ext-001"; + + assertEquals("group-1", group.id); + assertEquals("Engineering", group.displayName); + assertEquals("ext-001", group.externalId); + assertEquals("urn:ietf:params:scim:schemas:core:2.0:Group", group.schemas[0]); + assertEquals(meta, group.meta); + assertEquals(1, group.members.size()); + assertEquals("123", group.members.get(0).value); + } + + @Test + void testJacksonSerializationDeserialization() throws Exception { + SCIMEMUGroup group = new SCIMEMUGroup(); + group.schemas = new String[]{"schema1", "schema2"}; + group.id = "G1"; + group.displayName = "Developers"; + group.externalId = "EXT-DEV"; + SCIMMeta meta = new SCIMMeta(); + meta.created = "2025-01-01T00:00:00Z"; + meta.lastModified = "2025-02-02T00:00:00Z"; + group.meta = meta; + + SCIMMember member = new SCIMMember(); + member.value = "U123"; + group.members = List.of(member); + + ObjectMapper mapper = new ObjectMapper(); + + // Serialize + String json = mapper.writeValueAsString(group); + assertTrue(json.contains("\"id\":\"G1\"")); + assertTrue(json.contains("\"displayName\":\"Developers\"")); + assertTrue(json.contains("\"externalId\":\"EXT-DEV\"")); + assertTrue(json.contains("\"members\"")); + assertTrue(json.contains("\"schemas\"")); + + // Deserialize + SCIMEMUGroup restored = mapper.readValue(json, SCIMEMUGroup.class); + assertEquals("G1", restored.id); + assertEquals("Developers", restored.displayName); + assertEquals("EXT-DEV", restored.externalId); + assertEquals(2, restored.schemas.length); + assertEquals("U123", restored.members.get(0).value); + assertNotNull(restored.meta); + assertEquals("2025-01-01T00:00:00Z", restored.meta.created); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMNameTest.java b/src/test/java/org/kohsuke/github/SCIMNameTest.java new file mode 100644 index 0000000..1949e12 --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMNameTest.java @@ -0,0 +1,37 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMNameTest { + + @Test + void testFields() { + SCIMName name = new SCIMName(); + name.givenName = "John"; + name.familyName = "Doe"; + name.formatted = "John Doe"; + + assertEquals("John", name.givenName); + assertEquals("Doe", name.familyName); + assertEquals("John Doe", name.formatted); + } + + @Test + void testJsonSerializationDeserialization() throws Exception { + SCIMName n = new SCIMName(); + n.givenName = "Jane"; + n.familyName = "Smith"; + n.formatted = "Jane Smith"; + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(n); + assertTrue(json.contains("Jane")); + SCIMName restored = mapper.readValue(json, SCIMName.class); + assertEquals("Jane", restored.givenName); + assertEquals("Smith", restored.familyName); + assertEquals("Jane Smith", restored.formatted); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMOperationTest.java b/src/test/java/org/kohsuke/github/SCIMOperationTest.java new file mode 100644 index 0000000..0c37e2f --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMOperationTest.java @@ -0,0 +1,48 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMOperationTest { + + @Test + void testStringValueOperation() { + SCIMOperation op = new SCIMOperation<>("replace", "displayName", "Alice"); + + assertEquals("replace", op.op); + assertEquals("displayName", op.path); + assertEquals("Alice", op.value); + } + + @Test + void testIntegerValueOperation() { + SCIMOperation op = new SCIMOperation<>("add", "age", 30); + + assertEquals("add", op.op); + assertEquals("age", op.path); + assertEquals(30, op.value); + } + + @Test + void testComplexValueOperation() { + DummyValue val = new DummyValue("key1", "val1"); + SCIMOperation op = new SCIMOperation<>("replace", "metadata", val); + + assertEquals("replace", op.op); + assertEquals("metadata", op.path); + assertNotNull(op.value); + assertEquals("key1", op.value.key); + assertEquals("val1", op.value.value); + } + + static class DummyValue { + String key; + String value; + + DummyValue(String key, String value) { + this.key = key; + this.value = value; + } + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java b/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java new file mode 100644 index 0000000..af57c76 --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java @@ -0,0 +1,236 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"unchecked", "rawtypes"}) +class SCIMPageIteratorTest { + + @Test + void testCreateNormalAndWithOffsets() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockRequest); + + SCIMPageIterator iterator = + SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 10, 5); + + assertNotNull(iterator); + verify(mockBuilder).with("count", 10); + verify(mockBuilder).with("startIndex", 5); + verify(mockBuilder).build(); + } + + @Test + void testCreateWithoutPageOffset() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockRequest); + + SCIMPageIterator iterator = + SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 10, 0); + + assertNotNull(iterator); + verify(mockBuilder).with("count", 10); + verify(mockBuilder, never()).with(eq("startIndex"), anyInt()); + } + + @Test + void testThrowsGHExceptionWhenMalformedURL() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubRequest.Builder mockBuilder = mock(GitHubRequest.Builder.class); + + when(mockRequest.method()).thenReturn("GET"); + when(mockRequest.toBuilder()).thenReturn(mockBuilder); + when(mockBuilder.with(anyString(), anyInt())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenThrow(new MalformedURLException("Bad URL")); + + GHException ex = assertThrows(GHException.class, () -> + SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 10, 1)); + + assertTrue(ex.getMessage().contains("Unable to build GitHub SCIM API URL")); + } + + @Test + void testThrowsIllegalStateWhenNotGET() { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + when(mockRequest.method()).thenReturn("POST"); + + IllegalStateException ex = assertThrows(IllegalStateException.class, + () -> new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, mockRequest)); + + assertTrue(ex.getMessage().contains("Request method \"GET\" is required")); + } + + @Test + void shouldIterateThroughPages() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + + when(mockRequest.method()).thenReturn("GET"); + + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .set("startIndex", 0) + .build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(fakeRequest); + when(fakeInfo.statusCode()).thenReturn(200); + + SCIMSearchResult result1 = new SCIMSearchResult() {{ + startIndex = 0; + itemsPerPage = 100; + totalResults = 150; + }}; + + SCIMSearchResult result2 = new SCIMSearchResult() {{ + startIndex = 100; + itemsPerPage = 100; + totalResults = 150; + }}; + + GitHubResponse response1 = new GitHubResponse<>(fakeInfo, result1); + GitHubResponse response2 = new GitHubResponse<>(fakeInfo, result2); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenReturn((GitHubResponse) response1) + .thenReturn((GitHubResponse) response2); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, mockRequest); + + assertTrue(iterator.hasNext(), "Iterator should have first page"); + assertEquals(result1, iterator.next(), "First result should match"); + assertTrue(iterator.hasNext(), "Iterator should have second page"); + assertEquals(result2, iterator.next(), "Second result should match"); + assertFalse(iterator.hasNext(), "Iterator should have no more pages"); + } + + @Test + void shouldThrowWhenNoMoreElements() throws IOException { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest mockRequest = mock(GitHubRequest.class); + + when(mockRequest.method()).thenReturn("GET"); + + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .set("startIndex", 0) + .build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(fakeRequest); + when(fakeInfo.statusCode()).thenReturn(200); + + SCIMSearchResult result1 = new SCIMSearchResult(); + result1.startIndex = 0; + result1.itemsPerPage = 100; + result1.totalResults = 50; + + GitHubResponse response1 = new GitHubResponse<>(fakeInfo, result1); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenReturn((GitHubResponse) response1); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, fakeRequest); + + assertTrue(iterator.hasNext()); + assertEquals(result1, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase(100, 100, true), // ainda há próxima página → deve lançar GHException + new TestCase(50, 100, false) // sem próxima página → deve retornar finalResponse + ); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + void shouldHandleFinalResponseBehaviorBasedOnPagination(TestCase testCase) throws IOException { + // Arrange + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .set("startIndex", 0) + .build(); + + GitHubResponse.ResponseInfo fakeInfo = mock(GitHubResponse.ResponseInfo.class); + when(fakeInfo.request()).thenReturn(fakeRequest); + when(fakeInfo.statusCode()).thenReturn(200); + + SCIMSearchResult result = new SCIMSearchResult(); + result.startIndex = 0; + result.itemsPerPage = testCase.itemsPerPage; + result.totalResults = testCase.totalResults; + + GitHubResponse response = new GitHubResponse<>(fakeInfo, result); + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenReturn((GitHubResponse) response); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, fakeRequest); + + assertTrue(iterator.hasNext()); + iterator.next(); + + // Act + Assert + if (testCase.shouldThrow) { + assertTrue(iterator.hasNext()); + assertThrows(GHException.class, iterator::finalResponse); + } else { + assertFalse(iterator.hasNext()); + GitHubResponse finalResp = iterator.finalResponse(); + assertNotNull(finalResp); + assertEquals(response, finalResp, "Final response should be the last one retrieved"); + } + } + + private static class TestCase { + final int totalResults; + final int itemsPerPage; + final boolean shouldThrow; + + TestCase(int totalResults, int itemsPerPage, boolean shouldThrow) { + this.totalResults = totalResults; + this.itemsPerPage = itemsPerPage; + this.shouldThrow = shouldThrow; + } + + @Override + public String toString() { + return shouldThrow + ? "Throws GHException (still has next)" + : "Returns finalResponse (iteration complete)"; + } + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java b/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java new file mode 100644 index 0000000..8ad146e --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java @@ -0,0 +1,218 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"unchecked", "rawtypes"}) +class SCIMPagedSearchIterableTest { + + @Test + void testAdaptReturnsResourcesAndCachesResult() { + SCIMSearchResult result = new SCIMSearchResult<>(); + result.Resources = new String[]{"X", "Y"}; + + Iterator> baseIterator = mock(Iterator.class); + when(baseIterator.hasNext()).thenReturn(true, false); + when(baseIterator.next()).thenReturn(result); + + SCIMPagedSearchIterable iterable = + new SCIMPagedSearchIterable<>(mock(GitHub.class), mock(GitHubRequest.class), (Class) SCIMSearchResult.class); + + Iterator adapted = iterable.adapt(baseIterator); + + assertTrue(adapted.hasNext()); + String[] arr = adapted.next(); + assertArrayEquals(new String[]{"X", "Y"}, arr); + assertFalse(adapted.hasNext()); + } + + @Test + void testWithPageOffsetAndPageSizeFluentAPI() { + GitHub mockRoot = mock(GitHub.class); + GitHubRequest mockReq = mock(GitHubRequest.class); + + SCIMPagedSearchIterable iterable = + new SCIMPagedSearchIterable<>(mockRoot, mockReq, (Class) SCIMSearchResult.class); + + SCIMPagedSearchIterable result1 = iterable.withPageSize(25); + SCIMPagedSearchIterable result2 = iterable.withPageOffset(3); + + assertSame(iterable, result2); + assertNotNull(result1); + } + + @Test + void testListThrowsGHExceptionWhenMalformedUrl() throws Exception { + GitHub github = GitHub.connectAnonymously(); + GHOrganization org = github.getOrganization("square"); + + @SuppressWarnings("unchecked") + SCIMSearchBuilder builder = new SCIMSearchBuilder( + github, + org, + (Class>) (Class) SCIMSearchResult.class) { + + @Override + protected String getApiUrl() { + return "/scim/v2/Users"; + } + + @Override + public SCIMPagedSearchIterable list() { + try { + throw new MalformedURLException("URL malformada!"); + } catch (MalformedURLException e) { + throw new GHException("", e); + } + } + }; + + GHException thrown = assertThrows(GHException.class, builder::list); + assertTrue(thrown.getCause() instanceof MalformedURLException); + assertEquals("URL malformada!", thrown.getCause().getMessage()); + } + + @Test + void testGetTotalCountReturnsResultTotalResults() throws Exception { + GitHub github = GitHub.connectAnonymously(); + GitHubRequest request = mock(GitHubRequest.class); + + @SuppressWarnings("unchecked") + SCIMPagedSearchIterable iterable = new SCIMPagedSearchIterable( + github, + request, + (Class>) (Class) SCIMSearchResult.class + ); + + SCIMSearchResult fakeResult = new SCIMSearchResult<>(); + fakeResult.totalResults = 42; + fakeResult.startIndex = 0; + fakeResult.itemsPerPage = 100; + iterable.result = fakeResult; + + int total = iterable.getTotalCount(); + assertEquals(42, total, "getTotalCount deve retornar o valor de result.totalResults"); + } + + @Test + void testIsIncompleteWhenResultsAreComplete() { + GitHub github = mock(GitHub.class); + GitHubRequest request = mock(GitHubRequest.class); + + @SuppressWarnings("unchecked") + SCIMPagedSearchIterable iterable = + new SCIMPagedSearchIterable<>( + github, + request, + (Class>) (Class) SCIMSearchResult.class + ); + + SCIMSearchResult fakeResult = new SCIMSearchResult<>(); + fakeResult.totalResults = 100; + fakeResult.startIndex = 0; + fakeResult.itemsPerPage = 100; + iterable.result = fakeResult; + + boolean incomplete = iterable.isIncomplete(); + assertTrue(incomplete, "Quando totalResults <= startIndex + itemsPerPage, deve ser true"); + } + + @Test + void testIsIncompleteWhenResultsAreNotComplete() { + GitHub github = mock(GitHub.class); + GitHubRequest request = mock(GitHubRequest.class); + + @SuppressWarnings("unchecked") + SCIMPagedSearchIterable iterable = + new SCIMPagedSearchIterable<>( + github, + request, + (Class>) (Class) SCIMSearchResult.class + ); + + SCIMSearchResult fakeResult = new SCIMSearchResult<>(); + fakeResult.totalResults = 200; + fakeResult.startIndex = 0; + fakeResult.itemsPerPage = 100; + iterable.result = fakeResult; + + boolean incomplete = iterable.isIncomplete(); + assertFalse(incomplete, "Quando totalResults > startIndex + itemsPerPage, deve ser false"); + } + + @Test + void testPopulateWhenResultIsNotNull() throws Exception { + GitHub github = mock(GitHub.class); + GitHubRequest request = mock(GitHubRequest.class); + + @SuppressWarnings("unchecked") + SCIMPagedSearchIterable iterable = + spy(new SCIMPagedSearchIterable<>( + github, + request, + (Class>) (Class) SCIMSearchResult.class + )); + + iterable.result = new SCIMSearchResult<>(); + java.lang.reflect.Method m = SCIMPagedSearchIterable.class.getDeclaredMethod("populate"); + m.setAccessible(true); + m.invoke(iterable); + + verify(iterable, never()).iterator(); + } + + @Test + void shouldCallIteratorWhenResultIsNull() throws MalformedURLException, NoSuchFieldException, IllegalAccessException { + GitHub mockRoot = mock(GitHub.class); + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .method("GET") + .build(); + + SCIMPagedSearchIterable iterable = + spy(new SCIMPagedSearchIterable<>(mockRoot, fakeRequest, (Class) SCIMSearchResult.class)); + + PagedIterator mockIterator = mock(PagedIterator.class); + when(mockIterator.hasNext()).thenReturn(false); + + doReturn(mockIterator).when(iterable).iterator(); + + Field resultField = SCIMPagedSearchIterable.class.getDeclaredField("result"); + resultField.setAccessible(true); + resultField.set(iterable, null); + + iterable.populate(); + verify(iterable, times(1)).iterator(); + verify(mockIterator, times(1)).hasNext(); + } + + @Test + void shouldNotCallIteratorWhenResultIsNotNull() throws Exception { + GitHub mockRoot = mock(GitHub.class); + GitHubRequest fakeRequest = GitHubRequest.newBuilder() + .withApiUrl("https://api.github.com") + .withUrlPath("/scim/v2/Users") + .method("GET") + .build(); + + SCIMPagedSearchIterable iterable = + spy(new SCIMPagedSearchIterable<>(mockRoot, fakeRequest, (Class) SCIMSearchResult.class)); + + PagedIterator mockIterator = mock(PagedIterator.class); + doReturn(mockIterator).when(iterable).iterator(); + + Field resultField = SCIMPagedSearchIterable.class.getDeclaredField("result"); + resultField.setAccessible(true); + resultField.set(iterable, new SCIMSearchResult<>()); + + iterable.populate(); + verify(iterable, never()).iterator(); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMPatchOperationsTest.java b/src/test/java/org/kohsuke/github/SCIMPatchOperationsTest.java new file mode 100644 index 0000000..93752df --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMPatchOperationsTest.java @@ -0,0 +1,163 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMPatchOperationsTest { + + @Test + void testReplaceString() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + + ops.replace("displayName", "Alice"); + assertEquals(1, ops.operations.size()); + + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("replace", op.op); + assertEquals("displayName", op.path); + assertEquals("Alice", op.value); + } + + @Test + void testReplaceString_nullValue() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + + ops.replace("displayName", (String) null); + assertEquals("", ops.operations.get(0).value); // Should become empty string + } + + @Test + void testReplaceBoolean() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + + ops.replace("active", true); + SCIMPatchOperations.Operation op = ops.operations.get(0); + + assertEquals("replace", op.op); + assertEquals("active", op.path); + assertEquals(true, op.value); + } + + @Test + void testReplaceEmail_nonNull() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + SCIMEmail email = new SCIMEmail(); + email.value = "user@example.com"; + email.primary = true; + + ops.replace(email); + assertEquals(1, ops.operations.size()); + + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("emails", op.path); + assertTrue(op.value instanceof List); + + List list = (List) op.value; + assertEquals(1, list.size()); + assertTrue(list.get(0) instanceof SCIMEmail); + } + + @Test + void testReplaceEmail_null() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + ops.replace((SCIMEmail) null); + + assertEquals(1, ops.operations.size()); + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("emails", op.path); + + // Value should be a list of maps with {"value": ""} + List list = (List) op.value; + assertTrue(list.get(0) instanceof Map); + assertEquals("", ((Map) list.get(0)).get("value")); + } + + @Test + void testReplaceRole_nonNull() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + SCIMRole role = new SCIMRole(); + role.value = "developer"; + role.primary = true; + + ops.replace(role); + assertEquals(1, ops.operations.size()); + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("roles", op.path); + + List roles = (List) op.value; + assertEquals(1, roles.size()); + assertTrue(roles.get(0) instanceof SCIMRole); + } + + @Test + void testReplaceRole_null() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + ops.replace((SCIMRole) null); + + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("roles", op.path); + List values = (List) op.value; + assertEquals("", ((Map) values.get(0)).get("value")); + } + + @Test + void testAddMembers() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + List members = Arrays.asList("m1", "m2"); + + ops.addMembers(members); + + assertEquals(1, ops.operations.size()); + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("add", op.op); + assertEquals("members", op.path); + + List list = (List) op.value; + assertEquals(2, list.size()); + assertTrue(list.get(0) instanceof SCIMPatchOperations.Member); + } + + @Test + void testRemoveMembers() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + List members = Arrays.asList("m3", "m4"); + + ops.removeMembers(members); + + assertEquals(1, ops.operations.size()); + SCIMPatchOperations.Operation op = ops.operations.get(0); + assertEquals("remove", op.op); + assertEquals("members", op.path); + + List list = (List) op.value; + assertEquals("m3", ((SCIMPatchOperations.Member) list.get(0)).value); + } + + @Test + void testHasAttributesChange_true_false() { + SCIMPatchOperations ops = new SCIMPatchOperations(); + assertFalse(ops.hasAttributesChange()); + + ops.replace("x", "y"); + assertTrue(ops.hasAttributesChange()); + } + + @Test + void testJsonSerializationDeserialization() throws Exception { + SCIMPatchOperations ops = new SCIMPatchOperations(); + ops.replace("field", "value"); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(ops); + assertTrue(json.contains("PatchOp")); + assertTrue(json.contains("replace")); + + SCIMPatchOperations restored = mapper.readValue(json, SCIMPatchOperations.class); + assertNotNull(restored.schemas); + assertEquals(1, restored.operations.size()); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMSearchResultTest.java b/src/test/java/org/kohsuke/github/SCIMSearchResultTest.java new file mode 100644 index 0000000..699ffe4 --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMSearchResultTest.java @@ -0,0 +1,64 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMSearchResultTest { + + static class DummyResource { + public String name; + } + + @Test + void testFieldAssignmentAndAccess() { + SCIMSearchResult result = new SCIMSearchResult<>(); + + result.totalResults = 5; + result.itemsPerPage = 2; + result.startIndex = 1; + + DummyResource res1 = new DummyResource(); + res1.name = "first"; + DummyResource res2 = new DummyResource(); + res2.name = "second"; + result.Resources = new DummyResource[]{res1, res2}; + + assertEquals(5, result.totalResults); + assertEquals(2, result.itemsPerPage); + assertEquals(1, result.startIndex); + assertEquals("second", result.Resources[1].name); + } + + @Test + void testJacksonSerializationDeserialization() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + // Build a sample result + SCIMSearchResult original = new SCIMSearchResult<>(); + original.totalResults = 10; + original.itemsPerPage = 5; + original.startIndex = 2; + DummyResource dummy = new DummyResource(); + dummy.name = "foo"; + original.Resources = new DummyResource[]{dummy}; + + // Serialize to JSON + String json = mapper.writeValueAsString(original); + assertTrue(json.contains("\"totalResults\":10")); + assertTrue(json.contains("\"itemsPerPage\":5")); + assertTrue(json.contains("\"startIndex\":2")); + assertTrue(json.contains("\"Resources\"")); + + // Deserialize back + SCIMSearchResult restored = + mapper.readValue(json, mapper.getTypeFactory() + .constructParametricType(SCIMSearchResult.class, DummyResource.class)); + + assertEquals(10, restored.totalResults); + assertEquals(5, restored.itemsPerPage); + assertEquals(2, restored.startIndex); + assertEquals("foo", restored.Resources[0].name); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMUserSearchBuilderTest.java b/src/test/java/org/kohsuke/github/SCIMUserSearchBuilderTest.java new file mode 100644 index 0000000..3653be1 --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMUserSearchBuilderTest.java @@ -0,0 +1,42 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SCIMUserSearchBuilderTest { + + @Test + void testGetApiUrl() throws IOException { + GitHub github = GitHub.connectAnonymously(); + GHOrganization org = github.getOrganization("square"); + + SCIMUserSearchBuilder builder = new SCIMUserSearchBuilder(github, org); + assertNotNull(builder.getApiUrl()); + } + + @Test + void testGetApiUrlReturnsExpectedPath() throws IOException { + GitHub github = GitHub.connectAnonymously(); + GHOrganization org = github.getOrganization("square"); + org.login = "square"; + + SCIMUserSearchBuilder builder = new SCIMUserSearchBuilder(github, org); + String expected = "/scim/v2/organizations/square/Users"; + + assertEquals(expected, builder.getApiUrl()); + } + + @Test + void testConstructorInitializesSuperclassCorrectly() throws IOException { + GitHub github = GitHub.connectAnonymously(); + GHOrganization org = github.getOrganization("square"); + org.login = "square"; + + SCIMUserSearchBuilder builder = new SCIMUserSearchBuilder(github, org); + assertNotNull(builder); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMUserTest.java b/src/test/java/org/kohsuke/github/SCIMUserTest.java new file mode 100644 index 0000000..9e6a006 --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMUserTest.java @@ -0,0 +1,50 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SCIMUserTest { + + @Test + void testFields() { + SCIMUser user = new SCIMUser(); + user.id = "u123"; + user.userName = "user1"; + user.externalId = "ext123"; + user.active = true; + + SCIMName name = new SCIMName(); + name.givenName = "Alice"; + user.name = name; + + SCIMEmail email = new SCIMEmail(); + email.value = "alice@example.com"; + user.emails = new SCIMEmail[]{email}; + + assertEquals("u123", user.id); + assertEquals("user1", user.userName); + assertEquals("ext123", user.externalId); + assertTrue(user.active); + assertEquals("Alice", user.name.givenName); + assertEquals("alice@example.com", user.emails[0].value); + } + + @Test + void testJsonSerializationDeserialization() throws Exception { + SCIMUser user = new SCIMUser(); + user.id = "u321"; + user.userName = "bob"; + user.externalId = "ext-bob"; + user.active = false; + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(user); + assertTrue(json.contains("u321")); + SCIMUser restored = mapper.readValue(json, SCIMUser.class); + assertEquals("u321", restored.id); + assertEquals("bob", restored.userName); + assertEquals("ext-bob", restored.externalId); + } +} diff --git a/src/test/java/org/kohsuke/github/TestableGitHubExt.java b/src/test/java/org/kohsuke/github/TestableGitHubExt.java new file mode 100644 index 0000000..3e58c20 --- /dev/null +++ b/src/test/java/org/kohsuke/github/TestableGitHubExt.java @@ -0,0 +1,21 @@ +package org.kohsuke.github; + +import org.jetbrains.annotations.NotNull; +import org.kohsuke.github.authorization.AuthorizationProvider; + +import java.io.IOException; + +class TestableGitHubExt extends GitHubExt { + private final Requester requester; + + TestableGitHubExt(String apiUrl, HttpConnector connector, Requester requester) throws IOException { + super(apiUrl, connector, RateLimitHandler.WAIT, AbuseLimitHandler.WAIT, new GitHubRateLimitChecker(), AuthorizationProvider.ANONYMOUS); + this.requester = requester; + } + + @NotNull + @Override + Requester createRequest() { + return requester; + } +} diff --git a/src/test/java/util/SchemaDefinitionTest.java b/src/test/java/util/SchemaDefinitionTest.java new file mode 100644 index 0000000..9eb8554 --- /dev/null +++ b/src/test/java/util/SchemaDefinitionTest.java @@ -0,0 +1,256 @@ +package util; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaDefinitionTest { + + @Test + void testNewBuilderOverload() { + ObjectClass objectClass = new ObjectClass("TestClass"); + + SchemaDefinition.Builder builder = + SchemaDefinition.newBuilder(objectClass, String.class, Integer.class); + + // Assert + assertNotNull(builder); + } + + @Test + void testAddUidShouldCreateAndAddAttributeMapper() throws Exception { + // Arrange + ObjectClass objectClass = new ObjectClass("testClass"); + + // Cria o builder com tipos genéricos simples + SchemaDefinition.Builder builder = + new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); + + // Lambdas dummy (só pra satisfazer os parâmetros) + BiConsumer create = (value, obj) -> {}; + BiConsumer update = (value, obj) -> {}; + Function read = s -> "valor-" + s; + + // Act + builder.addUid( + "uidField", + SchemaDefinition.Types.STRING, + create, + update, + read, + "fetchUid", + AttributeInfo.Flags.REQUIRED + ); + + // Assert + // Acessa o campo privado 'attributes' + Field field = builder.getClass().getDeclaredField("attributes"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + List attributes = (List) field.get(builder); + + assertEquals(1, attributes.size(), "Deveria conter 1 AttributeMapper"); + Object attr = attributes.get(0); + assertNotNull(attr, "AttributeMapper não deveria ser nulo"); + + // Verifica os campos internos via reflexão + Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); + connectorNameField.setAccessible(true); + assertEquals("__UID__", connectorNameField.get(attr)); + + Field nameField = attr.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + assertEquals("uidField", nameField.get(attr)); + + Field fetchField = attr.getClass().getDeclaredField("fetchField"); + fetchField.setAccessible(true); + assertEquals("fetchUid", fetchField.get(attr)); + + Method isReadableAttributes = builder.getClass().getDeclaredMethod("isReadableAttributes"); + } + + @Test + void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { + // Arrange + ObjectClass objectClass = new ObjectClass("testClass"); + + // Cria o builder com tipos genéricos simples + SchemaDefinition.Builder builder = + new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); + + // Lambdas dummy + BiConsumer createOrUpdate = (value, obj) -> {}; + Function read = s -> "name-" + s; + + // Act + builder.addName( + "displayName", + SchemaDefinition.Types.STRING, + createOrUpdate, + read, + "fetchName", + AttributeInfo.Flags.NOT_UPDATEABLE + ); + + // Assert + // Acessa o campo privado 'attributes' + Field field = builder.getClass().getDeclaredField("attributes"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + List attributes = (List) field.get(builder); + + assertEquals(1, attributes.size(), "Deveria conter 1 AttributeMapper"); + Object attr = attributes.get(0); + assertNotNull(attr, "AttributeMapper não deveria ser nulo"); + + // Verifica se o campo 'connectorName' é __NAME__ + Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); + connectorNameField.setAccessible(true); + assertEquals(Name.NAME, connectorNameField.get(attr)); + + // Verifica o nome do atributo passado + Field nameField = attr.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + assertEquals("displayName", nameField.get(attr)); + + // Verifica o campo 'fetchField' + Field fetchField = attr.getClass().getDeclaredField("fetchField"); + fetchField.setAccessible(true); + assertEquals("fetchName", fetchField.get(attr)); + } + + @Test + void testAddEnableShouldCreateAndAddAttributeMapper() throws Exception { + // Arrange + ObjectClass objectClass = new ObjectClass("testClass"); + + SchemaDefinition.Builder builder = + new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); + + // Lambdas dummy + BiConsumer create = (value, obj) -> {}; + BiConsumer update = (value, obj) -> {}; + Function read = s -> "enabled-" + s; + + // Act + builder.addEnable( + "enabledFlag", + SchemaDefinition.Types.STRING, + create, + update, + read, + "fetchEnable", + AttributeInfo.Flags.NOT_CREATABLE + ); + + + // Assert + // Acessa o campo privado 'attributes' + Field field = builder.getClass().getDeclaredField("attributes"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + List attributes = (List) field.get(builder); + + assertEquals(1, attributes.size(), "Deveria conter 1 AttributeMapper"); + Object attr = attributes.get(0); + assertNotNull(attr, "AttributeMapper não deveria ser nulo"); + + // Verifica se o campo 'connectorName' é __ENABLE__ + Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); + connectorNameField.setAccessible(true); + assertEquals(OperationalAttributes.ENABLE_NAME, connectorNameField.get(attr)); + + // Verifica o nome do atributo passado + Field nameField = attr.getClass().getDeclaredField("name"); + nameField.setAccessible(true); + assertEquals("enabledFlag", nameField.get(attr)); + + // Verifica o campo 'fetchField' + Field fetchField = attr.getClass().getDeclaredField("fetchField"); + fetchField.setAccessible(true); + assertEquals("fetchEnable", fetchField.get(attr)); + } + + @Test + void testBuildSchemaInfo_UUID() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.UUID, AttributeInfo.Flags.REQUIRED); + assertNotNull(info); + } + + @Test + void testBuildSchemaInfo_STRING_CASE_IGNORE() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.STRING_CASE_IGNORE, AttributeInfo.Flags.NOT_CREATABLE); + assertNotNull(info); + } + + @Test + void testBuildSchemaInfo_STRING_URI() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.STRING_URI, AttributeInfo.Flags.NOT_UPDATEABLE); + assertNotNull(info); + } + + @Test + void testBuildSchemaInfo_STRING_LDAP_DN() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.STRING_LDAP_DN, + AttributeInfo.Flags.NOT_READABLE, AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT); + assertNotNull(info); + } + + @Test + void testBuildSchemaInfo_XML() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.XML, AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT); + assertNotNull(info); + } + + @Test + void testBuildSchemaInfo_JSON() throws Exception { + ObjectClassInfo info = buildForType(SchemaDefinition.Types.JSON); + assertNotNull(info); + } + + @SuppressWarnings("unchecked") + private ObjectClassInfo buildForType(SchemaDefinition.Types type, AttributeInfo.Flags... flags) throws Exception { + ObjectClass objectClass = new ObjectClass("testClass"); + SchemaDefinition.Builder builder = + new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); + + Field field = builder.getClass().getDeclaredField("attributes"); + field.setAccessible(true); + List attributes = (List) field.get(builder); + + Constructor ctor = Class.forName("jp.openstandia.connector.util.SchemaDefinition$AttributeMapper").getDeclaredConstructor( + String.class, String.class, SchemaDefinition.Types.class, + BiConsumer.class, BiConsumer.class, + Function.class, String.class, AttributeInfo.Flags[].class + ); + ctor.setAccessible(true); + + attributes.add(ctor.newInstance( + "attr_" + type.hashCode(), + "attr_" + type.hashCode(), + type, + null, null, null, + "fetch_" + type.hashCode(), + flags.length == 0 ? new AttributeInfo.Flags[]{} : flags + )); + + Method method = builder.getClass().getDeclaredMethod("buildSchemaInfo"); + method.setAccessible(true); + return (ObjectClassInfo) method.invoke(builder); + } +} + From 2c5ea60d13440b2d0795b8d72657ac1b6dd630ae Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Wed, 11 Feb 2026 10:22:55 -0300 Subject: [PATCH 3/3] added more tests, update dependencies --- pom.xml | 211 +------ .../github/GitHubEMUGroupHandler.java | 2 +- .../org/kohsuke/github/GHEnterpriseExt.java | 4 + .../github/AbstractGitHubHandlerTest.java | 454 ++++++++++++++ .../connector/github/CreateUserOpTest.java | 34 -- .../connector/github/DeleteUsersOpTest.java | 21 - .../github/GitHubEMUUserHandlerTest.java | 20 +- .../connector/github/SearchGroupsOpTest.java | 54 -- .../connector/github/SearchSeatsOpTest.java | 53 -- .../connector/github/SearchUsersOpTest.java | 55 -- .../connector/github/TestOpTest.java | 15 - .../connector/github/UpdateGroupsOpTest.java | 81 --- .../connector/github/UpdateUsersOpTest.java | 93 --- .../github/rest/GitHubEMURESTClientTest.java | 578 ++++++++++++++++++ .../SchemaDefinitionAttributeMapperTest.java | 461 ++++++++++++++ .../kohsuke/github/GHEnterpriseExtTest.java | 4 +- .../github/GitHubCopilotSeatHandlerTest.java | 337 ++++++++++ .../kohsuke/github/GitHubCopilotSeatTest.java | 136 +++++ .../GitHubCopilotSeatsSearchBuilderTest.java | 8 +- .../github/GitHubEMUUserHandlerTest.java | 253 +++++++- .../org/kohsuke/github/ObjectHandlerTest.java | 175 ++++++ .../org/kohsuke/github/SCIMEMUGroupTest.java | 279 ++++++++- .../kohsuke/github/SchemaDefinitionTest.java | 192 ++++++ ...tGitHubCopilotSeatPagedSearchIterable.java | 43 ++ .../github/TestSCIMPagedSearchIterable.java | 43 ++ .../java/org/kohsuke/github/UtilsTest.java | 208 +++++++ src/test/java/util/SchemaDefinitionTest.java | 32 - 27 files changed, 3168 insertions(+), 678 deletions(-) create mode 100644 src/test/java/jp/openstandia/connector/github/AbstractGitHubHandlerTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/TestOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java create mode 100644 src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java create mode 100644 src/test/java/org/kohsuke/github/GitHubCopilotSeatHandlerTest.java create mode 100644 src/test/java/org/kohsuke/github/ObjectHandlerTest.java create mode 100644 src/test/java/org/kohsuke/github/SchemaDefinitionTest.java create mode 100644 src/test/java/org/kohsuke/github/TestGitHubCopilotSeatPagedSearchIterable.java create mode 100644 src/test/java/org/kohsuke/github/TestSCIMPagedSearchIterable.java create mode 100644 src/test/java/org/kohsuke/github/UtilsTest.java diff --git a/pom.xml b/pom.xml index 944d0e6..7d79cf0 100644 --- a/pom.xml +++ b/pom.xml @@ -119,43 +119,6 @@ - - org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - ${project.source.version} - ${java.home}/bin/javadoc - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - --pinentry-mode - loopback - - - - - sign-artifacts - verify - - sign - - - - org.jacoco jacoco-maven-plugin @@ -204,9 +167,15 @@ - org.junit.jupiter - junit-jupiter - ${junit.version} + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + org.mockito + mockito-inline + 5.2.0 test @@ -241,7 +210,7 @@ com.squareup.okhttp3 okhttp - 4.9.0 + 4.12.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/java/jp/openstandia/connector/github/GitHubEMUGroupHandler.java b/src/main/java/jp/openstandia/connector/github/GitHubEMUGroupHandler.java index 35f6584..72d32f7 100644 --- a/src/main/java/jp/openstandia/connector/github/GitHubEMUGroupHandler.java +++ b/src/main/java/jp/openstandia/connector/github/GitHubEMUGroupHandler.java @@ -117,7 +117,7 @@ public static SchemaDefinition.Builder createSchema(AbstractGitHubConfiguration NOT_CREATABLE, NOT_UPDATEABLE ); - LOGGER.ok("The constructed GitHub EMU User schema"); + LOGGER.ok("The constructed GitHub EMU Group schema"); return sb; } diff --git a/src/main/java/org/kohsuke/github/GHEnterpriseExt.java b/src/main/java/org/kohsuke/github/GHEnterpriseExt.java index 0803038..a037b89 100644 --- a/src/main/java/org/kohsuke/github/GHEnterpriseExt.java +++ b/src/main/java/org/kohsuke/github/GHEnterpriseExt.java @@ -160,6 +160,10 @@ public GitHubCopilotSeat getCopilotSeatByDisplayName(String copilotSeatDisplayNa .list() .toList(); + if (allSeats.isEmpty()) { + return null; + } + return allSeats.stream() .filter(seat -> copilotSeatDisplayName.equals(seat.assignee.login)) .findFirst() diff --git a/src/test/java/jp/openstandia/connector/github/AbstractGitHubHandlerTest.java b/src/test/java/jp/openstandia/connector/github/AbstractGitHubHandlerTest.java new file mode 100644 index 0000000..793b277 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/AbstractGitHubHandlerTest.java @@ -0,0 +1,454 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.ObjectHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.spi.SearchResultsHandler; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AbstractGitHubHandlerTest { + static class TestConfiguration extends AbstractGitHubConfiguration { + @Override + public void validate() { + // no-op for unit tests + } + } + + static class TestHandler implements ObjectHandler { + static final ObjectClass OC = new ObjectClass("Test"); + + private String instanceName; + + private final SchemaDefinition schemaDefinition; + private final QueryBehavior behavior; + + interface QueryBehavior { + void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options); + } + + TestHandler(SchemaDefinition schemaDefinition, QueryBehavior behavior) { + this.schemaDefinition = schemaDefinition; + this.behavior = behavior; + } + + @Override + public ObjectHandler setInstanceName(String instanceName) { + this.instanceName = instanceName; + return this; + } + + public String getInstanceName() { + return instanceName; + } + + @Override + public Uid create(Set attributes) { + return new Uid("created"); + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + return Collections.emptySet(); + } + + @Override + public void delete(Uid uid, OperationOptions options) { + // no-op + } + + @Override + public void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options) { + behavior.query(filter, resultsHandler, options); + } + + @Override + public SchemaDefinition getSchemaDefinition() { + return schemaDefinition; + } + + @Override + public int getByUid(Uid uid, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(OC).setUid(uid).setName("n1").build()); + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(OC).setUid(uid).setName("n2").build()); + return 10; + } + + @Override + public int getByName(Name name, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(OC).setUid("u1").setName(name).build()); + return 10; + } + + @Override + public int getByMembers(Attribute attribute, ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(OC).setUid("u1").setName("n").build()); + return 10; + } + + @Override + public int getAll(ResultsHandler resultsHandler, OperationOptions options, + Set returnAttributesSet, Set fetchFieldsSet, + boolean allowPartialAttributeValues, int pageSize, int pageOffset) { + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(OC).setUid("u1").setName("n").build()); + return 10; + } + } + + static class TestConnector extends AbstractGitHubConnector> { + + final GitHubClient> clientFactory; + final AbstractGitHubSchema schemaFactory; + + TestConnector(GitHubClient> clientFactory, + AbstractGitHubSchema schemaFactory) { + this.clientFactory = clientFactory; + this.schemaFactory = schemaFactory; + } + + @Override + protected GitHubClient> newClient(TestConfiguration configuration) { + return clientFactory; + } + + @Override + protected AbstractGitHubSchema newGitHubSchema(TestConfiguration configuration, + GitHubClient> client) { + return schemaFactory; + } + } + + static class TestEMUConnector extends GitHubEMUConnector { + private final GitHubClient clientFactory; + private final GitHubEMUSchema schemaFactory; + + TestEMUConnector(GitHubClient clientFactory, GitHubEMUSchema schemaFactory) { + this.clientFactory = clientFactory; + this.schemaFactory = schemaFactory; + } + + @Override + protected GitHubClient newClient(GitHubEMUConfiguration configuration) { + return clientFactory; + } + + @Override + protected GitHubEMUSchema newGitHubSchema(GitHubEMUConfiguration configuration, GitHubClient client) { + return schemaFactory; + } + } + + @Test + void getConfigurationShouldReturnConfiguration() { + TestConnector connector = new TestConnector(mock(GitHubClient.class), mock(AbstractGitHubSchema.class)); + TestConfiguration cfg = new TestConfiguration(); + connector.configuration = cfg; + + assertSame(cfg, connector.getConfiguration()); + } + + @Test + void initShouldCreateClientAndLoadSchemaAndWrapRuntimeException() { + GitHubClient> client = mock(GitHubClient.class); + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + when(schema.getSchema()).thenReturn(minimalSchema()); + + TestConnector connector = new TestConnector(client, schema); + + connector.init(new TestConfiguration()); + verify(schema).getSchema(); + + TestConnector failing = new TestConnector(client, schema) { + @Override + protected GitHubClient> newClient(TestConfiguration configuration) { + throw new RuntimeException("boom"); + } + }; + + ConnectorException ex = assertThrows(ConnectorException.class, () -> failing.init(new TestConfiguration())); + assertNotNull(ex.getCause()); + } + + @Test + void schemaShouldReturnSchemaAndWrapRuntimeException() { + GitHubClient> client = mock(GitHubClient.class); + Schema expected = minimalSchema(); + + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + when(schema.getSchema()).thenReturn(expected); + + TestConnector connector = new TestConnector(client, schema); + connector.configuration = new TestConfiguration(); + connector.client = client; + + assertSame(expected, connector.schema()); + + TestConnector failing = new TestConnector(client, schema) { + @Override + protected AbstractGitHubSchema newGitHubSchema(TestConfiguration configuration, + GitHubClient> client) { + throw new RuntimeException("boom"); + } + }; + failing.configuration = new TestConfiguration(); + failing.client = client; + + assertThrows(ConnectorException.class, failing::schema); + } + + private static ObjectClassInfo objectClassInfoWithUidAndName(String objectClassName) { + Set set = new HashSet<>(); + set.add(AttributeInfo.Flags.REQUIRED); + set.add(AttributeInfo.Flags.NOT_CREATABLE); + + return new ObjectClassInfoBuilder() + .setType(objectClassName) + .addAttributeInfo(AttributeInfoBuilder.build(Uid.NAME, String.class, set)) + .addAttributeInfo(AttributeInfoBuilder.build(Name.NAME, String.class, set)) + .build(); + } + + private static Schema minimalSchema() { + SchemaBuilder sb = new SchemaBuilder(AbstractGitHubConnector.class); + + sb.defineObjectClass(objectClassInfoWithUidAndName(ObjectClass.ACCOUNT_NAME)); + + return sb.build(); + } + + @Test + void createUpdateDeleteShouldValidateInputsAndWrapRuntimeExceptions() { + ObjectClass oc = TestHandler.OC; + + + ObjectHandler handler = mock(ObjectHandler.class); + when(handler.setInstanceName(anyString())).thenReturn(handler); + + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + + when(schema.getSchema()).thenReturn(minimalSchema()); + when(schema.getSchemaHandler(oc)).thenReturn(handler); + + TestConnector connector = new TestConnector(mock(GitHubClient.class), schema); + connector.configuration = new TestConfiguration(); + connector.client = mock(GitHubClient.class); + connector.instanceName = "inst"; + + assertThrows(InvalidAttributeValueException.class, () -> connector.create(oc, null, null)); + assertThrows(InvalidAttributeValueException.class, () -> connector.create(oc, Collections.emptySet(), null)); + + when(handler.create(anySet())).thenThrow(new RuntimeException("boom")); + assertThrows(ConnectorException.class, + () -> connector.create(oc, Set.of(AttributeBuilder.build("a", "b")), null)); + + assertThrows(InvalidAttributeValueException.class, + () -> connector.updateDelta(oc, null, Set.of(), null)); + assertThrows(InvalidAttributeValueException.class, + () -> connector.updateDelta(oc, new Uid("u"), null, null)); + assertThrows(InvalidAttributeValueException.class, + () -> connector.updateDelta(oc, new Uid("u"), Collections.emptySet(), null)); + + when(handler.updateDelta(any(), anySet(), any())).thenThrow(new RuntimeException("boom")); + assertThrows(ConnectorException.class, + () -> connector.updateDelta(oc, new Uid("u"), Set.of(AttributeDeltaBuilder.build("x", "y")), null)); + + assertThrows(InvalidAttributeValueException.class, () -> connector.delete(oc, null, null)); + + doThrow(new RuntimeException("boom")).when(handler).delete(any(), any()); + assertThrows(ConnectorException.class, () -> connector.delete(oc, new Uid("u"), null)); + } + + @Test + void getSchemaHandlerShouldValidateObjectClassAndUnsupportedClassAndSetInstanceName() { + ObjectHandler handler = mock(ObjectHandler.class); + when(handler.setInstanceName(anyString())).thenReturn(handler); + when(handler.create(anySet())).thenReturn(new Uid("ok")); + + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + when(schema.getSchema()).thenReturn(minimalSchema()); + + when(schema.getSchemaHandler(any())).thenReturn(null); + + TestConnector connector = new TestConnector(mock(GitHubClient.class), schema); + connector.configuration = new TestConfiguration(); + connector.client = mock(GitHubClient.class); + + assertThrows(InvalidAttributeValueException.class, + () -> connector.create(null, Set.of(AttributeBuilder.build("a", "b")), null)); + + assertThrows(InvalidAttributeValueException.class, + () -> connector.create(new ObjectClass("Unknown"), Set.of(AttributeBuilder.build("a", "b")), null)); + + when(schema.getSchemaHandler(TestHandler.OC)).thenReturn(handler); + connector.instanceName = "inst"; + + Uid uid = connector.create(TestHandler.OC, Set.of(AttributeBuilder.build("a", "b")), null); + assertEquals("ok", uid.getUidValue()); + verify(handler).setInstanceName("inst"); + } + + @Test + void createFilterTranslatorShouldReturnTranslator() { + TestConnector connector = new TestConnector(mock(GitHubClient.class), mock(AbstractGitHubSchema.class)); + assertNotNull(connector.createFilterTranslator(new ObjectClass("x"), new OperationOptionsBuilder().build())); + } + + @Test + void executeQueryShouldUseSchemaHandlerQueryForNonEmuConnectorAndWrapRuntimeException() { + SchemaDefinition sd = mock(SchemaDefinition.class); + + TestHandler handler = new TestHandler(sd, (filter, resultsHandler, options) -> + resultsHandler.handle(new ConnectorObjectBuilder().setObjectClass(TestHandler.OC).setUid("u").setName("n").build()) + ); + + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + when(schema.getSchemaHandler(TestHandler.OC)).thenReturn(handler); + when(schema.getSchema()).thenReturn(minimalSchema()); + + TestConnector connector = new TestConnector(mock(GitHubClient.class), schema); + connector.configuration = new TestConfiguration(); + connector.client = mock(GitHubClient.class); + connector.schema = schema; + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + connector.executeQuery(TestHandler.OC, null, rh, new OperationOptionsBuilder().build()); + verify(rh).handle(any()); + + TestHandler throwing = new TestHandler(sd, (filter, resultsHandler, options) -> { throw new RuntimeException("boom"); }); + when(schema.getSchemaHandler(TestHandler.OC)).thenReturn(throwing); + + assertThrows(ConnectorException.class, + () -> connector.executeQuery(TestHandler.OC, null, rh, new OperationOptionsBuilder().build())); + } + + @Test + void executeQueryWithSearchResultShouldHandleAllFilterTypesAndPaginationResult() { + SchemaDefinition sd = mock(SchemaDefinition.class); + when(sd.getReturnedByDefaultAttributesSet()).thenReturn(Map.of( + Uid.NAME, "id", + Name.NAME, "userName" + )); + when(sd.getFetchField(anyString())).thenAnswer(inv -> inv.getArgument(0)); + + TestHandler handler = new TestHandler(sd, (filter, resultsHandler, options) -> {}); + + GitHubEMUSchema emuSchema = mock(GitHubEMUSchema.class); + when(emuSchema.getSchemaHandler(TestHandler.OC)).thenReturn(handler); + when(emuSchema.getSchema()).thenReturn(minimalSchema()); + + GitHubClient client = mock(GitHubClient.class); + + TestEMUConnector connector = new TestEMUConnector(client, emuSchema); + GitHubEMUConfiguration cfg = mock(GitHubEMUConfiguration.class); + when(cfg.getQueryPageSize()).thenReturn(2); + + connector.init(cfg); + connector.setInstanceName("inst"); + + SearchResultsHandler srh = mock(SearchResultsHandler.class); + when(srh.handle(any())).thenReturn(true); + + OperationOptions options = new OperationOptionsBuilder() + .setPageSize(2) + .setPagedResultsOffset(3) + .build(); + + connector.executeQuery(TestHandler.OC, GitHubFilter.By(new Uid("u1")), srh, options); + connector.executeQuery(TestHandler.OC, GitHubFilter.By(new Name("n1")), srh, options); + connector.executeQuery(TestHandler.OC, GitHubFilter.ByMember( + "members.User.value", + GitHubFilter.FilterType.EXACT_MATCH, + AttributeBuilder.build("members.User.value", "x") + ), srh, options); + connector.executeQuery(TestHandler.OC, null, srh, options); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SearchResult.class); + verify(srh, atLeastOnce()).handleResult(captor.capture()); + assertTrue(captor.getAllValues().stream().allMatch(r -> r.getRemainingPagedResults() >= 0)); + } + + @Test + void testShouldRecreateClientSetInstanceNameAndCallClientTestWrapRuntimeException() { + GitHubClient> client1 = mock(GitHubClient.class); + GitHubClient> client2 = mock(GitHubClient.class); + + AbstractGitHubSchema schema = mock(AbstractGitHubSchema.class); + when(schema.getSchema()).thenReturn(minimalSchema()); + + TestConnector connector = new TestConnector(client2, schema); + connector.configuration = new TestConfiguration(); + connector.client = client1; + connector.instanceName = "inst"; + + connector.test(); + + verify(client1).close(); + verify(client2).setInstanceName("inst"); + verify(client2).test(); + + doThrow(new RuntimeException("boom")).when(client2).test(); + assertThrows(ConnectorException.class, connector::test); + } + + @Test + void disposeShouldCloseAndNullOutClient() { + GitHubClient> client = mock(GitHubClient.class); + + TestConnector connector = new TestConnector(client, mock(AbstractGitHubSchema.class)); + connector.client = client; + + connector.dispose(); + verify(client).close(); + assertNull(connector.client); + + connector.dispose(); // should be safe when already null + } + + @Test + void checkAliveShouldDoNothing() { + TestConnector connector = new TestConnector(mock(GitHubClient.class), mock(AbstractGitHubSchema.class)); + connector.checkAlive(); + } + + @Test + void setInstanceNameShouldSetAndForwardToClient() { + GitHubClient> client = mock(GitHubClient.class); + TestConnector connector = new TestConnector(client, mock(AbstractGitHubSchema.class)); + connector.client = client; + + connector.setInstanceName("inst"); + assertEquals("inst", connector.instanceName); + verify(client).setInstanceName("inst"); + } + + @Test + void processRuntimeExceptionShouldReturnSameConnectorExceptionOrWrapOtherRuntime() { + TestConnector connector = new TestConnector(mock(GitHubClient.class), mock(AbstractGitHubSchema.class)); + + ConnectorException ce = new ConnectorException("x"); + assertSame(ce, connector.processRuntimeException(ce)); + + RuntimeException re = new RuntimeException("boom"); + ConnectorException wrapped = connector.processRuntimeException(re); + assertSame(re, wrapped.getCause()); + } + +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java b/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java deleted file mode 100644 index 8bab742..0000000 --- a/src/test/java/jp/openstandia/connector/github/CreateUserOpTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.junit.jupiter.api.Test; - -import java.util.HashSet; -import java.util.Set; - -import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class CreateUserOpTest extends AbstractEMUTest { - - private Set userEntry() { - - Set attributeSet = new HashSet<>(); - attributeSet.add(AttributeBuilder.build(Name.NAME, "")); - attributeSet.add(AttributeBuilder.build("externalId", "")); - attributeSet.add(AttributeBuilder.build("displayName", "")); - attributeSet.add(AttributeBuilder.build("primaryEmail", "")); - attributeSet.add(AttributeBuilder.build("primaryRole", "User")); - attributeSet.add(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME, true)); - return attributeSet; - } - - @Test() - void shouldCreateOrReturnExistentUser() { - ConnectorFacade facade = newFacade(); - Uid uid = facade.create(USER_OBJECT_CLASS, userEntry(), null); - assertNotNull(uid); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java deleted file mode 100644 index f0697e1..0000000 --- a/src/test/java/jp/openstandia/connector/github/DeleteUsersOpTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.Uid; -import org.junit.jupiter.api.Test; - -import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class DeleteUsersOpTest extends AbstractEMUTest { - - String userUidToDelete = ""; - - @Test() - void shouldDeleteUserIfExists() { - ConnectorFacade facade = newFacade(); - facade.delete(USER_OBJECT_CLASS, new Uid(userUidToDelete), null); - assertNotNull(userUidToDelete); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java index adb4d6b..22b25c3 100644 --- a/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java +++ b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java @@ -42,7 +42,7 @@ private static GitHubEMUUserHandler newHandler() { } @Test - void instancia_handler_ok() { + void instanciaHandlerOk() { GitHubEMUUserHandler handler = newHandler(); assertNotNull(handler); } @@ -55,24 +55,6 @@ public void testCreateSchema() { SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); assertNotNull(builder); } - - @Test - public void testUidLambdaExecution() { - GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); - GitHubClient client = mock(GitHubClient.class); - - SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); - SchemaDefinition definition = builder.build(); - - // Agora precisamos de um "source" que tenha o campo 'id' - SCIMEMUUser user = new SCIMEMUUser(); - user.id = UUID.randomUUID().toString(); - - // A mágica: simular a extração do UID usando o schema - String extractedId = definition.getReturnedByDefaultAttributesSet().get(user.id); - - assertEquals(user.id, extractedId); - } } diff --git a/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java deleted file mode 100644 index e39bce9..0000000 --- a/src/test/java/jp/openstandia/connector/github/SearchGroupsOpTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; - -import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; - -class SearchGroupsOpTest extends AbstractEMUTest { - - String groupUid = ""; - String groupName = ""; - - @Test() - void shouldReturnAllGroups() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - facade.search(GROUP_OBJECT_CLASS, null, handler, null); - List objects = handler.getObjects(); - assertTrue(objects.size() > 1, "Size: " + objects.size()); - } - - @Test() - void shouldReturnGroupByUid() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(GROUP_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } - - @Test() - void shouldReturnGroupByName() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Name.NAME, groupName); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(GROUP_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java deleted file mode 100644 index c3fe2c8..0000000 --- a/src/test/java/jp/openstandia/connector/github/SearchSeatsOpTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import static jp.openstandia.connector.github.GitHubCopilotSeatHandler.SEAT_OBJECT_CLASS; - -import java.util.List; - -class SearchSeatsOpTest extends AbstractEMUTest { - - String seatUid = ""; - String seatName = ""; - - @Test() - void shouldReturnAllSeats() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - facade.search(SEAT_OBJECT_CLASS, null, handler, null); - List objects = handler.getObjects(); - assertTrue(objects.size() > 1, "Size: " + objects.size()); - } - - @Test() - void shouldReturnSeatByUid() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Uid.NAME, seatUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(SEAT_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } - - @Test() - void shouldReturnSeatByName() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Name.NAME, seatName); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(SEAT_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java deleted file mode 100644 index 30685d3..0000000 --- a/src/test/java/jp/openstandia/connector/github/SearchUsersOpTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; - -import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; - -class SearchUsersOpTest extends AbstractEMUTest { - - String userUid = ""; - String userName = ""; - - @Test() - void shouldReturnAllUsers() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - facade.search(USER_OBJECT_CLASS, null, handler, null); - List objects = handler.getObjects(); - assertTrue(objects.size() > 1, "Size: " + objects.size()); - } - - @Test() - void shouldReturnUserByUid() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(USER_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } - - @Test() - void shouldReturnUserByUsername() { - ConnectorFacade facade = newFacade(); - ListResultHandler handler = new ListResultHandler(); - - Attribute attribute = AttributeBuilder.build(Name.NAME, userName); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(USER_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/TestOpTest.java b/src/test/java/jp/openstandia/connector/github/TestOpTest.java deleted file mode 100644 index 64d861b..0000000 --- a/src/test/java/jp/openstandia/connector/github/TestOpTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.junit.jupiter.api.Test; - -class TestOpTest extends AbstractEMUTest { - - @Test() - void shouldInitializeConnection() { - ConnectorFacade facade = newFacade(); - facade.test(); - } -} - diff --git a/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java deleted file mode 100644 index fd20fd2..0000000 --- a/src/test/java/jp/openstandia/connector/github/UpdateGroupsOpTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import static jp.openstandia.connector.github.GitHubEMUGroupHandler.GROUP_OBJECT_CLASS; -import static org.junit.jupiter.api.Assertions.assertEquals; - -class UpdateGroupsOpTest extends AbstractEMUTest { - - String userUid = ""; - String groupUidToUpdate = ""; - - @Test() - void shouldAddUserToGroup() { - // Create an AttributeDelta to add user uid - Set attributes = new HashSet<>(); - AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); - deltaBuilder.setName("members.User.value"); - deltaBuilder.addValueToAdd(userUid); - attributes.add(deltaBuilder.build()); - - // Call updateDelta to update the group - ConnectorFacade facade = newFacade(); - facade.updateDelta(GROUP_OBJECT_CLASS, new Uid(groupUidToUpdate), attributes, null); - - // Retrieve and verify the updated object - ListResultHandler handler = new ListResultHandler(); - Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUidToUpdate); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(GROUP_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - - ConnectorObject object = objects.get(0); - Attribute memberOfAttr = object.getAttributeByName("members.User.value"); - assertNotNull(memberOfAttr); - - List grupos = memberOfAttr.getValue(); - assertTrue(grupos.contains(userUid)); - } - - @Test() - void shouldRemoveUserFromGroup() { - // Create an AttributeDelta to add user uid - Set attributes = new HashSet<>(); - AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); - deltaBuilder.setName("members.User.value"); - deltaBuilder.addValueToRemove(userUid); - attributes.add(deltaBuilder.build()); - - // Call updateDelta to update the group - ConnectorFacade facade = newFacade(); - facade.updateDelta(GROUP_OBJECT_CLASS, new Uid(groupUidToUpdate), attributes, null); - - // Retrieve and verify the updated object - ListResultHandler handler = new ListResultHandler(); - Attribute attribute = AttributeBuilder.build(Uid.NAME, groupUidToUpdate); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(GROUP_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - - ConnectorObject object = objects.get(0); - Attribute memberOfAttr = object.getAttributeByName("members.User.value"); - assertNotNull(memberOfAttr); - - List grupos = memberOfAttr.getValue(); - assertFalse(grupos.contains(userUid)); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java b/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java deleted file mode 100644 index 5288892..0000000 --- a/src/test/java/jp/openstandia/connector/github/UpdateUsersOpTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package jp.openstandia.connector.github; - -import jp.openstandia.connector.github.testutil.AbstractEMUTest; -import org.identityconnectors.framework.api.ConnectorFacade; -import org.identityconnectors.framework.common.objects.*; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import static jp.openstandia.connector.github.GitHubEMUUserHandler.USER_OBJECT_CLASS; - -class UpdateUsersOpTest extends AbstractEMUTest { - - String userUid = ""; - String attrToUpdate = ""; - String attrNewValue = ""; - - @Test() - void shouldActivateUser() { - Set attributes = new HashSet<>(); - - AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); - deltaBuilder.setName(OperationalAttributes.ENABLE_NAME); - deltaBuilder.addValueToReplace(true); - attributes.add(deltaBuilder.build()); - - ConnectorFacade facade = newFacade(); - facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); - - ListResultHandler handler = new ListResultHandler(); - Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(USER_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } - - @Test() - void shouldInactivateUser() { - Set attributes = new HashSet<>(); - - AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); - deltaBuilder.setName(OperationalAttributes.ENABLE_NAME); - deltaBuilder.addValueToReplace(false); - attributes.add(deltaBuilder.build()); - - ConnectorFacade facade = newFacade(); - facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); - - ListResultHandler handler = new ListResultHandler(); - Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(USER_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - } - - @Test() - void shouldUpdateAttrValue() { - ConnectorFacade facade = newFacade(); - - // Create an AttributeDelta to update the status of uid - Set attributes = new HashSet<>(); - AttributeDeltaBuilder deltaBuilder = new AttributeDeltaBuilder(); - deltaBuilder.setName(attrToUpdate); - deltaBuilder.addValueToReplace(attrNewValue); - attributes.add(deltaBuilder.build()); - - // Call updateDelta to update the status - facade.updateDelta(USER_OBJECT_CLASS, new Uid(userUid), attributes, null); - - // Retrieve and verify the updated object - ListResultHandler handler = new ListResultHandler(); - Attribute attribute = AttributeBuilder.build(Uid.NAME, userUid); - EqualsFilter filter = new EqualsFilter(attribute); - - facade.search(USER_OBJECT_CLASS, filter, handler, null); - List objects = handler.getObjects(); - assertEquals(1, objects.size()); - - ConnectorObject object = objects.get(0); - Attribute nameAttr = object.getAttributeByName(attrToUpdate); - assertNotNull(nameAttr); - assertEquals(attrNewValue, nameAttr.getValue().get(0)); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java new file mode 100644 index 0000000..ae90c4e --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java @@ -0,0 +1,578 @@ +package jp.openstandia.connector.github.rest; + +import jp.openstandia.connector.github.GitHubEMUConfiguration; +import jp.openstandia.connector.github.GitHubEMUSchema; +import org.kohsuke.github.TestSCIMPagedSearchIterable; +import jp.openstandia.connector.util.QueryHandler; +import org.identityconnectors.framework.common.exceptions.*; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.Uid; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.*; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GitHubEMURESTClientTest { + + @Mock GitHubEMUConfiguration configuration; + + @Mock + GitHubExt apiClient; + @Mock + GHEnterpriseExt enterprise; + + @Mock OperationOptions options; + + // ---------- testable client (constructor-safe) ---------- + static class TestableClient extends GitHubEMURESTClient { + static final AtomicInteger authCalls = new AtomicInteger(0); + + TestableClient(GitHubEMUConfiguration configuration) { + super(configuration); + } + + @Override + public void auth() { + //TestableClient.authCalls.set(0); + // No network; just count calls + authCalls.incrementAndGet(); + } + } + + private TestableClient client; + + @BeforeEach + void setUp() throws Exception { + // Avoid NPE if something accidentally calls configuration in overridden auth() + //when(configuration.getEnterpriseSlug()).thenReturn("ent"); + + client = new TestableClient(configuration); + + // inject mocks (apiClient is private -> reflection) + setPrivateField(client, "apiClient", apiClient); + + // enterpriseApiClient is package-private -> direct access + client.enterpriseApiClient = enterprise; + } + + // ---------- reflection helpers ---------- + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field f = target.getClass().getSuperclass().getDeclaredField(fieldName); + f.setAccessible(true); + f.set(target, value); + } + + private static void setPrivateLong(Object target, String fieldName, long value) throws Exception { + Field f = target.getClass().getSuperclass().getDeclaredField(fieldName); + f.setAccessible(true); + f.setLong(target, value); + } + + // ======================================================= + // setInstanceName + // ======================================================= + @Test + void setInstanceName_setsValue() { + client.setInstanceName("myInstance"); + // no getter; just validate no exception and used in logging paths + assertDoesNotThrow(() -> client.setInstanceName("another")); + } + + // ======================================================= + // test() + // ======================================================= + @Test + void test_success_callsApiUrlValidity() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); // prevent withAuth calling auth() + + client.test(); + + verify(apiClient, times(1)).checkApiUrlValidity(); + } + + @Test + void test_whenRuntimeException_wrapsConnectorException() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + doThrow(new RuntimeException("boom")).when(apiClient).checkApiUrlValidity(); + + ConnectorException ex = assertThrows(ConnectorException.class, () -> client.test()); + assertTrue(ex.getMessage().contains("isn't active")); + } + + // ======================================================= + // auth() + // ======================================================= + @Test + void auth_isOverridden_noNetwork_calledByCtorAtLeastOnce() { + // Constructor calls auth(); our override increments counter + assertTrue(client.authCalls.get() >= 1); + } + +// // ======================================================= +// // handleApiException(Exception) +// // ======================================================= + @Test + void handleApiException_400_mapsToInvalidAttributeValueException() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 400 Bad Request"); + ConnectorException mapped = client.handleApiException(e); + + assertTrue(mapped instanceof InvalidAttributeValueException); + } + + @Test + void handleApiException_401_mapsToConnectionFailedUnauthorized() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 401 Unauthorized"); + ConnectorException mapped = client.handleApiException(e); + assertTrue(mapped instanceof ConnectionFailedException); // your UnauthorizedException extends ConnectionFailedException + } + + @Test + void handleApiException_403_mapsToPermissionDenied() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 403 Forbidden"); + ConnectorException mapped = client.handleApiException(e); + assertTrue(mapped instanceof PermissionDeniedException); + } + + @Test + void handleApiException_404_mapsToUnknownUid() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 404 Not Found"); + ConnectorException mapped = client.handleApiException(e); + assertTrue(mapped instanceof UnknownUidException); + } + + @Test + void handleApiException_409_mapsToAlreadyExists() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 409 Conflict"); + ConnectorException mapped = client.handleApiException(e); + assertTrue(mapped instanceof AlreadyExistsException); + } + + @Test + void handleApiException_429_mapsToRetryable() { + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 429 Too Many Requests"); + ConnectorException mapped = client.handleApiException(e); + assertTrue(mapped instanceof RetryableException); + } + + @Test + void handleApiException_otherException_mapsToConnectorIOException() { + ConnectorException mapped = client.handleApiException(new RuntimeException("x")); + assertTrue(mapped instanceof ConnectorIOException); + } + + private GHFileNotFoundException ghFileNotFoundWithStatus(String statusLine) { + GHFileNotFoundException ex = mock(GHFileNotFoundException.class); + Map> headers = new HashMap<>(); + headers.put(null, List.of(statusLine)); // code reads get(null) + when(ex.getResponseHeaderFields()).thenReturn(headers); + return ex; + } + + // ======================================================= + // withAuth(Callable) + // ======================================================= + @Test + void withAuth_whenLastAuthenticatedNonZero_callsAuth() throws Exception { + setPrivateLong(client, "lastAuthenticated", 123L); + + String out = client.withAuth(() -> "ok"); + + assertEquals("ok", out); + assertTrue(client.authCalls.get() >= 2); // ctor auth + this auth + } + + @Test + void withAuth_whenCallableThrows_mapsAndThrowsConnectorException() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + Exception e = ghFileNotFoundWithStatus("HTTP/1.1 404 Not Found"); + + assertThrows(UnknownUidException.class, () -> + client.withAuth(() -> { throw e; }) + ); + } + + // ======================================================= + // createEMUUser + // ======================================================= + @Test + void createEMUUser_returnsUidWithName() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUUser created = new SCIMEMUUser(); + created.id = "123"; + created.userName = "jdoe"; + + when(enterprise.createSCIMEMUUser(any())).thenReturn(created); + + Uid uid = client.createEMUUser(new SCIMEMUUser()); + + assertEquals("123", uid.getUidValue()); + assertEquals("jdoe", uid.getNameHintValue()); + verify(enterprise).createSCIMEMUUser(any(SCIMEMUUser.class)); + } + + // ======================================================= + // patchEMUUser + // ======================================================= + @Test + void patchEMUUser_callsUpdate() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMPatchOperations ops = new SCIMPatchOperations(); + when(enterprise.updateSCIMEMUUser(eq("u1"), eq(ops))).thenReturn(new SCIMEMUUser()); + + client.patchEMUUser(new Uid("u1"), ops); + + verify(enterprise).updateSCIMEMUUser("u1", ops); + } + + // ======================================================= + // deleteEMUUser + // ======================================================= + @Test + void deleteEMUUser_callsDelete() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + client.deleteEMUUser(new Uid("u1"), options); + + verify(enterprise).deleteSCIMUser("u1"); + } + + // ======================================================= + // getEMUUser(Uid) + // ======================================================= + @Test + void getEMUUser_byUid_callsGet() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUUser user = new SCIMEMUUser(); + user.id = "u1"; + when(enterprise.getSCIMEMUUser("u1")).thenReturn(user); + + SCIMEMUUser out = client.getEMUUser(new Uid("u1"), options, Set.of()); + + assertSame(user, out); + verify(enterprise).getSCIMEMUUser("u1"); + } + + // ======================================================= + // getEMUUser(Name) + // ======================================================= + @Test + void getEMUUser_byName_callsGetByUserName() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUUser user = new SCIMEMUUser(); + user.userName = "jdoe"; + when(enterprise.getSCIMEMUUserByUserName("jdoe")).thenReturn(user); + + SCIMEMUUser out = client.getEMUUser(new Name("jdoe"), options, Set.of()); + + assertSame(user, out); + verify(enterprise).getSCIMEMUUserByUserName("jdoe"); + } + + // ======================================================= + // getEMUUsers + // ======================================================= + @Test + void getEMUUsers_noOffset_iteratesAllUntilHandlerFalse_returnsTotal() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUUser u1 = new SCIMEMUUser(); u1.id = "1"; + SCIMEMUUser u2 = new SCIMEMUUser(); u2.id = "2"; + + SCIMPagedSearchIterable iterable = new TestSCIMPagedSearchIterable<>(List.of(u1, u2), 77); + when(enterprise.listSCIMUsers(10, 0)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(u1)).thenReturn(true); + when(handler.handle(u2)).thenReturn(false); // stop early + + int total = client.getEMUUsers(handler, options, Set.of(), 10, 0); + + assertEquals(77, total); + verify(handler).handle(u1); + verify(handler).handle(u2); + } + + @Test + void getEMUUsers_withOffset_paginatesAndStopsAtPageSize_returnsTotal() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUUser u1 = new SCIMEMUUser(); u1.id = "1"; + SCIMEMUUser u2 = new SCIMEMUUser(); u2.id = "2"; + SCIMEMUUser u3 = new SCIMEMUUser(); u3.id = "3"; + + SCIMPagedSearchIterable iterable = new TestSCIMPagedSearchIterable<>(List.of(u1, u2, u3), 99); + when(enterprise.listSCIMUsers(2, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(any())).thenReturn(true); + + int total = client.getEMUUsers(handler, options, Set.of(), 2, 1); + + assertEquals(99, total); + verify(handler, times(2)).handle(any()); // stops at pageSize=2 + } + + private static org.kohsuke.github.PagedIterator pagedIteratorOf(List items) { + Iterator it = items.iterator(); + + @SuppressWarnings("unchecked") + org.kohsuke.github.PagedIterator pit = mock(org.kohsuke.github.PagedIterator.class); + + when(pit.hasNext()).thenAnswer(inv -> it.hasNext()); + when(pit.next()).thenAnswer(inv -> it.next()); + + return pit; + } + + + + // helper for SCIMPagedSearchIterable + @SuppressWarnings("unchecked") + private static SCIMPagedSearchIterable mockPagedIterable(List items, int totalCount) { + SCIMPagedSearchIterable iterable = mock(SCIMPagedSearchIterable.class); + when(iterable.iterator()).thenReturn(pagedIteratorOf(items)); + when(iterable.getTotalCount()).thenReturn(totalCount); + return iterable; + } + + // ======================================================= + // createEMUGroup + // ======================================================= + @Test + void createEMUGroup_returnsUidWithName() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUGroup created = new SCIMEMUGroup(); + created.id = "g1"; + created.displayName = "devs"; + + when(enterprise.createSCIMEMUGroup(any())).thenReturn(created); + + Uid uid = client.createEMUGroup(mock(GitHubEMUSchema.class), new SCIMEMUGroup()); + + assertEquals("g1", uid.getUidValue()); + assertEquals("devs", uid.getNameHintValue()); + verify(enterprise).createSCIMEMUGroup(any(SCIMEMUGroup.class)); + } + + // ======================================================= + // patchEMUGroup + // ======================================================= + @Test + void patchEMUGroup_callsUpdate() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMPatchOperations ops = new SCIMPatchOperations(); + when(enterprise.updateSCIMEMUGroup(eq("g1"), eq(ops))).thenReturn(new SCIMEMUGroup()); + + client.patchEMUGroup(new Uid("g1"), ops); + + verify(enterprise).updateSCIMEMUGroup("g1", ops); + } + + // ======================================================= + // deleteEMUGroup + // ======================================================= + @Test + void deleteEMUGroup_callsDelete() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + client.deleteEMUGroup(new Uid("g1"), options); + + verify(enterprise).deleteSCIMGroup("g1"); + } + + // ======================================================= + // getEMUGroup(Uid) + // ======================================================= + @Test + void getEMUGroup_byUid_callsGet() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUGroup g = new SCIMEMUGroup(); g.id = "g1"; + when(enterprise.getSCIMEMUGroup("g1")).thenReturn(g); + + SCIMEMUGroup out = client.getEMUGroup(new Uid("g1"), options, Set.of()); + + assertSame(g, out); + verify(enterprise).getSCIMEMUGroup("g1"); + } + + // ======================================================= + // getEMUGroup(Name) + // ======================================================= + @Test + void getEMUGroup_byName_callsGetByDisplayName() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUGroup g = new SCIMEMUGroup(); g.displayName = "devs"; + when(enterprise.getSCIMEMUGroupByDisplayName("devs")).thenReturn(g); + + SCIMEMUGroup out = client.getEMUGroup(new Name("devs"), options, Set.of()); + + assertSame(g, out); + verify(enterprise).getSCIMEMUGroupByDisplayName("devs"); + } + + // ======================================================= + // getCopilotSeat(Uid) + // ======================================================= + @Test + void getCopilotSeat_byUid_callsGetByUid() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + when(enterprise.getCopilotSeatByUid("u1")).thenReturn(seat); + + GitHubCopilotSeat out = client.getCopilotSeat(new Uid("u1"), options, Set.of()); + + assertSame(seat, out); + verify(enterprise).getCopilotSeatByUid("u1"); + } + + // ======================================================= + // getCopilotSeat(Name) + // ======================================================= + @Test + void getCopilotSeat_byName_callsGetByDisplayName() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + when(enterprise.getCopilotSeatByDisplayName("Jane Doe")).thenReturn(seat); + + GitHubCopilotSeat out = client.getCopilotSeat(new Name("Jane Doe"), options, Set.of()); + + assertSame(seat, out); + verify(enterprise).getCopilotSeatByDisplayName("Jane Doe"); + } + + // ======================================================= + // getCopilotSeats + // ======================================================= + @Test + void getCopilotSeats_noOffset_iteratesAllUntilHandlerFalse_returnsTotalSeats() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + GitHubCopilotSeat s1 = new GitHubCopilotSeat(); + GitHubCopilotSeat s2 = new GitHubCopilotSeat(); + + GitHubCopilotSeatPagedSearchIterable iterable = + new TestGitHubCopilotSeatPagedSearchIterable<>(List.of(s1, s2), 50); + + when(enterprise.listAllSeats(10, 0)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(s1)).thenReturn(true); + when(handler.handle(s2)).thenReturn(false); + + int total = client.getCopilotSeats(handler, options, Set.of(), 10, 0); + + assertEquals(50, total); + verify(handler).handle(s1); + verify(handler).handle(s2); + } + + @Test + void getCopilotSeats_withOffset_stopsAtPageSize_returnsTotalSeats() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + GitHubCopilotSeat s1 = new GitHubCopilotSeat(); + GitHubCopilotSeat s2 = new GitHubCopilotSeat(); + GitHubCopilotSeat s3 = new GitHubCopilotSeat(); + + GitHubCopilotSeatPagedSearchIterable iterable = + new TestGitHubCopilotSeatPagedSearchIterable<>(List.of(s1, s2, s3), 123); + + when(enterprise.listAllSeats(2, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(any())).thenReturn(true); + + int total = client.getCopilotSeats(handler, options, Set.of(), 2, 1); + + assertEquals(123, total); + verify(handler, times(2)).handle(any()); + } +// +// private static GitHubCopilotSeatPagedSearchIterable mockSeatIterable(List items, int totalSeats) { +// @SuppressWarnings("unchecked") +// GitHubCopilotSeatPagedSearchIterable iterable = mock(GitHubCopilotSeatPagedSearchIterable.class); +// when(iterable.iterator()).thenReturn((PagedIterator) items.iterator()); +// when(iterable.getTotalSeats()).thenReturn(totalSeats); +// return iterable; +// } +// +// // ======================================================= +// // getEMUGroups +// // ======================================================= + @Test + void getEMUGroups_noOffset_iteratesAllUntilHandlerFalse_returnsTotal() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUGroup g1 = new SCIMEMUGroup(); g1.id = "1"; + SCIMEMUGroup g2 = new SCIMEMUGroup(); g2.id = "2"; + + SCIMPagedSearchIterable iterable = new TestSCIMPagedSearchIterable<>(List.of(g1, g2), 88); + when(enterprise.listSCIMGroups(10, 0)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(g1)).thenReturn(true); + when(handler.handle(g2)).thenReturn(false); + + int total = client.getEMUGroups(handler, options, Set.of(), 10, 0); + + assertEquals(88, total); + verify(handler).handle(g1); + verify(handler).handle(g2); + } + + @Test + void getEMUGroups_withOffset_stopsAtPageSize_returnsTotal() throws Exception { + setPrivateLong(client, "lastAuthenticated", 0L); + + SCIMEMUGroup g1 = new SCIMEMUGroup(); g1.id = "1"; + SCIMEMUGroup g2 = new SCIMEMUGroup(); g2.id = "2"; + SCIMEMUGroup g3 = new SCIMEMUGroup(); g3.id = "3"; + + SCIMPagedSearchIterable iterable = new TestSCIMPagedSearchIterable<>(List.of(g1, g2, g3), 200); + when(enterprise.listSCIMGroups(2, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(any())).thenReturn(true); + + int total = client.getEMUGroups(handler, options, Set.of(), 2, 1); + + assertEquals(200, total); + verify(handler, times(2)).handle(any()); + } + + // ======================================================= + // close() + // ======================================================= + @Test + void close_doesNothing() { + assertDoesNotThrow(() -> client.close()); + } +} diff --git a/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java new file mode 100644 index 0000000..6f03e04 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java @@ -0,0 +1,461 @@ +package jp.openstandia.connector.util; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.Attribute; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaDefinitionAttributeMapperTest { + + static class CreateDest { + final AtomicReference last = new AtomicReference<>(); + } + + static class UpdateDest { + final AtomicReference last = new AtomicReference<>(); + final AtomicReference> add = new AtomicReference<>(); + final AtomicReference> remove = new AtomicReference<>(); + } + + static class Source { + Object value; + Source(Object value) { this.value = value; } + } + + // ---------- isStringType() coverage ---------- + @Test + void isStringType_shouldReturnTrueForStringish_andFalseForNonStringish() { + var m1 = new SchemaDefinition.AttributeMapper<>( + "a", SchemaDefinition.Types.STRING, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + (Source s) -> null, null + ); + assertTrue(m1.isStringType()); + + var m2 = new SchemaDefinition.AttributeMapper<>( + "a", SchemaDefinition.Types.UUID, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + (Source s) -> null, null + ); + assertTrue(m2.isStringType()); + + var m3 = new SchemaDefinition.AttributeMapper<>( + "a", SchemaDefinition.Types.INTEGER, + (Integer v, CreateDest d) -> {}, (Integer v, UpdateDest d) -> {}, + (Source s) -> null, null + ); + assertFalse(m3.isStringType()); + } + + // ---------- apply(Attribute, C) coverage (single) ---------- + @Test + void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() { + // create == null -> early return + var noCreate = new SchemaDefinition.AttributeMapper<>( + "x", SchemaDefinition.Types.STRING, + null, (String v, UpdateDest d) -> {}, + (Source s) -> null, null + ); + noCreate.apply(AttributeBuilder.build("x", "v"), new CreateDest()); // should not throw + + // STRING-ish branch uses AttributeUtil.getAsStringValue + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "s", SchemaDefinition.Types.STRING, + (String v, CreateDest dest) -> dest.last.set(v), + (String v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + m.apply(AttributeBuilder.build("s", "abc"), d); + assertEquals("abc", d.last.get()); + } + + // INTEGER + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "i", SchemaDefinition.Types.INTEGER, + (Integer v, CreateDest dest) -> dest.last.set(v), + (Integer v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + m.apply(AttributeBuilder.build("i", 7), d); + assertEquals(7, d.last.get()); + } + +// // LONG +// { +// CreateDest d = new CreateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "l", SchemaDefinition.Types.LONG, +// (Long v, CreateDest dest) -> dest.last.set(v), +// (Long v, UpdateDest dest) -> {}, +// (Source s) -> null, null +// ); +// m.apply(AttributeBuilder.build("l", 9L), d); +// assertEquals(9L, d.last.get()); +// } +// +// // FLOAT +// { +// CreateDest d = new CreateDest(); +// var m = new SchemaDefinition.AttributeMapper( +// "f", SchemaDefinition.Types.FLOAT, +// (Float v, CreateDest dest) -> dest.last.set(v), +// (Float v, UpdateDest dest) -> {}, +// (Source s) -> null, null +// ); +// m.apply(AttributeBuilder.build("f", 1.25f), d); +// assertEquals(1.25f, (Float) d.last.get(), 0.0001); +// } +// +// // DOUBLE +// { +// CreateDest d = new CreateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "d", SchemaDefinition.Types.DOUBLE, +// (Double v, CreateDest dest) -> dest.last.set(v), +// (Double v, UpdateDest dest) -> {}, +// (Source s) -> null, null +// ); +// m.apply(AttributeBuilder.build("d", 2.5d), d); +// assertEquals(2.5d, (Double) d.last.get(), 0.0001); +// } + + // BOOLEAN + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "b", SchemaDefinition.Types.BOOLEAN, + (Boolean v, CreateDest dest) -> dest.last.set(v), + (Boolean v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + m.apply(AttributeBuilder.build("b", true), d); + assertEquals(true, d.last.get()); + } + + // BIG_DECIMAL + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "bd", SchemaDefinition.Types.BIG_DECIMAL, + (BigDecimal v, CreateDest dest) -> dest.last.set(v), + (BigDecimal v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + m.apply(AttributeBuilder.build("bd", new BigDecimal("12.34")), d); + assertEquals(new BigDecimal("12.34"), d.last.get()); + } + + // DATE_STRING branch (formats ZonedDateTime -> String) + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "ds", SchemaDefinition.Types.DATE_STRING, + (String v, CreateDest dest) -> dest.last.set(v), + (String v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + + ZonedDateTime zdt = ZonedDateTime.of(2026, 2, 10, 0, 0, 0, 0, ZoneId.systemDefault()); + m.apply(AttributeBuilder.build("ds", zdt), d); + assertEquals("2026-02-10", d.last.get()); // ISO_LOCAL_DATE default + } + + // DATETIME_STRING branch (formats ZonedDateTime -> String) + // Also covers the "custom datetimeFormat" path (note: implementation uses dateFormat in else) + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "dts", SchemaDefinition.Types.DATETIME_STRING, + (String v, CreateDest dest) -> dest.last.set(v), + (String v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + + // Cover default datetime formatting + ZonedDateTime zdt = ZonedDateTime.now(ZoneId.systemDefault()); + m.apply(AttributeBuilder.build("dts", zdt), d); + assertNotNull(d.last.get()); + + // Cover custom-formatter branch (datetimeFormat != null) + // (Implementation uses dateFormat field in else; set both to avoid null) + m.dateFormat(DateTimeFormatter.ISO_LOCAL_DATE); + m.datetimeFormat(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + d.last.set(null); + m.apply(AttributeBuilder.build("dts", zdt), d); + assertNotNull(d.last.get()); + } + + // GUARDED_STRING + { + CreateDest d = new CreateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "gs", SchemaDefinition.Types.GUARDED_STRING, + (GuardedString v, CreateDest dest) -> dest.last.set(v), + (GuardedString v, UpdateDest dest) -> {}, + (Source s) -> null, null + ); + GuardedString gs = new GuardedString("secret".toCharArray()); + m.apply(AttributeBuilder.build("gs", gs), d); + assertSame(gs, d.last.get()); + } + + + } + + // ---------- apply(Attribute, C) coverage (multiple) ---------- + + + // ---------- apply(AttributeDelta, U) coverage (single replace) ---------- + @Test + void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { + // replace == null -> early return + var noReplace = new SchemaDefinition.AttributeMapper<>( + "x", SchemaDefinition.Types.STRING, + (String v, CreateDest d) -> {}, + null, + (Source s) -> null, null + ); + noReplace.apply(AttributeDeltaBuilder.build("x", "v"), new UpdateDest()); // should not throw + + // String-ish + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "s", SchemaDefinition.Types.STRING, + (String v, CreateDest dest) -> {}, + (String v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + m.apply(AttributeDeltaBuilder.build("s", "new"), d); + assertEquals("new", d.last.get()); + } + + // Integer + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "i", SchemaDefinition.Types.INTEGER, + (Integer v, CreateDest dest) -> {}, + (Integer v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + m.apply(AttributeDeltaBuilder.build("i", 10), d); + assertEquals(10, d.last.get()); + } + +// // Long +// { +// UpdateDest d = new UpdateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "l", SchemaDefinition.Types.LONG, +// (Long v, CreateDest dest) -> {}, +// (Long v, UpdateDest dest) -> dest.last.set(v), +// (Source s) -> null, null +// ); +// m.apply(AttributeDeltaBuilder.build("l", 10L), d); +// assertEquals(10L, d.last.get()); +// } +// +// // Float +// { +// UpdateDest d = new UpdateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "f", SchemaDefinition.Types.FLOAT, +// (Float v, CreateDest dest) -> {}, +// (Float v, UpdateDest dest) -> dest.last.set(v), +// (Source s) -> null, null +// ); +// m.apply(AttributeDeltaBuilder.build("f", 3.0f), d); +// assertEquals(3.0f, (Float) d.last.get(), 0.0001); +// } +// +// // Double +// { +// UpdateDest d = new UpdateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "d", SchemaDefinition.Types.DOUBLE, +// (Double v, CreateDest dest) -> {}, +// (Double v, UpdateDest dest) -> dest.last.set(v), +// (Source s) -> null, null +// ); +// m.apply(AttributeDeltaBuilder.build("d", 3.5d), d); +// assertEquals(3.5d, (Double) d.last.get(), 0.0001); +// } + + // Boolean + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "b", SchemaDefinition.Types.BOOLEAN, + (Boolean v, CreateDest dest) -> {}, + (Boolean v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + m.apply(AttributeDeltaBuilder.build("b", true), d); + assertEquals(true, d.last.get()); + } + + // BigDecimal + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "bd", SchemaDefinition.Types.BIG_DECIMAL, + (BigDecimal v, CreateDest dest) -> {}, + (BigDecimal v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + m.apply(AttributeDeltaBuilder.build("bd", new BigDecimal("1.00")), d); + assertEquals(new BigDecimal("1.00"), d.last.get()); + } + + // DATE / DATETIME -> replace accepts ZonedDateTime directly + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "dt", SchemaDefinition.Types.DATE, + (ZonedDateTime v, CreateDest dest) -> {}, + (ZonedDateTime v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + ZonedDateTime zdt = ZonedDateTime.now(); + m.apply(AttributeDeltaBuilder.build("dt", zdt), d); + assertEquals(zdt, d.last.get()); + } + + // DATE_STRING -> formats to String + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "ds", SchemaDefinition.Types.DATE_STRING, + (String v, CreateDest dest) -> {}, + (String v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + ZonedDateTime zdt = ZonedDateTime.of(2026, 2, 10, 0, 0, 0, 0, ZoneId.systemDefault()); + m.apply(AttributeDeltaBuilder.build("ds", zdt), d); + assertEquals("2026-02-10", d.last.get()); + } + + // DATETIME_STRING -> formats to String + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "dts", SchemaDefinition.Types.DATETIME_STRING, + (String v, CreateDest dest) -> {}, + (String v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + ZonedDateTime zdt = ZonedDateTime.now(); + m.apply(AttributeDeltaBuilder.build("dts", zdt), d); + assertNotNull(d.last.get()); + } + + // GuardedString + { + UpdateDest d = new UpdateDest(); + var m = new SchemaDefinition.AttributeMapper<>( + "gs", SchemaDefinition.Types.GUARDED_STRING, + (GuardedString v, CreateDest dest) -> {}, + (GuardedString v, UpdateDest dest) -> dest.last.set(v), + (Source s) -> null, null + ); + GuardedString gs = new GuardedString("secret".toCharArray()); + m.apply(AttributeDeltaBuilder.build("gs", gs), d); + assertSame(gs, d.last.get()); + } + + // else branch +// { +// UpdateDest d = new UpdateDest(); +// var m = new SchemaDefinition.AttributeMapper<>( +// "x", SchemaDefinition.Types.JSON, +// (Object v, CreateDest dest) -> {}, +// (Object v, UpdateDest dest) -> dest.last.set(v), +// (Source s) -> null, null +// ); +// m.apply(AttributeDeltaBuilder.build("x", 999), d); +// assertEquals(999, d.last.get()); +// } + } + + // ---------- apply(R) coverage ---------- + @Test + void applyRead_shouldCoverNullRead_nullValue_singleAndMultiple_dateAndDatetime_andEmptyStream() { + // read == null -> returns null + var mNoRead = new SchemaDefinition.AttributeMapper<>( + "x", SchemaDefinition.Types.STRING, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + null, null + ); + assertNull(mNoRead.apply(new Source("v"))); + + // read returns null -> returns null + var mNullValue = new SchemaDefinition.AttributeMapper<>( + "x", SchemaDefinition.Types.STRING, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + (Source s) -> null, null + ); + assertNull(mNullValue.apply(new Source("v"))); + + // single DATE_STRING -> converts String to ZonedDateTime + { + Function read = s -> "2026-02-10"; + var m = new SchemaDefinition.AttributeMapper<>( + "date", "date", SchemaDefinition.Types.DATE_STRING, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + read, null + ); + + Attribute a = m.apply(new Source("ignored")); + assertNotNull(a); + assertEquals("date", a.getName()); + assertTrue(a.getValue().get(0) instanceof ZonedDateTime); + } + + // single DATETIME_STRING -> converts String to ZonedDateTime + { + Function read = s -> "2026-02-10T10:20:30-03:00"; + var m = new SchemaDefinition.AttributeMapper<>( + "dt", "dt", SchemaDefinition.Types.DATETIME_STRING, + (String v, CreateDest d) -> {}, (String v, UpdateDest d) -> {}, + read, null + ); + + Attribute a = m.apply(new Source("ignored")); + assertNotNull(a); + assertEquals("dt", a.getName()); + assertTrue(a.getValue().get(0) instanceof ZonedDateTime); + } + + // single "other" -> returns attribute with value + { + Function read = s -> 123; + var m = new SchemaDefinition.AttributeMapper<>( + "x", "x", SchemaDefinition.Types.INTEGER, + (Integer v, CreateDest d) -> {}, (Integer v, UpdateDest d) -> {}, + read, null + ); + + Attribute a = m.apply(new Source("ignored")); + assertNotNull(a); + assertEquals(123, a.getValue().get(0)); + } + + } +} + diff --git a/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java b/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java index f8df22a..9debfde 100644 --- a/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java +++ b/src/test/java/org/kohsuke/github/GHEnterpriseExtTest.java @@ -208,7 +208,7 @@ void testGetCopilotSeatByDisplayNameMultipleResultsReturnsNull() throws IOExcept doReturn(mockBuilder).when(enterprise).searchCopilotSeats(); - GitHubCopilotSeat result = enterprise.getCopilotSeatByDisplayName("user1"); + GitHubCopilotSeat result = enterprise.getCopilotSeatByDisplayName("user3"); assertNull(result); } @@ -320,7 +320,6 @@ void testGetSCIMGroupByDisplayNameSingleResult() throws IOException { @Test void testGetSCIMGroupByDisplayNameNoOrMultipleResultsReturnsNull() throws IOException { - // Cenário 1: lista vazia SCIMEMUGroupSearchBuilder mockBuilder = mock(SCIMEMUGroupSearchBuilder.class); SCIMPagedSearchIterable mockIterable = mock(SCIMPagedSearchIterable.class); when(mockIterable.toList()).thenReturn(Collections.emptyList()); @@ -331,7 +330,6 @@ void testGetSCIMGroupByDisplayNameNoOrMultipleResultsReturnsNull() throws IOExce SCIMEMUGroup result1 = enterprise.getSCIMEMUGroupByDisplayName("Unknown"); assertNull(result1); - // Cenário 2: múltiplos resultados SCIMEMUGroup g1 = new SCIMEMUGroup(); SCIMEMUGroup g2 = new SCIMEMUGroup(); when(mockIterable.toList()).thenReturn(Arrays.asList(g1, g2)); diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatHandlerTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatHandlerTest.java new file mode 100644 index 0000000..becfae9 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatHandlerTest.java @@ -0,0 +1,337 @@ +package org.kohsuke.github; + +import jp.openstandia.connector.github.GitHubCopilotSeatHandler; +import jp.openstandia.connector.github.GitHubEMUConfiguration; +import jp.openstandia.connector.github.GitHubEMUSchema; +import jp.openstandia.connector.util.QueryHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class GitHubCopilotSeatHandlerTest { + + private GitHubEMUConfiguration config; + private jp.openstandia.connector.github.GitHubClient client; + + @BeforeEach + void setup() { + config = mock(GitHubEMUConfiguration.class); + client = mock(jp.openstandia.connector.github.GitHubClient.class); + } + + @Test + void createSchemaShouldMapCreateUpdateAndReadPathsIncludingNullBranches() throws Exception { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + + GitHubCopilotSeat dest = new GitHubCopilotSeat(); + ensureNested(dest); + + Set attrs = Set.of( + AttributeBuilder.build(Name.NAME, "login1"), + AttributeBuilder.build("last_activity_editor", "vscode"), + AttributeBuilder.build("plan_type", "business"), + AttributeBuilder.build("assignee.type", "User"), + AttributeBuilder.build("assigning_team.slug", "team-a") + ); + + GitHubCopilotSeat created = sd.apply(attrs, dest); + assertNotNull(created); + + GitHubCopilotSeat dest2 = new GitHubCopilotSeat(); + ensureNested(dest2); + sd.apply(Set.of(AttributeBuilder.build("assigning_team.slug", (Object) null)), dest2); + + SCIMPatchOperations patch = mock(SCIMPatchOperations.class); + sd.applyDelta(Set.of( + AttributeDeltaBuilder.build(Name.NAME, "login2"), + AttributeDeltaBuilder.build("last_activity_editor", "idea"), + AttributeDeltaBuilder.build("plan_type", "enterprise"), + AttributeDeltaBuilder.build("assignee.type", "Organization"), + AttributeDeltaBuilder.build("assigning_team.slug", "team-b") + ), patch); + + verify(patch).replace(eq("displayName"), eq("login2")); + verify(patch).replace(eq("last_activity_editor"), eq("idea")); + verify(patch).replace(eq("plan_type"), eq("enterprise")); + verify(patch).replace(eq("assignee.type"), eq("Organization")); + verify(patch).replace(eq("assigning_team.slug"), eq("team-b")); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + ensureNested(seat); + + setNestedField(seat, "assignee", "id", "seat-id-1"); + setNestedField(seat, "assignee", "login", "login-read"); + setNestedField(seat, "assignee", "type", "User"); + + setField(seat, "created_at", "2025-01-01T00:00:00Z"); + setField(seat, "last_authenticated_at", null); + setField(seat, "updated_at", "2025-01-01T00:00:00Z"); + setField(seat, "last_activity_at", null); + + setField(seat, "pending_cancellation_date", "2025-10-24"); + + setField(seat, "last_activity_editor", "vscode"); + setField(seat, "plan_type", "business"); + setNestedField(seat, "assigning_team", "slug", "team-read"); + + Set attrToGet = Set.of( + "created_at", + "updated_at", + "pending_cancellation_date", + "last_authenticated_at", + "last_activity_at", + "last_activity_editor", + "plan_type", + "assignee.type", + "assigning_team.slug" + ); + + ConnectorObject co = sd.toConnectorObjectBuilder(seat, attrToGet, false).build(); + + assertEquals("seat-id-1", co.getUid().getUidValue()); + assertEquals("login-read", co.getName().getNameValue()); + + assertNotNull(co.getAttributeByName("created_at")); + assertNotNull(co.getAttributeByName("updated_at")); + assertNotNull(co.getAttributeByName("pending_cancellation_date")); + + assertNull(co.getAttributeByName("last_authenticated_at")); + assertNull(co.getAttributeByName("last_activity_at")); + + assertEquals("vscode", co.getAttributeByName("last_activity_editor").getValue().get(0)); + assertEquals("business", co.getAttributeByName("plan_type").getValue().get(0)); + assertEquals("User", co.getAttributeByName("assignee.type").getValue().get(0)); + assertEquals("team-read", co.getAttributeByName("assigning_team.slug").getValue().get(0)); + } + + @Test + void createUpdateDeltaDeleteShouldReturnDefaults() { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + assertNull(handler.create(Set.of(AttributeBuilder.build(Name.NAME, "x")))); + assertEquals(Set.of(), handler.updateDelta(new Uid("u"), Set.of(), null)); + handler.delete(new Uid("u"), null); + } + + @Test + void getByUidShouldReturn1WhenFoundElse0() throws Exception { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + Uid uid = new Uid("seat-id-1"); + + when(client.getCopilotSeat(eq(uid), any(), any())).thenReturn(null); + assertEquals(0, handler.getByUid(uid, rh, null, null, null, false, 0, 0)); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + ensureNested(seat); + setNestedField(seat, "assignee", "id", "seat-id-1"); + setNestedField(seat, "assignee", "login", "login1"); + + when(client.getCopilotSeat(eq(uid), any(), any())).thenReturn(seat); + assertEquals(1, handler.getByUid(uid, rh, null, Set.of(), null, false, 0, 0)); + + verify(rh).handle(any(ConnectorObject.class)); + } + + @Test + void getByNameShouldReturn1WhenFoundElse0() throws Exception { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + Name name = new Name("login1"); + + when(client.getCopilotSeat(eq(name), any(), any())).thenReturn(null); + assertEquals(0, handler.getByName(name, rh, null, Set.of(), null, false, 0, 0)); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + ensureNested(seat); + setNestedField(seat, "assignee", "id", "seat-id-1"); + setNestedField(seat, "assignee", "login", "login1"); + + when(client.getCopilotSeat(eq(name), any(), any())).thenReturn(seat); + assertEquals(1, handler.getByName(name, rh, null, Set.of(), null, false, 0, 0)); + + verify(rh).handle(any(ConnectorObject.class)); + } + + @Test + void getAllShouldDelegateToClientAndMapResults() throws Exception { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + when(client.getCopilotSeats(any(), any(), any(), anyInt(), anyInt())).thenAnswer(inv -> { + @SuppressWarnings("unchecked") + QueryHandler qh = (QueryHandler) inv.getArgument(0); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + ensureNested(seat); + setNestedField(seat, "assignee", "id", "seat-id-1"); + setNestedField(seat, "assignee", "login", "login1"); + + qh.handle(seat); + return 1; + }); + + int count = handler.getAll(rh, null, Set.of(), null, false, 10, 0); + + assertEquals(1, count); + verify(rh).handle(any(ConnectorObject.class)); + } + + @Test + void getByMembersShouldReturn0FromDefaultImplementation() { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + int count = handler.getByMembers( + AttributeBuilder.build("members.User.value", "x"), + mock(ResultsHandler.class), + null, Set.of(), null, false, 10, 0 + ); + + assertEquals(0, count); + } + + @Test + void queryShouldThrowUnsupportedOperationExceptionViaSuper() { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + assertThrows(UnsupportedOperationException.class, + () -> handler.query(null, mock(ResultsHandler.class), null)); + } + + @Test + void toConnectorObjectShouldDelegateToDefaultImplementation() throws Exception { + SchemaDefinition sd = + GitHubCopilotSeatHandler.createSchema(config, client) + .build(); + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler( + mock(GitHubEMUConfiguration.class), + client, + mock(GitHubEMUSchema.class), + sd + ); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + ensureNested(seat); + setNestedField(seat, "assignee", "id", "seat-id-1"); + setNestedField(seat, "assignee", "login", "login1"); + + ConnectorObject co = handler.toConnectorObject(handler.getSchemaDefinition(), seat, Set.of(), false); + + assertEquals("seat-id-1", co.getUid().getUidValue()); + assertEquals("login1", co.getName().getNameValue()); + } + + private static void ensureNested(GitHubCopilotSeat seat) throws Exception { + ensureFieldObject(seat, "assignee"); + ensureFieldObject(seat, "assigning_team"); + } + + private static void ensureFieldObject(Object obj, String fieldName) throws Exception { + Field f = findField(obj.getClass(), fieldName); + f.setAccessible(true); + Object current = f.get(obj); + if (current == null) { + Object nested = f.getType().getDeclaredConstructor().newInstance(); + f.set(obj, nested); + } + } + + private static void setNestedField(Object root, String nestedField, String innerField, Object value) throws Exception { + Field nf = findField(root.getClass(), nestedField); + nf.setAccessible(true); + Object nested = nf.get(root); + if (nested == null) { + nested = nf.getType().getDeclaredConstructor().newInstance(); + nf.set(root, nested); + } + Field inner = findField(nested.getClass(), innerField); + inner.setAccessible(true); + inner.set(nested, value); + } + + private static void setField(Object root, String field, Object value) throws Exception { + Field f = findField(root.getClass(), field); + f.setAccessible(true); + f.set(root, value); + } + + private static Field findField(Class type, String name) throws NoSuchFieldException { + Class c = type; + while (c != null) { + try { + return c.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + c = c.getSuperclass(); + } + } + throw new NoSuchFieldException(type.getName() + "#" + name); + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java index 5c8b086..7ca019d 100644 --- a/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatTest.java @@ -1,8 +1,16 @@ package org.kohsuke.github; import com.fasterxml.jackson.databind.ObjectMapper; +import jp.openstandia.connector.github.GitHubCopilotSeatHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; import org.junit.jupiter.api.Test; +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + import static org.junit.jupiter.api.Assertions.*; class GitHubCopilotSeatTest { @@ -43,4 +51,132 @@ void testJsonSerializationDeserialization() throws Exception { assertEquals("2025-01-01", restored.created_at); assertEquals("enterprise", restored.plan_type); } + @Test + void createSchemaBuildsExpectedSchemaInfoAndFetchFields() { + SchemaDefinition.Builder sb = GitHubCopilotSeatHandler.createSchema(null, null); + SchemaDefinition schema = sb.build(); + + assertEquals("GitHubCopilotSeat", schema.getType()); + + ObjectClassInfo oci = schema.getObjectClassInfo(); + + AttributeInfo uidInfo = findAttr(oci, Uid.NAME); + AttributeInfo nameInfo = findAttr(oci, Name.NAME); + + assertEquals("id", uidInfo.getNativeName()); + assertFalse(uidInfo.isCreateable()); + assertFalse(uidInfo.isUpdateable()); + assertTrue(uidInfo.isReadable()); + + assertEquals("displayName", nameInfo.getNativeName()); + assertTrue(nameInfo.isRequired()); + + assertEquals("id", schema.getFetchField(Uid.NAME)); + assertNotNull(schema.getFetchField(Name.NAME)); + + assertEquals("assigning_team.slug", schema.getFetchField("assigning_team.slug")); + assertEquals("assignee.type", schema.getFetchField("assignee.type")); + + AttributeInfo createdAt = findAttr(oci, "created_at"); + assertFalse(createdAt.isCreateable()); + assertFalse(createdAt.isUpdateable()); + + AttributeInfo updatedAt = findAttr(oci, "updated_at"); + assertFalse(updatedAt.isCreateable()); + assertFalse(updatedAt.isUpdateable()); + } + + @Test + void createSchemaReadMappersAreExecutableViaToConnectorObjectBuilder() { + SchemaDefinition schema = GitHubCopilotSeatHandler.createSchema(null, null).build(); + + GitHubCopilotSeat seat = new GitHubCopilotSeat(); + seat.assignee = new GitHubCopilotSeatAssignee(); + seat.assignee.id = "a-123"; + seat.assignee.login = "jdoe"; + seat.assignee.type = "User"; + + seat.assigning_team = new GitHubCopilotSeatAssigningTeam(); + seat.assigning_team.slug = "team-x"; + + seat.created_at = "2024-01-01T10:30:20+00:00"; + seat.last_authenticated_at = "2024-01-02T10:20:30+00:00"; + seat.updated_at = "2024-01-03T10:20:30+00:00"; + seat.last_activity_at = "2024-01-04T10:20:30+00:00"; + seat.pending_cancellation_date = "2024-01-05"; + + seat.last_activity_editor = "vim"; + seat.plan_type = "business"; + + Set attrsToGet = schema.getObjectClassInfo().getAttributeInfo().stream() + .map(AttributeInfo::getName) + .collect(Collectors.toSet()); + + ConnectorObject co = schema.toConnectorObjectBuilder(seat, attrsToGet, false).build(); + + assertEquals("a-123", co.getUid().getUidValue()); + assertEquals("jdoe", co.getName().getNameValue()); + + assertEquals("vim", AttributeUtil.getStringValue(co.getAttributeByName("last_activity_editor"))); + assertEquals("business", AttributeUtil.getStringValue(co.getAttributeByName("plan_type"))); + assertEquals("User", AttributeUtil.getStringValue(co.getAttributeByName("assignee.type"))); + assertEquals("team-x", AttributeUtil.getStringValue(co.getAttributeByName("assigning_team.slug"))); + + Object createdVal = AttributeUtil.getSingleValue(co.getAttributeByName("created_at")); + assertNotNull(createdVal); + assertTrue(createdVal instanceof ZonedDateTime); + + Object updatedVal = AttributeUtil.getSingleValue(co.getAttributeByName("updated_at")); + assertNotNull(updatedVal); + assertTrue(updatedVal instanceof ZonedDateTime); + } + + @Test + void createSchemaCreateMappersAreExecutableViaApply() { + SchemaDefinition schema = GitHubCopilotSeatHandler.createSchema(null, null).build(); + + GitHubCopilotSeat dest = new GitHubCopilotSeat(); + dest.assignee = new GitHubCopilotSeatAssignee(); + dest.assigning_team = new GitHubCopilotSeatAssigningTeam(); + + Set attrs = new HashSet<>(); + attrs.add(AttributeBuilder.build(Name.NAME, "new-login")); + attrs.add(AttributeBuilder.build("last_activity_editor", "nano")); + attrs.add(AttributeBuilder.build("plan_type", "enterprise")); + attrs.add(AttributeBuilder.build("assignee.type", "Bot")); + attrs.add(AttributeBuilder.build("assigning_team.slug", "team-z")); + + schema.apply(attrs, dest); + + assertEquals("new-login", dest.assignee.login); + assertEquals("nano", dest.last_activity_editor); + assertEquals("enterprise", dest.plan_type); + assertEquals("Bot", dest.assignee.type); + assertEquals("team-z", dest.assigning_team.slug); + } + + @Test + void createSchemaAssigningTeamSlugCreateMapperIgnoresNullSource() { + SchemaDefinition schema = GitHubCopilotSeatHandler.createSchema(null, null).build(); + + GitHubCopilotSeat dest = new GitHubCopilotSeat(); + dest.assignee = new GitHubCopilotSeatAssignee(); + dest.assigning_team = new GitHubCopilotSeatAssigningTeam(); + dest.assigning_team.slug = "existing"; + + Set attrs = Set.of( + AttributeBuilder.build("assigning_team.slug", (String) null) + ); + + schema.apply(attrs, dest); + + assertEquals("existing", dest.assigning_team.slug); + } + + private static AttributeInfo findAttr(ObjectClassInfo oci, String name) { + return oci.getAttributeInfo().stream() + .filter(a -> a.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new AssertionError("AttributeInfo not found: " + name)); + } } diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java index e14f2f4..72aeb39 100644 --- a/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatsSearchBuilderTest.java @@ -17,9 +17,13 @@ class GitHubCopilotSeatsSearchBuilderTest { @BeforeEach void setup() { mockGitHub = mock(GitHub.class); - mockEnterprise = mock(GHEnterpriseExt.class); - when(mockEnterprise.getLogin()).thenReturn("test-enterprise"); + mockEnterprise = spy(new GHEnterpriseExt()); mockEnterprise.login = "test-enterprise"; + + Requester mockRequester = mock(Requester.class, RETURNS_SELF); + mockEnterprise.root = mockGitHub; + + when(mockGitHub.createRequest()).thenReturn(mockRequester); } @Test diff --git a/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java index b696657..3f4495e 100644 --- a/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java +++ b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java @@ -1,20 +1,18 @@ package org.kohsuke.github; +import jp.openstandia.connector.github.*; import jp.openstandia.connector.github.GitHubClient; -import jp.openstandia.connector.github.GitHubEMUConfiguration; -import jp.openstandia.connector.github.GitHubEMUSchema; -import jp.openstandia.connector.github.GitHubEMUUserHandler; import jp.openstandia.connector.util.SchemaDefinition; import org.identityconnectors.framework.common.objects.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.OffsetDateTime; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class GitHubEMUUserHandlerTest { - private GitHubEMUConfiguration config; private jp.openstandia.connector.github.GitHubClient client; private GitHubEMUSchema schema; @@ -58,7 +56,7 @@ void testCreateUser() { } @Test - void testUpdateDelta_withChanges() { + void testUpdateDeltaWithChanges() { Uid uid = new Uid("abc"); Set deltas = new HashSet<>(); @@ -69,8 +67,7 @@ void testUpdateDelta_withChanges() { }); when(patchOps.hasAttributesChange()).thenReturn(true); - // Spy to check call - jp.openstandia.connector.github.GitHubClient spyClient = spy(client); + jp.openstandia.connector.github.GitHubClient spyClient = client; handler = new GitHubEMUUserHandler(config, spyClient, schema, schemaDefinition); SCIMPatchOperations dest = new SCIMPatchOperations(); @@ -87,13 +84,13 @@ void testUpdateDelta_withChanges() { } @Test - void testUpdateDelta_withoutChanges() { + void testUpdateDeltaWithoutChanges() { Uid uid = new Uid("abc"); Set deltas = new HashSet<>(); when(schemaDefinition.applyDelta(eq(deltas), any())).thenAnswer(inv -> null); - GitHubClient spyClient = spy(client); + GitHubClient spyClient = client; handler = new GitHubEMUUserHandler(config, spyClient, schema, schemaDefinition); handler.updateDelta(uid, deltas, new OperationOptionsBuilder().build()); @@ -112,14 +109,15 @@ void testDeleteUser() { } @Test - void testGetByUid_found() { + void testGetByUidFound() { Uid uid = new Uid("uid-1"); OperationOptions options = new OperationOptionsBuilder().build(); ResultsHandler handlerMock = mock(ResultsHandler.class); SCIMEMUUser user = new SCIMEMUUser(); + when(client.getEMUUser(eq(uid), eq(options), any())).thenReturn(user); - //when(schemaDefinition.toConnectorObject(any(), anyBoolean(), any())).thenReturn(mock(ConnectorObject.class)); + when(schemaDefinition.toConnectorObjectBuilder(user, Set.of(), false)).thenReturn(mock(ConnectorObjectBuilder.class)); int result = handler.getByUid(uid, handlerMock, options, Set.of(), Set.of(), false, 0, 0); @@ -149,7 +147,7 @@ void testGetByName_found() { SCIMEMUUser user = new SCIMEMUUser(); when(client.getEMUUser(eq(name), eq(options), any())).thenReturn(user); - //when(schemaDefinition.toConnectorObjectBuilder(any(), anyBoolean(), any())).thenReturn(mock(ConnectorObject.class)); + when(schemaDefinition.toConnectorObjectBuilder(user, Set.of(), false)).thenReturn(mock(ConnectorObjectBuilder.class)); int result = handler.getByName(name, handlerMock, options, Set.of(), Set.of(), false, 0, 0); @@ -181,4 +179,235 @@ void testGetAllUsers() { assertEquals(3, result); verify(client).getEMUUsers(any(), any(), any(), anyInt(), anyInt()); } + + @Test + void createSchemaShouldBuildSchemaAndMapAllAttributes() { + SchemaDefinition.Builder builder = + GitHubEMUUserHandler.createSchema(config, client); + + assertNotNull(builder); + + SchemaDefinition schema = + builder.build(); + + // ---------- CREATE mapping ---------- + SCIMEMUUser target = new SCIMEMUUser(); + + Set attrs = Set.of( + AttributeBuilder.build(Name.NAME, "jdoe"), + AttributeBuilder.build("externalId", "ext-1"), + AttributeBuilder.build("displayName", "John Doe"), + AttributeBuilder.build("name.formatted", "John Doe"), + AttributeBuilder.build("name.givenName", "John"), + AttributeBuilder.build("name.familyName", "Doe"), + AttributeBuilder.build("primaryEmail", "john@acme.com"), + AttributeBuilder.build("primaryRole", "developer"), + AttributeBuilder.buildEnabled(true) + ); + + SCIMEMUUser mapped = schema.apply(attrs, target); + + assertEquals("jdoe", mapped.userName); + assertEquals("ext-1", mapped.externalId); + assertEquals("John Doe", mapped.displayName); + assertTrue(mapped.active); + + assertNotNull(mapped.name); + assertEquals("John Doe", mapped.name.formatted); + assertEquals("John", mapped.name.givenName); + assertEquals("Doe", mapped.name.familyName); + + assertEquals(1, mapped.emails.size()); + assertEquals("john@acme.com", mapped.emails.get(0).value); + assertTrue(mapped.emails.get(0).primary); + + assertEquals(1, mapped.roles.size()); + assertEquals("developer", mapped.roles.get(0).value); + assertTrue(mapped.roles.get(0).primary); + + // ---------- READ mapping ---------- + SCIMEMUUser source = new SCIMEMUUser(); + source.id = "uuid-1"; + source.userName = "jdoe"; + source.externalId = "ext-1"; + source.active = true; + + SCIMName name = new SCIMName(); + name.formatted = "John Doe"; + name.givenName = "John"; + name.familyName = "Doe"; + source.name = name; + + SCIMEmail email = new SCIMEmail(); + email.value = "john@acme.com"; + email.primary = true; + source.emails = List.of(email); + + SCIMRole role = new SCIMRole(); + role.value = "developer"; + role.primary = true; + source.roles = List.of(role); + + SCIMMember group = new SCIMMember(); + group.ref = "/Groups/123"; + group.value = "123"; + source.groups = List.of(group); + + SCIMMeta meta = new SCIMMeta(); + meta.created = String.valueOf(OffsetDateTime.now()); + meta.lastModified = String.valueOf(OffsetDateTime.now()); + source.meta = meta; + + Set attrsToGetSet = new HashSet<>(Set.of()); + attrsToGetSet.add("primaryEmail"); + attrsToGetSet.add("primaryRole"); + attrsToGetSet.add("groups"); + attrsToGetSet.add("meta.created"); + attrsToGetSet.add("meta.lastModified"); + + ConnectorObject co = + handler.toConnectorObject( + schema, source, attrsToGetSet, false + ); + + assertEquals("uuid-1", co.getUid().getUidValue()); + assertEquals("jdoe", co.getName().getNameValue()); + + assertEquals("john@acme.com", + co.getAttributeByName("primaryEmail").getValue().get(0)); + + assertEquals("developer", + co.getAttributeByName("primaryRole").getValue().get(0)); + + assertNotNull(co.getAttributeByName("meta.created")); + assertNotNull(co.getAttributeByName("meta.lastModified")); + } + + @Test + void createSchemaShouldHandleNullEmailAndRoleOnUpdate() { + SchemaDefinition.Builder builder = + GitHubEMUUserHandler.createSchema( + config, + client + ); + + SchemaDefinition schema = + builder.build(); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + Set deltas = Set.of( + AttributeDeltaBuilder.build("primaryEmail", ""), + AttributeDeltaBuilder.build("primaryRole", "") + ); + + schema.applyDelta(deltas, dest); + + assertTrue(dest.hasAttributesChange()); + } + + @Test + void applyDeltaShouldCallReplace_forSimpleAttributes() { + SchemaDefinition schema = buildSchema(); + SCIMPatchOperations dest = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build(Name.NAME, "newUserName"), + + AttributeDeltaBuilder.build(OperationalAttributes.ENABLE_NAME, true), + + AttributeDeltaBuilder.build("externalId", "ext-123"), + + AttributeDeltaBuilder.build("displayName", "John Doe"), + + AttributeDeltaBuilder.build("name.formatted", "John Doe"), + AttributeDeltaBuilder.build("name.givenName", "John"), + AttributeDeltaBuilder.build("name.familyName", "Doe") + ); + + schema.applyDelta(deltas, dest); + + verify(dest).replace(eq("userName"), eq("newUserName")); + verify(dest).replace(eq("active"), eq(true)); + verify(dest).replace(eq("externalId"), eq("ext-123")); + verify(dest).replace(eq("displayName"), eq("John Doe")); + + verify(dest).replace(eq("name.formatted"), eq("John Doe")); + verify(dest).replace(eq("name.givenName"), eq("John")); + verify(dest).replace(eq("name.familyName"), eq("Doe")); + + verifyNoMoreInteractions(dest); + } + + private SchemaDefinition buildSchema() { + return GitHubEMUUserHandler.createSchema(config, client).build(); + } + + @Test + void applyDeltaShouldCallReplace_withNewPrimaryEmailObjectWhenPrimaryEmailIsNonNull() { + SchemaDefinition schema = buildSchema(); + SCIMPatchOperations dest = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build("primaryEmail", "john@acme.com") + ); + + schema.applyDelta(deltas, dest); + + verify(dest).replace(argThat((SCIMEmail e) -> + e != null + && "john@acme.com".equals(e.value) + && Boolean.TRUE.equals(e.primary) + )); + verifyNoMoreInteractions(dest); + } + + @Test + void applyDeltaShouldCallReplace_withNullEmailWhenPrimaryEmailIsNull() { + SchemaDefinition schema = buildSchema(); + SCIMPatchOperations dest = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build("primaryEmail", (Object) null) + ); + + schema.applyDelta(deltas, dest); + + verify(dest).replace((SCIMEmail) isNull()); + verifyNoMoreInteractions(dest); + } + + @Test + void applyDeltaShouldCallReplaceWithNewPrimaryRoleObjectWhenPrimaryRoleIsNonNull() { + SchemaDefinition schema = buildSchema(); + SCIMPatchOperations dest = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build("primaryRole", "developer") + ); + + schema.applyDelta(deltas, dest); + + verify(dest).replace(argThat((SCIMRole r) -> + r != null + && "developer".equals(r.value) + && Boolean.TRUE.equals(r.primary) + )); + verifyNoMoreInteractions(dest); + } + + @Test + void applyDeltaShouldCallReplaceWithNullRoleWhenPrimaryRoleIsNull() { + SchemaDefinition schema = buildSchema(); + SCIMPatchOperations dest = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build("primaryRole", (Object) null) + ); + + schema.applyDelta(deltas, dest); + + verify(dest).replace((SCIMRole) isNull()); + verifyNoMoreInteractions(dest); + } } diff --git a/src/test/java/org/kohsuke/github/ObjectHandlerTest.java b/src/test/java/org/kohsuke/github/ObjectHandlerTest.java new file mode 100644 index 0000000..f7ed353 --- /dev/null +++ b/src/test/java/org/kohsuke/github/ObjectHandlerTest.java @@ -0,0 +1,175 @@ +package org.kohsuke.github; + +import jp.openstandia.connector.github.GitHubFilter; +import jp.openstandia.connector.util.ObjectHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ObjectHandlerTest { + + static class Dummy { + String id; + String name; + } + + private static SchemaDefinition buildSchema() { + SchemaDefinition.Builder b = SchemaDefinition.newBuilder( + new ObjectClass("Dummy"), Dummy.class, Dummy.class, Dummy.class); + + b.addUid("id", SchemaDefinition.Types.STRING, + (val, dest) -> dest.id = val, + (src) -> src.id, + "id"); + + b.addName("name", SchemaDefinition.Types.STRING, + (val, dest) -> dest.name = val, + (src) -> src.name, + "name"); + + return b.build(); + } + + static class TestHandler implements ObjectHandler { + private final SchemaDefinition schema; + + TestHandler(SchemaDefinition schema) { + this.schema = schema; + } + + @Override + public ObjectHandler setInstanceName(String instanceName) { + return this; + } + + @Override + public Uid create(Set attributes) { + return new Uid("x"); + } + + @Override + public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { + return Collections.emptySet(); + } + + @Override + public void delete(Uid uid, OperationOptions options) { + // no-op + } + + @Override + public SchemaDefinition getSchemaDefinition() { + return schema; + } + } + + @Test + void getByUidThrowsUnsupportedOperationException() { + ObjectHandler h = new TestHandler(buildSchema()); + + assertThrows(UnsupportedOperationException.class, () -> + h.getByUid( + new Uid("1"), + obj -> true, + new OperationOptionsBuilder().build(), + Collections.emptySet(), + Collections.emptySet(), + false, + 10, + 0 + ) + ); + } + + @Test + void getByNameThrowsUnsupportedOperationException() { + ObjectHandler h = new TestHandler(buildSchema()); + + assertThrows(UnsupportedOperationException.class, () -> + h.getByName( + new Name("n"), + obj -> true, + new OperationOptionsBuilder().build(), + Collections.emptySet(), + Collections.emptySet(), + false, + 10, + 0 + ) + ); + } + + @Test + void getByMembersReturnsZero() { + ObjectHandler h = new TestHandler(buildSchema()); + + int out = h.getByMembers( + AttributeBuilder.build("members", "a"), + obj -> true, + new OperationOptionsBuilder().build(), + Collections.emptySet(), + Collections.emptySet(), + false, + 10, + 0 + ); + + assertEquals(0, out); + } + + @Test + void getAllThrowsUnsupportedOperationException() { + ObjectHandler h = new TestHandler(buildSchema()); + + assertThrows(UnsupportedOperationException.class, () -> + h.getAll( + obj -> true, + new OperationOptionsBuilder().build(), + Collections.emptySet(), + Collections.emptySet(), + false, + 10, + 0 + ) + ); + } + + @Test + void queryThrowsUnsupportedOperationException() { + ObjectHandler h = new TestHandler(buildSchema()); + + assertThrows(UnsupportedOperationException.class, () -> + h.query( + (GitHubFilter) null, + obj -> true, + new OperationOptionsBuilder().build() + ) + ); + } + + @Test + void toConnectorObjectBuildsConnectorObjectFromSchema() { + SchemaDefinition schema = buildSchema(); + ObjectHandler h = new TestHandler(schema); + + Dummy src = new Dummy(); + src.id = "id-1"; + src.name = "name-1"; + + Set returnAttrs = new HashSet<>(); + returnAttrs.add(Uid.NAME); + returnAttrs.add(Name.NAME); + + ConnectorObject co = h.toConnectorObject(schema, src, returnAttrs, false); + + assertNotNull(co); + assertEquals("id-1", co.getUid().getUidValue()); + assertEquals("name-1", co.getName().getNameValue()); + } +} diff --git a/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java b/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java index 05421ca..628b34f 100644 --- a/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java +++ b/src/test/java/org/kohsuke/github/SCIMEMUGroupTest.java @@ -1,14 +1,37 @@ package org.kohsuke.github; import com.fasterxml.jackson.databind.ObjectMapper; +import jp.openstandia.connector.github.GitHubEMUConfiguration; +import jp.openstandia.connector.github.GitHubEMUGroupHandler; +import jp.openstandia.connector.github.GitHubEMUSchema; +import jp.openstandia.connector.util.ObjectHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.OffsetDateTime; import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; class SCIMEMUGroupTest { + private GitHubEMUConfiguration config; + private jp.openstandia.connector.github.GitHubClient client; + private GitHubEMUSchema schema; + + @BeforeEach + void setup() { + config = mock(GitHubEMUConfiguration.class); + client = mock(jp.openstandia.connector.github.GitHubClient.class); + schema = mock(GitHubEMUSchema.class); + } + @Test void testFieldAssignmentsAndAccess() { SCIMEMUGroup group = new SCIMEMUGroup(); @@ -54,7 +77,6 @@ void testJacksonSerializationDeserialization() throws Exception { ObjectMapper mapper = new ObjectMapper(); - // Serialize String json = mapper.writeValueAsString(group); assertTrue(json.contains("\"id\":\"G1\"")); assertTrue(json.contains("\"displayName\":\"Developers\"")); @@ -62,7 +84,6 @@ void testJacksonSerializationDeserialization() throws Exception { assertTrue(json.contains("\"members\"")); assertTrue(json.contains("\"schemas\"")); - // Deserialize SCIMEMUGroup restored = mapper.readValue(json, SCIMEMUGroup.class); assertEquals("G1", restored.id); assertEquals("Developers", restored.displayName); @@ -72,4 +93,258 @@ void testJacksonSerializationDeserialization() throws Exception { assertNotNull(restored.meta); assertEquals("2025-01-01T00:00:00Z", restored.meta.created); } + + @Test + void createSchemaShouldMapCreateDeltaAndReadPaths() { + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + SCIMEMUGroup dest = new SCIMEMUGroup(); + + Set attrs = Set.of( + AttributeBuilder.build(Name.NAME, "Engineering"), + AttributeBuilder.build("externalId", "ext-001"), + AttributeBuilder.build("members.User.value", List.of("u1", "u2")) + ); + + SCIMEMUGroup mapped = sd.apply(attrs, dest); + + assertEquals("Engineering", mapped.displayName); + assertEquals("ext-001", mapped.externalId); + assertNotNull(mapped.members); + assertEquals(2, mapped.members.size()); + assertEquals("u1", mapped.members.get(0).value); + assertEquals("u2", mapped.members.get(1).value); + + SCIMPatchOperations patch = mock(SCIMPatchOperations.class); + + Set deltas = Set.of( + AttributeDeltaBuilder.build(Name.NAME, "Eng-Updated"), + AttributeDeltaBuilder.build("externalId", "ext-002"), + new AttributeDeltaBuilder() + .setName("members.User.value") + .addValueToAdd("u3") + .addValueToRemove("u2") + .build() + ); + + sd.applyDelta(deltas, patch); + + verify(patch).replace(eq("displayName"), eq("Eng-Updated")); + verify(patch).replace(eq("externalId"), eq("ext-002")); + verify(patch).addMembers(argThat(list -> list != null && list.contains("u3"))); + verify(patch).removeMembers(argThat(list -> list != null && list.contains("u2"))); + + SCIMEMUGroup src = new SCIMEMUGroup(); + src.id = "gid-1"; + src.displayName = "Engineering"; + src.externalId = "ext-001"; + + SCIMMember userMember = new SCIMMember(); + userMember.ref = "/Users/u1"; + userMember.value = "u1"; + + SCIMMember notUserMember = new SCIMMember(); + notUserMember.ref = "/Groups/g2"; + notUserMember.value = "g2"; + + src.members = List.of(userMember, notUserMember); + + SCIMMeta meta = new SCIMMeta(); + meta.created = OffsetDateTime.now().toString(); + meta.lastModified = OffsetDateTime.now().toString(); + src.meta = meta; + + ObjectHandler oh = new ObjectHandler() { + @Override public ObjectHandler setInstanceName(String instanceName) { return this; } + @Override public Uid create(Set attributes) { return null; } + @Override public Set updateDelta(Uid uid, Set modifications, OperationOptions options) { return null; } + @Override public void delete(Uid uid, OperationOptions options) {} + @Override public SchemaDefinition getSchemaDefinition() { return sd; } + }; + + Set attributesToGet = Set.of("members.User.value", "meta.created", "meta.lastModified"); + + ConnectorObject co = oh.toConnectorObject(sd, src, attributesToGet, false); + + assertEquals("gid-1", co.getUid().getUidValue()); + assertEquals("Engineering", co.getName().getNameValue()); + + Attribute membersAttr = co.getAttributeByName("members.User.value"); + assertNotNull(membersAttr); + assertEquals(List.of("u1"), membersAttr.getValue()); + + assertNotNull(co.getAttributeByName("meta.created")); + assertNotNull(co.getAttributeByName("meta.lastModified")); + } + + @Test + void createShouldCallClientCreateEMUGroupAndReturnUid() { + + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + Uid expected = new Uid("gid-1", new Name("Engineering")); + when(client.createEMUGroup(eq(schema), any(SCIMEMUGroup.class))).thenReturn(expected); + + Set attrs = Set.of( + AttributeBuilder.build(Name.NAME, "Engineering"), + AttributeBuilder.build("externalId", "ext-001") + ); + + Uid actual = handler.create(attrs); + + assertSame(expected, actual); + verify(client).createEMUGroup(eq(schema), any(SCIMEMUGroup.class)); + } + + @Test + void updateDeltaShouldPatchOnlyWhenHasChangesAndReturnNull() { + + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + Uid uid = new Uid("gid-1"); + + Set empty = Set.of(); + assertNull(handler.updateDelta(uid, empty, null)); + verify(client, never()).patchEMUGroup(any(), any()); + + Set mods = Set.of(AttributeDeltaBuilder.build(Name.NAME, "Eng-Updated")); + assertNull(handler.updateDelta(uid, mods, null)); + verify(client).patchEMUGroup(eq(uid), any(SCIMPatchOperations.class)); + } + + @Test + void deleteShouldCallClientDeleteEMUGroup() { + + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + Uid uid = new Uid("gid-1"); + OperationOptions options = new OperationOptionsBuilder().build(); + + handler.delete(uid, options); + + verify(client).deleteEMUGroup(eq(uid), eq(options)); + } + + @Test + void getByUid_shouldReturn1WhenFound_else0() { + + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + Uid uid = new Uid("gid-1"); + + when(client.getEMUGroup(eq(uid), any(), any())).thenReturn(null); + assertEquals(0, handler.getByUid(uid, rh, null, Set.of(), null, false, 0, 0)); + + SCIMEMUGroup g = new SCIMEMUGroup(); + g.id = "gid-1"; + g.displayName = "Engineering"; + when(client.getEMUGroup(eq(uid), any(), any())).thenReturn(g); + + assertEquals(1, handler.getByUid(uid, rh, null, Set.of(), null, false, 0, 0)); + verify(rh).handle(any(ConnectorObject.class)); + } + + @Test + void getByName_shouldReturn1WhenFound_else0() { + + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + Name name = new Name("Engineering"); + + when(client.getEMUGroup(eq(name), any(), any())).thenReturn(null); + assertEquals(0, handler.getByName(name, rh, null, Set.of(), null, false, 0, 0)); + + SCIMEMUGroup g = new SCIMEMUGroup(); + g.id = "gid-1"; + g.displayName = "Engineering"; + when(client.getEMUGroup(eq(name), any(), any())).thenReturn(g); + + assertEquals(1, handler.getByName(name, rh, null, Set.of(), null, false, 0, 0)); + verify(rh).handle(any(ConnectorObject.class)); + } + + + @Test + void getByMembersShouldFilterGroupsByMemberIdsCoverContainsTrueAndFalse() { + SchemaDefinition sd = + GitHubEMUGroupHandler.createSchema(config, client) + .build(); + + GitHubEMUGroupHandler handler = + new GitHubEMUGroupHandler(mock(GitHubEMUConfiguration.class), client, schema, sd); + + ResultsHandler rh = mock(ResultsHandler.class); + when(rh.handle(any())).thenReturn(true); + + Attribute membersFilter = AttributeBuilder.build("members.User.value", List.of("u1", "u2")); + + SCIMEMUGroup match = new SCIMEMUGroup(); + match.id = "g-match"; + match.displayName = "Match"; + match.members = List.of(member("u1"), member("u2")); + + SCIMEMUGroup noMatch = new SCIMEMUGroup(); + noMatch.id = "g-nomatch"; + noMatch.displayName = "NoMatch"; + noMatch.members = List.of(member("u1")); + + when(client.getEMUGroups(any(), any(), any(), anyInt(), anyInt())).thenAnswer(inv -> { + @SuppressWarnings("unchecked") + jp.openstandia.connector.util.QueryHandler qh = + (jp.openstandia.connector.util.QueryHandler) inv.getArgument(0); + + boolean cont1 = qh.handle(match); + assertTrue(cont1); + + boolean cont2 = qh.handle(noMatch); + assertTrue(cont2); + + return 2; + }); + + int count = handler.getByMembers(membersFilter, rh, null, Set.of(), null, false, 10, 0); + assertEquals(2, count); + + verify(rh, atLeastOnce()).handle(any(ConnectorObject.class)); + } + + private static SCIMMember member(String uid) { + SCIMMember m = new SCIMMember(); + m.ref = "/Users/" + uid; + m.value = uid; + return m; + } } diff --git a/src/test/java/org/kohsuke/github/SchemaDefinitionTest.java b/src/test/java/org/kohsuke/github/SchemaDefinitionTest.java new file mode 100644 index 0000000..50ef96b --- /dev/null +++ b/src/test/java/org/kohsuke/github/SchemaDefinitionTest.java @@ -0,0 +1,192 @@ +package org.kohsuke.github; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaDefinitionTest { + + static class Dummy { + String id; + String name; + String defaultAttr; + String notReturnedByDefault; + String notReadable; + String nullRead; + } + + private static SchemaDefinition buildSchema() { + SchemaDefinition.Builder b = SchemaDefinition.newBuilder( + new ObjectClass("Dummy"), Dummy.class, Dummy.class, Dummy.class); + + b.addUid("id", SchemaDefinition.Types.STRING, + (val, dest) -> dest.id = val, + (src) -> src.id, + "id"); + + b.addName("name", SchemaDefinition.Types.STRING, + (val, dest) -> dest.name = val, + (src) -> src.name, + "name", + AttributeInfo.Flags.REQUIRED); + + b.add("defaultAttr", SchemaDefinition.Types.STRING, + (val, dest) -> dest.defaultAttr = val, + (src) -> src.defaultAttr, + "defaultAttr"); + + b.add("notReturnedByDefault", SchemaDefinition.Types.STRING, + (val, dest) -> dest.notReturnedByDefault = val, + (src) -> src.notReturnedByDefault, + "notReturnedByDefault", + AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT); + + b.add("notReadable", SchemaDefinition.Types.STRING, + (val, dest) -> dest.notReadable = val, + (src) -> src.notReadable, + "notReadable", + AttributeInfo.Flags.NOT_READABLE, AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT); + + b.add("nullRead", SchemaDefinition.Types.STRING, + (val, dest) -> dest.nullRead = val, + (src) -> null, + "nullRead"); + + return b.build(); + } + + @Test + void getReturnedByDefaultAttributesSetAndIsReturnedByDefaultAttribute() { + SchemaDefinition schema = buildSchema(); + + Map returned = schema.getReturnedByDefaultAttributesSet(); + + assertTrue(returned.containsKey(Uid.NAME)); + assertTrue(returned.containsKey(Name.NAME)); + assertTrue(returned.containsKey("defaultAttr")); + assertTrue(returned.containsKey("nullRead")); + assertFalse(returned.containsKey("notReturnedByDefault")); + + assertTrue(schema.isReturnedByDefaultAttribute("defaultAttr")); + assertFalse(schema.isReturnedByDefaultAttribute("notReturnedByDefault")); + assertFalse(schema.isReturnedByDefaultAttribute("norReadable")); + } + + @Test + void isReadableAttributes() { + SchemaDefinition schema = buildSchema(); + + assertTrue(schema.isReadableAttributes("defaultAttr")); + assertFalse(schema.isReadableAttributes("notReadable")); + + assertTrue(schema.isReadableAttributes("unknown")); + } + + @Test + void applyAppliesAllAttributesAndThrowsOnInvalidAttribute() { + SchemaDefinition schema = buildSchema(); + + Dummy dest = new Dummy(); + Set attrs = new HashSet<>(); + attrs.add(AttributeBuilder.build(Uid.NAME, "u-1")); + attrs.add(AttributeBuilder.build(Name.NAME, "n-1")); + attrs.add(AttributeBuilder.build("defaultAttr", "v1")); + attrs.add(AttributeBuilder.build("notReturnedByDefault", "v2")); + attrs.add(AttributeBuilder.build("notReadable", "v3")); + + Dummy out = schema.apply(attrs, dest); + assertSame(dest, out); + + assertEquals("u-1", dest.id); + assertEquals("n-1", dest.name); + assertEquals("v1", dest.defaultAttr); + assertEquals("v2", dest.notReturnedByDefault); + assertEquals("v3", dest.notReadable); + + InvalidAttributeValueException ex = assertThrows( + InvalidAttributeValueException.class, + () -> schema.apply(Set.of(AttributeBuilder.build("doesNotExist", "x")), new Dummy())); + assertTrue(ex.getMessage().contains("Invalid attribute")); + } + + @Test + void applyDeltaReturnsChangedFlagAndThrowsOnInvalidDelta() { + SchemaDefinition schema = buildSchema(); + + assertFalse(schema.applyDelta(Collections.emptySet(), new Dummy())); + + Dummy dest = new Dummy(); + Set deltas = Set.of( + AttributeDeltaBuilder.build("defaultAttr", "dv") + ); + + assertTrue(schema.applyDelta(deltas, dest)); + assertEquals("dv", dest.defaultAttr); + + assertThrows(InvalidAttributeValueException.class, + () -> schema.applyDelta(Set.of(AttributeDeltaBuilder.build("bad", "x")), new Dummy())); + } + + @Test + void toConnectorObjectBuilderReturnsIncompleteAttributesWhenAllowed() { + SchemaDefinition schema = buildSchema(); + + Dummy src = new Dummy(); + src.id = "id-1"; + src.name = "name-1"; + src.defaultAttr = "d"; + src.notReturnedByDefault = "nr"; + src.notReadable = "x"; + src.nullRead = "ignored"; + + Set attrsToGet = new HashSet<>(Arrays.asList( + Uid.NAME, Name.NAME, + "defaultAttr", "notReturnedByDefault", "notReadable", "nullRead")); + + ConnectorObject co = schema.toConnectorObjectBuilder(src, attrsToGet, true).build(); + + assertEquals("id-1", co.getUid().getUidValue()); + assertEquals("name-1", co.getName().getNameValue()); + + assertEquals("d", AttributeUtil.getStringValue(co.getAttributeByName("defaultAttr"))); + + Attribute notReturned = co.getAttributeByName("notReturnedByDefault"); + assertNotNull(notReturned); + assertEquals(AttributeValueCompleteness.INCOMPLETE, notReturned.getAttributeValueCompleteness()); + + assertEquals("x", AttributeUtil.getStringValue(co.getAttributeByName("notReadable"))); + + assertNull(co.getAttributeByName("nullRead")); + } + + @Test + void toConnectorObjectBuilderReturnsActualValuesWhenPartialNotAllowed() { + SchemaDefinition schema = buildSchema(); + + Dummy src = new Dummy(); + src.id = "id-2"; + src.name = "name-2"; + src.defaultAttr = "d2"; + src.notReturnedByDefault = "nr2"; + + Set attrsToGet = new HashSet<>(Arrays.asList( + Uid.NAME, Name.NAME, + "defaultAttr", "notReturnedByDefault")); + + ConnectorObject co = schema.toConnectorObjectBuilder(src, attrsToGet, false).build(); + + assertEquals("id-2", co.getUid().getUidValue()); + assertEquals("name-2", co.getName().getNameValue()); + assertEquals("d2", AttributeUtil.getStringValue(co.getAttributeByName("defaultAttr"))); + + Attribute notReturned = co.getAttributeByName("notReturnedByDefault"); + assertNotNull(notReturned); + assertEquals("nr2", AttributeUtil.getStringValue(notReturned)); + assertEquals(AttributeValueCompleteness.COMPLETE, notReturned.getAttributeValueCompleteness()); + } +} diff --git a/src/test/java/org/kohsuke/github/TestGitHubCopilotSeatPagedSearchIterable.java b/src/test/java/org/kohsuke/github/TestGitHubCopilotSeatPagedSearchIterable.java new file mode 100644 index 0000000..0706c8a --- /dev/null +++ b/src/test/java/org/kohsuke/github/TestGitHubCopilotSeatPagedSearchIterable.java @@ -0,0 +1,43 @@ +package org.kohsuke.github; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class TestGitHubCopilotSeatPagedSearchIterable extends GitHubCopilotSeatPagedSearchIterable { + private final List items; + private final int totalSeats; + + public TestGitHubCopilotSeatPagedSearchIterable(List items, int totalSeats) { + super(null, null, null); + this.items = items; + this.totalSeats = totalSeats; + } + + @Override + public int getTotalSeats() { + return totalSeats; + } + + @Override + public PagedIterator _iterator(int pageSize) { + final Iterator pages = new Iterator<>() { + private int idx = 0; + + @Override + public boolean hasNext() { + return idx < items.size(); + } + + @Override + public T[] next() { + if (!hasNext()) throw new NoSuchElementException(); + @SuppressWarnings("unchecked") + T[] page = (T[]) new Object[] { items.get(idx++) }; + return page; + } + }; + + return new PagedIterator<>(pages, null); + } +} diff --git a/src/test/java/org/kohsuke/github/TestSCIMPagedSearchIterable.java b/src/test/java/org/kohsuke/github/TestSCIMPagedSearchIterable.java new file mode 100644 index 0000000..f2542e3 --- /dev/null +++ b/src/test/java/org/kohsuke/github/TestSCIMPagedSearchIterable.java @@ -0,0 +1,43 @@ +package org.kohsuke.github; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class TestSCIMPagedSearchIterable extends SCIMPagedSearchIterable { + private final List items; + private final int totalCount; + + public TestSCIMPagedSearchIterable(List items, int totalCount) { + super(null, null, null); + this.items = items; + this.totalCount = totalCount; + } + + @Override + public int getTotalCount() { + return totalCount; + } + + @Override + public PagedIterator _iterator(int pageSize) { + final Iterator pages = new Iterator<>() { + private int idx = 0; + + @Override + public boolean hasNext() { + return idx < items.size(); + } + + @Override + public T[] next() { + if (!hasNext()) throw new NoSuchElementException(); + @SuppressWarnings("unchecked") + T[] page = (T[]) new Object[] { items.get(idx++) }; + return page; + } + }; + + return new PagedIterator<>(pages, null); + } +} diff --git a/src/test/java/org/kohsuke/github/UtilsTest.java b/src/test/java/org/kohsuke/github/UtilsTest.java new file mode 100644 index 0000000..ee8b67d --- /dev/null +++ b/src/test/java/org/kohsuke/github/UtilsTest.java @@ -0,0 +1,208 @@ +package org.kohsuke.github; + +import jp.openstandia.connector.util.SchemaDefinition; +import jp.openstandia.connector.util.Utils; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.Test; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class UtilsTest { + + static class Dummy { + String id; + String name; + String defaultAttr; + String notReturnedByDefault; + } + + private static SchemaDefinition buildSchema() { + SchemaDefinition.Builder b = SchemaDefinition.newBuilder( + new ObjectClass("Dummy"), Dummy.class, Dummy.class, Dummy.class); + + b.addUid("id", SchemaDefinition.Types.STRING, + (val, dest) -> dest.id = val, + (src) -> src.id, + "id"); + + b.addName("name", SchemaDefinition.Types.STRING, + (val, dest) -> dest.name = val, + (src) -> src.name, + "name", + AttributeInfo.Flags.REQUIRED); + + b.add("defaultAttr", SchemaDefinition.Types.STRING, + (val, dest) -> dest.defaultAttr = val, + (src) -> src.defaultAttr, + "defaultAttr"); + + b.add("notReturnedByDefault", SchemaDefinition.Types.STRING, + (val, dest) -> dest.notReturnedByDefault = val, + (src) -> src.notReturnedByDefault, + "notReturnedByDefault", + AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT); + + return b.build(); + } + + @Test + void toZoneDateTimeAndVariantsCoverNullAndConversions() { + ZoneId sys = ZoneId.systemDefault(); + + assertNull(Utils.toZoneDateTime((String) null)); + ZonedDateTime z1 = Utils.toZoneDateTime("2024-01-02"); + assertEquals(LocalDate.parse("2024-01-02").atStartOfDay(sys), z1); + + assertNull(Utils.toZoneDateTime(DateTimeFormatter.ISO_INSTANT, null)); + ZonedDateTime z2 = Utils.toZoneDateTime(DateTimeFormatter.ISO_INSTANT, "2024-01-01T00:00:00Z"); + assertEquals(Instant.parse("2024-01-01T00:00:00Z"), z2.toInstant()); + + assertNull(Utils.toZoneDateTimeForEpochMilli(null)); + ZonedDateTime z3 = Utils.toZoneDateTimeForEpochMilli("0"); + assertEquals(Instant.EPOCH, z3.toInstant()); + + assertNull(Utils.toZoneDateTimeForISO8601OffsetDateTime(null)); + ZonedDateTime z4 = Utils.toZoneDateTimeForISO8601OffsetDateTime("2024-01-01T10:20:30+02:00"); + assertEquals(OffsetDateTime.parse("2024-01-01T10:20:30+02:00").toInstant(), z4.toInstant()); + + assertNull(Utils.toZoneDateTime((Date) null)); + Date d = Date.from(Instant.parse("2024-01-03T12:00:00Z")); + ZonedDateTime z5 = Utils.toZoneDateTime(d); + assertEquals(d.toInstant(), z5.toInstant()); + } + + @Test + void shouldReturnOverloadsCoverNullAndContainsBranches() { + assertTrue(Utils.shouldReturn(null, "a", true)); + assertFalse(Utils.shouldReturn(null, "a", false)); + + Set set = new HashSet<>(Arrays.asList("x", "y")); + assertTrue(Utils.shouldReturn(set, "x", false)); + assertFalse(Utils.shouldReturn(set, "z", true)); + + assertTrue(Utils.shouldReturn(set, "y")); + assertFalse(Utils.shouldReturn(set, "nope")); + } + + @Test + void createIncompleteAttributeSetsNameCompletenessAndEmptyListValue() { + Attribute a = Utils.createIncompleteAttribute("attr1"); + assertEquals("attr1", a.getName()); + assertEquals(AttributeValueCompleteness.INCOMPLETE, a.getAttributeValueCompleteness()); + + assertNotNull(a.getValue()); + assertEquals(0, a.getValue().size()); + assertTrue(((List) a.getValue()).isEmpty()); + } + + @Test + void shouldAllowPartialAndReturnDefaultAttributesCoverTrueFalseNull() { + OperationOptions allowTrue = new OperationOptionsBuilder() + .setAllowPartialAttributeValues(true) + .build(); + assertTrue(Utils.shouldAllowPartialAttributeValues(allowTrue)); + + OperationOptions allowFalse = new OperationOptionsBuilder() + .setAllowPartialAttributeValues(false) + .build(); + assertFalse(Utils.shouldAllowPartialAttributeValues(allowFalse)); + + OperationOptions allowNull = new OperationOptionsBuilder().build(); + assertFalse(Utils.shouldAllowPartialAttributeValues(allowNull)); + + OperationOptions defTrue = new OperationOptionsBuilder() + .setReturnDefaultAttributes(true) + .build(); + assertTrue(Utils.shouldReturnDefaultAttributes(defTrue)); + + OperationOptions defFalse = new OperationOptionsBuilder() + .setReturnDefaultAttributes(false) + .build(); + assertFalse(Utils.shouldReturnDefaultAttributes(defFalse)); + + OperationOptions defNull = new OperationOptionsBuilder().build(); + assertFalse(Utils.shouldReturnDefaultAttributes(defNull)); + } + + @Test + void createFullAttributesToGetWhenReturnDefaultTrueAddsDefaultsPlusKnownAttrsIgnoresUnknown() { + SchemaDefinition schema = buildSchema(); + + OperationOptions options = new OperationOptionsBuilder() + .setReturnDefaultAttributes(true) + .setAttributesToGet("notReturnedByDefault", "unknownAttr") + .build(); + + Map m = Utils.createFullAttributesToGet(schema, options); + + assertTrue(m.containsKey(Uid.NAME)); + assertTrue(m.containsKey(Name.NAME)); + assertTrue(m.containsKey("defaultAttr")); + + assertEquals("notReturnedByDefault", m.get("notReturnedByDefault")); + + assertFalse(m.containsKey("unknownAttr")); + } + + @Test + void createFullAttributesToGetWhenAttrsToGetNullAndReturnDefaultNullDefaultsToReturnedByDefault() { + SchemaDefinition schema = buildSchema(); + + OperationOptions options = new OperationOptionsBuilder().build(); + + Map m = Utils.createFullAttributesToGet(schema, options); + + assertTrue(m.containsKey(Uid.NAME)); + assertTrue(m.containsKey(Name.NAME)); + assertTrue(m.containsKey("defaultAttr")); + + assertFalse(m.containsKey("notReturnedByDefault")); + } + + @Test + void createFullAttributesToGetWhenReturnDefaultFalseOnlyUsesAttrsToGet() { + SchemaDefinition schema = buildSchema(); + + OperationOptions options = new OperationOptionsBuilder() + .setReturnDefaultAttributes(false) + .setAttributesToGet("defaultAttr") + .build(); + + Map m = Utils.createFullAttributesToGet(schema, options); + + assertFalse(m.containsKey(Uid.NAME)); + assertFalse(m.containsKey(Name.NAME)); + + assertEquals("defaultAttr", m.get("defaultAttr")); + } + + @Test + void resolvePageSizeAndOffsetCoverBothBranches() { + OperationOptions withValues = new OperationOptionsBuilder() + .setPageSize(25) + .setPagedResultsOffset(3) + .build(); + + assertEquals(25, Utils.resolvePageSize(withValues, 10)); + assertEquals(3, Utils.resolvePageOffset(withValues)); + + OperationOptions noValues = new OperationOptionsBuilder().build(); + assertEquals(10, Utils.resolvePageSize(noValues, 10)); + assertEquals(0, Utils.resolvePageOffset(noValues)); + } + + @Test + void handleEmptyAsNullAndHandleNullAsEmptyCoverAllBranches() { + assertNull(Utils.handleEmptyAsNull(null)); + assertNull(Utils.handleEmptyAsNull("")); + assertEquals("x", Utils.handleEmptyAsNull("x")); + + assertEquals("", Utils.handleNullAsEmpty(null)); + assertEquals("y", Utils.handleNullAsEmpty("y")); + } +} + diff --git a/src/test/java/util/SchemaDefinitionTest.java b/src/test/java/util/SchemaDefinitionTest.java index 9eb8554..12988d0 100644 --- a/src/test/java/util/SchemaDefinitionTest.java +++ b/src/test/java/util/SchemaDefinitionTest.java @@ -2,17 +2,12 @@ import jp.openstandia.connector.util.SchemaDefinition; import org.identityconnectors.framework.common.objects.*; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Function; @@ -27,25 +22,20 @@ void testNewBuilderOverload() { SchemaDefinition.Builder builder = SchemaDefinition.newBuilder(objectClass, String.class, Integer.class); - // Assert assertNotNull(builder); } @Test void testAddUidShouldCreateAndAddAttributeMapper() throws Exception { - // Arrange ObjectClass objectClass = new ObjectClass("testClass"); - // Cria o builder com tipos genéricos simples SchemaDefinition.Builder builder = new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); - // Lambdas dummy (só pra satisfazer os parâmetros) BiConsumer create = (value, obj) -> {}; BiConsumer update = (value, obj) -> {}; Function read = s -> "valor-" + s; - // Act builder.addUid( "uidField", SchemaDefinition.Types.STRING, @@ -56,8 +46,6 @@ void testAddUidShouldCreateAndAddAttributeMapper() throws Exception { AttributeInfo.Flags.REQUIRED ); - // Assert - // Acessa o campo privado 'attributes' Field field = builder.getClass().getDeclaredField("attributes"); field.setAccessible(true); @SuppressWarnings("unchecked") @@ -67,7 +55,6 @@ void testAddUidShouldCreateAndAddAttributeMapper() throws Exception { Object attr = attributes.get(0); assertNotNull(attr, "AttributeMapper não deveria ser nulo"); - // Verifica os campos internos via reflexão Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); connectorNameField.setAccessible(true); assertEquals("__UID__", connectorNameField.get(attr)); @@ -79,24 +66,18 @@ void testAddUidShouldCreateAndAddAttributeMapper() throws Exception { Field fetchField = attr.getClass().getDeclaredField("fetchField"); fetchField.setAccessible(true); assertEquals("fetchUid", fetchField.get(attr)); - - Method isReadableAttributes = builder.getClass().getDeclaredMethod("isReadableAttributes"); } @Test void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { - // Arrange ObjectClass objectClass = new ObjectClass("testClass"); - // Cria o builder com tipos genéricos simples SchemaDefinition.Builder builder = new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); - // Lambdas dummy BiConsumer createOrUpdate = (value, obj) -> {}; Function read = s -> "name-" + s; - // Act builder.addName( "displayName", SchemaDefinition.Types.STRING, @@ -106,8 +87,6 @@ void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { AttributeInfo.Flags.NOT_UPDATEABLE ); - // Assert - // Acessa o campo privado 'attributes' Field field = builder.getClass().getDeclaredField("attributes"); field.setAccessible(true); @SuppressWarnings("unchecked") @@ -117,17 +96,14 @@ void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { Object attr = attributes.get(0); assertNotNull(attr, "AttributeMapper não deveria ser nulo"); - // Verifica se o campo 'connectorName' é __NAME__ Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); connectorNameField.setAccessible(true); assertEquals(Name.NAME, connectorNameField.get(attr)); - // Verifica o nome do atributo passado Field nameField = attr.getClass().getDeclaredField("name"); nameField.setAccessible(true); assertEquals("displayName", nameField.get(attr)); - // Verifica o campo 'fetchField' Field fetchField = attr.getClass().getDeclaredField("fetchField"); fetchField.setAccessible(true); assertEquals("fetchName", fetchField.get(attr)); @@ -135,18 +111,15 @@ void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { @Test void testAddEnableShouldCreateAndAddAttributeMapper() throws Exception { - // Arrange ObjectClass objectClass = new ObjectClass("testClass"); SchemaDefinition.Builder builder = new SchemaDefinition.Builder<>(objectClass, String.class, String.class, String.class); - // Lambdas dummy BiConsumer create = (value, obj) -> {}; BiConsumer update = (value, obj) -> {}; Function read = s -> "enabled-" + s; - // Act builder.addEnable( "enabledFlag", SchemaDefinition.Types.STRING, @@ -158,8 +131,6 @@ void testAddEnableShouldCreateAndAddAttributeMapper() throws Exception { ); - // Assert - // Acessa o campo privado 'attributes' Field field = builder.getClass().getDeclaredField("attributes"); field.setAccessible(true); @SuppressWarnings("unchecked") @@ -169,17 +140,14 @@ void testAddEnableShouldCreateAndAddAttributeMapper() throws Exception { Object attr = attributes.get(0); assertNotNull(attr, "AttributeMapper não deveria ser nulo"); - // Verifica se o campo 'connectorName' é __ENABLE__ Field connectorNameField = attr.getClass().getDeclaredField("connectorName"); connectorNameField.setAccessible(true); assertEquals(OperationalAttributes.ENABLE_NAME, connectorNameField.get(attr)); - // Verifica o nome do atributo passado Field nameField = attr.getClass().getDeclaredField("name"); nameField.setAccessible(true); assertEquals("enabledFlag", nameField.get(attr)); - // Verifica o campo 'fetchField' Field fetchField = attr.getClass().getDeclaredField("fetchField"); fetchField.setAccessible(true); assertEquals("fetchEnable", fetchField.get(attr));