From cbd791713469d8e5c6a147eb3f776ecfa3242e9a Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:26:25 +0200 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9E=95=20:=20add=20spring-security-tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 1df9f9e..2fbedfa 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,11 @@ test + + org.springframework.security + spring-security-test + + org.springframework.boot -- 2.51.2 From 8d1a3e6ed28ffa090a3101b8957b1d8903d97845 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:26:41 +0200 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=94=A7=20:=20do=20not=20use=20flywa?= =?UTF-8?q?y=20when=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 93e7d05..bef1c43 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -13,3 +13,5 @@ spring.security.oauth2.client.registration.gitlab.client-secret=${gitlab.client- spring.security.oauth2.client.registration.gitlab.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.gitlab.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.gitlab.scope[0]=api + +spring.flyway.enabled=false -- 2.51.2 From dd8d47cbffd071a11406f8e6f9c68ad5b33d3a1e Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:44:43 +0200 Subject: [PATCH 03/14] =?UTF-8?q?=E2=9C=85=20:=20test=20role=20enforcement?= =?UTF-8?q?=20for=20quiz=20edition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classrooms/ui/QuizControllerMVCTest.java | 62 +++++++++++++++++++ .../classrooms/ui/WithMockClassroomUser.java | 16 +++++ ...ckClassroomUserSecurityContextFactory.java | 36 +++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUser.java create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUserSecurityContextFactory.java diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java new file mode 100644 index 0000000..68329cd --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java @@ -0,0 +1,62 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class QuizControllerMVCTest { + + @Autowired + private MockMvc mockMvc; + + @Nested + @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) + class TeacherRole { + + @Test + void shouldAccessQuizListPage() throws Exception { + mockMvc.perform(get("/quiz")) + .andDo(print()) + .andExpect(status().isOk()); + } + + @Test + void shouldAccessNewQuizPage() throws Exception { + mockMvc.perform(get("/quiz/new/edit")) + .andDo(print()) + .andExpect(status().isOk()); + } + + } + + @Nested + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + class StudentRole { + + @Test + void shouldNotAccessQuizListPage() throws Exception { + mockMvc.perform(get("/quiz")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + void shouldNotAccessNewQuizPage() throws Exception { + mockMvc.perform(get("/quiz/new/edit")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + } + +} diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUser.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUser.java new file mode 100644 index 0000000..0503f57 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUser.java @@ -0,0 +1,16 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockClassroomUserSecurityContextFactory.class) +public @interface WithMockClassroomUser { + String username() default "luke.skywalker"; + + ClassroomRole[] roles() default {ClassroomRole.STUDENT}; + +} diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUserSecurityContextFactory.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUserSecurityContextFactory.java new file mode 100644 index 0000000..15c529c --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/WithMockClassroomUserSecurityContextFactory.java @@ -0,0 +1,36 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +import java.util.Arrays; +import java.util.Map; + +public class WithMockClassroomUserSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockClassroomUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + var authorities = Arrays.stream(customUser.roles()) + .map(it -> new SimpleGrantedAuthority("ROLE_" + it.name())) + .toList(); + + Map attributes = Map.of( + "name", customUser.username(), + "avatar_url", "" + ); + + // create a fake gitlab user + OAuth2User principal = new DefaultOAuth2User(authorities, attributes, "name"); + var auth = new OAuth2AuthenticationToken(principal, principal.getAuthorities(), "gitlab"); + + context.setAuthentication(auth); + return context; + } +} -- 2.51.2 From e4a09d2c74cd583cf498b5b9762ce8dd163a2351 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:53:34 +0200 Subject: [PATCH 04/14] =?UTF-8?q?=E2=9C=85=20:=20test=20role=20enforcement?= =?UTF-8?q?=20for=20home=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classrooms/ui/HomeControllerMVCTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/HomeControllerMVCTest.java diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/HomeControllerMVCTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/HomeControllerMVCTest.java new file mode 100644 index 0000000..fd9bc57 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/HomeControllerMVCTest.java @@ -0,0 +1,50 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRepository; +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class HomeControllerMVCTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ClassroomRepository classroomRepository; + + @Nested + @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) + class TeacherRole { + + @Test + void shouldAccessHomePage() throws Exception { + mockMvc.perform(get("/")) + .andDo(print()) + .andExpect(status().isOk()); + } + } + + @Nested + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + class StudentRole { + + @Test + void shouldNotAccessHomePage() throws Exception { + mockMvc.perform(get("/")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + } +} -- 2.51.2 From 3965f6b1f23a944de7c5da00bf500e2f342827ed Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:56:05 +0200 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=85=20:=20test=20role=20enforcement?= =?UTF-8?q?=20for=20new=20classroom=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classrooms/ui/ClassroomController.java | 2 + .../ui/ClassroomControllerMVCTest.java | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomControllerMVCTest.java diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomController.java b/src/main/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomController.java index 8327655..fc56b8e 100644 --- a/src/main/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomController.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomController.java @@ -2,6 +2,7 @@ package fr.univ_lille.gitlab.classrooms.ui; import fr.univ_lille.gitlab.classrooms.domain.Classroom; import fr.univ_lille.gitlab.classrooms.domain.ClassroomRepository; +import jakarta.annotation.security.RolesAllowed; import org.gitlab4j.api.GitLabApiException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; @@ -15,6 +16,7 @@ import java.util.UUID; @Controller @RequestMapping("/classrooms") +@RolesAllowed("TEACHER") public class ClassroomController { private ClassroomRepository classroomRepository; diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomControllerMVCTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomControllerMVCTest.java new file mode 100644 index 0000000..64b7368 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/ClassroomControllerMVCTest.java @@ -0,0 +1,46 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class ClassroomControllerMVCTest { + + @Autowired + private MockMvc mockMvc; + + @Nested + @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) + class TeacherRole { + + @Test + void shouldAccessNewClassroomPage() throws Exception { + mockMvc.perform(get("/classrooms/new")) + .andDo(print()) + .andExpect(status().isOk()); + } + } + + @Nested + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + class StudentRole { + + @Test + void shouldNotAccessHomePage() throws Exception { + mockMvc.perform(get("/classrooms/new")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + } + +} -- 2.51.2 From 65ca38aa48f7b0bbfbd2dbb69b81408a6daaba85 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:09:09 +0200 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=91=B7=20:=20add=20deployment=20to?= =?UTF-8?q?=20clever-cloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6d23cae..a193ada 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,3 +29,16 @@ verify: stage: test script: - 'mvn $MAVEN_CLI_OPTS verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar $SONAR_CLI_OPTS' + +deploy: + image: clevercloud/clever-tools + stage: deploy + rules: + - if: $CI_COMMIT_REF_NAME == 'main' + script: + - clever deploy + environment: + name: production + deployment_tier: production + url: https://gitlab-classrooms.cleverapps.io/ + action: start -- 2.51.2 From 32b758fe354e8da293e02cfe7a2d091c473cc9dc Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:26:25 +0200 Subject: [PATCH 07/14] =?UTF-8?q?=E2=9E=95=20:=20add=20spring-security-tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 2fbedfa..dc1f753 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ org.springframework.security spring-security-test + test -- 2.51.2 From 19fd88538d762b446c4f71de1188320811b6d552 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 11:44:43 +0200 Subject: [PATCH 08/14] =?UTF-8?q?=E2=9C=85=20:=20test=20role=20enforcement?= =?UTF-8?q?=20for=20quiz=20edition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classrooms/ui/QuizControllerTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java new file mode 100644 index 0000000..b36920a --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java @@ -0,0 +1,64 @@ +package fr.univ_lille.gitlab.classrooms.ui; + +import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class QuizControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Nested + @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) + class TeacherRole { + + @Test + void shouldAccessQuizListPage() throws Exception { + mockMvc.perform(get("/quiz")) + .andDo(print()) + .andExpect(status().isOk()); + } + + @Test + void shouldAccessNewQuizPage() throws Exception { + mockMvc.perform(get("/quiz/new/edit")) + .andDo(print()) + .andExpect(status().isOk()); + } + + } + + @Nested + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + class StudentRole { + + @Test + void shouldNotAccessQuizListPage() throws Exception { + mockMvc.perform(get("/quiz")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + void shouldNotAccessNewQuizPage() throws Exception { + mockMvc.perform(get("/quiz/new/edit")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + } + +} -- 2.51.2 From 0ac9e880b55c264cc5d8d3e8c9bb6d01ecc3c90d Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:17:17 +0200 Subject: [PATCH 09/14] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20:=20upgrade=20spring?= =?UTF-8?q?-boot-starter-parent=20to=203.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dc1f753..f873177 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.0.5 + 3.1.2 fr.univ-lille -- 2.51.2 From bfd4e87832957f13fea3f77c06de6ff7f59a5ef0 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:21:14 +0200 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=94=A5=20:=20delete=20unused=20mock?= =?UTF-8?q?=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mock/ClassroomRepositoryGitlabImpl.java | 1 - .../mock/ClassroomRepositoryMock.java | 24 ------------------- 2 files changed, 25 deletions(-) delete mode 100644 src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryMock.java diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java b/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java index fff593b..1e29a3c 100644 --- a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Repository; * Implementation of the Classroom repository that uses the Gitlab API */ @Repository -@Primary class ClassroomRepositoryGitlabImpl implements ClassroomRepository { GitLabApi gitLabApi; diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryMock.java b/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryMock.java deleted file mode 100644 index 606dfe4..0000000 --- a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryMock.java +++ /dev/null @@ -1,24 +0,0 @@ -package fr.univ_lille.gitlab.classrooms.mock; - -import fr.univ_lille.gitlab.classrooms.domain.Classroom; -import fr.univ_lille.gitlab.classrooms.domain.ClassroomRepository; -import org.springframework.stereotype.Repository; - -import java.util.HashMap; -import java.util.Map; - -@Repository -class ClassroomRepositoryMock implements ClassroomRepository { - - Map classrooms = new HashMap<>(); - - @Override - public Iterable findAllClassrooms() { - return classrooms.values(); - } - - @Override - public void saveClassroom(Classroom classroom) { - this.classrooms.put(classroom.id(), classroom); - } -} -- 2.51.2 From 1d7a38ff85d4f3d862f1af63ddd3d2e046274f1b Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:24:58 +0200 Subject: [PATCH 11/14] =?UTF-8?q?=E2=9C=85=20:=20add=20main=20test=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GitlabClassroomsApplicationTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/test/java/fr/univ_lille/gitlab/classrooms/GitlabClassroomsApplicationTest.java diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/GitlabClassroomsApplicationTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/GitlabClassroomsApplicationTest.java new file mode 100644 index 0000000..5941d59 --- /dev/null +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/GitlabClassroomsApplicationTest.java @@ -0,0 +1,14 @@ +package fr.univ_lille.gitlab.classrooms; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class GitlabClassroomsApplicationTest { + + @Test + void contextLoads() { + } +} \ No newline at end of file -- 2.51.2 From 7b50438d39e216479369c26c4cefd246bf789b0d Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:26:01 +0200 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=9A=9A=20:=20move=20file=20to=20git?= =?UTF-8?q?lab=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{mock => gitlab}/ClassroomRepositoryGitlabImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/fr/univ_lille/gitlab/classrooms/{mock => gitlab}/ClassroomRepositoryGitlabImpl.java (96%) diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java b/src/main/java/fr/univ_lille/gitlab/classrooms/gitlab/ClassroomRepositoryGitlabImpl.java similarity index 96% rename from src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java rename to src/main/java/fr/univ_lille/gitlab/classrooms/gitlab/ClassroomRepositoryGitlabImpl.java index 1e29a3c..c1e2089 100644 --- a/src/main/java/fr/univ_lille/gitlab/classrooms/mock/ClassroomRepositoryGitlabImpl.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/gitlab/ClassroomRepositoryGitlabImpl.java @@ -1,4 +1,4 @@ -package fr.univ_lille.gitlab.classrooms.mock; +package fr.univ_lille.gitlab.classrooms.gitlab; import fr.univ_lille.gitlab.classrooms.domain.Classroom; import fr.univ_lille.gitlab.classrooms.domain.ClassroomRepository; -- 2.51.2 From 55497c808a74bf69182ee56ca30dbf103846848e Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 15:27:19 +0200 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=94=A5=20:=20remove=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/fr/univ_lille/gitlab/classrooms/quiz/Quiz.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/fr/univ_lille/gitlab/classrooms/quiz/Quiz.java b/src/main/java/fr/univ_lille/gitlab/classrooms/quiz/Quiz.java index 319ce06..7723960 100644 --- a/src/main/java/fr/univ_lille/gitlab/classrooms/quiz/Quiz.java +++ b/src/main/java/fr/univ_lille/gitlab/classrooms/quiz/Quiz.java @@ -34,12 +34,6 @@ public class Quiz { return questions; } - public void reset() { - for (Question question : questions) { - question.reset(); - } - } - /** * Ugly method to answer questions from a form submission * -- 2.51.2 From 8ca6775882a8cfddd4263bc926a8883b00aae244 Mon Sep 17 00:00:00 2001 From: Julien WITTOUCK Date: Thu, 3 Aug 2023 16:02:54 +0200 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9C=85=20:=20fully=20test=20MVC=20for?= =?UTF-8?q?=20QuizController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classrooms/ui/QuizControllerMVCTest.java | 93 ++++++++++++++- .../classrooms/ui/QuizControllerTest.java | 110 ++++++++++-------- 2 files changed, 155 insertions(+), 48 deletions(-) diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java index 68329cd..fc82ba3 100644 --- a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerMVCTest.java @@ -1,16 +1,28 @@ package fr.univ_lille.gitlab.classrooms.ui; import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; +import fr.univ_lille.gitlab.classrooms.quiz.QuizEntity; +import fr.univ_lille.gitlab.classrooms.quiz.QuizRepository; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.Optional; + +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @@ -19,6 +31,9 @@ class QuizControllerMVCTest { @Autowired private MockMvc mockMvc; + @MockBean + private QuizRepository quizRepository; + @Nested @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) class TeacherRole { @@ -37,6 +52,41 @@ class QuizControllerMVCTest { .andExpect(status().isOk()); } + @Test + void shouldAccessQuizEditPage() throws Exception { + var quiz = new QuizEntity(); + quiz.setName("death-star-quiz"); + quiz.setMarkdownContent(""" + # who build the Death Star ? + (x) the Galactic Empire + ( ) Sauron + """); + when(quizRepository.findById("death-star-quiz")).thenReturn(Optional.of(quiz)); + + mockMvc.perform(get("/quiz/death-star-quiz/edit")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("quiz/edit")) + .andExpect(model().attributeExists("quiz")) + .andExpect(model().attribute("quiz", hasProperty("name", equalTo("death-star-quiz")))); + } + + @Test + void shouldSaveQuiz() throws Exception { + var quiz = new QuizEntity(); + quiz.setName("death-star-quiz"); + quiz.setMarkdownContent(""" + # who build the Death Star ? + (x) the Galactic Empire + ( ) Sauron + """); + when(quizRepository.findById("death-star-quiz")).thenReturn(Optional.of(quiz)); + + mockMvc.perform(post("/quiz/death-star-quiz/edit").with(csrf())) + .andDo(print()) + .andExpect(status().isOk()); + } + } @Nested @@ -57,6 +107,47 @@ class QuizControllerMVCTest { .andExpect(status().isForbidden()); } + @Test + void shouldNotAccessQuizEditPage() throws Exception { + mockMvc.perform(get("/quiz/death-star-quiz/edit")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + @Test + void shouldNotSaveQuiz() throws Exception { + mockMvc.perform(post("/quiz/death-star-quiz/edit")) + .andDo(print()) + .andExpect(status().isForbidden()); + } + + } + + @Test + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + void shouldReturn404_whenQuizDoesNotExists() throws Exception { + mockMvc.perform(get("/quiz/unknown")) + .andDo(print()) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) + void shouldReturnQuizPage_whenQuizExists() throws Exception { + var quiz = new QuizEntity(); + quiz.setName("death-star-quiz"); + quiz.setMarkdownContent(""" + # who build the Death Star ? + (x) the Galactic Empire + ( ) Sauron + """); + when(quizRepository.findById("death-star-quiz")).thenReturn(Optional.of(quiz)); + + mockMvc.perform(get("/quiz/death-star-quiz")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("quiz")) + .andExpect(model().attribute("quiz", notNullValue())); } } diff --git a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java index b36920a..8bee74e 100644 --- a/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java +++ b/src/test/java/fr/univ_lille/gitlab/classrooms/ui/QuizControllerTest.java @@ -1,64 +1,80 @@ package fr.univ_lille.gitlab.classrooms.ui; -import fr.univ_lille.gitlab.classrooms.domain.ClassroomRole; -import org.junit.jupiter.api.Nested; +import fr.univ_lille.gitlab.classrooms.quiz.Quiz; +import fr.univ_lille.gitlab.classrooms.quiz.QuizEntity; +import fr.univ_lille.gitlab.classrooms.quiz.QuizRepository; +import fr.univ_lille.gitlab.classrooms.quiz.QuizScoreRepository; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; - -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.ui.ConcurrentModel; +import org.springframework.ui.Model; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) class QuizControllerTest { - @Autowired - private MockMvc mockMvc; + @InjectMocks + private QuizController quizController; + + @Mock + private QuizRepository quizRepository; - @Nested - @WithMockClassroomUser(username = "obiwan.kenobi", roles = {ClassroomRole.TEACHER}) - class TeacherRole { + @Mock + private QuizScoreRepository quizScoreRepository; - @Test - void shouldAccessQuizListPage() throws Exception { - mockMvc.perform(get("/quiz")) - .andDo(print()) - .andExpect(status().isOk()); - } + @Mock + private Authentication authentication; - @Test - void shouldAccessNewQuizPage() throws Exception { - mockMvc.perform(get("/quiz/new/edit")) - .andDo(print()) - .andExpect(status().isOk()); - } + @Mock + private Model model; + @Test + void submitQuizAnswers_shouldOutputAnError_whenQuizIsNotFullyAnswered(){ + var quiz = new QuizEntity(); + quiz.setMarkdownContent(""" + # a question + [ ] wrong answer + [x] good answer + """); + when(quizRepository.findById("testQuiz")).thenReturn(Optional.of(quiz)); + + quizController.submitQuizAnswers(model, "testQuiz", Map.of(), authentication); + + verify(model).addAttribute("message", "Il manque des réponses à certaines questions."); } - @Nested - @WithMockClassroomUser(username = "luke.skywalker", roles = {ClassroomRole.STUDENT}) - class StudentRole { + @Test + void submitQuizAnswers_shouldSaveTheScore_whenQuizIsAnswered(){ + var quizId = "testQuiz"; + var quiz = new QuizEntity(); + quiz.setName(quizId); + quiz.setMarkdownContent(""" + # a question + [ ] wrong answer + [x] good answer + """); + when(quizRepository.findById(quizId)).thenReturn(Optional.of(quiz)); + + // get the answer id from the first answer of the quiz + var answerKey = Quiz.fromMarkdown(quiz.getMarkdownContent(), quizId).getQuestions().get(0).getAnswers().get(0).getId(); - @Test - void shouldNotAccessQuizListPage() throws Exception { - mockMvc.perform(get("/quiz")) - .andDo(print()) - .andExpect(status().isForbidden()); - } + var result = quizController.submitQuizAnswers(model, quizId, Map.of(answerKey, "wrong answer"), authentication); - @Test - void shouldNotAccessNewQuizPage() throws Exception { - mockMvc.perform(get("/quiz/new/edit")) - .andDo(print()) - .andExpect(status().isForbidden()); - } + assertThat(result).isEqualTo("quiz-submitted-with-answers-correction"); + verify(quizScoreRepository).save(any()); } } -- 2.51.2