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