diff --git a/pom.xml b/pom.xml
index 866b1ae..8f9385a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,7 @@ SOFTWARE.
1.20.1
3.2.0
1.19.3
+ 4.0.2
@@ -153,6 +154,12 @@ SOFTWARE.
testcontainers
${testcontainers.version}
+
+ org.glassfish.grizzly
+ grizzly-http-server
+ ${grizzly.version}
+ test
+
org.testcontainers
postgresql
diff --git a/src/main/java/git/tracehub/pmo/controller/ProjectController.java b/src/main/java/git/tracehub/pmo/controller/ProjectController.java
index bf48b5e..30d8e8a 100644
--- a/src/main/java/git/tracehub/pmo/controller/ProjectController.java
+++ b/src/main/java/git/tracehub/pmo/controller/ProjectController.java
@@ -17,6 +17,8 @@
package git.tracehub.pmo.controller;
+import com.jcabi.github.RtGithub;
+import git.tracehub.pmo.platforms.RepoPath;
import git.tracehub.pmo.platforms.github.InviteCollaborator;
import git.tracehub.pmo.project.Project;
import git.tracehub.pmo.project.Projects;
@@ -106,9 +108,11 @@ public Project employ(
*/
if (new ExistsRole(jwt, "user_github").value()) {
new InviteCollaborator(
- created.getLocation(),
+ new RepoPath(created.getLocation()).value(),
"tracehubgit",
- new IdpToken(jwt, "github", this.url).value()
+ new RtGithub(
+ new IdpToken(jwt, "github", this.url).value()
+ )
).exec();
}
return created;
diff --git a/src/main/java/git/tracehub/pmo/platforms/github/InviteCollaborator.java b/src/main/java/git/tracehub/pmo/platforms/github/InviteCollaborator.java
index 4ada9c9..358d643 100644
--- a/src/main/java/git/tracehub/pmo/platforms/github/InviteCollaborator.java
+++ b/src/main/java/git/tracehub/pmo/platforms/github/InviteCollaborator.java
@@ -18,9 +18,8 @@
package git.tracehub.pmo.platforms.github;
import com.jcabi.github.Coordinates;
-import com.jcabi.github.RtGithub;
+import com.jcabi.github.Github;
import git.tracehub.pmo.platforms.Action;
-import git.tracehub.pmo.platforms.RepoPath;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
@@ -43,18 +42,16 @@ public final class InviteCollaborator implements Action {
private final String username;
/**
- * Token.
+ * Github.
*/
- private final String token;
+ private final Github github;
@Override
@SneakyThrows
public void exec() {
- new RtGithub(this.token).repos()
+ this.github.repos()
.get(
- new Coordinates.Simple(
- new RepoPath(this.location).value()
- )
+ new Coordinates.Simple(this.location)
).collaborators()
.add(this.username);
}
diff --git a/src/main/java/git/tracehub/pmo/security/IdpToken.java b/src/main/java/git/tracehub/pmo/security/IdpToken.java
index d31a528..33925e8 100644
--- a/src/main/java/git/tracehub/pmo/security/IdpToken.java
+++ b/src/main/java/git/tracehub/pmo/security/IdpToken.java
@@ -20,6 +20,7 @@
import com.jcabi.http.Request;
import com.jcabi.http.request.JdkRequest;
import com.jcabi.http.response.RestResponse;
+import java.net.HttpURLConnection;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.cactoos.Scalar;
@@ -54,15 +55,8 @@ public final class IdpToken implements Scalar {
@Override
@SneakyThrows
public String value() {
- /*
- * @todo #1:45min/DEV fix 403 Forbidden error when trying to get
- * token from IDP. It seems that the user hasn't enough permissions
- * to get the token from IDP. We need to configure Keycloak to allow
- * the user to read the token. See the following link for more info:
- * https://www.keycloak.org/docs/latest/server_admin/#retrieving-external-idp-tokens
- */
- new JdkRequest(
- "%s//broker/%s/token".formatted(
+ return new JdkRequest(
+ "%s/broker/%s/token".formatted(
this.url,
this.provider
)
@@ -72,8 +66,11 @@ public String value() {
HttpHeaders.AUTHORIZATION,
"Bearer %s".formatted(this.jwt.getTokenValue())
).fetch()
- .as(RestResponse.class);
- return null;
+ .as(RestResponse.class)
+ .assertStatus(HttpURLConnection.HTTP_OK)
+ .body()
+ .split("&")[0]
+ .split("=")[1];
}
}
diff --git a/src/test/java/git/tracehub/pmo/controller/ProjectControllerTest.java b/src/test/java/git/tracehub/pmo/controller/ProjectControllerTest.java
new file mode 100644
index 0000000..7cdc406
--- /dev/null
+++ b/src/test/java/git/tracehub/pmo/controller/ProjectControllerTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2023-2024 Tracehub.git
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to read
+ * the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package git.tracehub.pmo.controller;
+
+import git.tracehub.pmo.project.Projects;
+import io.github.eocqrs.eokson.Jocument;
+import io.github.eocqrs.eokson.JsonOf;
+import org.cactoos.io.ResourceOf;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+/**
+ * Test suite for {@link ProjectController}.
+ *
+ * @since 0.0.0
+ */
+@ActiveProfiles("web")
+@ExtendWith(SpringExtension.class)
+@WebMvcTest(controllers = ProjectController.class)
+final class ProjectControllerTest {
+
+ /**
+ * Mocked mvc.
+ */
+ @Autowired
+ private MockMvc mvc;
+
+ /**
+ * Projects.
+ */
+ @MockBean
+ @SuppressWarnings("PMD.UnusedPrivateField")
+ private Projects projects;
+
+ @Test
+ void returnsForbiddenOnUnauthorizedUser() throws Exception {
+ this.mvc.perform(
+ MockMvcRequestBuilders.post("/")
+ .contentType(MediaType.APPLICATION_JSON)
+ ).andExpect(MockMvcResultMatchers.status().isForbidden());
+ }
+
+ @Test
+ void returnsProjectByUser() throws Exception {
+ this.mvc.perform(
+ MockMvcRequestBuilders.get("/")
+ .with(SecurityMockMvcRequestPostProcessors.jwt())
+ .contentType(MediaType.APPLICATION_JSON)
+ ).andExpect(MockMvcResultMatchers.status().isOk());
+ }
+
+ @Test
+ void returnsProjectById() throws Exception {
+ this.mvc.perform(
+ MockMvcRequestBuilders.get("/74bb5ec8-0e6b-4618-bfa4-a0b76b7b312d")
+ .with(SecurityMockMvcRequestPostProcessors.jwt())
+ .contentType(MediaType.APPLICATION_JSON)
+ ).andExpect(MockMvcResultMatchers.status().isOk());
+ }
+
+ @Test
+ void createsNewProject() throws Exception {
+ this.mvc.perform(
+ MockMvcRequestBuilders.post("/")
+ .with(SecurityMockMvcRequestPostProcessors.jwt())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(
+ new Jocument(
+ new JsonOf(
+ new ResourceOf("data/project.json").stream()
+ )
+ ).toString()
+ )
+ ).andExpect(MockMvcResultMatchers.status().isCreated());
+ }
+
+}
diff --git a/src/test/java/git/tracehub/pmo/controller/package-info.java b/src/test/java/git/tracehub/pmo/controller/package-info.java
new file mode 100644
index 0000000..5ca9dfa
--- /dev/null
+++ b/src/test/java/git/tracehub/pmo/controller/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2024 Tracehub.git
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to read
+ * the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * Controllers Tests.
+ *
+ * @since 0.0.0
+ */
+package git.tracehub.pmo.controller;
diff --git a/src/test/java/git/tracehub/pmo/platforms/github/InviteCollaboratorTest.java b/src/test/java/git/tracehub/pmo/platforms/github/InviteCollaboratorTest.java
new file mode 100644
index 0000000..ad99202
--- /dev/null
+++ b/src/test/java/git/tracehub/pmo/platforms/github/InviteCollaboratorTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023-2024 Tracehub.git
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to read
+ * the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package git.tracehub.pmo.platforms.github;
+
+import com.jcabi.github.Repo;
+import com.jcabi.github.Repos;
+import com.jcabi.github.mock.MkGithub;
+import java.io.IOException;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.core.IsEqual;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test suite for {@link InviteCollaborator}.
+ *
+ * @since 0.0.0
+ */
+final class InviteCollaboratorTest {
+
+ @Test
+ void invitesCollaboratorSuccessfully() throws IOException {
+ final String collaborator = "name";
+ final MkGithub github = new MkGithub("user");
+ final Repo repo = github.repos().create(
+ new Repos.RepoCreate("repo", false)
+ );
+ new InviteCollaborator("user/repo", collaborator, github).exec();
+ MatcherAssert.assertThat(
+ "Collaborator %s isn't invited as expected"
+ .formatted(collaborator),
+ repo.collaborators().isCollaborator(collaborator),
+ new IsEqual<>(true)
+ );
+ }
+
+ @Test
+ void trowsOnInvalidLocation() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> new InviteCollaborator("user", "user", new MkGithub("user"))
+ .exec(),
+ "Exception is not thrown or valid"
+ );
+ }
+
+}
diff --git a/src/test/java/git/tracehub/pmo/platforms/github/package-info.java b/src/test/java/git/tracehub/pmo/platforms/github/package-info.java
new file mode 100644
index 0000000..1db16f4
--- /dev/null
+++ b/src/test/java/git/tracehub/pmo/platforms/github/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023-2024 Tracehub.git
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to read
+ * the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/**
+ * Github Platform Tests.
+ *
+ * @since 0.0.0
+ */
+package git.tracehub.pmo.platforms.github;
diff --git a/src/test/java/git/tracehub/pmo/security/IpdTokenTest.java b/src/test/java/git/tracehub/pmo/security/IpdTokenTest.java
new file mode 100644
index 0000000..1444bd7
--- /dev/null
+++ b/src/test/java/git/tracehub/pmo/security/IpdTokenTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2023-2024 Tracehub.git
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to read
+ * the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package git.tracehub.pmo.security;
+
+import com.jcabi.http.mock.MkAnswer;
+import com.jcabi.http.mock.MkContainer;
+import com.jcabi.http.mock.MkGrizzlyContainer;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.core.IsEqual;
+import org.junit.jupiter.api.Test;
+import org.llorllale.cactoos.matchers.Assertion;
+import org.llorllale.cactoos.matchers.Throws;
+import org.mockito.Mockito;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+/**
+ * Test suite for {@link IdpToken}.
+ *
+ * @since 0.0.0
+ */
+final class IpdTokenTest {
+
+ @Test
+ void retrievesTokenSuccessfully() throws IOException {
+ final String expected = "token";
+ final MkContainer container = new MkGrizzlyContainer()
+ .next(
+ new MkAnswer.Simple(
+ HttpURLConnection.HTTP_OK,
+ "access_token=%s&expires_in=3600&token_type=bearer"
+ .formatted(expected)
+ )
+ ).start();
+ final String url = container.home().toString();
+ final String token = new IdpToken(
+ Mockito.mock(Jwt.class),
+ "provider",
+ url.substring(0, url.length() - 1)
+ ).value();
+ MatcherAssert.assertThat(
+ "Access token %s isn't correct".formatted(token),
+ token,
+ new IsEqual<>(expected)
+ );
+ container.stop();
+ }
+
+ @Test
+ @SuppressWarnings("JTCOP.RuleAssertionMessage")
+ void throwsOnInvalidHost() {
+ new Assertion<>(
+ "Exception is not thrown or valid",
+ () -> new IdpToken(
+ Mockito.mock(Jwt.class),
+ "provider",
+ "http://localhost:1000"
+ ).value(),
+ new Throws<>(IOException.class)
+ ).affirm();
+ }
+
+}
diff --git a/src/test/resources/application-web.yaml b/src/test/resources/application-web.yaml
new file mode 100644
index 0000000..ada6504
--- /dev/null
+++ b/src/test/resources/application-web.yaml
@@ -0,0 +1,15 @@
+application:
+ title: IT
+ version: 0.0.1
+spring:
+ datasource:
+ username: test
+ password: test
+ liquibase:
+ user: test
+ password: test
+ security:
+ oauth2:
+ resourceserver:
+ jwt:
+ issuer-uri: url
diff --git a/src/test/resources/data/project.json b/src/test/resources/data/project.json
new file mode 100644
index 0000000..33ce260
--- /dev/null
+++ b/src/test/resources/data/project.json
@@ -0,0 +1,5 @@
+{
+ "name": "IT project",
+ "active": true,
+ "location": "github@hizmailovich/draft:master"
+}
\ No newline at end of file
diff --git a/src/test/resources/data/realm.json b/src/test/resources/data/realm.json
index 550b39f..edd501e 100644
--- a/src/test/resources/data/realm.json
+++ b/src/test/resources/data/realm.json
@@ -43,13 +43,35 @@
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 30,
+ "roles": {
+ "realm": [
+ {
+ "id": "79dad14b-4887-4002-a3f3-05dbd3c8d1bf",
+ "name": "user_github",
+ "description": "",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "test",
+ "attributes": {}
+ },
+ {
+ "id": "d6258175-81e1-4185-956f-19be7552950e",
+ "name": "read-token",
+ "description": "",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "test",
+ "attributes": {}
+ }
+ ]
+ },
"defaultRole": {
"id": "ae0de638-8b16-4d7f-9c1d-e1a55129f895",
- "name": "default-roles-baeldung",
+ "name": "default-roles-test",
"description": "${role_default-roles}",
"composite": true,
"clientRole": false,
- "containerId": "baeldung"
+ "containerId": "test"
},
"requiredCredentials": [
"password"
@@ -112,13 +134,13 @@
"clientId": "account",
"name": "${client_account}",
"rootUrl": "${authBaseUrl}",
- "baseUrl": "/realms/baeldung/account/",
+ "baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
- "/realms/baeldung/account/*"
+ "/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
@@ -153,13 +175,13 @@
"clientId": "account-console",
"name": "${client_account-console}",
"rootUrl": "${authBaseUrl}",
- "baseUrl": "/realms/baeldung/account/",
+ "baseUrl": "/realms/test/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
- "/realms/baeldung/account/*"
+ "/realms/test/account/*"
],
"webOrigins": [],
"notBefore": 0,
@@ -414,6 +436,16 @@
]
}
],
+ "groups": [
+ {
+ "id": "c0dde556-f6b3-4025-8de2-55e42319e1d3",
+ "name": "broker",
+ "path": "/broker"
+ }
+ ],
+ "defaultGroups": [
+ "/broker"
+ ],
"clientScopes": [
{
"id": "b7ffedbd-ba94-4fd4-ba1e-0145252e10ef",
@@ -1795,9 +1827,16 @@
"clientRoles": {
"account": [
"view-profile",
- "manage-account"
+ "manage-account",
+ "user_github"
+ ],
+ "broker": [
+ "read-token"
]
- }
+ },
+ "realmRoles": [
+ "user_github"
+ ]
}
]
}
\ No newline at end of file