From 1d51136baddb4f97defdff19d0c49227ffa35c31 Mon Sep 17 00:00:00 2001 From: nmcorrea Date: Thu, 14 Aug 2025 14:35:56 -0300 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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)); From bbddb0fe42d06982070fb48c1b0654b492f81457 Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Thu, 26 Feb 2026 15:32:11 -0300 Subject: [PATCH 04/10] added tests for schema definition --- .../connector/util/SchemaDefinition.java | 6 +- .../SchemaDefinitionAttributeMapperTest.java | 4 - .../kohsuke/github/SCIMSearchBuilderTest.java | 58 ++++ src/test/java/util/SchemaDefinitionTest.java | 280 +++++++++++++++++- 4 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java diff --git a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java index 35dedd0..995e0e9 100644 --- a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java +++ b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java @@ -374,9 +374,9 @@ public static class Types { public static final Types JSON = new Types(String.class); public static final Types UUID = new Types(String.class); public static final Types INTEGER = new Types(Integer.class); - public static final Types LONG = new Types(Long.class); - public static final Types FLOAT = new Types(Float.class); - public static final Types DOUBLE = new Types(Double.class); + public static final Types LONG = new Types(Long.class); + public static final Types FLOAT = new Types(Float.class); + public static final Types DOUBLE = new Types(Double.class); public static final Types BOOLEAN = new Types(Boolean.class); public static final Types BIG_DECIMAL = new Types(BigDecimal.class); public static final Types DATE_STRING = new Types(ZonedDateTime.class); diff --git a/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java index 6f03e04..8cb8ee5 100644 --- a/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java +++ b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java @@ -217,10 +217,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() } - // ---------- apply(Attribute, C) coverage (multiple) ---------- - - - // ---------- apply(AttributeDelta, U) coverage (single replace) ---------- @Test void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { // replace == null -> early return diff --git a/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java new file mode 100644 index 0000000..1ec897e --- /dev/null +++ b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java @@ -0,0 +1,58 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class SCIMSearchBuilderTest { + + private GitHub gitHub; + private Requester requester; + private SCIMSearchBuilder builder; + + @BeforeEach + void setUp() throws Exception { + gitHub = mock(GitHub.class); + requester = mock(Requester.class); + GHOrganization org = mock(GHOrganization.class); + + when(gitHub.createRequest()).thenReturn(requester); + when(requester.withUrlPath(anyString())).thenReturn(requester); + when(requester.with(anyString(), Collections.singleton(anyString()))).thenReturn(requester); + + builder = new SCIMSearchBuilder( + gitHub, + org, + (Class>) (Class) SCIMSearchResult.class) { + + @Override + protected String getApiUrl() { + return "/scim/v2/Users"; + } + + }; + } + + @Test + void escape_shouldEscapeDoubleQuotes() throws Exception { + Method escapeMethod = SCIMSearchBuilder.class + .getDeclaredMethod("escape", String.class); + + escapeMethod.setAccessible(true); + + String input = "a\"b"; + String escaped = (String) escapeMethod.invoke(builder, input); + + assertEquals("a\\\"b", escaped); + } + +} \ No newline at end of file diff --git a/src/test/java/util/SchemaDefinitionTest.java b/src/test/java/util/SchemaDefinitionTest.java index 12988d0..3c18367 100644 --- a/src/test/java/util/SchemaDefinitionTest.java +++ b/src/test/java/util/SchemaDefinitionTest.java @@ -1,20 +1,29 @@ package util; 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 org.kohsuke.github.SCIMPatchOperations; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.List; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Function; +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.*; import static org.junit.jupiter.api.Assertions.*; class SchemaDefinitionTest { + private static final DateTimeFormatter DEFAULT_DATE_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE; + @Test void testNewBuilderOverload() { ObjectClass objectClass = new ObjectClass("TestClass"); @@ -109,6 +118,275 @@ void testAddNameShouldCreateAndAddAttributeMapper() throws Exception { assertEquals("fetchName", fetchField.get(attr)); } + static class Dummy { + public String id; + public Float attr; + public String displayName; + public Double attrDouble; + public Long attrLong; + public List attrList; + public List attrListDatetime; + public String attrDate; + public String attrDatetime; + } + + private String formatDate(ZonedDateTime zonedDateTime) { + if (zonedDateTime == null) { + return null; + } + return zonedDateTime.format(DEFAULT_DATE_FORMAT); + } + + private static SchemaDefinition getSchemaDefinition(Dummy dummy) { + ObjectClass objectClass = new ObjectClass("testClass"); + + SchemaDefinition.Builder builder = new SchemaDefinition.Builder<>(objectClass, Dummy.class, SCIMPatchOperations.class, Dummy.class); + + builder.addUid("dummyId", + SchemaDefinition.Types.UUID, + null, + (source) -> source.id, + "id", + NOT_CREATABLE, NOT_UPDATEABLE + ); + + builder.addName("displayName", + SchemaDefinition.Types.STRING_CASE_IGNORE, + (source, dest) -> dest.displayName = source, + (source, dest) -> dest.replace("displayName", source), + (source) -> source.displayName, + null, + REQUIRED + ); + + builder.add("attr", + SchemaDefinition.Types.FLOAT, + (value, obj) -> obj.attr = value, + (value, obj) -> obj.replace("attr", String.valueOf(value)), + (source) -> source.attr, + "attr" + ); + + builder.add("attrDouble", + SchemaDefinition.Types.DOUBLE, + (value, obj) -> obj.attrDouble = value, + (value, obj) -> obj.replace("attrDouble", String.valueOf(value)), + (source) -> source.attrDouble, + "attrDouble" + ); + + builder.add("attrLong", + SchemaDefinition.Types.LONG, + (value, obj) -> obj.attrLong = value, + (value, obj) -> obj.replace("attrLong", String.valueOf(value)), + (source) -> source.attrLong, + "attrLong" + ); + + builder.addAsMultiple( + "attrList", + SchemaDefinition.Types.DATE_STRING, + (source, obj) -> obj.attrList = source, + (add, obj) -> {}, + (remove, obj) -> {}, + (source) -> source.attrList != null ? source.attrList.stream() : null, + null + ); + + builder.addAsMultiple( + "attrListDatetime", + SchemaDefinition.Types.DATETIME_STRING, + (source, obj) -> obj.attrListDatetime = source, + (add, obj) -> {}, + (remove, obj) -> {}, + (source) -> source.attrListDatetime != null ? source.attrListDatetime.stream() : null, + null + ); + + builder.add("attrDate", + SchemaDefinition.Types.DATE_STRING, + (value, obj) -> obj.attrDate = value, + (value, obj) -> obj.replace("attrDate", String.valueOf(value)), + (source) -> source.attrDate, + "attrDate" + ); + + builder.add("attrDateTime", + SchemaDefinition.Types.DATETIME_STRING, + (value, obj) -> obj.attrDatetime = value, + (value, obj) -> obj.replace("attrDateTime", String.valueOf(value)), + (source) -> source.attrDatetime, + "attrDateTime" + ); + + return builder.build(); + } + + @Test + void testSchemaDefinitionApplyForSetOfAttributes() { + Dummy dummy = new Dummy(); + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + + Set attributeSet = new HashSet<>(); + attributeSet.add(AttributeBuilder.build("attr", 20.0f)); + attributeSet.add(AttributeBuilder.build("attrDouble", 20.0)); + attributeSet.add(AttributeBuilder.build("attrLong", 20L)); + attributeSet.add(AttributeBuilder.build("attrList", List.of(toZoneDateTime("2023-02-23")))); + attributeSet.add(AttributeBuilder.build("attrListDatetime", List.of(toZoneDateTime("2023-02-23")))); + + schemaDefinition.apply(attributeSet, dummy); + + assertEquals(20, dummy.attr, "Deveria conter o valor 20 no atributo dummy.attr"); + assertEquals(20.0, dummy.attrDouble); + assertEquals(20L, dummy.attrLong); + assertEquals(1, dummy.attrList.size()); + assertEquals(1, dummy.attrListDatetime.size()); + } + + @Test + void testSchemaDefinitionApplyForReadAttributesAndBuildConnectorObject() { + Dummy dummy = new Dummy(); + dummy.id = "dummyId"; + dummy.displayName = "displayName"; + dummy.attr = 20.0F; + dummy.attrDouble = 20.0; + dummy.attrLong = 20L; + dummy.attrListDatetime = List.of("2023-03-02T02:01:00+01:00"); + dummy.attrList = List.of("2026-08-09"); + + Set attrToGet = new HashSet<>(); + attrToGet.add("attr"); + attrToGet.add("attrDouble"); + attrToGet.add("attrLong"); + attrToGet.add("attrListDatetime"); + attrToGet.add("attrList"); + + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + + ConnectorObjectBuilder connectorObjectBuilder = schemaDefinition.toConnectorObjectBuilder(dummy, attrToGet, false); + ConnectorObject connectorObject = connectorObjectBuilder.build(); + + assertNotNull(connectorObjectBuilder); + assertNotNull(connectorObject); + } + + @Test + void testSchemaDefinitionApplyForReadAttributesTypeDateNull() { + Dummy dummy = new Dummy(); + dummy.id = "dummyId"; + dummy.displayName = "displayName"; + dummy.attrList = new ArrayList<>(); + + Set attrToGet = new HashSet<>(); + attrToGet.add("attrList"); + + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + + ConnectorObjectBuilder connectorObjectBuilder = schemaDefinition.toConnectorObjectBuilder(dummy, attrToGet, false); + ConnectorObject connectorObject = connectorObjectBuilder.build(); + + assertNotNull(connectorObjectBuilder); + assertNotNull(connectorObject); + } + + @Test + void testSchemaDefinitionApplyForSetOfAttributeDeltasWithValueToAdd() { + Dummy dummy = new Dummy(); + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + + Set deltas = new HashSet<>(); + + AttributeDeltaBuilder delta1 = new AttributeDeltaBuilder(); + delta1.setName("attrList"); + delta1.addValueToAdd(List.of(toZoneDateTime("2023-02-23"))); + deltas.add(delta1.build()); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + schemaDefinition.applyDelta(deltas, dest); + + } + + @Test + void testSchemaDefinitionApplyForSetOfAttributeDeltasWithValueToRemove() { + Dummy dummy = new Dummy(); + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + + Set deltas = new HashSet<>(); + + AttributeDeltaBuilder delta1 = new AttributeDeltaBuilder(); + delta1.setName("attrList"); + delta1.addValueToRemove(List.of(toZoneDateTime("2023-02-23"))); + deltas.add(delta1.build()); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + schemaDefinition.applyDelta(deltas, dest); + + } + + @Test + void testSchemaDefinitionApplyForDatetimeStringWithSetOfAttributeDeltasWithValueToAdd() { + Dummy dummy = new Dummy(); + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + Set deltas = new HashSet<>(); + + AttributeDeltaBuilder delta1 = new AttributeDeltaBuilder(); + delta1.setName("attrListDatetime"); + delta1.addValueToAdd(List.of(toZoneDateTime("2023-02-23"))); + deltas.add(delta1.build()); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + schemaDefinition.applyDelta(deltas, dest); + + } + + @Test + void testSchemaDefinitionApplyForDatetimeStringWithSetOfAttributeDeltasWithValueToRemove() { + Dummy dummy = new Dummy(); + + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + Set deltas = new HashSet<>(); + + AttributeDeltaBuilder delta1 = new AttributeDeltaBuilder(); + delta1.setName("attrListDatetime"); + delta1.addValueToRemove(List.of(toZoneDateTime("2023-02-23"))); + deltas.add(delta1.build()); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + schemaDefinition.applyDelta(deltas, dest); + + } + + @Test + void testSchemaDefinitionApplyForDoubleLongFloatWithSetOfAttributeDeltasWithValueToReplace() { + Dummy dummy = new Dummy(); + SchemaDefinition schemaDefinition = getSchemaDefinition(dummy); + Set deltas = new HashSet<>(); + + AttributeDeltaBuilder delta1 = new AttributeDeltaBuilder(); + delta1.setName("attrDouble"); + delta1.addValueToReplace(20.0); + deltas.add(delta1.build()); + + AttributeDeltaBuilder delta2 = new AttributeDeltaBuilder(); + delta2.setName("attr"); + delta2.addValueToReplace(20.0F); + deltas.add(delta2.build()); + + AttributeDeltaBuilder delta3 = new AttributeDeltaBuilder(); + delta3.setName("attrLong"); + delta3.addValueToReplace(20L); + deltas.add(delta3.build()); + + SCIMPatchOperations dest = new SCIMPatchOperations(); + + schemaDefinition.applyDelta(deltas, dest); + + } + @Test void testAddEnableShouldCreateAndAddAttributeMapper() throws Exception { ObjectClass objectClass = new ObjectClass("testClass"); From 935efbf0cf92030d0bdff74760333cde9a7ce674 Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Thu, 26 Feb 2026 16:29:30 -0300 Subject: [PATCH 05/10] added tests for schema definition --- .../SchemaDefinitionAttributeMapperTest.java | 127 +++--------------- src/test/java/util/SchemaDefinitionTest.java | 17 ++- 2 files changed, 26 insertions(+), 118 deletions(-) diff --git a/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java index 8cb8ee5..5284197 100644 --- a/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java +++ b/src/test/java/jp/openstandia/connector/util/SchemaDefinitionAttributeMapperTest.java @@ -1,6 +1,5 @@ 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; @@ -33,7 +32,6 @@ static class Source { Source(Object value) { this.value = value; } } - // ---------- isStringType() coverage ---------- @Test void isStringType_shouldReturnTrueForStringish_andFalseForNonStringish() { var m1 = new SchemaDefinition.AttributeMapper<>( @@ -58,7 +56,6 @@ void isStringType_shouldReturnTrueForStringish_andFalseForNonStringish() { assertFalse(m3.isStringType()); } - // ---------- apply(Attribute, C) coverage (single) ---------- @Test void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() { // create == null -> early return @@ -67,7 +64,7 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() null, (String v, UpdateDest d) -> {}, (Source s) -> null, null ); - noCreate.apply(AttributeBuilder.build("x", "v"), new CreateDest()); // should not throw + noCreate.apply(AttributeBuilder.build("x", "v"), new CreateDest()); // STRING-ish branch uses AttributeUtil.getAsStringValue { @@ -82,7 +79,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() assertEquals("abc", d.last.get()); } - // INTEGER { CreateDest d = new CreateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -95,46 +91,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() 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<>( @@ -147,7 +103,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() assertEquals(true, d.last.get()); } - // BIG_DECIMAL { CreateDest d = new CreateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -160,7 +115,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() assertEquals(new BigDecimal("12.34"), d.last.get()); } - // DATE_STRING branch (formats ZonedDateTime -> String) { CreateDest d = new CreateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -172,11 +126,9 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() 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 + assertEquals("2026-02-10", d.last.get()); } - // 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<>( @@ -186,13 +138,10 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() (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); @@ -200,7 +149,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() assertNotNull(d.last.get()); } - // GUARDED_STRING { CreateDest d = new CreateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -219,7 +167,6 @@ void applyAttribute_single_shouldCoverAllScalarTypeBranches_andNullCreateNoop() @Test void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { - // replace == null -> early return var noReplace = new SchemaDefinition.AttributeMapper<>( "x", SchemaDefinition.Types.STRING, (String v, CreateDest d) -> {}, @@ -228,7 +175,6 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { ); noReplace.apply(AttributeDeltaBuilder.build("x", "v"), new UpdateDest()); // should not throw - // String-ish { UpdateDest d = new UpdateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -254,46 +200,19 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { 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 + // 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()); + } + { UpdateDest d = new UpdateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -319,7 +238,6 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { assertEquals(new BigDecimal("1.00"), d.last.get()); } - // DATE / DATETIME -> replace accepts ZonedDateTime directly { UpdateDest d = new UpdateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -333,7 +251,6 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { assertEquals(zdt, d.last.get()); } - // DATE_STRING -> formats to String { UpdateDest d = new UpdateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -347,7 +264,6 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { assertEquals("2026-02-10", d.last.get()); } - // DATETIME_STRING -> formats to String { UpdateDest d = new UpdateDest(); var m = new SchemaDefinition.AttributeMapper<>( @@ -375,18 +291,7 @@ void applyDelta_single_shouldCoverReplaceBranches_andNullReplaceNoop() { 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 ---------- diff --git a/src/test/java/util/SchemaDefinitionTest.java b/src/test/java/util/SchemaDefinitionTest.java index 3c18367..3124310 100644 --- a/src/test/java/util/SchemaDefinitionTest.java +++ b/src/test/java/util/SchemaDefinitionTest.java @@ -128,13 +128,7 @@ static class Dummy { public List attrListDatetime; public String attrDate; public String attrDatetime; - } - - private String formatDate(ZonedDateTime zonedDateTime) { - if (zonedDateTime == null) { - return null; - } - return zonedDateTime.format(DEFAULT_DATE_FORMAT); + public String attrJson; } private static SchemaDefinition getSchemaDefinition(Dummy dummy) { @@ -219,6 +213,14 @@ private static SchemaDefinition getSchemaDefinition(Dummy dummy) { "attrDateTime" ); + builder.add("attrJson", + SchemaDefinition.Types.JSON, + (value, obj) -> obj.attrJson = value, + (value, obj) -> obj.replace("attrJson", String.valueOf(value)), + (source) -> source.attrJson, + "attrJson" + ); + return builder.build(); } @@ -233,6 +235,7 @@ void testSchemaDefinitionApplyForSetOfAttributes() { attributeSet.add(AttributeBuilder.build("attrLong", 20L)); attributeSet.add(AttributeBuilder.build("attrList", List.of(toZoneDateTime("2023-02-23")))); attributeSet.add(AttributeBuilder.build("attrListDatetime", List.of(toZoneDateTime("2023-02-23")))); + attributeSet.add(AttributeBuilder.build("attrJson", "{}")); schemaDefinition.apply(attributeSet, dummy); From ff1106484fbe23e56dfea1f1c558d45451bddfb2 Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Mon, 2 Mar 2026 14:19:34 -0300 Subject: [PATCH 06/10] addd more tests for build schema --- .../connector/github/GitHubClientTest.java | 5 -- .../github/SCIMPagedSearchIterableTest.java | 3 +- .../kohsuke/github/SCIMSearchBuilderTest.java | 51 ++----------------- 3 files changed, 7 insertions(+), 52 deletions(-) diff --git a/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java index d25ba07..ff92b05 100644 --- a/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java +++ b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java @@ -5,11 +5,6 @@ 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; diff --git a/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java b/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java index 8ad146e..5c5b42b 100644 --- a/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java +++ b/src/test/java/org/kohsuke/github/SCIMPagedSearchIterableTest.java @@ -74,7 +74,7 @@ public SCIMPagedSearchIterable list() { }; GHException thrown = assertThrows(GHException.class, builder::list); - assertTrue(thrown.getCause() instanceof MalformedURLException); + assertInstanceOf(MalformedURLException.class, thrown.getCause()); assertEquals("URL malformada!", thrown.getCause().getMessage()); } @@ -213,6 +213,7 @@ void shouldNotCallIteratorWhenResultIsNotNull() throws Exception { resultField.set(iterable, new SCIMSearchResult<>()); iterable.populate(); + iterable._iterator(2); verify(iterable, never()).iterator(); } } diff --git a/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java index 1ec897e..7bf1c3b 100644 --- a/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java +++ b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java @@ -2,57 +2,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; class SCIMSearchBuilderTest { - private GitHub gitHub; - private Requester requester; - private SCIMSearchBuilder builder; - - @BeforeEach - void setUp() throws Exception { - gitHub = mock(GitHub.class); - requester = mock(Requester.class); - GHOrganization org = mock(GHOrganization.class); - - when(gitHub.createRequest()).thenReturn(requester); - when(requester.withUrlPath(anyString())).thenReturn(requester); - when(requester.with(anyString(), Collections.singleton(anyString()))).thenReturn(requester); - - builder = new SCIMSearchBuilder( - gitHub, - org, - (Class>) (Class) SCIMSearchResult.class) { - - @Override - protected String getApiUrl() { - return "/scim/v2/Users"; - } - - }; - } - - @Test - void escape_shouldEscapeDoubleQuotes() throws Exception { - Method escapeMethod = SCIMSearchBuilder.class - .getDeclaredMethod("escape", String.class); - - escapeMethod.setAccessible(true); - - String input = "a\"b"; - String escaped = (String) escapeMethod.invoke(builder, input); - assertEquals("a\\\"b", escaped); - } } \ No newline at end of file From 17ddf9f7ff263015b13c49c4382790e3a6200eeb Mon Sep 17 00:00:00 2001 From: Jean M Santos Date: Tue, 3 Mar 2026 09:27:50 -0300 Subject: [PATCH 07/10] added tests for schema build and user handler --- .../github/AbstractGitHubSchema.java | 6 -- .../connector/util/SchemaDefinition.java | 7 +- .../github/GitHubEMUUserHandlerTest.java | 76 +++++++++++++++++++ 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/main/java/jp/openstandia/connector/github/AbstractGitHubSchema.java b/src/main/java/jp/openstandia/connector/github/AbstractGitHubSchema.java index e448653..df3db42 100644 --- a/src/main/java/jp/openstandia/connector/github/AbstractGitHubSchema.java +++ b/src/main/java/jp/openstandia/connector/github/AbstractGitHubSchema.java @@ -51,12 +51,6 @@ protected void buildSchema(SchemaBuilder builder, SchemaDefinition schemaDefinit this.schemaHandlerMap.put(schemaDefinition.getType(), handler); } - protected void buildSchema(SchemaBuilder builder, ObjectClassInfo objectClassInfo, Function callback) { - builder.defineObjectClass(objectClassInfo); - ObjectHandler handler = callback.apply(objectClassInfo); - this.schemaHandlerMap.put(objectClassInfo.getType(), handler); - } - public ObjectHandler getSchemaHandler(ObjectClass objectClass) { return schemaHandlerMap.get(objectClass.getObjectClassValue()); } diff --git a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java index 995e0e9..a3c3e0d 100644 --- a/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java +++ b/src/main/java/jp/openstandia/connector/util/SchemaDefinition.java @@ -578,12 +578,7 @@ public void apply(Attribute source, C dest) { BigDecimal value = AttributeUtil.getBigDecimalValue(source); create.accept((T) value, dest); - } else if (type == Types.DATE || type == Types.DATETIME) { - ZonedDateTime date = (ZonedDateTime) AttributeUtil.getSingleValue(source); - String formatted = formatDate(date); - create.accept((T) formatted, dest); - - } else if (type == Types.DATE_STRING) { + } else if (type == Types.DATE_STRING) { ZonedDateTime date = (ZonedDateTime) AttributeUtil.getSingleValue(source); String formatted = formatDate(date); create.accept((T) formatted, dest); diff --git a/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java index 3f4495e..0c00ef4 100644 --- a/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java +++ b/src/test/java/org/kohsuke/github/GitHubEMUUserHandlerTest.java @@ -230,6 +230,7 @@ void createSchemaShouldBuildSchemaAndMapAllAttributes() { source.id = "uuid-1"; source.userName = "jdoe"; source.externalId = "ext-1"; + source.displayName = "John Doe"; source.active = true; SCIMName name = new SCIMName(); @@ -264,6 +265,10 @@ void createSchemaShouldBuildSchemaAndMapAllAttributes() { attrsToGetSet.add("groups"); attrsToGetSet.add("meta.created"); attrsToGetSet.add("meta.lastModified"); + attrsToGetSet.add("displayName"); + attrsToGetSet.add(OperationalAttributes.ENABLE_NAME); + attrsToGetSet.add("externalId"); + attrsToGetSet.add("name.formatted"); ConnectorObject co = handler.toConnectorObject( @@ -272,17 +277,88 @@ void createSchemaShouldBuildSchemaAndMapAllAttributes() { assertEquals("uuid-1", co.getUid().getUidValue()); assertEquals("jdoe", co.getName().getNameValue()); + assertEquals("John Doe", co.getAttributeByName("displayName").getValue().get(0)); + assertEquals("John Doe", co.getAttributeByName("name.formatted").getValue().get(0)); + assertEquals("ext-1", co.getAttributeByName("externalId").getValue().get(0)); assertEquals("john@acme.com", co.getAttributeByName("primaryEmail").getValue().get(0)); assertEquals("developer", co.getAttributeByName("primaryRole").getValue().get(0)); + assertTrue((Boolean) co.getAttributeByName(OperationalAttributes.ENABLE_NAME).getValue().get(0)); assertNotNull(co.getAttributeByName("meta.created")); assertNotNull(co.getAttributeByName("meta.lastModified")); } + @Test + void createUserWithFormattedNameNotDefined() { + SchemaDefinition.Builder builder = + GitHubEMUUserHandler.createSchema(config, client); + + assertNotNull(builder); + + SchemaDefinition schema = + builder.build(); + + SCIMEMUUser source = new SCIMEMUUser(); + source.id = "uuid-1"; + source.userName = "jdoe"; + source.externalId = "ext-1"; + source.displayName = "John Doe"; + source.active = true; + + source.name = null; + + Set attrsToGetSet = new HashSet<>(Set.of()); + attrsToGetSet.add("name.formatted"); + attrsToGetSet.add("name.givenName"); + attrsToGetSet.add("name.familyName"); + + ConnectorObject co = + handler.toConnectorObject( + schema, source, attrsToGetSet, false + ); + assertNull(co.getAttributeByName("name.formatted")); + assertNull(co.getAttributeByName("name.givenName")); + assertNull(co.getAttributeByName("name.familyName")); + } + + + @Test + void createUserWithNameGivenNameAndFamilyNameNull() { + SchemaDefinition.Builder builder = + GitHubEMUUserHandler.createSchema(config, client); + + assertNotNull(builder); + + SchemaDefinition schema = + builder.build(); + + SCIMEMUUser source = new SCIMEMUUser(); + source.id = "uuid-1"; + source.userName = "jdoe"; + source.externalId = "ext-1"; + source.displayName = "John Doe"; + source.active = true; + + SCIMName name = new SCIMName(); + name.formatted = "John Doe"; + source.name = name; + + Set attrsToGetSet = new HashSet<>(Set.of()); + attrsToGetSet.add("name.formatted"); + attrsToGetSet.add("name.givenName"); + attrsToGetSet.add("name.familyName"); + + ConnectorObject co = + handler.toConnectorObject( + schema, source, attrsToGetSet, false + ); + assertEquals("John Doe", co.getAttributeByName("name.formatted").getValue().get(0)); + } + @Test void createSchemaShouldHandleNullEmailAndRoleOnUpdate() { SchemaDefinition.Builder builder = From 1e9054dcb189ac48e9ba6d31115a74d81043b20c Mon Sep 17 00:00:00 2001 From: "lucas.vicente" Date: Tue, 3 Mar 2026 09:39:26 -0300 Subject: [PATCH 08/10] added test to org.kohsuke.github --- .../GitHubCopilotSeatPageIteratorTest.java | 335 ++++++++++++++++++ ...HubCopilotSeatPagedSearchIterableTest.java | 164 +++++++++ .../github/rest/GitHubEMURESTClientTest.java | 76 ++++ .../github/rest/SCIMPageIteratorTest.java | 330 +++++++++++++++++ .../github/rest/SCIMSearchBuilderTest.java | 150 ++++++++ 5 files changed, 1055 insertions(+) create mode 100644 src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java new file mode 100644 index 0000000..35e902d --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java @@ -0,0 +1,335 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.NoSuchElementException; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import org.mockito.ArgumentCaptor; + +@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 shouldCallSendRequestAndCoverParserLambda() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.method("GET").build(); + + GitHubCopilotSeatsSearchResult mockBody = new GitHubCopilotSeatsSearchResult(); + + GitHubResponse.ResponseInfo mockInfo = mock(GitHubResponse.ResponseInfo.class); + when(mockInfo.request()).thenReturn(request); + when(mockInfo.statusCode()).thenReturn(200); + + GitHubResponse fakeResponse = + new GitHubResponse<>(mockInfo, mockBody); + + when(mockClient.sendRequest(eq(request), any())) + .thenReturn((GitHubResponse) fakeResponse); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + // triggera o fetch + o lambda do parser automaticamente + assertTrue(iterator.hasNext()); + + verify(mockClient).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\"" + ); + + 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"); + } + + @Test + void testCreateWithPageSizeZeroOrNegative() 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); + + GitHubCopilotSeatPageIterator iterator = + GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 0, 10); + + assertNotNull(iterator); + verify(mockBuilder, never()).with(anyString(), anyInt()); + } + + @Test + void fetch_shouldReturnEarlyWhenNextAlreadySet() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + setPrivateField(iterator, "next", new GitHubCopilotSeatsSearchResult()); + + Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + + verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); + } + + @Test + void fetch_shouldThrowGHExceptionOnIOException() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenThrow(new IOException("network error")); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + setPrivateField(iterator, "next", null); + setPrivateField(iterator, "nextRequest", request); + + GHException ex = assertThrows(GHException.class, () -> { + try { + Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + + assertTrue(ex.getMessage().contains("Failed to retrieve")); + } + + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java new file mode 100644 index 0000000..b514872 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java @@ -0,0 +1,164 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +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(); + 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(); + + 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(); + } + @Test + void _iterator_callsCreateAndAdaptWithCorrectParameters() { + GitHub mockRoot = mock(GitHub.class); + GitHubClient mockClient = mock(GitHubClient.class); + when(mockRoot.getClient()).thenReturn(mockClient); + + GitHubRequest mockRequest = mock(GitHubRequest.class); + + GitHubCopilotSeatPagedSearchIterable iterable = + new GitHubCopilotSeatPagedSearchIterable<>( + mockRoot, + mockRequest, + (Class>) (Class) GitHubCopilotSeatsSearchResult.class); + + try (MockedStatic mockedStatic = mockStatic(GitHubCopilotSeatPageIterator.class)) { + GitHubCopilotSeatPageIterator> mockPageIterator = mock(GitHubCopilotSeatPageIterator.class); + when(mockPageIterator.hasNext()).thenReturn(false); + + mockedStatic.when(() -> GitHubCopilotSeatPageIterator.create( + any(GitHubClient.class), + any(Class.class), + any(GitHubRequest.class), + anyInt(), + anyInt())) + .thenReturn(mockPageIterator); + + Iterator iterator = iterable.iterator(); + + assertNotNull(iterator); + verify(mockRoot).getClient(); + mockedStatic.verify(() -> GitHubCopilotSeatPageIterator.create( + any(GitHubClient.class), + any(Class.class), + any(GitHubRequest.class), + anyInt(), + anyInt())); + } + } +} + diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java index ae90c4e..af59862 100644 --- a/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java +++ b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java @@ -13,9 +13,15 @@ import org.kohsuke.github.*; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.MockedConstruction; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.doNothing; + import java.lang.reflect.Field; +import java.io.IOException; import java.util.*; +import java.util.function.Consumer; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; @@ -575,4 +581,74 @@ void getEMUGroups_withOffset_stopsAtPageSize_returnsTotal() throws Exception { void close_doesNothing() { assertDoesNotThrow(() -> client.close()); } + + @Test + void getEMUUsers_withOffset_handlerReturnsFalse_triggersBreak() 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), 100); + when(enterprise.listSCIMUsers(5, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(u1)).thenReturn(true); + when(handler.handle(u2)).thenReturn(false); // ← força o break + + int total = client.getEMUUsers(handler, options, Set.of(), 5, 1); + + assertEquals(100, total); + verify(handler).handle(u1); + verify(handler).handle(u2); + } + + @Test + void getCopilotSeats_withOffset_handlerReturnsFalse_triggersBreak() 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(3, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(s1)).thenReturn(true); + when(handler.handle(s2)).thenReturn(false); // ← força o break + + int total = client.getCopilotSeats(handler, options, Set.of(), 3, 1); + + assertEquals(50, total); + verify(handler).handle(s1); + verify(handler).handle(s2); + } + + @Test + void getEMUGroups_withOffset_handlerReturnsFalse_triggersBreak() 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(4, 1)).thenReturn(iterable); + + @SuppressWarnings("unchecked") + QueryHandler handler = mock(QueryHandler.class); + when(handler.handle(g1)).thenReturn(true); + when(handler.handle(g2)).thenReturn(false); // ← força o break + + int total = client.getEMUGroups(handler, options, Set.of(), 4, 1); + + assertEquals(88, total); + verify(handler).handle(g1); + verify(handler).handle(g2); + } } diff --git a/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java b/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java new file mode 100644 index 0000000..027c100 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java @@ -0,0 +1,330 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.NoSuchElementException; +import java.util.function.Function; +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)"; + } + } + + @Test + void create_withPageSizeZeroOrNegative_doesNotCallWith() 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); + + SCIMPageIterator iterator = + SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 0, 10); + + assertNotNull(iterator); + verify(mockBuilder, never()).with(anyString(), anyInt()); + } + + @Test + void fetch_returnsEarlyWhenNextAlreadySet() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); + + setPrivateField(iterator, "next", new SCIMSearchResult()); + + Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + + verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); + } + + @Test + void fetch_throwsGHExceptionOnIOException() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + doThrow(new IOException("network error")) + .when(mockClient) + .sendRequest(any(GitHubRequest.class), any()); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); + + setPrivateField(iterator, "next", null); + setPrivateField(iterator, "nextRequest", request); + + GHException ex = assertThrows(GHException.class, () -> { + try { + Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + + assertTrue(ex.getMessage().contains("Failed to retrieve")); + } + + @Test + void findNextURL_returnsNullWhenNoMorePages() throws MalformedURLException { + GitHubResponse response = mock(GitHubResponse.class); + SCIMSearchResult body = new SCIMSearchResult(); + body.startIndex = 100; + body.itemsPerPage = 50; + body.totalResults = 120; + + when(response.body()).thenReturn(body); + when(response.request()).thenReturn(mock(GitHubRequest.class)); + + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mock(GitHubClient.class), SCIMSearchResult.class, request); + + assertNull(iterator.findNextURL(response)); + } + + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java b/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java new file mode 100644 index 0000000..ebca70c --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java @@ -0,0 +1,150 @@ +package org.kohsuke.github; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.MalformedURLException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("unchecked") +class SCIMSearchBuilderTest { + + @Mock + private GitHub root; + + @Mock + private GHOrganization organization; + + @Mock + private GHEnterpriseExt enterprise; + + @Mock + private Requester requester; + + @Mock + private GitHubRequest gitHubRequest; + + @BeforeEach + void setUp() { + when(root.createRequest()).thenReturn(requester); + } + + @Test + void constructor_withOrganization_setsCorrectHeadersAndPath() { + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + verify(requester).withUrlPath("/scim/test"); + verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); + verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); + verify(requester).rateLimit(RateLimitTarget.SEARCH); + } + + @Test + void constructor_withEnterprise_setsCorrectHeadersAndPath() { + TestSCIMSearchBuilderEnterprise builder = new TestSCIMSearchBuilderEnterprise(root, enterprise); + + verify(requester).withUrlPath("/scim/test"); + verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); + verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); + verify(requester).rateLimit(RateLimitTarget.SEARCH); + } + + @Test + void list_emptyFilter_doesNotSetFilter() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + verify(requester, never()).set(eq("filter"), anyString()); + } + + @Test + void list_withOneFilter_setsFilterCorrectly() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("userName", "john.doe"); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + verify(requester).set("filter", "userName eq \"john.doe\""); + } + + @Test + void list_withMultipleFilters_joinsWithAnd() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("userName", "john.doe"); + builder.eq("active", "true"); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + + verify(requester).set(eq("filter"), argThat((String str) -> + str.contains("userName eq \"john.doe\"") && + str.contains("active eq \"true\"") && + str.contains(" and ") + )); + } + + @Test + void list_escapesDoubleQuotes() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("displayName", "O'Reilly \"quoted\""); + + builder.list(); + + verify(requester).set("filter", "displayName eq \"O'Reilly \\\"quoted\\\"\""); + } + + @Test + void list_whenBuildThrowsMalformedURLException_wrapsInGHException() throws MalformedURLException { + doThrow(new MalformedURLException("bad url")).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + GHException ex = assertThrows(GHException.class, builder::list); + assertInstanceOf(MalformedURLException.class, ex.getCause()); + } + + private static class TestSCIMSearchBuilderOrg extends SCIMSearchBuilder { + + @SuppressWarnings("unchecked") + TestSCIMSearchBuilderOrg(GitHub root, GHOrganization org) { + super(root, org, (Class>) (Class) SCIMSearchResult.class); + } + + @Override + protected String getApiUrl() { + return "/scim/test"; + } + } + + private static class TestSCIMSearchBuilderEnterprise extends SCIMSearchBuilder { + + @SuppressWarnings("unchecked") + TestSCIMSearchBuilderEnterprise(GitHub root, GHEnterpriseExt ent) { + super(root, ent, (Class>) (Class) SCIMSearchResult.class); + } + + @Override + protected String getApiUrl() { + return "/scim/test"; + } + } +} \ No newline at end of file From 0553168213a65e8cdeca4eaf2d1e6800e33033ed Mon Sep 17 00:00:00 2001 From: "lucas.vicente" Date: Tue, 3 Mar 2026 09:45:51 -0300 Subject: [PATCH 09/10] fixed tests paths --- .../GitHubCopilotSeatPageIteratorTest.java | 335 ------------------ ...HubCopilotSeatPagedSearchIterableTest.java | 164 --------- .../github/rest/SCIMPageIteratorTest.java | 330 ----------------- .../github/rest/SCIMSearchBuilderTest.java | 150 -------- .../GitHubCopilotSeatPageIteratorTest.java | 93 ++++- ...HubCopilotSeatPagedSearchIterableTest.java | 42 ++- .../kohsuke/github/SCIMPageIteratorTest.java | 94 +++++ .../kohsuke/github/SCIMSearchBuilderTest.java | 137 ++++++- 8 files changed, 346 insertions(+), 999 deletions(-) delete mode 100644 src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java delete mode 100644 src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java deleted file mode 100644 index 35e902d..0000000 --- a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPageIteratorTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package org.kohsuke.github; - -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.util.NoSuchElementException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; -import org.mockito.ArgumentCaptor; - -@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 shouldCallSendRequestAndCoverParserLambda() throws Exception { - GitHubClient mockClient = mock(GitHubClient.class); - GitHubRequest request = fakeRequest.method("GET").build(); - - GitHubCopilotSeatsSearchResult mockBody = new GitHubCopilotSeatsSearchResult(); - - GitHubResponse.ResponseInfo mockInfo = mock(GitHubResponse.ResponseInfo.class); - when(mockInfo.request()).thenReturn(request); - when(mockInfo.statusCode()).thenReturn(200); - - GitHubResponse fakeResponse = - new GitHubResponse<>(mockInfo, mockBody); - - when(mockClient.sendRequest(eq(request), any())) - .thenReturn((GitHubResponse) fakeResponse); - - GitHubCopilotSeatPageIterator iterator = - new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); - - // triggera o fetch + o lambda do parser automaticamente - assertTrue(iterator.hasNext()); - - verify(mockClient).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\"" - ); - - 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"); - } - - @Test - void testCreateWithPageSizeZeroOrNegative() 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); - - GitHubCopilotSeatPageIterator iterator = - GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 0, 10); - - assertNotNull(iterator); - verify(mockBuilder, never()).with(anyString(), anyInt()); - } - - @Test - void fetch_shouldReturnEarlyWhenNextAlreadySet() throws Exception { - GitHubClient mockClient = mock(GitHubClient.class); - GitHubRequest request = fakeRequest.build(); - - GitHubCopilotSeatPageIterator iterator = - new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); - - setPrivateField(iterator, "next", new GitHubCopilotSeatsSearchResult()); - - Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); - fetchMethod.setAccessible(true); - fetchMethod.invoke(iterator); - - verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); - } - - @Test - void fetch_shouldThrowGHExceptionOnIOException() throws Exception { - GitHubClient mockClient = mock(GitHubClient.class); - GitHubRequest request = fakeRequest.build(); - - when(mockClient.sendRequest(any(GitHubRequest.class), any())) - .thenThrow(new IOException("network error")); - - GitHubCopilotSeatPageIterator iterator = - new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); - - setPrivateField(iterator, "next", null); - setPrivateField(iterator, "nextRequest", request); - - GHException ex = assertThrows(GHException.class, () -> { - try { - Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); - fetchMethod.setAccessible(true); - fetchMethod.invoke(iterator); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - }); - - assertTrue(ex.getMessage().contains("Failed to retrieve")); - } - - private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java deleted file mode 100644 index b514872..0000000 --- a/src/test/java/jp/openstandia/connector/github/rest/GitHubCopilotSeatPagedSearchIterableTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.kohsuke.github; - -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; - -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(); - 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(); - - 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(); - } - @Test - void _iterator_callsCreateAndAdaptWithCorrectParameters() { - GitHub mockRoot = mock(GitHub.class); - GitHubClient mockClient = mock(GitHubClient.class); - when(mockRoot.getClient()).thenReturn(mockClient); - - GitHubRequest mockRequest = mock(GitHubRequest.class); - - GitHubCopilotSeatPagedSearchIterable iterable = - new GitHubCopilotSeatPagedSearchIterable<>( - mockRoot, - mockRequest, - (Class>) (Class) GitHubCopilotSeatsSearchResult.class); - - try (MockedStatic mockedStatic = mockStatic(GitHubCopilotSeatPageIterator.class)) { - GitHubCopilotSeatPageIterator> mockPageIterator = mock(GitHubCopilotSeatPageIterator.class); - when(mockPageIterator.hasNext()).thenReturn(false); - - mockedStatic.when(() -> GitHubCopilotSeatPageIterator.create( - any(GitHubClient.class), - any(Class.class), - any(GitHubRequest.class), - anyInt(), - anyInt())) - .thenReturn(mockPageIterator); - - Iterator iterator = iterable.iterator(); - - assertNotNull(iterator); - verify(mockRoot).getClient(); - mockedStatic.verify(() -> GitHubCopilotSeatPageIterator.create( - any(GitHubClient.class), - any(Class.class), - any(GitHubRequest.class), - anyInt(), - anyInt())); - } - } -} - diff --git a/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java b/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java deleted file mode 100644 index 027c100..0000000 --- a/src/test/java/jp/openstandia/connector/github/rest/SCIMPageIteratorTest.java +++ /dev/null @@ -1,330 +0,0 @@ -package org.kohsuke.github; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.util.NoSuchElementException; -import java.util.function.Function; -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)"; - } - } - - @Test - void create_withPageSizeZeroOrNegative_doesNotCallWith() 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); - - SCIMPageIterator iterator = - SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 0, 10); - - assertNotNull(iterator); - verify(mockBuilder, never()).with(anyString(), anyInt()); - } - - @Test - void fetch_returnsEarlyWhenNextAlreadySet() throws Exception { - GitHubClient mockClient = mock(GitHubClient.class); - GitHubRequest request = mock(GitHubRequest.class); - when(request.method()).thenReturn("GET"); - - SCIMPageIterator iterator = - new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); - - setPrivateField(iterator, "next", new SCIMSearchResult()); - - Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); - fetchMethod.setAccessible(true); - fetchMethod.invoke(iterator); - - verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); - } - - @Test - void fetch_throwsGHExceptionOnIOException() throws Exception { - GitHubClient mockClient = mock(GitHubClient.class); - GitHubRequest request = mock(GitHubRequest.class); - when(request.method()).thenReturn("GET"); - - doThrow(new IOException("network error")) - .when(mockClient) - .sendRequest(any(GitHubRequest.class), any()); - - SCIMPageIterator iterator = - new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); - - setPrivateField(iterator, "next", null); - setPrivateField(iterator, "nextRequest", request); - - GHException ex = assertThrows(GHException.class, () -> { - try { - Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); - fetchMethod.setAccessible(true); - fetchMethod.invoke(iterator); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - }); - - assertTrue(ex.getMessage().contains("Failed to retrieve")); - } - - @Test - void findNextURL_returnsNullWhenNoMorePages() throws MalformedURLException { - GitHubResponse response = mock(GitHubResponse.class); - SCIMSearchResult body = new SCIMSearchResult(); - body.startIndex = 100; - body.itemsPerPage = 50; - body.totalResults = 120; - - when(response.body()).thenReturn(body); - when(response.request()).thenReturn(mock(GitHubRequest.class)); - - GitHubRequest request = mock(GitHubRequest.class); - when(request.method()).thenReturn("GET"); - - SCIMPageIterator iterator = - new SCIMPageIterator<>(mock(GitHubClient.class), SCIMSearchResult.class, request); - - assertNull(iterator.findNextURL(response)); - } - - private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } -} diff --git a/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java b/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java deleted file mode 100644 index ebca70c..0000000 --- a/src/test/java/jp/openstandia/connector/github/rest/SCIMSearchBuilderTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.kohsuke.github; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.net.MalformedURLException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@SuppressWarnings("unchecked") -class SCIMSearchBuilderTest { - - @Mock - private GitHub root; - - @Mock - private GHOrganization organization; - - @Mock - private GHEnterpriseExt enterprise; - - @Mock - private Requester requester; - - @Mock - private GitHubRequest gitHubRequest; - - @BeforeEach - void setUp() { - when(root.createRequest()).thenReturn(requester); - } - - @Test - void constructor_withOrganization_setsCorrectHeadersAndPath() { - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - - verify(requester).withUrlPath("/scim/test"); - verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); - verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); - verify(requester).rateLimit(RateLimitTarget.SEARCH); - } - - @Test - void constructor_withEnterprise_setsCorrectHeadersAndPath() { - TestSCIMSearchBuilderEnterprise builder = new TestSCIMSearchBuilderEnterprise(root, enterprise); - - verify(requester).withUrlPath("/scim/test"); - verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); - verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); - verify(requester).rateLimit(RateLimitTarget.SEARCH); - } - - @Test - void list_emptyFilter_doesNotSetFilter() throws MalformedURLException { - doReturn(gitHubRequest).when(requester).build(); - - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - - SCIMPagedSearchIterable iterable = builder.list(); - - assertNotNull(iterable); - verify(requester, never()).set(eq("filter"), anyString()); - } - - @Test - void list_withOneFilter_setsFilterCorrectly() throws MalformedURLException { - doReturn(gitHubRequest).when(requester).build(); - - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - builder.eq("userName", "john.doe"); - - SCIMPagedSearchIterable iterable = builder.list(); - - assertNotNull(iterable); - verify(requester).set("filter", "userName eq \"john.doe\""); - } - - @Test - void list_withMultipleFilters_joinsWithAnd() throws MalformedURLException { - doReturn(gitHubRequest).when(requester).build(); - - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - builder.eq("userName", "john.doe"); - builder.eq("active", "true"); - - SCIMPagedSearchIterable iterable = builder.list(); - - assertNotNull(iterable); - - verify(requester).set(eq("filter"), argThat((String str) -> - str.contains("userName eq \"john.doe\"") && - str.contains("active eq \"true\"") && - str.contains(" and ") - )); - } - - @Test - void list_escapesDoubleQuotes() throws MalformedURLException { - doReturn(gitHubRequest).when(requester).build(); - - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - builder.eq("displayName", "O'Reilly \"quoted\""); - - builder.list(); - - verify(requester).set("filter", "displayName eq \"O'Reilly \\\"quoted\\\"\""); - } - - @Test - void list_whenBuildThrowsMalformedURLException_wrapsInGHException() throws MalformedURLException { - doThrow(new MalformedURLException("bad url")).when(requester).build(); - - TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); - - GHException ex = assertThrows(GHException.class, builder::list); - assertInstanceOf(MalformedURLException.class, ex.getCause()); - } - - private static class TestSCIMSearchBuilderOrg extends SCIMSearchBuilder { - - @SuppressWarnings("unchecked") - TestSCIMSearchBuilderOrg(GitHub root, GHOrganization org) { - super(root, org, (Class>) (Class) SCIMSearchResult.class); - } - - @Override - protected String getApiUrl() { - return "/scim/test"; - } - } - - private static class TestSCIMSearchBuilderEnterprise extends SCIMSearchBuilder { - - @SuppressWarnings("unchecked") - TestSCIMSearchBuilderEnterprise(GitHub root, GHEnterpriseExt ent) { - super(root, ent, (Class>) (Class) SCIMSearchResult.class); - } - - @Override - protected String getApiUrl() { - return "/scim/test"; - } - } -} \ No newline at end of file diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java index 0c06429..35e902d 100644 --- a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPageIteratorTest.java @@ -4,13 +4,16 @@ import java.io.IOException; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.util.NoSuchElementException; +import java.util.function.Function; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import org.mockito.ArgumentCaptor; @SuppressWarnings({"unchecked", "rawtypes"}) class GitHubCopilotSeatPageIteratorTest { @@ -163,37 +166,29 @@ void shouldReturnFinalResponseWhenNoNextPage() throws IOException, NoSuchFieldEx } @Test - void shouldCallSendRequestInsideFetch() throws Exception { + void shouldCallSendRequestAndCoverParserLambda() throws Exception { GitHubClient mockClient = mock(GitHubClient.class); GitHubRequest request = fakeRequest.method("GET").build(); + GitHubCopilotSeatsSearchResult mockBody = new GitHubCopilotSeatsSearchResult(); + 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())) + when(mockClient.sendRequest(eq(request), 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); + // triggera o fetch + o lambda do parser automaticamente + assertTrue(iterator.hasNext()); - fetchMethod.invoke(iterator); - verify(mockClient, times(1)).sendRequest(eq(request), any()); + verify(mockClient).sendRequest(eq(request), any()); } @Test @@ -258,7 +253,7 @@ void shouldReturnNullWhenLinkHeaderIsNull() throws MalformedURLException { void shouldReturnNullWhenLinkHeaderDoesNotContainNextRel() throws MalformedURLException { GitHubResponse mockResponse = mock(GitHubResponse.class); when(mockResponse.headerField("Link")).thenReturn( - "; rel=\"last\"" // sem "next" + "; rel=\"last\"" ); GitHubRequest request = fakeRequest.method("GET").build(); @@ -271,4 +266,70 @@ void shouldReturnNullWhenLinkHeaderDoesNotContainNextRel() throws MalformedURLEx GitHubRequest result = iterator.findNextURL(mockResponse); assertNull(result, "Deveria retornar null quando não há rel=\"next\" no header Link"); } + + @Test + void testCreateWithPageSizeZeroOrNegative() 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); + + GitHubCopilotSeatPageIterator iterator = + GitHubCopilotSeatPageIterator.create(mockClient, GitHubCopilotSeatsSearchResult.class, mockRequest, 0, 10); + + assertNotNull(iterator); + verify(mockBuilder, never()).with(anyString(), anyInt()); + } + + @Test + void fetch_shouldReturnEarlyWhenNextAlreadySet() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + setPrivateField(iterator, "next", new GitHubCopilotSeatsSearchResult()); + + Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + + verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); + } + + @Test + void fetch_shouldThrowGHExceptionOnIOException() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = fakeRequest.build(); + + when(mockClient.sendRequest(any(GitHubRequest.class), any())) + .thenThrow(new IOException("network error")); + + GitHubCopilotSeatPageIterator iterator = + new GitHubCopilotSeatPageIterator<>(mockClient, GitHubCopilotSeatsSearchResult.class, request); + + setPrivateField(iterator, "next", null); + setPrivateField(iterator, "nextRequest", request); + + GHException ex = assertThrows(GHException.class, () -> { + try { + Method fetchMethod = GitHubCopilotSeatPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + + assertTrue(ex.getMessage().contains("Failed to retrieve")); + } + + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } } diff --git a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java index 8493bc5..b514872 100644 --- a/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java +++ b/src/test/java/org/kohsuke/github/GitHubCopilotSeatPagedSearchIterableTest.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.net.MalformedURLException; @@ -95,8 +96,8 @@ void shouldCallIteratorWhenResultIsNull() throws MalformedURLException, NoSuchFi iterable.populate(); // Assert - verify(iterable, times(1)).iterator(); // Verifica que chamou iterator() - verify(mockIterator, times(1)).hasNext(); // Verifica que tentou iterar + verify(iterable, times(1)).iterator(); + verify(mockIterator, times(1)).hasNext(); } @Test @@ -121,6 +122,43 @@ void shouldNotCallIteratorWhenResultIsNotNull() throws Exception { iterable.populate(); verify(iterable, never()).iterator(); } + @Test + void _iterator_callsCreateAndAdaptWithCorrectParameters() { + GitHub mockRoot = mock(GitHub.class); + GitHubClient mockClient = mock(GitHubClient.class); + when(mockRoot.getClient()).thenReturn(mockClient); + + GitHubRequest mockRequest = mock(GitHubRequest.class); + GitHubCopilotSeatPagedSearchIterable iterable = + new GitHubCopilotSeatPagedSearchIterable<>( + mockRoot, + mockRequest, + (Class>) (Class) GitHubCopilotSeatsSearchResult.class); + + try (MockedStatic mockedStatic = mockStatic(GitHubCopilotSeatPageIterator.class)) { + GitHubCopilotSeatPageIterator> mockPageIterator = mock(GitHubCopilotSeatPageIterator.class); + when(mockPageIterator.hasNext()).thenReturn(false); + + mockedStatic.when(() -> GitHubCopilotSeatPageIterator.create( + any(GitHubClient.class), + any(Class.class), + any(GitHubRequest.class), + anyInt(), + anyInt())) + .thenReturn(mockPageIterator); + + Iterator iterator = iterable.iterator(); + + assertNotNull(iterator); + verify(mockRoot).getClient(); + mockedStatic.verify(() -> GitHubCopilotSeatPageIterator.create( + any(GitHubClient.class), + any(Class.class), + any(GitHubRequest.class), + anyInt(), + anyInt())); + } + } } diff --git a/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java b/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java index af57c76..027c100 100644 --- a/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java +++ b/src/test/java/org/kohsuke/github/SCIMPageIteratorTest.java @@ -3,10 +3,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.MalformedURLException; import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -233,4 +238,93 @@ public String toString() { : "Returns finalResponse (iteration complete)"; } } + + @Test + void create_withPageSizeZeroOrNegative_doesNotCallWith() 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); + + SCIMPageIterator iterator = + SCIMPageIterator.create(mockClient, SCIMSearchResult.class, mockRequest, 0, 10); + + assertNotNull(iterator); + verify(mockBuilder, never()).with(anyString(), anyInt()); + } + + @Test + void fetch_returnsEarlyWhenNextAlreadySet() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); + + setPrivateField(iterator, "next", new SCIMSearchResult()); + + Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + + verify(mockClient, never()).sendRequest(any(GitHubRequest.class), any()); + } + + @Test + void fetch_throwsGHExceptionOnIOException() throws Exception { + GitHubClient mockClient = mock(GitHubClient.class); + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + doThrow(new IOException("network error")) + .when(mockClient) + .sendRequest(any(GitHubRequest.class), any()); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mockClient, SCIMSearchResult.class, request); + + setPrivateField(iterator, "next", null); + setPrivateField(iterator, "nextRequest", request); + + GHException ex = assertThrows(GHException.class, () -> { + try { + Method fetchMethod = SCIMPageIterator.class.getDeclaredMethod("fetch"); + fetchMethod.setAccessible(true); + fetchMethod.invoke(iterator); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + + assertTrue(ex.getMessage().contains("Failed to retrieve")); + } + + @Test + void findNextURL_returnsNullWhenNoMorePages() throws MalformedURLException { + GitHubResponse response = mock(GitHubResponse.class); + SCIMSearchResult body = new SCIMSearchResult(); + body.startIndex = 100; + body.itemsPerPage = 50; + body.totalResults = 120; + + when(response.body()).thenReturn(body); + when(response.request()).thenReturn(mock(GitHubRequest.class)); + + GitHubRequest request = mock(GitHubRequest.class); + when(request.method()).thenReturn("GET"); + + SCIMPageIterator iterator = + new SCIMPageIterator<>(mock(GitHubClient.class), SCIMSearchResult.class, request); + + assertNull(iterator.findNextURL(response)); + } + + private static void setPrivateField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } } diff --git a/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java index 7bf1c3b..ebca70c 100644 --- a/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java +++ b/src/test/java/org/kohsuke/github/SCIMSearchBuilderTest.java @@ -6,12 +6,145 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; +import java.net.MalformedURLException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("unchecked") class SCIMSearchBuilderTest { + @Mock + private GitHub root; + + @Mock + private GHOrganization organization; + + @Mock + private GHEnterpriseExt enterprise; + + @Mock + private Requester requester; + + @Mock + private GitHubRequest gitHubRequest; + + @BeforeEach + void setUp() { + when(root.createRequest()).thenReturn(requester); + } + + @Test + void constructor_withOrganization_setsCorrectHeadersAndPath() { + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + verify(requester).withUrlPath("/scim/test"); + verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); + verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); + verify(requester).rateLimit(RateLimitTarget.SEARCH); + } + + @Test + void constructor_withEnterprise_setsCorrectHeadersAndPath() { + TestSCIMSearchBuilderEnterprise builder = new TestSCIMSearchBuilderEnterprise(root, enterprise); + + verify(requester).withUrlPath("/scim/test"); + verify(requester).withHeader(SCIMConstants.HEADER_ACCEPT, SCIMConstants.SCIM_ACCEPT); + verify(requester).withHeader(SCIMConstants.HEADER_API_VERSION, SCIMConstants.GITHUB_API_VERSION); + verify(requester).rateLimit(RateLimitTarget.SEARCH); + } + + @Test + void list_emptyFilter_doesNotSetFilter() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + verify(requester, never()).set(eq("filter"), anyString()); + } + + @Test + void list_withOneFilter_setsFilterCorrectly() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("userName", "john.doe"); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + verify(requester).set("filter", "userName eq \"john.doe\""); + } + + @Test + void list_withMultipleFilters_joinsWithAnd() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("userName", "john.doe"); + builder.eq("active", "true"); + + SCIMPagedSearchIterable iterable = builder.list(); + + assertNotNull(iterable); + + verify(requester).set(eq("filter"), argThat((String str) -> + str.contains("userName eq \"john.doe\"") && + str.contains("active eq \"true\"") && + str.contains(" and ") + )); + } + + @Test + void list_escapesDoubleQuotes() throws MalformedURLException { + doReturn(gitHubRequest).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + builder.eq("displayName", "O'Reilly \"quoted\""); + + builder.list(); + + verify(requester).set("filter", "displayName eq \"O'Reilly \\\"quoted\\\"\""); + } + + @Test + void list_whenBuildThrowsMalformedURLException_wrapsInGHException() throws MalformedURLException { + doThrow(new MalformedURLException("bad url")).when(requester).build(); + + TestSCIMSearchBuilderOrg builder = new TestSCIMSearchBuilderOrg(root, organization); + + GHException ex = assertThrows(GHException.class, builder::list); + assertInstanceOf(MalformedURLException.class, ex.getCause()); + } + + private static class TestSCIMSearchBuilderOrg extends SCIMSearchBuilder { + + @SuppressWarnings("unchecked") + TestSCIMSearchBuilderOrg(GitHub root, GHOrganization org) { + super(root, org, (Class>) (Class) SCIMSearchResult.class); + } + + @Override + protected String getApiUrl() { + return "/scim/test"; + } + } + + private static class TestSCIMSearchBuilderEnterprise extends SCIMSearchBuilder { + @SuppressWarnings("unchecked") + TestSCIMSearchBuilderEnterprise(GitHub root, GHEnterpriseExt ent) { + super(root, ent, (Class>) (Class) SCIMSearchResult.class); + } + @Override + protected String getApiUrl() { + return "/scim/test"; + } + } } \ No newline at end of file From d573f73a9d341b0253da5db06f4fce1bb4e45cce Mon Sep 17 00:00:00 2001 From: "lucas.vicente" Date: Tue, 3 Mar 2026 18:00:16 -0300 Subject: [PATCH 10/10] added tests --- .../github/AbstractGitHubConnectorTest.java | 242 ++++++++++++++++++ .../connector/github/GitHubClientTest.java | 52 +++- .../github/GitHubCopilotSeatHandlerTest.java | 34 +++ .../github/GitHubEMUConfigurationTest.java | 30 +++ .../github/GitHubEMUGroupHandlerTest.java | 207 +++++++++++++++ .../github/GitHubEMUUserHandlerTest.java | 134 ++++++++-- .../github/rest/GitHubEMURESTClientTest.java | 127 ++++----- 7 files changed, 707 insertions(+), 119 deletions(-) create mode 100644 src/test/java/jp/openstandia/connector/github/AbstractGitHubConnectorTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubCopilotSeatHandlerTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubEMUConfigurationTest.java create mode 100644 src/test/java/jp/openstandia/connector/github/GitHubEMUGroupHandlerTest.java diff --git a/src/test/java/jp/openstandia/connector/github/AbstractGitHubConnectorTest.java b/src/test/java/jp/openstandia/connector/github/AbstractGitHubConnectorTest.java new file mode 100644 index 0000000..5f158c9 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/AbstractGitHubConnectorTest.java @@ -0,0 +1,242 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.ObjectHandler; +import jp.openstandia.connector.util.SchemaDefinition; +import jp.openstandia.connector.util.Utils; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.spi.SearchResultsHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AbstractGitHubConnectorTest { + + @Mock + private GitHubEMUConfiguration configuration; + + @Mock + private GitHubClient client; + + @Mock + private GitHubEMUSchema schema; + + @Mock + private ObjectHandler objectHandler; + + @Mock + private SchemaDefinition schemaDefinition; + + @Mock + private ResultsHandler resultsHandler; + + @Mock + private SearchResultsHandler searchResultsHandler; + + private static class TestConnector extends AbstractGitHubConnector { + + TestConnector(GitHubEMUConfiguration cfg, GitHubClient cl, GitHubEMUSchema sch) { + this.configuration = cfg; + this.client = cl; + this.schema = sch; + } + + @Override + protected GitHubClient newClient(GitHubEMUConfiguration configuration) { + return client; + } + + @Override + protected GitHubEMUSchema newGitHubSchema(GitHubEMUConfiguration configuration, GitHubClient client) { + return schema; + } + } + + private TestConnector connector; + + private void setUpCommonMocks() { + when(configuration.getQueryPageSize()).thenReturn(100); + when(schema.getSchemaHandler(any(ObjectClass.class))).thenReturn(objectHandler); + when(objectHandler.getSchemaDefinition()).thenReturn(schemaDefinition); + } + + private void setFilterField(Object filter, String fieldName, Object value) { + try { + Field f = GitHubFilter.class.getDeclaredField(fieldName); + f.setAccessible(true); + f.set(filter, value); + } catch (Exception e) { + throw new RuntimeException("Failed to set field " + fieldName + " on GitHubFilter mock", e); + } + } + + @Test + void executeQueryWithSearchResult_byUid_coversLine138() { + setUpCommonMocks(); + connector = new TestConnector(configuration, client, schema); + + ObjectClass oc = new ObjectClass("EMUUser"); + OperationOptions options = new OperationOptionsBuilder().build(); + + GitHubFilter filter = mock(GitHubFilter.class); + doReturn(true).when(filter).isByUid(); + setFilterField(filter, "uid", new Uid("uid-123")); + + when(objectHandler.getByUid(any(), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt())) + .thenReturn(5); + + try (MockedStatic utils = mockStatic(Utils.class)) { + utils.when(() -> Utils.resolvePageSize(any(), anyInt())).thenReturn(100); + utils.when(() -> Utils.resolvePageOffset(any())).thenReturn(0); + utils.when(() -> Utils.createFullAttributesToGet(any(), any())).thenReturn(Map.of()); + utils.when(() -> Utils.shouldAllowPartialAttributeValues(any())).thenReturn(false); + + connector.executeQueryWithSearchResult(oc, filter, resultsHandler, options); + } + + verify(objectHandler).getByUid(eq(new Uid("uid-123")), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt()); + } + + @Test + void executeQueryWithSearchResult_byName_coversLine152() { + setUpCommonMocks(); + connector = new TestConnector(configuration, client, schema); + + ObjectClass oc = new ObjectClass("EMUUser"); + OperationOptions options = new OperationOptionsBuilder().build(); + + GitHubFilter filter = mock(GitHubFilter.class); + doReturn(true).when(filter).isByName(); + setFilterField(filter, "name", new Name("testuser")); + + when(objectHandler.getByName(any(), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt())) + .thenReturn(3); + + try (MockedStatic utils = mockStatic(Utils.class)) { + utils.when(() -> Utils.resolvePageSize(any(), anyInt())).thenReturn(100); + utils.when(() -> Utils.resolvePageOffset(any())).thenReturn(0); + utils.when(() -> Utils.createFullAttributesToGet(any(), any())).thenReturn(Map.of()); + utils.when(() -> Utils.shouldAllowPartialAttributeValues(any())).thenReturn(false); + + connector.executeQueryWithSearchResult(oc, filter, resultsHandler, options); + } + + verify(objectHandler).getByName(any(), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt()); + } + + @Test + void executeQueryWithSearchResult_byMembers() { + setUpCommonMocks(); + connector = new TestConnector(configuration, client, schema); + + ObjectClass oc = new ObjectClass("EMUGroup"); + OperationOptions options = new OperationOptionsBuilder().build(); + + GitHubFilter filter = mock(GitHubFilter.class); + doReturn(true).when(filter).isByMembers(); + setFilterField(filter, "attributeValue", AttributeBuilder.build("members.User.value", "member-uuid")); + + when(objectHandler.getByMembers(any(), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt())) + .thenReturn(2); + + try (MockedStatic utils = mockStatic(Utils.class)) { + utils.when(() -> Utils.resolvePageSize(any(), anyInt())).thenReturn(100); + utils.when(() -> Utils.resolvePageOffset(any())).thenReturn(0); + utils.when(() -> Utils.createFullAttributesToGet(any(), any())).thenReturn(Map.of()); + utils.when(() -> Utils.shouldAllowPartialAttributeValues(any())).thenReturn(false); + + connector.executeQueryWithSearchResult(oc, filter, resultsHandler, options); + } + + verify(objectHandler).getByMembers(any(), any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt()); + } + + @Test + void executeQueryWithSearchResult_noFilter_coversGetAll() { + setUpCommonMocks(); + connector = new TestConnector(configuration, client, schema); + + ObjectClass oc = new ObjectClass("EMUUser"); + OperationOptions options = new OperationOptionsBuilder().build(); + + when(objectHandler.getAll(any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt())) + .thenReturn(10); + + try (MockedStatic utils = mockStatic(Utils.class)) { + utils.when(() -> Utils.resolvePageSize(any(), anyInt())).thenReturn(100); + utils.when(() -> Utils.resolvePageOffset(any())).thenReturn(0); + utils.when(() -> Utils.createFullAttributesToGet(any(), any())).thenReturn(Map.of()); + utils.when(() -> Utils.shouldAllowPartialAttributeValues(any())).thenReturn(false); + + connector.executeQueryWithSearchResult(oc, null, resultsHandler, options); + } + + verify(objectHandler).getAll(any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt()); + } + + @Test + void executeQueryWithSearchResult_pagination_coversSearchResult() { + setUpCommonMocks(); + connector = new TestConnector(configuration, client, schema); + + ObjectClass oc = new ObjectClass("EMUUser"); + OperationOptions options = new OperationOptionsBuilder().build(); + + when(objectHandler.getAll(any(), any(), anySet(), anySet(), anyBoolean(), anyInt(), anyInt())) + .thenReturn(50); + + try (MockedStatic utils = mockStatic(Utils.class)) { + utils.when(() -> Utils.resolvePageSize(any(), anyInt())).thenReturn(100); + utils.when(() -> Utils.resolvePageOffset(any())).thenReturn(2); + utils.when(() -> Utils.createFullAttributesToGet(any(), any())).thenReturn(Map.of()); + utils.when(() -> Utils.shouldAllowPartialAttributeValues(any())).thenReturn(false); + + connector.executeQueryWithSearchResult(oc, null, searchResultsHandler, options); + } + + verify(searchResultsHandler).handleResult(any(SearchResult.class)); + } + + @Test + void testMethod_coversLines232And233() { + GitHubClient newClientMock = mock(GitHubClient.class); + TestConnectorForTest conn = new TestConnectorForTest(configuration, newClientMock); + + conn.setInstanceName("test-instance"); + + conn.test(); + + verify(newClientMock, times(2)).setInstanceName("test-instance"); + verify(newClientMock, times(1)).close(); + verify(newClientMock, times(1)).test(); + } + + private static class TestConnectorForTest extends AbstractGitHubConnector { + private final GitHubClient forcedClient; + + TestConnectorForTest(GitHubEMUConfiguration cfg, GitHubClient cl) { + this.configuration = cfg; + this.forcedClient = cl; + this.client = cl; + } + + @Override + protected GitHubClient newClient(GitHubEMUConfiguration configuration) { + return forcedClient; + } + + @Override + protected GitHubEMUSchema newGitHubSchema(GitHubEMUConfiguration configuration, GitHubClient client) { + return mock(GitHubEMUSchema.class); + } + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java index ff92b05..6feacd3 100644 --- a/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java +++ b/src/test/java/jp/openstandia/connector/github/GitHubClientTest.java @@ -8,16 +8,18 @@ import okhttp3.Authenticator; import okhttp3.OkHttpClient; import org.identityconnectors.common.security.GuardedString; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import java.io.IOException; 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() {} @@ -33,7 +35,6 @@ private AbstractGitHubConfiguration baseConfig( 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(""); @@ -49,7 +50,6 @@ void createClient_withoutProxy_usesTimeouts_and_noProxy() { 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()); @@ -61,7 +61,6 @@ 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(""); @@ -76,9 +75,7 @@ void createClient_withProxy_withoutAuth_setsProxyOnly() { 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"); } @@ -98,11 +95,46 @@ void createClient_withProxy_andAuth_setsProxyAndAuthenticator() { 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"); } + + @Test + void createClient_withProxy_andAuth_coversProxyAuthenticatorLambda() throws IOException { + DummyClient client = new DummyClient(); + AbstractGitHubConfiguration cfg = baseConfig(1000, 1000, 1000); + + when(cfg.getHttpProxyHost()).thenReturn("proxy.local"); + when(cfg.getHttpProxyPort()).thenReturn(8080); + when(cfg.getHttpProxyUser()).thenReturn("testuser"); + when(cfg.getHttpProxyPassword()).thenReturn(new GuardedString("testpass".toCharArray())); + + OkHttpClient ok = client.createClient(cfg); + + Authenticator pa = ok.proxyAuthenticator(); + + Request request = new Request.Builder() + .url("http://example.com") + .build(); + + Response response = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(407) + .message("Proxy Authentication Required") + .body(ResponseBody.create("", MediaType.get("text/plain"))) + .build(); + + Request authenticated = pa.authenticate(null, response); + + assertNotNull(authenticated); + + String authHeader = authenticated.header("Proxy-Authorization"); + assertNotNull(authHeader, "Deveria ter header Proxy-Authorization"); + assertTrue(authHeader.startsWith("Basic "), "Deveria começar com 'Basic '"); + assertTrue(authHeader.contains("dGVzdHVzZXI6"), "Deveria conter base64 do username (senha é zerada pelo GuardedString)"); + } } diff --git a/src/test/java/jp/openstandia/connector/github/GitHubCopilotSeatHandlerTest.java b/src/test/java/jp/openstandia/connector/github/GitHubCopilotSeatHandlerTest.java new file mode 100644 index 0000000..77c0029 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubCopilotSeatHandlerTest.java @@ -0,0 +1,34 @@ +package jp.openstandia.connector.github; + +import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +class GitHubCopilotSeatHandlerTest { + + @Test + void query_coversTheSuperCall() { + GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); + GitHubClient client = mock(GitHubClient.class); + GitHubEMUSchema schema = mock(GitHubEMUSchema.class); + SchemaDefinition schemaDefinition = mock(SchemaDefinition.class); + + GitHubCopilotSeatHandler handler = new GitHubCopilotSeatHandler(config, client, schema, schemaDefinition) { + @Override + public void query(GitHubFilter filter, ResultsHandler resultsHandler, OperationOptions options) { + super.query(filter, resultsHandler, options); + } + }; + + GitHubFilter filter = mock(GitHubFilter.class); + ResultsHandler resultsHandler = mock(ResultsHandler.class); + OperationOptions options = new OperationOptionsBuilder().build(); + + assertThrows(UnsupportedOperationException.class, () -> + handler.query(filter, resultsHandler, options) + ); + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/GitHubEMUConfigurationTest.java b/src/test/java/jp/openstandia/connector/github/GitHubEMUConfigurationTest.java new file mode 100644 index 0000000..2454908 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubEMUConfigurationTest.java @@ -0,0 +1,30 @@ +package jp.openstandia.connector.github; + +import org.identityconnectors.common.security.GuardedString; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitHubEMUConfigurationTest { + + @Test + void validate_doesNothing_whenRequiredFieldsAreSet() { + GitHubEMUConfiguration config = new GitHubEMUConfiguration(); + + config.setEnterpriseSlug("my-enterprise"); + config.setAccessToken(new GuardedString("ghp_xxxxxxxx".toCharArray())); + config.setEndpointURL("https://api.github.com"); + + assertDoesNotThrow(config::validate); + } + + @Test + void validate_doesNothing_evenWithMinimalFields() { + GitHubEMUConfiguration config = new GitHubEMUConfiguration(); + + config.setEnterpriseSlug("test-enterprise"); + config.setAccessToken(new GuardedString("token".toCharArray())); + + assertDoesNotThrow(config::validate); + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/GitHubEMUGroupHandlerTest.java b/src/test/java/jp/openstandia/connector/github/GitHubEMUGroupHandlerTest.java new file mode 100644 index 0000000..3b15a71 --- /dev/null +++ b/src/test/java/jp/openstandia/connector/github/GitHubEMUGroupHandlerTest.java @@ -0,0 +1,207 @@ +package jp.openstandia.connector.github; + +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 org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.SCIMEMUGroup; +import org.kohsuke.github.SCIMMember; +import org.kohsuke.github.SCIMPatchOperations; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("unchecked") +class GitHubEMUGroupHandlerTest { + + @Mock + private GitHubEMUConfiguration configuration; + + @Mock + private GitHubClient client; + + @Mock + private GitHubEMUSchema schema; + + @Mock + private ResultsHandler resultsHandler; + + private SchemaDefinition schemaDefinition; + private GitHubEMUGroupHandler handler; + + @BeforeEach + void setUp() { + SchemaDefinition.Builder builder = GitHubEMUGroupHandler.createSchema(configuration, client); + schemaDefinition = builder.build(); + + handler = new GitHubEMUGroupHandler(configuration, client, schema, schemaDefinition); + } + + @Test + void testCreate() { + Set attributes = Set.of( + new Name("test-group"), + AttributeBuilder.build("externalId", "ext-123"), + AttributeBuilder.build("members.User.value", "user-uuid-1", "user-uuid-2") + ); + + when(client.createEMUGroup(eq(schema), any(SCIMEMUGroup.class))) + .thenReturn(new Uid("group-created-uuid")); + + Uid uid = handler.create(attributes); + + assertEquals("group-created-uuid", uid.getUidValue()); + verify(client, times(1)).createEMUGroup(eq(schema), any(SCIMEMUGroup.class)); + } + + @Test + void testUpdateDelta() { + Uid uid = new Uid("group-uuid"); + Set modifications = Set.of( + AttributeDeltaBuilder.build(Name.NAME, "new-display-name"), + AttributeDeltaBuilder.build("externalId", "new-ext-456") + ); + + doNothing().when(client).patchEMUGroup(eq(uid), any(SCIMPatchOperations.class)); + + handler.updateDelta(uid, modifications, null); + + verify(client, times(1)).patchEMUGroup(eq(uid), any(SCIMPatchOperations.class)); + } + + @Test + void testDelete() { + Uid uid = new Uid("group-to-delete"); + OperationOptions options = new OperationOptionsBuilder().build(); + + doNothing().when(client).deleteEMUGroup(eq(uid), eq(options)); + + handler.delete(uid, options); + + verify(client, times(1)).deleteEMUGroup(eq(uid), eq(options)); + } + + @Test + void testGetByUid() { + Uid uid = new Uid("group-uuid"); + OperationOptions options = new OperationOptionsBuilder().build(); + Set returnAttributesSet = Set.of("__UID__", "__NAME__", "externalId"); + Set fetchFieldsSet = Collections.emptySet(); + + SCIMEMUGroup group = new SCIMEMUGroup(); + group.id = "group-uuid"; + group.displayName = "Test Group"; + group.externalId = "ext-123"; + + when(client.getEMUGroup(eq(uid), eq(options), eq(fetchFieldsSet))).thenReturn(group); + + int count = handler.getByUid(uid, resultsHandler, options, + returnAttributesSet, fetchFieldsSet, false, 100, 0); + + assertEquals(1, count); + verify(resultsHandler, times(1)).handle(any(ConnectorObject.class)); + } + + @Test + void testGetByName() { + Name name = new Name("test-group"); + OperationOptions options = new OperationOptionsBuilder().build(); + Set returnAttributesSet = Set.of("__UID__", "__NAME__", "externalId"); + Set fetchFieldsSet = Collections.emptySet(); + + SCIMEMUGroup group = new SCIMEMUGroup(); + group.id = "group-uuid"; + group.displayName = "test-group"; + group.externalId = "ext-456"; + + when(client.getEMUGroup(eq(name), eq(options), eq(fetchFieldsSet))).thenReturn(group); + + int count = handler.getByName(name, resultsHandler, options, + returnAttributesSet, fetchFieldsSet, false, 100, 0); + + assertEquals(1, count); + verify(resultsHandler, times(1)).handle(any(ConnectorObject.class)); + } + + @Test + void testGetAll() { + OperationOptions options = new OperationOptionsBuilder().build(); + Set returnAttributesSet = Set.of("__UID__", "displayName", "externalId"); + Set fetchFieldsSet = Collections.emptySet(); + int pageSize = 100; + int pageOffset = 0; + + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass((Class) QueryHandler.class); + + when(client.getEMUGroups(callbackCaptor.capture(), eq(options), eq(fetchFieldsSet), eq(pageSize), eq(pageOffset))) + .thenReturn(1); + + int count = handler.getAll(resultsHandler, options, + returnAttributesSet, fetchFieldsSet, false, pageSize, pageOffset); + + assertEquals(1, count); + + QueryHandler callback = callbackCaptor.getValue(); + SCIMEMUGroup group = new SCIMEMUGroup(); + group.id = "g1"; + group.displayName = "Group 1"; + callback.handle(group); + + verify(resultsHandler, times(1)).handle(any(ConnectorObject.class)); + } + + @Test + void testGetByMembers() { + String memberId = "8a8a8a8a-8a8a-8a8a-8a8a-8a8a8a8a8a8a"; + Attribute attribute = AttributeBuilder.build("members.User.value", memberId); + + OperationOptions options = new OperationOptionsBuilder().build(); + Set returnAttributesSet = Set.of("__UID__", "displayName", "externalId"); + Set fetchFieldsSet = Collections.emptySet(); + int pageSize = 100; + int pageOffset = 0; + + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass((Class) QueryHandler.class); + + when(client.getEMUGroups(callbackCaptor.capture(), eq(options), eq(fetchFieldsSet), eq(pageSize), eq(pageOffset))) + .thenReturn(2); + + int count = handler.getByMembers(attribute, resultsHandler, options, + returnAttributesSet, fetchFieldsSet, false, pageSize, pageOffset); + + assertEquals(2, count); + + QueryHandler callback = callbackCaptor.getValue(); + + // Grupo que MATCH → precisa ter id + displayName + SCIMEMUGroup matching = new SCIMEMUGroup(); + matching.id = "group-matching-uuid"; + matching.displayName = "Matching Group"; + matching.externalId = "ext-matching-123"; + SCIMMember m1 = new SCIMMember(); + m1.value = memberId; + matching.members = List.of(m1); + callback.handle(matching); + + // Grupo que NÃO match + SCIMEMUGroup nonMatching = new SCIMEMUGroup(); + nonMatching.id = "group-nonmatching-uuid"; + nonMatching.displayName = "Non-matching Group"; + nonMatching.members = List.of(); + callback.handle(nonMatching); + + verify(resultsHandler, times(1)).handle(any(ConnectorObject.class)); + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java index 22b25c3..2f180f6 100644 --- a/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java +++ b/src/test/java/jp/openstandia/connector/github/GitHubEMUUserHandlerTest.java @@ -1,24 +1,14 @@ package jp.openstandia.connector.github; import jp.openstandia.connector.util.SchemaDefinition; +import org.identityconnectors.framework.common.objects.*; import org.junit.jupiter.api.Test; -import org.kohsuke.github.SCIMEMUUser; -import org.kohsuke.github.SCIMPatchOperations; +import org.kohsuke.github.*; +import jp.openstandia.connector.util.QueryHandler; +import org.mockito.ArgumentCaptor; -import java.util.UUID; +import java.util.*; -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.*; @@ -32,29 +22,117 @@ private static class DummyEMUClient implements GitHubClient { @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 instanciaHandlerOk() { - GitHubEMUUserHandler handler = newHandler(); + GitHubEMUUserHandler handler = new GitHubEMUUserHandler( + mock(GitHubEMUConfiguration.class), + new DummyEMUClient(), + mock(GitHubEMUSchema.class), + mock(SchemaDefinition.class) + ); assertNotNull(handler); } - @Test - public void testCreateSchema() { + void testCreateSchema() { GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); GitHubClient client = mock(GitHubClient.class); SchemaDefinition.Builder builder = GitHubEMUUserHandler.createSchema(config, client); assertNotNull(builder); } -} + @Test + void toConnectorObject_coversGetters() { + GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); + GitHubClient client = mock(GitHubClient.class); + GitHubEMUSchema schema = mock(GitHubEMUSchema.class); + + SchemaDefinition schemaDefinition = GitHubEMUUserHandler.createSchema(config, client).build(); + + GitHubEMUUserHandler handler = new GitHubEMUUserHandler(config, client, schema, schemaDefinition); + + Set returnAttributesSet = Set.of( + "__UID__", "__NAME__", OperationalAttributes.ENABLE_NAME, + "externalId", "displayName", + "name.formatted", "name.givenName", "name.familyName", + "primaryEmail", "primaryRole", "groups", + "meta.created", "meta.lastModified" + ); + + SCIMEMUUser fullUser = createFullUser(); + + ConnectorObject co = handler.toConnectorObject(schemaDefinition, fullUser, returnAttributesSet, false); + + assertNotNull(co); + } + + @Test + void getAll() { + GitHubEMUConfiguration config = mock(GitHubEMUConfiguration.class); + GitHubClient client = mock(GitHubClient.class); + GitHubEMUSchema schema = mock(GitHubEMUSchema.class); + + SchemaDefinition schemaDefinition = GitHubEMUUserHandler.createSchema(config, client).build(); + + GitHubEMUUserHandler handler = new GitHubEMUUserHandler(config, client, schema, schemaDefinition); + + OperationOptions options = new OperationOptionsBuilder().build(); + Set returnAttributesSet = Set.of("__UID__", "__NAME__"); + Set fetchFieldsSet = Collections.emptySet(); + + ResultsHandler resultsHandler = mock(ResultsHandler.class); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(QueryHandler.class); + + when(client.getEMUUsers(captor.capture(), eq(options), eq(fetchFieldsSet), eq(100), eq(0))) + .thenReturn(1); + + handler.getAll(resultsHandler, options, returnAttributesSet, fetchFieldsSet, false, 100, 0); + + captor.getValue().handle(createMinimalUser()); + verify(resultsHandler).handle(any(ConnectorObject.class)); + } + + private SCIMEMUUser createFullUser() { + SCIMEMUUser u = new SCIMEMUUser(); + u.id = "user-123"; + u.userName = "testuser"; + u.active = true; + u.externalId = "ext-123"; + u.displayName = "Test User"; + + u.name = new SCIMName(); + u.name.formatted = "Test User Full"; + u.name.givenName = "Test"; + u.name.familyName = "User"; + + SCIMEmail email = new SCIMEmail(); + email.value = "test@example.com"; + email.primary = true; + u.emails = List.of(email); + + SCIMRole role = new SCIMRole(); + role.value = "admin"; + role.primary = true; + u.roles = List.of(role); + SCIMMember g = new SCIMMember(); + g.value = "group-uuid"; + g.ref = "/scim/v2/Groups/group-uuid"; + u.groups = List.of(g); + + SCIMMeta meta = new SCIMMeta(); + meta.created = "2025-03-03T10:00:00Z"; + meta.lastModified = "2025-03-03T11:00:00Z"; + u.meta = meta; + + return u; + } + + private SCIMEMUUser createMinimalUser() { + SCIMEMUUser u = new SCIMEMUUser(); + u.id = "u1"; + u.userName = "user1"; + return u; + } +} \ No newline at end of file diff --git a/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java index af59862..aee73dd 100644 --- a/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java +++ b/src/test/java/jp/openstandia/connector/github/rest/GitHubEMURESTClientTest.java @@ -2,6 +2,7 @@ import jp.openstandia.connector.github.GitHubEMUConfiguration; import jp.openstandia.connector.github.GitHubEMUSchema; +import org.identityconnectors.common.security.GuardedString; import org.kohsuke.github.TestSCIMPagedSearchIterable; import jp.openstandia.connector.util.QueryHandler; import org.identityconnectors.framework.common.exceptions.*; @@ -39,7 +40,6 @@ class GitHubEMURESTClientTest { @Mock OperationOptions options; - // ---------- testable client (constructor-safe) ---------- static class TestableClient extends GitHubEMURESTClient { static final AtomicInteger authCalls = new AtomicInteger(0); @@ -49,8 +49,6 @@ static class TestableClient extends GitHubEMURESTClient { @Override public void auth() { - //TestableClient.authCalls.set(0); - // No network; just count calls authCalls.incrementAndGet(); } } @@ -59,19 +57,14 @@ public void auth() { @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); @@ -84,22 +77,15 @@ private static void setPrivateLong(Object target, String fieldName, long value) 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() + setPrivateLong(client, "lastAuthenticated", 0L); client.test(); @@ -116,18 +102,11 @@ void test_whenRuntimeException_wrapsConnectorException() throws Exception { 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"); @@ -140,7 +119,7 @@ void handleApiException_400_mapsToInvalidAttributeValueException() { 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 + assertTrue(mapped instanceof ConnectionFailedException); } @Test @@ -185,9 +164,6 @@ private GHFileNotFoundException ghFileNotFoundWithStatus(String statusLine) { return ex; } - // ======================================================= - // withAuth(Callable) - // ======================================================= @Test void withAuth_whenLastAuthenticatedNonZero_callsAuth() throws Exception { setPrivateLong(client, "lastAuthenticated", 123L); @@ -195,7 +171,7 @@ void withAuth_whenLastAuthenticatedNonZero_callsAuth() throws Exception { String out = client.withAuth(() -> "ok"); assertEquals("ok", out); - assertTrue(client.authCalls.get() >= 2); // ctor auth + this auth + assertTrue(client.authCalls.get() >= 2); } @Test @@ -209,9 +185,6 @@ void withAuth_whenCallableThrows_mapsAndThrowsConnectorException() throws Except ); } - // ======================================================= - // createEMUUser - // ======================================================= @Test void createEMUUser_returnsUidWithName() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -229,9 +202,6 @@ void createEMUUser_returnsUidWithName() throws Exception { verify(enterprise).createSCIMEMUUser(any(SCIMEMUUser.class)); } - // ======================================================= - // patchEMUUser - // ======================================================= @Test void patchEMUUser_callsUpdate() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -244,9 +214,6 @@ void patchEMUUser_callsUpdate() throws Exception { verify(enterprise).updateSCIMEMUUser("u1", ops); } - // ======================================================= - // deleteEMUUser - // ======================================================= @Test void deleteEMUUser_callsDelete() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -256,9 +223,6 @@ void deleteEMUUser_callsDelete() throws Exception { verify(enterprise).deleteSCIMUser("u1"); } - // ======================================================= - // getEMUUser(Uid) - // ======================================================= @Test void getEMUUser_byUid_callsGet() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -273,9 +237,6 @@ void getEMUUser_byUid_callsGet() throws Exception { verify(enterprise).getSCIMEMUUser("u1"); } - // ======================================================= - // getEMUUser(Name) - // ======================================================= @Test void getEMUUser_byName_callsGetByUserName() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -290,9 +251,6 @@ void getEMUUser_byName_callsGetByUserName() throws Exception { verify(enterprise).getSCIMEMUUserByUserName("jdoe"); } - // ======================================================= - // getEMUUsers - // ======================================================= @Test void getEMUUsers_noOffset_iteratesAllUntilHandlerFalse_returnsTotal() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -306,7 +264,7 @@ void getEMUUsers_noOffset_iteratesAllUntilHandlerFalse_returnsTotal() throws Exc @SuppressWarnings("unchecked") QueryHandler handler = mock(QueryHandler.class); when(handler.handle(u1)).thenReturn(true); - when(handler.handle(u2)).thenReturn(false); // stop early + when(handler.handle(u2)).thenReturn(false); int total = client.getEMUUsers(handler, options, Set.of(), 10, 0); @@ -333,7 +291,7 @@ void getEMUUsers_withOffset_paginatesAndStopsAtPageSize_returnsTotal() throws Ex int total = client.getEMUUsers(handler, options, Set.of(), 2, 1); assertEquals(99, total); - verify(handler, times(2)).handle(any()); // stops at pageSize=2 + verify(handler, times(2)).handle(any()); } private static org.kohsuke.github.PagedIterator pagedIteratorOf(List items) { @@ -348,9 +306,6 @@ private static org.kohsuke.github.PagedIterator pagedIteratorOf(List i return pit; } - - - // helper for SCIMPagedSearchIterable @SuppressWarnings("unchecked") private static SCIMPagedSearchIterable mockPagedIterable(List items, int totalCount) { SCIMPagedSearchIterable iterable = mock(SCIMPagedSearchIterable.class); @@ -359,9 +314,6 @@ private static SCIMPagedSearchIterable mockPagedIterable(List items, i return iterable; } - // ======================================================= - // createEMUGroup - // ======================================================= @Test void createEMUGroup_returnsUidWithName() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -379,9 +331,6 @@ void createEMUGroup_returnsUidWithName() throws Exception { verify(enterprise).createSCIMEMUGroup(any(SCIMEMUGroup.class)); } - // ======================================================= - // patchEMUGroup - // ======================================================= @Test void patchEMUGroup_callsUpdate() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -394,9 +343,6 @@ void patchEMUGroup_callsUpdate() throws Exception { verify(enterprise).updateSCIMEMUGroup("g1", ops); } - // ======================================================= - // deleteEMUGroup - // ======================================================= @Test void deleteEMUGroup_callsDelete() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -406,9 +352,6 @@ void deleteEMUGroup_callsDelete() throws Exception { verify(enterprise).deleteSCIMGroup("g1"); } - // ======================================================= - // getEMUGroup(Uid) - // ======================================================= @Test void getEMUGroup_byUid_callsGet() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -422,9 +365,6 @@ void getEMUGroup_byUid_callsGet() throws Exception { verify(enterprise).getSCIMEMUGroup("g1"); } - // ======================================================= - // getEMUGroup(Name) - // ======================================================= @Test void getEMUGroup_byName_callsGetByDisplayName() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -438,9 +378,6 @@ void getEMUGroup_byName_callsGetByDisplayName() throws Exception { verify(enterprise).getSCIMEMUGroupByDisplayName("devs"); } - // ======================================================= - // getCopilotSeat(Uid) - // ======================================================= @Test void getCopilotSeat_byUid_callsGetByUid() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -454,9 +391,6 @@ void getCopilotSeat_byUid_callsGetByUid() throws Exception { verify(enterprise).getCopilotSeatByUid("u1"); } - // ======================================================= - // getCopilotSeat(Name) - // ======================================================= @Test void getCopilotSeat_byName_callsGetByDisplayName() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -470,9 +404,6 @@ void getCopilotSeat_byName_callsGetByDisplayName() throws Exception { verify(enterprise).getCopilotSeatByDisplayName("Jane Doe"); } - // ======================================================= - // getCopilotSeats - // ======================================================= @Test void getCopilotSeats_noOffset_iteratesAllUntilHandlerFalse_returnsTotalSeats() throws Exception { setPrivateLong(client, "lastAuthenticated", 0L); @@ -574,9 +505,6 @@ void getEMUGroups_withOffset_stopsAtPageSize_returnsTotal() throws Exception { verify(handler, times(2)).handle(any()); } - // ======================================================= - // close() - // ======================================================= @Test void close_doesNothing() { assertDoesNotThrow(() -> client.close()); @@ -595,7 +523,7 @@ void getEMUUsers_withOffset_handlerReturnsFalse_triggersBreak() throws Exception @SuppressWarnings("unchecked") QueryHandler handler = mock(QueryHandler.class); when(handler.handle(u1)).thenReturn(true); - when(handler.handle(u2)).thenReturn(false); // ← força o break + when(handler.handle(u2)).thenReturn(false); int total = client.getEMUUsers(handler, options, Set.of(), 5, 1); @@ -619,7 +547,7 @@ void getCopilotSeats_withOffset_handlerReturnsFalse_triggersBreak() throws Excep @SuppressWarnings("unchecked") QueryHandler handler = mock(QueryHandler.class); when(handler.handle(s1)).thenReturn(true); - when(handler.handle(s2)).thenReturn(false); // ← força o break + when(handler.handle(s2)).thenReturn(false); int total = client.getCopilotSeats(handler, options, Set.of(), 3, 1); @@ -643,7 +571,7 @@ void getEMUGroups_withOffset_handlerReturnsFalse_triggersBreak() throws Exceptio @SuppressWarnings("unchecked") QueryHandler handler = mock(QueryHandler.class); when(handler.handle(g1)).thenReturn(true); - when(handler.handle(g2)).thenReturn(false); // ← força o break + when(handler.handle(g2)).thenReturn(false); int total = client.getEMUGroups(handler, options, Set.of(), 4, 1); @@ -651,4 +579,41 @@ void getEMUGroups_withOffset_handlerReturnsFalse_triggersBreak() throws Exceptio verify(handler).handle(g1); verify(handler).handle(g2); } + + @Test + void auth_catchIOException_coversLine105() { + lenient().when(configuration.getAccessToken()) + .thenReturn(new GuardedString("ghp_testtoken".toCharArray())); + + lenient().when(configuration.getEnterpriseSlug()) + .thenReturn("test-enterprise"); + + lenient().when(configuration.getEndpointURL()) + .thenReturn("https://api.github.com"); + + try (MockedStatic mocked = mockStatic(GitHubExt.class)) { + + mocked.when(() -> GitHubExt.build(any(GitHubBuilder.class))) + .thenThrow(new IOException("Simulated IO failure during auth")); + + ConnectionFailedException ex = assertThrows( + ConnectionFailedException.class, + () -> new GitHubEMURESTClient(configuration).auth() + ); + + assertTrue(ex.getMessage().contains("Failed to authenticate GitHub EMU API")); + assertInstanceOf(IOException.class, ex.getCause()); + } + } + + @Test + void handleApiException_GHFileNotFoundWithUnknownStatus_coversLoggerWithStatusCode() { + client.setInstanceName("test-instance"); + + GHFileNotFoundException ex = ghFileNotFoundWithStatus("HTTP/1.1 500 Internal Server Error"); + + ConnectorException result = client.handleApiException(ex); + + assertInstanceOf(ConnectorIOException.class, result); + } }