Skip to content

Commit db8f605

Browse files
authored
Feature+refractor/carver # controller testing & dependency management removal (#88)
* Removed spring-webmvc forced dependency * Fixed App User & UserLogs Pointer Loop * Created Controller Testing with MockMVC * Revert Dependency Conflict * Added Disable to Controller Testing
1 parent bbf6ee4 commit db8f605

16 files changed

+1519
-9
lines changed

backend/src/main/java/com/fmc/starterApp/models/entity/AppUser.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.fmc.starterApp.models.entity;
22

33
import java.util.List;
4+
5+
import com.fasterxml.jackson.annotation.JsonManagedReference;
6+
47
import jakarta.persistence.Column;
58
import jakarta.persistence.Entity;
69
import jakarta.persistence.FetchType;
@@ -80,5 +83,6 @@ public class AppUser {
8083
* a detailed history of user activity for auditing and monitoring purposes.
8184
*/
8285
@OneToMany(fetch = FetchType.LAZY, mappedBy = "appUser")
86+
@JsonManagedReference
8387
private List<UserLogs> userTimes;
8488
}

backend/src/main/java/com/fmc/starterApp/models/entity/UserLogs.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.hibernate.annotations.OnDeleteAction;
77
import org.springframework.format.annotation.DateTimeFormat;
88

9+
import com.fasterxml.jackson.annotation.JsonBackReference;
10+
911
import jakarta.persistence.Entity;
1012
import jakarta.persistence.FetchType;
1113
import jakarta.persistence.GeneratedValue;
@@ -52,6 +54,7 @@ public class UserLogs {
5254
@ManyToOne(fetch = FetchType.LAZY, optional = false)
5355
@JoinColumn(name = "userId", nullable = false)
5456
@OnDelete(action = OnDeleteAction.CASCADE)
57+
@JsonBackReference
5558
private AppUser appUser;
5659

5760
/**
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
package com.fmc.starterApp.controllers;
2+
3+
import static org.hamcrest.Matchers.hasSize;
4+
import static org.hamcrest.Matchers.notNullValue;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Disabled;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.jdbc.core.JdbcTemplate;
13+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
14+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
15+
import org.springframework.test.annotation.DirtiesContext;
16+
import org.springframework.test.context.TestPropertySource;
17+
import org.springframework.test.web.servlet.MockMvc;
18+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
19+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; // only for the destructive test
20+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
22+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
23+
24+
/**
25+
* Integration tests for {@link com.fmc.starterApp.controllers.AdminController}, verifying that the controller layer:
26+
* <ul>
27+
* <li>Exposes its endpoints correctly under <code>/api/admin</code>.</li>
28+
* <li>Performs input validation and error handling.</li>
29+
* <li>Enforces security rules: authenticated access required, and only users with STARTER_ADMIN role allowed.</li>
30+
* <li>Integrates end‑to‑end with service and persistence for POST→GET cycles.</li>
31+
* <li>Handles edge cases such as backend errors and Unicode input.</li>
32+
* </ul>
33+
*
34+
* <p>This test class uses MockMvc with Spring Security filters, an in‑memory H2 database configured
35+
* via <code>application-test.properties</code>, and JdbcTemplate for setup/teardown and one destructive
36+
* test annotated with {@link DirtiesContext}.</p>
37+
*/
38+
@Disabled("Excluded due to Spring Boot dep conflict. Omitting spring-webmvc from dependency mangement allows testing but breaks production")
39+
@SpringBootTest
40+
@AutoConfigureMockMvc
41+
@TestPropertySource(locations = "classpath:application-test.properties")
42+
public class AdminControllerIntegrationTest {
43+
44+
@Autowired private MockMvc mockMvc;
45+
@Autowired private JdbcTemplate jdbcTemplate;
46+
47+
// helper authorities
48+
private static final SimpleGrantedAuthority ADMIN = new SimpleGrantedAuthority("ROLE_STARTER_ADMIN");
49+
private static final SimpleGrantedAuthority READ = new SimpleGrantedAuthority("ROLE_STARTER_READ");
50+
51+
@BeforeEach
52+
void cleanup() {
53+
// Reset tables before each test
54+
jdbcTemplate.execute("DELETE FROM USER_LOGS");
55+
jdbcTemplate.execute("DELETE FROM USERS");
56+
}
57+
58+
// =========================================================================
59+
// ✅ 1. Basic Endpoint Tests (Web Test)
60+
// =========================================================================
61+
62+
/**
63+
* **usersAdminData - Endpoint Exists**
64+
* Verify that GET /api/admin/users with ADMIN role returns 200 OK.
65+
*/
66+
@Test
67+
void usersAdminData_withAdmin_returnsOk() throws Exception {
68+
// seed one user via POST
69+
String payload = """
70+
{ "userName":"Alice", "email":"alice@example.com" }
71+
""";
72+
mockMvc.perform(post("/api/admin/users")
73+
.with(jwt().authorities(ADMIN))
74+
.contentType(MediaType.APPLICATION_JSON)
75+
.content(payload))
76+
.andExpect(status().isOk());
77+
78+
// existence check
79+
mockMvc.perform(get("/api/admin/users")
80+
.with(jwt().authorities(ADMIN)))
81+
.andExpect(status().isOk());
82+
}
83+
84+
/**
85+
* **addKeyToRoles - Endpoint Exists**
86+
* Verify that POST /api/admin/users with ADMIN role returns 200 OK.
87+
*/
88+
@Test
89+
void addKeyToRoles_withAdmin_returnsOk() throws Exception {
90+
String payload = """
91+
{ "userName":"Bob", "email":"bob@example.com" }
92+
""";
93+
mockMvc.perform(post("/api/admin/users")
94+
.with(jwt().authorities(ADMIN))
95+
.contentType(MediaType.APPLICATION_JSON)
96+
.content(payload))
97+
.andExpect(status().isOk());
98+
}
99+
100+
// =========================================================================
101+
// ✅ 2. Input Validation and Error Handling Tests
102+
// =========================================================================
103+
104+
/**
105+
* **addKeyToRoles - Empty or Malformed JSON Test**
106+
* Verify that POST /api/admin/users with empty or invalid JSON returns 400 Bad Request.
107+
*/
108+
@Test
109+
void addKeyToRoles_invalidJson_returnsBadRequest() throws Exception {
110+
mockMvc.perform(post("/api/admin/users")
111+
.with(jwt().authorities(ADMIN))
112+
.contentType(MediaType.APPLICATION_JSON)
113+
.content(""))
114+
.andExpect(status().isBadRequest());
115+
116+
mockMvc.perform(post("/api/admin/users")
117+
.with(jwt().authorities(ADMIN))
118+
.contentType(MediaType.APPLICATION_JSON)
119+
.content("{ invalid json }"))
120+
.andExpect(status().isBadRequest());
121+
}
122+
123+
/**
124+
* **addKeyToRoles - Missing Email Field Test**
125+
* Verify that POST /api/admin/users without the email field returns 400 Bad Request.
126+
*/
127+
@Test
128+
void addKeyToRoles_missingEmailField_returnsBadRequest() throws Exception {
129+
String payload = """
130+
{ "userName":"NoEmail" }
131+
""";
132+
mockMvc.perform(post("/api/admin/users")
133+
.with(jwt().authorities(ADMIN))
134+
.contentType(MediaType.APPLICATION_JSON)
135+
.content(payload))
136+
.andExpect(status().isBadRequest());
137+
}
138+
139+
/**
140+
* **addKeyToRoles - Invalid Email Format Test**
141+
* Verify that POST /api/admin/users with malformed email returns 400 Bad Request.
142+
*/
143+
@Test
144+
void addKeyToRoles_invalidEmailFormat_returnsBadRequest() throws Exception {
145+
String payload = """
146+
{ "userName":"BadEmail", "email":"not-an-email" }
147+
""";
148+
mockMvc.perform(post("/api/admin/users")
149+
.with(jwt().authorities(ADMIN))
150+
.contentType(MediaType.APPLICATION_JSON)
151+
.content(payload))
152+
.andExpect(status().isBadRequest());
153+
}
154+
155+
// =========================================================================
156+
// ✅ 3. Security and Authorization Tests
157+
// =========================================================================
158+
159+
/**
160+
* **usersAdminData - No Authentication Test**
161+
* Verify that GET /api/admin/users without JWT returns 403 Forbidden.
162+
*/
163+
@Test
164+
void usersAdminData_noAuth_returnsForbidden() throws Exception {
165+
mockMvc.perform(get("/api/admin/users"))
166+
.andExpect(status().isForbidden());
167+
}
168+
169+
/**
170+
* **usersAdminData - Insufficient Role Test**
171+
* Verify that GET /api/admin/users with READ role returns 401 Unauthorized.
172+
*/
173+
@Test
174+
void usersAdminData_insufficientRole_returnsUnauthorized() throws Exception {
175+
mockMvc.perform(get("/api/admin/users")
176+
.with(jwt().authorities(READ)))
177+
.andExpect(status().isUnauthorized());
178+
}
179+
180+
/**
181+
* **addKeyToRoles - No Authentication Test**
182+
* Verify that POST /api/admin/users without JWT returns 403 Forbidden.
183+
*/
184+
@Test
185+
void addKeyToRoles_noAuth_returnsForbidden() throws Exception {
186+
mockMvc.perform(post("/api/admin/users")
187+
.contentType(MediaType.APPLICATION_JSON)
188+
.content("{\"userName\":\"Bob\",\"email\":\"bob@example.com\"}"))
189+
.andExpect(status().isForbidden());
190+
}
191+
192+
/**
193+
* **addKeyToRoles - Insufficient Role Test**
194+
* Verify that POST /api/admin/users with READ role returns 401 Unauthorized.
195+
*/
196+
@Test
197+
void addKeyToRoles_readRole_returnsUnauthorized() throws Exception {
198+
mockMvc.perform(post("/api/admin/users")
199+
.with(jwt().authorities(READ))
200+
.contentType(MediaType.APPLICATION_JSON)
201+
.content("{\"userName\":\"Bob\",\"email\":\"bob@example.com\"}"))
202+
.andExpect(status().isUnauthorized());
203+
}
204+
205+
// =========================================================================
206+
// ✅ 4. Integration and End‑to‑End Tests
207+
// =========================================================================
208+
209+
/**
210+
* **usersAdminData - Basic Retrieval Test**
211+
* Verify full POST → GET cycle returns correct totalUsers and user data.
212+
*/
213+
@Test
214+
void usersAdminData_returnsOkAndDto() throws Exception {
215+
String payload = """
216+
{ "userName":"Alice", "email":"alice@example.com" }
217+
""";
218+
mockMvc.perform(post("/api/admin/users")
219+
.with(jwt().authorities(ADMIN))
220+
.contentType(MediaType.APPLICATION_JSON)
221+
.content(payload))
222+
.andExpect(status().isOk());
223+
224+
mockMvc.perform(get("/api/admin/users")
225+
.with(jwt().authorities(ADMIN)))
226+
.andExpect(status().isOk())
227+
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
228+
.andExpect(jsonPath("$.totalUsers").value(1))
229+
.andExpect(jsonPath("$.users", hasSize(1)))
230+
.andExpect(jsonPath("$.users[0].email").value("alice@example.com"));
231+
}
232+
233+
/**
234+
* **addKeyToRoles - Basic Creation Test**
235+
* Verify that POST /api/admin/users with valid body returns 200 OK and the created user JSON.
236+
*/
237+
@Test
238+
void addKeyToRoles_returnsOkAndCreatedUser() throws Exception {
239+
String payload = """
240+
{ "userName":"Bob", "email":"bob@example.com" }
241+
""";
242+
mockMvc.perform(post("/api/admin/users")
243+
.with(jwt().authorities(ADMIN))
244+
.contentType(MediaType.APPLICATION_JSON)
245+
.content(payload))
246+
.andExpect(status().isOk())
247+
.andExpect(jsonPath("$.userId", notNullValue()))
248+
.andExpect(jsonPath("$.email").value("bob@example.com"));
249+
}
250+
251+
/**
252+
* **addKeyToRoles_thenGetUsers - End‑to‑End Cycle Test**
253+
* Verify full POST → GET cycle returns correct totalUsers and user data for multiple calls.
254+
*/
255+
@Test
256+
void addKeyToRoles_thenGetUsers_endToEnd() throws Exception {
257+
String payload = """
258+
{ "userName":"Dana","email":"dana@example.com" }
259+
""";
260+
mockMvc.perform(post("/api/admin/users")
261+
.with(jwt().authorities(ADMIN))
262+
.contentType(MediaType.APPLICATION_JSON)
263+
.content(payload))
264+
.andExpect(status().isOk());
265+
266+
mockMvc.perform(get("/api/admin/users")
267+
.with(jwt().authorities(ADMIN)))
268+
.andExpect(status().isOk())
269+
.andExpect(jsonPath("$.totalUsers").value(1))
270+
.andExpect(jsonPath("$.users[0].email").value("dana@example.com"));
271+
}
272+
273+
// =========================================================================
274+
// ✅ 5. Edge Case and Special Scenario Tests
275+
// =========================================================================
276+
277+
/**
278+
* **usersAdminData - Service Error Test**
279+
* Simulate a backend failure by dropping tables, expecting 400 Bad Request.
280+
*/
281+
@Test
282+
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
283+
void usersAdminData_serviceError_returnsBadRequest() throws Exception {
284+
jdbcTemplate.execute("DROP TABLE USER_LOGS");
285+
jdbcTemplate.execute("DROP TABLE USERS");
286+
287+
mockMvc.perform(get("/api/admin/users")
288+
.with(jwt().authorities(ADMIN)))
289+
.andExpect(status().isBadRequest());
290+
}
291+
292+
/**
293+
* **addKeyToRoles - Unicode Support Test**
294+
* Verify that POST /api/admin/users accepts and returns Unicode characters in userName.
295+
*/
296+
@Test
297+
void addKeyToRoles_specialCharactersInName_returnsOk() throws Exception {
298+
String payload = """
299+
{ "userName":"Ťëßt Üšêr","email":"testuser@example.com" }
300+
""";
301+
mockMvc.perform(post("/api/admin/users")
302+
.with(jwt().authorities(ADMIN))
303+
.contentType(MediaType.APPLICATION_JSON)
304+
.content(payload))
305+
.andExpect(status().isOk())
306+
.andExpect(jsonPath("$.userName").value("Ťëßt Üšêr"));
307+
}
308+
}

backend/src/test/java/com/fmc/starterApp/controllers/AdminControllerTest.java

Whitespace-only changes.

0 commit comments

Comments
 (0)