Skip to content

Commit

Permalink
Merge pull request #9 from ashearin/gitlabsyncer-unit-tests
Browse files Browse the repository at this point in the history
tests: add gitlabsyncer tests
  • Loading branch information
jhoward-lm authored Feb 13, 2025
2 parents a096537 + b5fbef7 commit fa505bb
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.common.HttpClientPool;

import alpine.Config;
import alpine.common.logging.Logger;

import net.minidev.json.JSONArray;
Expand All @@ -53,7 +54,6 @@ public class GitLabClient {
private static final String GRAPHQL_ENDPOINT = "/api/graphql";

private final String accessToken;
private final GitLabSyncer syncer;
private final URI baseURL;

private final Map<GitLabRole, List<Permissions>> rolePermissions = Map.of(
Expand Down Expand Up @@ -108,61 +108,55 @@ public class GitLabClient {
Permissions.TAG_MANAGEMENT,
Permissions.TAG_MANAGEMENT_DELETE));

public GitLabClient(final GitLabSyncer syncer, final URI baseURL, final String accessToken) {
public GitLabClient(final String accessToken) {
this.accessToken = accessToken;
this.baseURL = baseURL;
this.syncer = syncer;
this.baseURL = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER));;
}

public List<GitLabProject> getGitLabProjects() {
public List<GitLabProject> getGitLabProjects() throws IOException, URISyntaxException {
List<GitLabProject> projects = new ArrayList<>();

JSONObject variables = new JSONObject();
JSONObject queryObject = new JSONObject();

try {
queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8));
queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8));

URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT);
URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT);

HttpPost request = new HttpPost(builder.build());
request.setHeader("Authorization", "Bearer " + accessToken);
request.setHeader("Content-Type", "application/json");
HttpPost request = new HttpPost(builder.build());
request.setHeader("Authorization", "Bearer " + accessToken);
request.setHeader("Content-Type", "application/json");

while (true) {
queryObject.put("variables", variables);
while (true) {
queryObject.put("variables", variables);

StringEntity entity = new StringEntity(queryObject.toString(), StandardCharsets.UTF_8);
request.setEntity(entity);
StringEntity entity = new StringEntity(queryObject.toString(), StandardCharsets.UTF_8);
request.setEntity(entity);

try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) {
HttpEntity responseEntity = response.getEntity();
try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) {
HttpEntity responseEntity = response.getEntity();

if (responseEntity == null)
break;
if (responseEntity == null)
break;

String responseBody = EntityUtils.toString(responseEntity);
JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class);
JSONObject dataObject = (JSONObject) responseData.get("data");
JSONObject projectsObject = (JSONObject) dataObject.get("projects");
JSONArray nodes = (JSONArray) projectsObject.get("nodes");
String responseBody = EntityUtils.toString(responseEntity);
JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class);
JSONObject dataObject = (JSONObject) responseData.get("data");
JSONObject projectsObject = (JSONObject) dataObject.get("projects");
JSONArray nodes = (JSONArray) projectsObject.get("nodes");

for (Object nodeObject : nodes) {
JSONObject node = (JSONObject) nodeObject;
projects.add(GitLabProject.parse(node.toJSONString()));
}
for (Object nodeObject : nodes) {
JSONObject node = (JSONObject) nodeObject;
projects.add(GitLabProject.parse(node.toJSONString()));
}

JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo");
JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo");

if (!(boolean) pageInfo.get("hasNextPage"))
break;
if (!(boolean) pageInfo.get("hasNextPage"))
break;

variables.put("cursor", pageInfo.getAsString("endCursor"));
}
variables.put("cursor", pageInfo.getAsString("endCursor"));
}
} catch (IOException | URISyntaxException ex) {
LOGGER.error("An error occurred while querying GitLab GraphQL API", ex);
syncer.handleException(LOGGER, ex);
}

return projects;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL;
import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED;

import java.net.URI;
import java.net.URISyntaxException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
Expand All @@ -30,7 +31,6 @@
import org.dependencytrack.integrations.PermissionsSyncer;
import org.dependencytrack.model.Project;

import alpine.Config;
import alpine.common.logging.Logger;
import alpine.model.ConfigProperty;
import alpine.model.OidcUser;
Expand All @@ -43,17 +43,12 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission
private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName();

Check warning on line 43 in src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java#L43

Avoid unused private fields such as 'GENERAL_GROUP'.
private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/";

Check warning on line 44 in src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java#L44

Avoid unused private fields such as 'ROLE_CLAIM_PREFIX'.

private final String accessToken;
private final OidcUser user;
private GitLabClient gitLabClient;

public GitLabSyncer(final String accessToken, final OidcUser user) {
this.accessToken = accessToken;
public GitLabSyncer(final OidcUser user, final GitLabClient gitlabClient) {
this.user = user;
}

public String getAccessToken() {
return accessToken;
this.gitLabClient = gitlabClient;
}

@Override
Expand All @@ -75,24 +70,26 @@ public boolean isEnabled() {

@Override
public void synchronize() {
final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER));
gitLabClient = new GitLabClient(this, gitLabUrl, accessToken);

List<GitLabProject> gitLabProjects = gitLabClient.getGitLabProjects();
List<Project> projects = createProjects(gitLabProjects);
List<Team> teams = projects.stream()
.flatMap(project -> createProjectTeams(project).stream())
.toList();
List<String> teamNames = gitLabProjects.stream()
.map(gitLabProject -> "%s_%s".formatted(
gitLabProject.getFullPath(),
gitLabProject.getMaxAccessLevel().getStringValue().toString()))
.toList();

qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames);

teams = teams.stream().map(team -> qm.updateTeam(team)).toList();
projects = projects.stream().map(project -> qm.updateProject(project, false)).toList();
try {
List<GitLabProject> gitLabProjects = gitLabClient.getGitLabProjects();
List<Project> projects = createProjects(gitLabProjects);
List<Team> teams = projects.stream()
.flatMap(project -> createProjectTeams(project).stream())
.toList();
List<String> teamNames = gitLabProjects.stream()
.map(gitLabProject -> "%s_%s".formatted(
gitLabProject.getFullPath(),
gitLabProject.getMaxAccessLevel().getStringValue().toString()))
.toList();

qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames);

teams = teams.stream().map(team -> qm.updateTeam(team)).toList();
projects = projects.stream().map(project -> qm.updateProject(project, false)).toList();
} catch (IOException | URISyntaxException ex) {
LOGGER.error("An error occurred while querying GitLab GraphQL API", ex);
handleException(LOGGER, ex);
}
}

private List<Project> createProjects(List<GitLabProject> gitLabProjects) {
Expand All @@ -112,7 +109,6 @@ private List<Project> createProjects(List<GitLabProject> gitLabProjects) {
project.setActive(project.getLastBomImport() != null);
if (!project.isActive() && project.getInactiveSince() == null)
project.setInactiveSince(new Date());

projects.add(qm.updateProject(project, false));
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import alpine.model.OidcUser;

import org.dependencytrack.event.GitLabSyncEvent;
import org.dependencytrack.integrations.gitlab.GitLabClient;
import org.dependencytrack.integrations.gitlab.GitLabSyncer;
import org.dependencytrack.persistence.QueryManager;

Expand Down Expand Up @@ -71,7 +72,8 @@ public void inform(final Event event) {
LOGGER.info("Starting GitLab sync task");

try (QueryManager qm = new QueryManager()) {
GitLabSyncer syncer = new GitLabSyncer(accessToken, user);
GitLabClient gitLabClient = new GitLabClient(accessToken);
GitLabSyncer syncer = new GitLabSyncer(user, gitLabClient);
syncer.setQueryManager(qm);
syncer.synchronize();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* This file is part of Dependency-Track.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.dependencytrack.integrations.gitlab;

import org.junit.Assert;
import alpine.model.IConfigProperty;
import alpine.model.OidcUser;
import alpine.model.Permission;
import alpine.model.Team;
import java.net.URISyntaxException;
import java.io.IOException;
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.model.Project;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED;


/**
* This test suite validates the integration with the GitLab API.
*/
@RunWith(MockitoJUnitRunner.class)
public class GitLabSyncerTest extends PersistenceCapableTest {

@Mock
private OidcUser user;

@Mock
private GitLabClient gitLabClient;

@InjectMocks
private GitLabSyncer gitLabSyncer;

/**
* Validates that the integration metadata is correctly defined.
*/
@Test
public void testIntegrationMetadata() {
GitLabSyncer extension = new GitLabSyncer(user, gitLabClient);
Assert.assertEquals("GitLab", extension.name());
Assert.assertEquals("Synchronizes user permissions from connected GitLab instance", extension.description());
}

/**
* Validates that the integration is enabled when the GITLAB_ENABLED property is
* set to true.
*/
@Test
public void testIsEnabled() {
qm.createConfigProperty(
GITLAB_ENABLED.getGroupName(),
GITLAB_ENABLED.getPropertyName(),
"true",
IConfigProperty.PropertyType.BOOLEAN,
null
);
GitLabSyncer extension = new GitLabSyncer(user, gitLabClient);
extension.setQueryManager(qm);
Assert.assertTrue(extension.isEnabled());
}

/**
* Validates that the integration is disabled when the GITLAB_ENABLED property
* is set to false.
*/
@Test
public void testIsDisabled() {
qm.createConfigProperty(
GITLAB_ENABLED.getGroupName(),
GITLAB_ENABLED.getPropertyName(),
"false",
IConfigProperty.PropertyType.BOOLEAN,
null
);
GitLabSyncer extension = new GitLabSyncer(user, gitLabClient);
extension.setQueryManager(qm);
Assert.assertFalse(extension.isEnabled());
}

/**
* Validates that the synchronize method is correctly executed when the
* integration is enabled.
*/
@Test
public void testSynchronizeSuccess() {
qm.createConfigProperty(
GITLAB_ENABLED.getGroupName(),
GITLAB_ENABLED.getPropertyName(),
"true",
IConfigProperty.PropertyType.BOOLEAN,
null
);
qm.createOidcUser(
"test_user"
);
OidcUser testUser = new OidcUser();
testUser.setUsername("test_user");
GitLabClient mockClient = mock(GitLabClient.class);
GitLabSyncer extension = new GitLabSyncer(testUser, mockClient);
extension.setQueryManager(qm);
try{
when(mockClient.getGitLabProjects()).thenReturn(Arrays.asList(new GitLabProject("project1", "this/test/project1", GitLabRole.MAINTAINER),
new GitLabProject("project2", "that/test/project2", GitLabRole.REPORTER)));
extension.synchronize();
}catch (IOException | URISyntaxException ex) {
Assert.fail("Exception " + ex);
}
Project testProject1 = qm.getProject("this/test/project1", null);
Assert.assertFalse(testProject1.isActive());

Project testProject2 = qm.getProject("that/test/project2", null);
Assert.assertFalse(testProject2.isActive());

List<Team> testTeams = new ArrayList<Team>();
Team team1 = qm.getTeam("this/test/project1_MAINTAINER");
testTeams.add(team1);
List<Permission> t1perm = team1.getPermissions();
Assert.assertEquals(t1perm.size(), mockClient.getRolePermissions(GitLabRole.MAINTAINER).size());

Team team2 = qm.getTeam("that/test/project2_REPORTER");
testTeams.add(team2);
List<Permission> t2perm = team2.getPermissions();
Assert.assertEquals(t2perm.size(), mockClient.getRolePermissions(GitLabRole.REPORTER).size());

for (Team team : testTeams) {
List<OidcUser> testTeamUsers = team.getOidcUsers();
Assert.assertEquals(testTeamUsers.size(), 1);
Assert.assertEquals(testTeamUsers.get(0).getUsername(), "test_user");
}
}
}

0 comments on commit fa505bb

Please sign in to comment.