diff --git a/README.md b/README.md index 99dcccf5..cbbdb16b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This project provides an authentication API to be used by other applications to - [Microsoft Entra Azure AD oAuth2](#microsoft-entra-azure-ad-oauth2) - [Commands cheat-sheet](#commands-cheat-sheet) - [Simplified sequence diagram](#simplified-sequence-diagram) + - [Spring REST Docs](#spring-rest-docs) - [Sources](#sources) ## Getting Started @@ -169,3 +170,15 @@ Check if the project's structure is valid [Microsoft oAuth2 grant flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow) --- + +## Spring REST Docs + +The application contains an automated documentation from the Spring REST Docs package. The documentation is created when running the application in test environment. + +It creates an index.adoc file in the "src/asciidoc" folder and an index.html page in the "docs" folder. + +Link to the generated index.adoc : [src/asciidoc/index.adoc](src/asciidoc/index.adoc) + +Link to the generated index.html : [docs/index.html](docs/index.html) + +Link to the doc on GitHub : [https://orifinformatique.github.io/spring-auth/](https://orifinformatique.github.io/spring-auth/) \ No newline at end of file diff --git a/application.properties-dist b/application.properties-dist index a459d4df..4661b1a4 100644 --- a/application.properties-dist +++ b/application.properties-dist @@ -1,31 +1,3 @@ -# ╭──────────────────────────────────────────────────────────╮ -# │ oauth2 │ -# ╰──────────────────────────────────────────────────────────╯ -### Azure ### -custom.azure.account.tenant-id= - -# Azure Provider Configuration -spring.security.oauth2.client.provider.azure.issuer-uri=https://login.microsoftonline.com/${custom.azure.account.tenant-id}/v2.0 -spring.security.oauth2.client.provider.azure.authorization-uri=https://login.microsoftonline.com/${custom.azure.account.tenant-id}/oauth2/v2.0/authorize -spring.security.oauth2.client.provider.azure.token-uri=https://login.microsoftonline.com/${custom.azure.account.tenant-id}/oauth2/v2.0/token -spring.security.oauth2.client.provider.azure.user-info-uri=https://graph.microsoft.com/oidc/userinfo -spring.security.oauth2.client.provider.azure.jwk-set-uri=https://login.microsoftonline.com/${custom.azure.account.tenant-id}/discovery/v2.0/keys -spring.security.oauth2.client.provider.azure.user-name-attribute=email - -# Azure Client Configuration -spring.security.oauth2.client.registration.azure.client-id= -spring.security.oauth2.client.registration.azure.client-secret= -spring.security.oauth2.client.registration.azure.client-authentication-method=client_secret_post -spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} -spring.security.oauth2.client.registration.azure.scope=openid,profile,email,User.Read -spring.security.oauth2.client.registration.azure.client-name=Azure - -### Google exemple for multiple provider login ### -spring.security.oauth2.client.registration.google.client-id=0 -spring.security.oauth2.client.registration.google.client-secret=0 -spring.security.oauth2.client.registration.google.scope=0 - ### Logging levels ### logging.level.root=ERROR logging.level.ch.sectioninformatique=ERROR diff --git a/compose.yml b/compose.yml index e3cffe45..0a7e9a1b 100644 --- a/compose.yml +++ b/compose.yml @@ -9,6 +9,9 @@ services: context: . dockerfile: Dockerfile target: "${ENVIRONMENT}" # Use the environment variable to target the desired stage + volumes: + - ./src/asciidoc:/app/build/generated-snippets + - ./docs:/app/build/generated-snippets-html environment: # Pass environment variables to the container and the Spring Boot app SPRING_PROFILES_ACTIVE: "${ENVIRONMENT}" @@ -32,11 +35,11 @@ services: MARIADB_ROOT_PASSWORD: "${DB_PASSWORD}" MARIADB_DATABASE: "${ENVIRONMENT}_db" volumes: - - ./init.sql:/docker-entrypoint-initdb.d/init.sql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql networks: - spring_network healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] interval: 10s timeout: 5s retries: 5 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..430a7281 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,576 @@ + + + + + + + +Spring Auth API Documentation + + + + + +
+
+

User Endpoints

+
+
+

Get Authenticated User

+
+

This is an example output for the GET /users/me endpoint.

+
+
+
request
+
+
GET /users/me HTTP/1.1
+Accept: application/json
+Host: localhost:8080
+
+
+
+
response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 179
+
+{
+  "id" : 1,
+  "firstName" : "John",
+  "lastName" : "Doe",
+  "login" : "john@test.com",
+  "token" : null,
+  "refreshToken" : null,
+  "mainRole" : "USER",
+  "permissions" : null
+}
+
+
+
+

It gives informations about the authenticated user.

+
+
+
+

Get All Users

+
+

This is an example output for the GET /users/all endpoint.

+
+
+
request
+
+
GET /users/all HTTP/1.1
+Accept: application/json
+Host: localhost:8080
+
+
+
+
response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 698
+
+[ {
+  "id" : 1,
+  "firstName" : "John",
+  "lastName" : "Doe",
+  "login" : "john@test.com",
+  "password" : "pass",
+  "createdAt" : null,
+  "updatedAt" : null,
+  "mainRole" : null,
+  "authorities" : [ ],
+  "credentialsNonExpired" : true,
+  "accountNonExpired" : true,
+  "accountNonLocked" : true,
+  "username" : "john@test.com",
+  "enabled" : true
+}, {
+  "id" : 2,
+  "firstName" : "Jane",
+  "lastName" : "Smith",
+  "login" : "jane@test.com",
+  "password" : "pass",
+  "createdAt" : null,
+  "updatedAt" : null,
+  "mainRole" : null,
+  "authorities" : [ ],
+  "credentialsNonExpired" : true,
+  "accountNonExpired" : true,
+  "accountNonLocked" : true,
+  "username" : "jane@test.com",
+  "enabled" : true
+} ]
+
+
+
+

It gives informations about all users.

+
+
+
+

Promote User to Manager

+
+

This is an example output for the PUT /users/{userId}/promote-manager endpoint.

+
+
+
request
+
+
PUT /users/2/promote-manager HTTP/1.1
+Accept: application/json
+Host: localhost:8080
+Content-Type: application/x-www-form-urlencoded
+
+
+
+
response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 37
+
+User promoted to manager successfully
+
+
+
+

It promotes a user to the "MANAGER" role.

+
+
+
+
+
+ + + \ No newline at end of file diff --git a/env-dist b/env-dist index 40f46805..95150116 100644 --- a/env-dist +++ b/env-dist @@ -15,4 +15,11 @@ TEST_SPRING_DATASOURCE_URL=jdbc:mariadb://db:3306/test_db # User name and password used by Docker to create the database container DB_USERNAME=root -DB_PASSWORD=pwd \ No newline at end of file +DB_PASSWORD=pwd + +# Azure OAuth2 settings +# Set these values according to your Azure app registration +AZURE_REDIRECT_BASE_URL=http://localhost:8080 +AZURE_TENANT_ID= +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= \ No newline at end of file diff --git a/pom.xml b/pom.xml index 120cba4b..22cf4f63 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -11,7 +11,7 @@ ch.sectioninformatique spring-auth - 0.1.0 + 0.1.1-SNAPSHOT spring-auth Authentication API to be used by other applications to identify their users @@ -37,6 +37,12 @@ dotenv-java 3.0.0 + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + org.springframework.boot spring-boot-starter-data-jpa @@ -46,7 +52,7 @@ spring-boot-starter-web - org.springframework.boot + org.springframework.boot spring-boot-starter-validation @@ -57,7 +63,6 @@ org.projectlombok lombok - 1.18.36 provided @@ -81,6 +86,12 @@ org.springframework.boot spring-boot-starter-test test + + + org.junit.vintage + junit-vintage-engine + + org.springframework.security @@ -95,7 +106,6 @@ jakarta.validation jakarta.validation-api - 3.0.2 org.mapstruct @@ -121,6 +131,7 @@ 4.0.0 provided + @@ -143,6 +154,67 @@ + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.1 + + + generate-docs + prepare-package + + process-asciidoc + + + html + book + + + + convert-to-html + generate-resources + + process-asciidoc + + + src/asciidoc + index.adoc + /app/build/generated-snippets-html + + + + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + + + + maven-resources-plugin + + + copy-resources + prepare-package + + copy-resources + + + + ${project.build.outputDirectory}/static/docs + + + + + ${project.build.directory}/generated-docs + + + + + + + diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc new file mode 100644 index 00000000..b3c67a86 --- /dev/null +++ b/src/asciidoc/index.adoc @@ -0,0 +1,37 @@ += Spring Auth API Documentation + +== User Endpoints + +=== Get Authenticated User +This is an example output for the `GET /users/me` endpoint. + +.request +include::users/me/http-request.adoc[] + +.response +include::users/me/http-response.adoc[] + +It gives informations about the authenticated user. + + +=== Get All Users +This is an example output for the `GET /users/all` endpoint. + +.request +include::users/all/http-request.adoc[] + +.response +include::users/all/http-response.adoc[] + +It gives informations about all users. + +=== Promote User to Manager +This is an example output for the `PUT /users/{userId}/promote-manager` endpoint. + +.request +include::users/promote-manager/http-request.adoc[] + +.response +include::users/promote-manager/http-response.adoc[] + +It promotes a user to the "MANAGER" role. diff --git a/src/asciidoc/users/all/curl-request.adoc b/src/asciidoc/users/all/curl-request.adoc new file mode 100644 index 00000000..034c877c --- /dev/null +++ b/src/asciidoc/users/all/curl-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/users/all' -i -X GET \ + -H 'Accept: application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/all/http-request.adoc b/src/asciidoc/users/all/http-request.adoc new file mode 100644 index 00000000..507cb9e6 --- /dev/null +++ b/src/asciidoc/users/all/http-request.adoc @@ -0,0 +1,7 @@ +[source,http,options="nowrap"] +---- +GET /users/all HTTP/1.1 +Accept: application/json +Host: localhost:8080 + +---- \ No newline at end of file diff --git a/src/asciidoc/users/all/http-response.adoc b/src/asciidoc/users/all/http-response.adoc new file mode 100644 index 00000000..503ff03f --- /dev/null +++ b/src/asciidoc/users/all/http-response.adoc @@ -0,0 +1,38 @@ +[source,http,options="nowrap"] +---- +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 698 + +[ { + "id" : 1, + "firstName" : "John", + "lastName" : "Doe", + "login" : "john@test.com", + "password" : "pass", + "createdAt" : null, + "updatedAt" : null, + "mainRole" : null, + "authorities" : [ ], + "accountNonExpired" : true, + "accountNonLocked" : true, + "credentialsNonExpired" : true, + "username" : "john@test.com", + "enabled" : true +}, { + "id" : 2, + "firstName" : "Jane", + "lastName" : "Smith", + "login" : "jane@test.com", + "password" : "pass", + "createdAt" : null, + "updatedAt" : null, + "mainRole" : null, + "authorities" : [ ], + "accountNonExpired" : true, + "accountNonLocked" : true, + "credentialsNonExpired" : true, + "username" : "jane@test.com", + "enabled" : true +} ] +---- \ No newline at end of file diff --git a/src/asciidoc/users/all/httpie-request.adoc b/src/asciidoc/users/all/httpie-request.adoc new file mode 100644 index 00000000..9ee9879c --- /dev/null +++ b/src/asciidoc/users/all/httpie-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ http GET 'http://localhost:8080/users/all' \ + 'Accept:application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/all/request-body.adoc b/src/asciidoc/users/all/request-body.adoc new file mode 100644 index 00000000..dab5f81d --- /dev/null +++ b/src/asciidoc/users/all/request-body.adoc @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- + +---- \ No newline at end of file diff --git a/src/asciidoc/users/all/response-body.adoc b/src/asciidoc/users/all/response-body.adoc new file mode 100644 index 00000000..42079be6 --- /dev/null +++ b/src/asciidoc/users/all/response-body.adoc @@ -0,0 +1,34 @@ +[source,json,options="nowrap"] +---- +[ { + "id" : 1, + "firstName" : "John", + "lastName" : "Doe", + "login" : "john@test.com", + "password" : "pass", + "createdAt" : null, + "updatedAt" : null, + "mainRole" : null, + "authorities" : [ ], + "accountNonExpired" : true, + "accountNonLocked" : true, + "credentialsNonExpired" : true, + "username" : "john@test.com", + "enabled" : true +}, { + "id" : 2, + "firstName" : "Jane", + "lastName" : "Smith", + "login" : "jane@test.com", + "password" : "pass", + "createdAt" : null, + "updatedAt" : null, + "mainRole" : null, + "authorities" : [ ], + "accountNonExpired" : true, + "accountNonLocked" : true, + "credentialsNonExpired" : true, + "username" : "jane@test.com", + "enabled" : true +} ] +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/curl-request.adoc b/src/asciidoc/users/me/curl-request.adoc new file mode 100644 index 00000000..95b249a1 --- /dev/null +++ b/src/asciidoc/users/me/curl-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/users/me' -i -X GET \ + -H 'Accept: application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/http-request.adoc b/src/asciidoc/users/me/http-request.adoc new file mode 100644 index 00000000..79620cb9 --- /dev/null +++ b/src/asciidoc/users/me/http-request.adoc @@ -0,0 +1,7 @@ +[source,http,options="nowrap"] +---- +GET /users/me HTTP/1.1 +Accept: application/json +Host: localhost:8080 + +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/http-response.adoc b/src/asciidoc/users/me/http-response.adoc new file mode 100644 index 00000000..7044ca7f --- /dev/null +++ b/src/asciidoc/users/me/http-response.adoc @@ -0,0 +1,17 @@ +[source,http,options="nowrap"] +---- +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 179 + +{ + "id" : 1, + "firstName" : "John", + "lastName" : "Doe", + "login" : "john@test.com", + "token" : null, + "refreshToken" : null, + "mainRole" : "USER", + "permissions" : null +} +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/httpie-request.adoc b/src/asciidoc/users/me/httpie-request.adoc new file mode 100644 index 00000000..6200d74c --- /dev/null +++ b/src/asciidoc/users/me/httpie-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ http GET 'http://localhost:8080/users/me' \ + 'Accept:application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/request-body.adoc b/src/asciidoc/users/me/request-body.adoc new file mode 100644 index 00000000..dab5f81d --- /dev/null +++ b/src/asciidoc/users/me/request-body.adoc @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- + +---- \ No newline at end of file diff --git a/src/asciidoc/users/me/response-body.adoc b/src/asciidoc/users/me/response-body.adoc new file mode 100644 index 00000000..20e094a3 --- /dev/null +++ b/src/asciidoc/users/me/response-body.adoc @@ -0,0 +1,13 @@ +[source,json,options="nowrap"] +---- +{ + "id" : 1, + "firstName" : "John", + "lastName" : "Doe", + "login" : "john@test.com", + "token" : null, + "refreshToken" : null, + "mainRole" : "USER", + "permissions" : null +} +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/curl-request.adoc b/src/asciidoc/users/promote-manager/curl-request.adoc new file mode 100644 index 00000000..92a731c0 --- /dev/null +++ b/src/asciidoc/users/promote-manager/curl-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/users/2/promote-manager' -i -X PUT \ + -H 'Accept: application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/http-request.adoc b/src/asciidoc/users/promote-manager/http-request.adoc new file mode 100644 index 00000000..587ea3a2 --- /dev/null +++ b/src/asciidoc/users/promote-manager/http-request.adoc @@ -0,0 +1,8 @@ +[source,http,options="nowrap"] +---- +PUT /users/2/promote-manager HTTP/1.1 +Accept: application/json +Host: localhost:8080 +Content-Type: application/x-www-form-urlencoded + +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/http-response.adoc b/src/asciidoc/users/promote-manager/http-response.adoc new file mode 100644 index 00000000..b07b7150 --- /dev/null +++ b/src/asciidoc/users/promote-manager/http-response.adoc @@ -0,0 +1,8 @@ +[source,http,options="nowrap"] +---- +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 37 + +User promoted to manager successfully +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/httpie-request.adoc b/src/asciidoc/users/promote-manager/httpie-request.adoc new file mode 100644 index 00000000..fae8140e --- /dev/null +++ b/src/asciidoc/users/promote-manager/httpie-request.adoc @@ -0,0 +1,5 @@ +[source,bash] +---- +$ http PUT 'http://localhost:8080/users/2/promote-manager' \ + 'Accept:application/json' +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/request-body.adoc b/src/asciidoc/users/promote-manager/request-body.adoc new file mode 100644 index 00000000..dab5f81d --- /dev/null +++ b/src/asciidoc/users/promote-manager/request-body.adoc @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- + +---- \ No newline at end of file diff --git a/src/asciidoc/users/promote-manager/response-body.adoc b/src/asciidoc/users/promote-manager/response-body.adoc new file mode 100644 index 00000000..57621213 --- /dev/null +++ b/src/asciidoc/users/promote-manager/response-body.adoc @@ -0,0 +1,4 @@ +[source,json,options="nowrap"] +---- +User promoted to manager successfully +---- \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f418ca24..89fb601b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -47,6 +47,6 @@ spring.security.oauth2.client.registration.azure.client-id=${AZURE_CLIENT_ID} spring.security.oauth2.client.registration.azure.client-secret=${AZURE_CLIENT_SECRET} spring.security.oauth2.client.registration.azure.client-authentication-method=client_secret_post spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} +spring.security.oauth2.client.registration.azure.redirect-uri=${AZURE_REDIRECT_BASE_URL}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.azure.scope=openid,profile,email,User.Read spring.security.oauth2.client.registration.azure.client-name=Azure diff --git a/src/test/java/ch/sectioninformatique/auth/security/UserAuthenticationProviderTest.java b/src/test/java/ch/sectioninformatique/auth/security/UserAuthenticationProviderTest.java index 8ab5ea48..bfb58fd9 100644 --- a/src/test/java/ch/sectioninformatique/auth/security/UserAuthenticationProviderTest.java +++ b/src/test/java/ch/sectioninformatique/auth/security/UserAuthenticationProviderTest.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Base64; +import java.util.ArrayList; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/src/test/java/ch/sectioninformatique/auth/user/UserControllerTest.java b/src/test/java/ch/sectioninformatique/auth/user/UserControllerTest.java index 4c5524bd..2bdac36e 100644 --- a/src/test/java/ch/sectioninformatique/auth/user/UserControllerTest.java +++ b/src/test/java/ch/sectioninformatique/auth/user/UserControllerTest.java @@ -1,303 +1,122 @@ package ch.sectioninformatique.auth.user; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.http.ResponseEntity; +import org.mockito.Mockito; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import ch.sectioninformatique.auth.security.RoleRepository; +import ch.sectioninformatique.auth.security.UserAuthenticationProvider; import org.springframework.http.HttpStatus; -import java.util.Arrays; -import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -public class UserControllerTest { +import java.util.Arrays; +import java.util.List; - @Mock +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@ExtendWith(RestDocumentationExtension.class) +@WebMvcTest(controllers = UserController.class, excludeAutoConfiguration = { SecurityAutoConfiguration.class, + OAuth2ClientAutoConfiguration.class }) +@AutoConfigureMockMvc(addFilters = false) +@AutoConfigureRestDocs(outputDir = "build/generated-snippets") +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean private UserService userService; - @Mock - private SecurityContext securityContext; - - @Mock - private Authentication authentication; - - @Mock - private UserRepository userRepository; - - @Mock - private RoleRepository roleRepository; - - @Mock + @MockBean private UserMapper userMapper; - @InjectMocks - private UserController userController; - - @BeforeEach - void setUp() { - SecurityContextHolder.setContext(securityContext); - } - - @Test - void authenticatedUser_ReturnsCurrentUser() { - // Arrange - UserDto currentUser = new UserDto(1L, "John", "Doe", "john@test.com", null, null, "USER", null); - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(currentUser); - - // Act - ResponseEntity response = userController.authenticatedUser(); - - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals(currentUser, response.getBody()); - } - - @Test - void allUsers_ReturnsListOfUsers() { - // Arrange - List users = Arrays.asList( - new User(1L, "John", "Doe", "john@test.com", "pass", null, null, null), - new User(2L, "Jane", "Smith", "jane@test.com", "pass", null, null, null) - ); - when(userService.allUsers()).thenReturn(users); - - // Act - ResponseEntity> response = userController.allUsers(); - - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals(users, response.getBody()); - } - - @Test - void promoteToManager_Successful_ReturnsUserDto() { - // Arrange - Long userId = 1L; - UserDto expectedDto = new UserDto(1L, "John", "Doe", "john@test.com", null, null, "ROLE_MANAGER", null); - - when(userService.promoteToManager(userId)).thenReturn(expectedDto); - - // Act - ResponseEntity response = userController.promoteToManager(userId); - - // Assert - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals("User promoted to manager successfully", response.getBody()); - verify(userService).promoteToManager(userId); - } - - @Test - void promoteToManager_UserNotFound_ReturnsBadRequest() { - // Arrange - Long userId = 1L; - doThrow(new RuntimeException("User not found")).when(userService).promoteToManager(userId); - - // Act - ResponseEntity response = userController.promoteToManager(userId); - - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).promoteToManager(userId); - } - - @Test - void revokeManagerRole_SuccessfulRevocation_ReturnsOkResponse() { - // Arrange - Long userId = 1L; - doNothing().when(userService).revokeManagerRole(userId); - - // Act - ResponseEntity response = userController.revokeManagerRole(userId); - - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals("Manager role revoked successfully", response.getBody()); - verify(userService, times(1)).revokeManagerRole(userId); - } - - @Test - void revokeManagerRole_UserNotFound_ReturnsBadRequest() { - // Arrange - Long userId = 1L; - doThrow(new RuntimeException("User not found")).when(userService).revokeManagerRole(userId); - - // Act - ResponseEntity response = userController.revokeManagerRole(userId); - - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).revokeManagerRole(userId); - } - - @Test - void promoteToSuperManager_SuccessfulPromotion_ReturnsOkResponse() { - // Arrange - Long userId = 1L; - UserDto expectedUser = new UserDto(userId, "John", "Doe", "john@test.com", null, null, "ROLE_ADMIN", null); - when(userService.promoteToAdmin(userId)).thenReturn(expectedUser); - - // Act - ResponseEntity response = userController.promoteToAdmin(userId); - - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals("Admin role assigned successfully", response.getBody()); - verify(userService, times(1)).promoteToAdmin(userId); - } - - @Test - void promoteToAdmin_UserNotFound_ReturnsBadRequest() { - // Arrange - Long userId = 1L; - doThrow(new RuntimeException("User not found")).when(userService).promoteToAdmin(userId); - - // Act - ResponseEntity response = userController.promoteToAdmin(userId); - - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).promoteToAdmin(userId); - } - - @Test - void revokeAdminRole_SuccessfulRevocation_ReturnsOkResponse() { - // Arrange - Long userId = 1L; - doNothing().when(userService).revokeAdminRole(userId); + @MockBean + private RoleRepository roleRepository; - // Act - ResponseEntity response = userController.revokeAdminRole(userId); + @MockBean + private UserRepository userRepository; - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals("Admin role revoked successfully", response.getBody()); - verify(userService, times(1)).revokeAdminRole(userId); - } + @MockBean + private UserAuthenticationProvider userAuthenticationProvider; @Test - void revokeAdminRole_UserNotFound_ReturnsBadRequest() { - // Arrange - Long userId = 1L; - doThrow(new RuntimeException("User not found")).when(userService).revokeAdminRole(userId); + void authenticatedUser_ReturnsCurrentUser_Doc() throws Exception { - // Act - ResponseEntity response = userController.revokeAdminRole(userId); + UserDto mockUser = new UserDto(1L, "John", "Doe", "john@test.com", null, null, "USER", null); - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).revokeAdminRole(userId); - } + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getPrincipal()).thenReturn(mockUser); - @Test - void downgradeAdminRole_SuccessfulDowngrade_ReturnsOkResponse() { - // Arrange - Long userId = 1L; - doNothing().when(userService).downgradeAdminRole(userId); + SecurityContext securityContext = Mockito.mock(SecurityContext.class); + Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); - // Act - ResponseEntity response = userController.downgradeAdminRole(userId); + SecurityContextHolder.setContext(securityContext); - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals("Admin role downgraded successfully", response.getBody()); - verify(userService, times(1)).downgradeAdminRole(userId); + this.mockMvc.perform(get("/users/me") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("users/me", preprocessResponse(prettyPrint()))); } @Test - void downgradeAdminRole_UserNotFound_ReturnsBadRequest() { + void allUsers_ReturnsListOfUsers_Doc() throws Exception { // Arrange - Long userId = 1L; - doThrow(new RuntimeException("User not found")).when(userService).downgradeAdminRole(userId); - - // Act - ResponseEntity response = userController.downgradeAdminRole(userId); + List users = Arrays.asList( + new User(1L, "John", "Doe", "john@test.com", "pass", null, null, null), + new User(2L, "Jane", "Smith", "jane@test.com", "pass", null, null, null)); + Mockito.when(userService.allUsers()).thenReturn(users); - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).downgradeAdminRole(userId); + this.mockMvc.perform(get("/users/all") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("users/all", preprocessResponse(prettyPrint()))); } @Test - void deleteUser_SuccessfulDeletion_ReturnsOkResponse() { - // Arrange - Long userId = 1L; - UserDto authenticatedUser = new UserDto(userId, null, null, null, null, null, null, null); - authenticatedUser.setLogin("manager@test.com"); - authenticatedUser.setMainRole("ROLE_MANAGER"); + void promoteToManager_Successful_ReturnsUserDto_Doc() throws Exception { + UserDto mockUser = new UserDto(1L, "John", "Doe", "john@test.com", null, null, "Admin", null); - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(authenticatedUser); - doNothing().when(userService).deleteUser(userId); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getPrincipal()).thenReturn(mockUser); - // Act - ResponseEntity response = userController.deleteUser(userId); + SecurityContext securityContext = Mockito.mock(SecurityContext.class); + Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); - // Assert - assertEquals(200, response.getStatusCode().value()); - assertEquals("User deleted successfully", response.getBody()); - verify(userService, times(1)).deleteUser(userId); - } - - @Test - void deleteUser_UserNotFound_ReturnsBadRequest() { + SecurityContextHolder.setContext(securityContext); // Arrange - Long userId = 1L; - UserDto authenticatedUser = new UserDto(userId, null, null, null, null, null, null, null); - authenticatedUser.setLogin("manager@test.com"); - authenticatedUser.setMainRole("ROLE_MANAGER"); + Long userId = 2L; + UserDto expectedDto = new UserDto(2L, "Jane", "Smith", "jane@test.com", null, null, "MANAGER", null); - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(authenticatedUser); - doThrow(new RuntimeException("User not found")).when(userService).deleteUser(userId); - - // Act - ResponseEntity response = userController.deleteUser(userId); + when(userService.promoteToManager(userId)).thenReturn(expectedDto); - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("User not found", response.getBody()); - verify(userService, times(1)).deleteUser(userId); + this.mockMvc.perform(put("/users/{userId}/promote-manager", userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("users/promote-manager", preprocessResponse(prettyPrint()))); } - @Test - void deleteUser_UnauthorizedAccess_ReturnsBadRequest() { - // Arrange - Long userId = 1L; - UserDto authenticatedUser = new UserDto(userId, null, null, null, null, null, null, null); - authenticatedUser.setLogin("user@test.com"); - authenticatedUser.setMainRole("USER"); - - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.getPrincipal()).thenReturn(authenticatedUser); - doThrow(new RuntimeException("You don't have the necessary rights to perform this action")) - .when(userService).deleteUser(userId); - - // Act - ResponseEntity response = userController.deleteUser(userId); - - // Assert - assertEquals(400, response.getStatusCode().value()); - assertEquals("You don't have the necessary rights to perform this action", response.getBody()); - verify(userService, times(1)).deleteUser(userId); - } -} \ No newline at end of file +}