Skip to content

Commit 947280f

Browse files
committed
tests: adds integration tests for organizations controller.
1 parent 722542b commit 947280f

File tree

9 files changed

+229
-22
lines changed

9 files changed

+229
-22
lines changed

build-logic/spring-web-library/src/main/kotlin/com.codedifferently.studycrm.spring-web-library.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
implementation("org.springframework.boot:spring-boot-starter-web")
99
implementation("org.springframework.boot:spring-boot-starter-actuator")
1010
implementation("org.springframework.boot:spring-boot-starter-validation")
11+
implementation("org.springframework.boot:spring-boot-starter-security")
1112
runtimeOnly("org.springframework:spring-context-support")
1213
}
1314

common/common-web/common-web-lib/src/main/java/com/codedifferently/studycrm/common/web/exceptions/GlobalExceptionHandler.java

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.context.support.DefaultMessageSourceResolvable;
1010
import org.springframework.http.HttpStatus;
1111
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.access.AccessDeniedException;
1213
import org.springframework.web.bind.MethodArgumentNotValidException;
1314
import org.springframework.web.bind.annotation.ExceptionHandler;
1415
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -17,6 +18,15 @@
1718
@RestControllerAdvice
1819
public class GlobalExceptionHandler {
1920

21+
@ExceptionHandler(AccessDeniedException.class)
22+
protected ResponseEntity<Object> handleAccessDenied(
23+
java.nio.file.AccessDeniedException ex, WebRequest request) {
24+
Map<String, String> body = new HashMap<>();
25+
body.put("message", ex.getMessage());
26+
body.put("description", request.getDescription(false));
27+
return new ResponseEntity<>(body, HttpStatus.FORBIDDEN);
28+
}
29+
2030
@ExceptionHandler(MethodArgumentNotValidException.class)
2131
protected ResponseEntity<Object> handleMethodArgumentNotValid(
2232
MethodArgumentNotValidException ex, WebRequest request) {

organization-service/organization-service-domain/organization-domain-lib/src/test/java/com/codedifferently/studycrm/organizations/domain/OrganizationDomainTestConfiguration.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import static org.mockito.Mockito.mock;
44

55
import com.codedifferently.studycrm.common.domain.EntityAclManager;
6-
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
6+
import org.springframework.boot.autoconfigure.SpringBootApplication;
77
import org.springframework.boot.test.context.TestConfiguration;
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.Primary;
@@ -14,7 +14,7 @@
1414
import org.springframework.security.acls.model.MutableAclService;
1515

1616
@TestConfiguration
17-
@EnableAutoConfiguration
17+
@SpringBootApplication
1818
class OrganizationDomainTestConfiguration {
1919
private static PermissionEvaluator permissionEvaluator;
2020

organization-service/organization-service-domain/organization-domain-lib/src/test/java/com/codedifferently/studycrm/organizations/domain/OrganizationServiceTest.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import java.util.UUID;
1212
import org.junit.jupiter.api.BeforeEach;
1313
import org.junit.jupiter.api.Test;
14-
import org.junit.jupiter.api.extension.ExtendWith;
15-
import org.mockito.junit.jupiter.MockitoExtension;
1614
import org.springframework.beans.factory.annotation.Autowired;
1715
import org.springframework.boot.test.context.SpringBootTest;
1816
import org.springframework.security.access.AccessDeniedException;
@@ -23,8 +21,7 @@
2321
import org.springframework.test.context.ContextConfiguration;
2422

2523
@SpringBootTest
26-
@ContextConfiguration(classes = OrganizationDomainConfiguration.class)
27-
@ExtendWith(MockitoExtension.class)
24+
@ContextConfiguration(classes = OrganizationDomainTestConfiguration.class)
2825
class OrganizationServiceTest {
2926

3027
@Autowired private UserRepository userRepository;

organization-service/organization-service-web/organization-web-lib/build.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ testing {
2222
val integrationTest by getting(JvmTestSuite::class) {
2323
dependencies {
2424
implementation("io.eventuate.tram.sagas:eventuate-tram-sagas-spring-in-memory")
25+
implementation("org.springframework.security:spring-security-acl")
26+
implementation("com.codedifferently.studycrm.common.domain:common-domain-lib")
27+
implementation("com.codedifferently.studycrm.organization-service.domain:organization-domain-lib")
28+
implementation("com.codedifferently.studycrm.organization-service.api.web:organization-api-web-lib")
29+
implementation("com.codedifferently.studycrm.organization-service.sagas:organization-sagas-lib")
2530
}
2631
}
2732
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,52 @@
11
package com.codedifferently.studycrm.organizations.web;
22

3+
import static org.mockito.Mockito.mock;
4+
5+
import com.codedifferently.studycrm.organizations.domain.OrganizationService;
6+
import com.codedifferently.studycrm.organizations.sagas.OrganizationSagaService;
37
import io.eventuate.tram.sagas.spring.inmemory.TramSagaInMemoryConfiguration;
48
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.context.annotation.Bean;
510
import org.springframework.context.annotation.Configuration;
611
import org.springframework.context.annotation.Import;
12+
import org.springframework.context.annotation.Primary;
13+
import org.springframework.security.access.PermissionEvaluator;
14+
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
15+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
16+
import org.springframework.security.acls.AclPermissionEvaluator;
717

818
@SpringBootApplication(scanBasePackages = "com.codedifferently.studycrm.organizations")
919
@Configuration
1020
@Import({TramSagaInMemoryConfiguration.class})
11-
public class TestConfiguration {}
21+
public class TestConfiguration {
22+
private static PermissionEvaluator permissionEvaluator;
23+
24+
@Primary
25+
@Bean
26+
public OrganizationService mockOrganizationService() {
27+
return mock(OrganizationService.class);
28+
}
29+
30+
@Primary
31+
@Bean
32+
public OrganizationSagaService mockOrganizationSagaService() {
33+
return mock(OrganizationSagaService.class);
34+
}
35+
36+
@Primary
37+
@Bean
38+
public PermissionEvaluator mockPermissionEvaluator() {
39+
if (permissionEvaluator == null) {
40+
permissionEvaluator = mock(AclPermissionEvaluator.class);
41+
}
42+
return permissionEvaluator;
43+
}
44+
45+
@Primary
46+
@Bean
47+
public MethodSecurityExpressionHandler mockExpressionHandler() {
48+
var expressionHandler = new DefaultMethodSecurityExpressionHandler();
49+
expressionHandler.setPermissionEvaluator(mockPermissionEvaluator());
50+
return expressionHandler;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package com.codedifferently.studycrm.organizations.web;
1+
package com.codedifferently.studycrm.organizations.web.config;
22

3+
import com.codedifferently.studycrm.organizations.web.TestConfiguration;
34
import org.junit.jupiter.api.Test;
45
import org.springframework.boot.test.context.SpringBootTest;
56
import org.springframework.test.context.ContextConfiguration;
@@ -10,6 +11,6 @@ class OrganizationsWebConfigurationTest {
1011

1112
@Test
1213
void testWebConfiguration_loads() throws Exception {
13-
System.out.println(getClass());
14+
// no-op: Just confirms that the config loaded successfully.
1415
}
1516
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package com.codedifferently.studycrm.organizations.web.controllers;
2+
3+
import static org.junit.jupiter.api.Assertions.assertThrows;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.reset;
6+
import static org.mockito.Mockito.when;
7+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
8+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
9+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
10+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
11+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
12+
13+
import com.codedifferently.studycrm.organizations.api.web.UserDetails;
14+
import com.codedifferently.studycrm.organizations.domain.Organization;
15+
import com.codedifferently.studycrm.organizations.domain.OrganizationService;
16+
import com.codedifferently.studycrm.organizations.sagas.OrganizationSagaService;
17+
import com.codedifferently.studycrm.organizations.web.TestConfiguration;
18+
import java.util.ArrayList;
19+
import java.util.Optional;
20+
import java.util.UUID;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.mockito.stubbing.Answer;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.boot.test.context.SpringBootTest;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.security.acls.AclPermissionEvaluator;
28+
import org.springframework.security.test.context.support.WithMockUser;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.test.web.servlet.MockMvc;
31+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
32+
import org.springframework.web.context.WebApplicationContext;
33+
34+
@SpringBootTest
35+
@ContextConfiguration(classes = TestConfiguration.class)
36+
public class OrganizationsControllerTest {
37+
38+
@Autowired private AclPermissionEvaluator aclPermissionEvaluator;
39+
40+
@Autowired private OrganizationService organizationService;
41+
42+
@Autowired private OrganizationSagaService organizationSagaService;
43+
44+
private MockMvc mockMvc;
45+
46+
@BeforeEach
47+
void setUp(WebApplicationContext wac) {
48+
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
49+
reset(aclPermissionEvaluator, organizationService, organizationSagaService);
50+
}
51+
52+
@Test
53+
@WithMockUser
54+
void testGetOrganization_WhenNotAuthorized_ReturnsNotAllowed() throws Exception {
55+
// Arrange
56+
when(aclPermissionEvaluator.hasPermission(any(), any(), any(), any())).thenReturn(false);
57+
58+
// Act
59+
assertThrows(
60+
Exception.class,
61+
() -> mockMvc.perform(get("/organizations/{organizationId}", UUID.randomUUID())));
62+
}
63+
64+
@Test
65+
@WithMockUser
66+
void testGetOrganization_WhenNotExists_ReturnsNotFound() throws Exception {
67+
// Arrange
68+
var orgId = UUID.randomUUID();
69+
when(aclPermissionEvaluator.hasPermission(any(), any(), any(), any())).thenReturn(true);
70+
when(organizationService.findOrganizationById(orgId)).thenReturn(Optional.empty());
71+
72+
// Act
73+
mockMvc.perform(get("/organizations/{organizationId}", orgId)).andExpect(status().isNotFound());
74+
}
75+
76+
@Test
77+
@WithMockUser
78+
void testGetOrganization_WhenExists_ReturnsOrganization() throws Exception {
79+
// Arrange
80+
var orgId = UUID.randomUUID();
81+
var expected = Organization.builder().id(orgId).name("Test Organization").build();
82+
when(aclPermissionEvaluator.hasPermission(any(), any(), any(), any())).thenReturn(true);
83+
when(organizationService.findOrganizationById(orgId)).thenReturn(Optional.of(expected));
84+
85+
// Act
86+
mockMvc
87+
.perform(get("/organizations/{organizationId}", orgId))
88+
.andExpect(status().isOk())
89+
.andExpect(jsonPath("$.organizationId").value(orgId.toString()))
90+
.andExpect(jsonPath("$.name").value(expected.getName()));
91+
}
92+
93+
@Test
94+
@WithMockUser
95+
void testCreateOrganization_WhenNotValid_ReportsErrors() throws Exception {
96+
// Act
97+
mockMvc
98+
.perform(post("/organizations").contentType(MediaType.APPLICATION_JSON).content("{}"))
99+
.andExpect(status().isBadRequest())
100+
.andExpect(
101+
content()
102+
.json(
103+
"{\"errors\":[\"Organization name is required\",\"User details are required\"]}"));
104+
}
105+
106+
@Test
107+
@WithMockUser
108+
void testCreateOrganization_WhenValid_ReturnsResponse() throws Exception {
109+
// Arrange
110+
var orgId = UUID.randomUUID();
111+
var expectedOrg = Organization.builder().name("Test Organization").build();
112+
var expectedUser =
113+
UserDetails.builder()
114+
.username("testUser")
115+
116+
.firstName("John")
117+
.lastName("Doe")
118+
.build();
119+
when(organizationSagaService.createOrganization(expectedOrg, expectedUser))
120+
.then(
121+
(Answer<Organization>)
122+
invocation -> {
123+
expectedOrg.setId(orgId);
124+
return expectedOrg;
125+
});
126+
127+
// Act
128+
mockMvc
129+
.perform(
130+
post("/organizations")
131+
.contentType(MediaType.APPLICATION_JSON)
132+
.content(
133+
"{\"organizationName\":\"Test Organization\",\"userDetails\":{\"username\":\"testUser\"},\"userDetails\":{\"username\":\"testUser\",\"email\":\"[email protected]\",\"firstName\":\"John\",\"lastName\":\"Doe\"}}"))
134+
.andExpect(status().isOk())
135+
.andExpect(jsonPath("$.organizationId").value(orgId.toString()));
136+
}
137+
138+
@Test
139+
@WithMockUser
140+
void testGetAll_ReturnsOrganizations() throws Exception {
141+
// Arrange
142+
var expectedOrgs = new ArrayList<Organization>();
143+
expectedOrgs.add(
144+
Organization.builder().id(UUID.randomUUID()).name("Test Organization 1").build());
145+
expectedOrgs.add(
146+
Organization.builder().id(UUID.randomUUID()).name("Test Organization 2").build());
147+
148+
when(aclPermissionEvaluator.hasPermission(any(), any(), any())).thenReturn(true);
149+
when(organizationService.findAllOrganizations()).thenReturn(expectedOrgs);
150+
151+
// Act
152+
mockMvc
153+
.perform(get("/organizations"))
154+
.andExpect(status().isOk())
155+
.andExpect(
156+
jsonPath("$.organizations[0].organizationId")
157+
.value(expectedOrgs.get(0).getId().toString()))
158+
.andExpect(
159+
jsonPath("$.organizations[1].organizationId")
160+
.value(expectedOrgs.get(1).getId().toString()));
161+
}
162+
}

organization-service/organization-service-web/organization-web-lib/src/main/java/com/codedifferently/studycrm/organizations/web/controllers/OrganizationsController.java

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
package com.codedifferently.studycrm.organizations.web.controllers;
22

3-
import com.codedifferently.studycrm.organizations.api.web.CreateOrganizationRequest;
4-
import com.codedifferently.studycrm.organizations.api.web.CreateOrganizationResponse;
5-
import com.codedifferently.studycrm.organizations.api.web.GetOrganizationResponse;
6-
import com.codedifferently.studycrm.organizations.api.web.GetOrganizationsResponse;
7-
import com.codedifferently.studycrm.organizations.api.web.UserDetails;
8-
import com.codedifferently.studycrm.organizations.domain.Organization;
9-
import com.codedifferently.studycrm.organizations.domain.OrganizationService;
3+
import com.codedifferently.studycrm.organizations.api.web.*;
4+
import com.codedifferently.studycrm.organizations.domain.*;
105
import com.codedifferently.studycrm.organizations.sagas.OrganizationSagaService;
116
import jakarta.validation.Valid;
127
import java.util.UUID;
@@ -17,12 +12,7 @@
1712
import org.springframework.http.HttpStatus;
1813
import org.springframework.http.ResponseEntity;
1914
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
20-
import org.springframework.web.bind.annotation.GetMapping;
21-
import org.springframework.web.bind.annotation.PathVariable;
22-
import org.springframework.web.bind.annotation.PostMapping;
23-
import org.springframework.web.bind.annotation.RequestBody;
24-
import org.springframework.web.bind.annotation.RequestMapping;
25-
import org.springframework.web.bind.annotation.RestController;
15+
import org.springframework.web.bind.annotation.*;
2616

2717
@RestController
2818
@RequestMapping(value = "/organizations")

0 commit comments

Comments
 (0)