fix(project): hide unavailable create action
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
- **Requirement IDs:** `AUTH-001`, `AUTH-002`, `AUTH-006`, `PRJ-001`, `PRJ-004`–`PRJ-006`, `SEC-001`, `ERR-001`
|
- **Requirement IDs:** `AUTH-001`, `AUTH-002`, `AUTH-006`, `PRJ-001`, `PRJ-004`–`PRJ-006`, `SEC-001`, `ERR-001`
|
||||||
- **Scenario IDs:** `AC-AUTH-001`, `AC-AUTH-002`, `AC-AUTH-007`, `I1-PRJ-05`
|
- **Scenario IDs:** `AC-AUTH-001`, `AC-AUTH-002`, `AC-AUTH-007`, `I1-PRJ-05`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.feature.project.controller.ProjectControllerTest`
|
- **Test class/method:** `com.lab.labtimesheet.feature.project.controller.ProjectControllerTest`
|
||||||
- **Implementation commit:** `25a855e`
|
- **Implementation commits:** `25a855e`, `a9ee99a`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
@@ -68,6 +68,18 @@ export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|||||||
[INFO] BUILD SUCCESS
|
[INFO] BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Mentor-only control regression
|
||||||
|
|
||||||
|
**RED:** the focused MockMvc run reported two expected failures: `GET /projects/new` returned `200` for an Intern instead of non-disclosing `404`, and the member page rendered the `Add member` form for a non-owner.
|
||||||
|
|
||||||
|
**GREEN:** rerunning `./mvnw -Dtest=ProjectControllerTest test` after the controller/DTO/template correction passed 7 tests with zero failures, errors, or skips.
|
||||||
|
|
||||||
|
## Role-aware Project-list action regression
|
||||||
|
|
||||||
|
**RED:** the focused MockMvc run reported two expected failures after adding the list-action regression: the controller still resolved only a user ID, so the Mentor fixture was queried as user `0`, and an Intern-facing Project list rendered the `Create Project` link.
|
||||||
|
|
||||||
|
**GREEN:** rerunning `./mvnw -Dtest=ProjectControllerTest test` after resolving the public actor view and conditionally rendering the link passed 8 tests with zero failures, errors, or skips.
|
||||||
|
|
||||||
## External-test boundaries
|
## External-test boundaries
|
||||||
|
|
||||||
This slice does not prove PostgreSQL query correctness, a real login flow, shared-shell navigation, browser accessibility, or Iteration 2 invitation/exit/completion pages. The activation route remains deferred with `I1-PRJ-04` until the Task feature query dependency is available.
|
This slice does not prove PostgreSQL query correctness, a real login flow, shared-shell navigation, browser accessibility, or Iteration 2 invitation/exit/completion pages. The activation route remains deferred with `I1-PRJ-04` until the Task feature query dependency is available.
|
||||||
|
|||||||
+3
-1
@@ -30,7 +30,9 @@ public class ProjectController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String list(Principal principal, Model model) {
|
public String list(Principal principal, Model model) {
|
||||||
model.addAttribute("projects", pages.listVisible(actorId(principal)));
|
var actor = pages.authenticatedActor(principal.getName());
|
||||||
|
model.addAttribute("projects", pages.listVisible(actor.userId()));
|
||||||
|
model.addAttribute("canCreateProject", "MENTOR".equals(actor.role()));
|
||||||
return "projects/list";
|
return "projects/list";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>Projects</h1>
|
<h1>Projects</h1>
|
||||||
<a href="/projects/new">Create Project</a>
|
<a th:if="${canCreateProject}" href="/projects/new">Create Project</a>
|
||||||
<p th:if="${#lists.isEmpty(projects)}">No authorized Projects.</p>
|
<p th:if="${#lists.isEmpty(projects)}">No authorized Projects.</p>
|
||||||
<table th:unless="${#lists.isEmpty(projects)}">
|
<table th:unless="${#lists.isEmpty(projects)}">
|
||||||
<caption>Authorized Projects</caption>
|
<caption>Authorized Projects</caption>
|
||||||
|
|||||||
+18
-2
@@ -44,7 +44,8 @@ class ProjectControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "mentor@example.test")
|
@WithMockUser(username = "mentor@example.test")
|
||||||
void listsOnlyTheAuthenticatedUsersAuthorizedProjects() throws Exception {
|
void listsOnlyTheAuthenticatedUsersAuthorizedProjects() throws Exception {
|
||||||
when(pages.authenticatedUserId("mentor@example.test")).thenReturn(10L);
|
when(pages.authenticatedActor("mentor@example.test"))
|
||||||
|
.thenReturn(new ProjectActorView(10L, "MENTOR"));
|
||||||
when(pages.listVisible(10L)).thenReturn(List.of(new ProjectSummary(
|
when(pages.listVisible(10L)).thenReturn(List.of(new ProjectSummary(
|
||||||
30L,
|
30L,
|
||||||
"Intern Portal Refresh",
|
"Intern Portal Refresh",
|
||||||
@@ -55,11 +56,26 @@ class ProjectControllerTest {
|
|||||||
mvc.perform(get("/projects"))
|
mvc.perform(get("/projects"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(view().name("projects/list"))
|
.andExpect(view().name("projects/list"))
|
||||||
.andExpect(model().attributeExists("projects"));
|
.andExpect(model().attributeExists("projects"))
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
|
||||||
|
.string(containsString("Create Project")));
|
||||||
|
|
||||||
verify(pages).listVisible(10L);
|
verify(pages).listVisible(10L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "member@example.test")
|
||||||
|
void nonMentorProjectListOmitsTheCreateLink() throws Exception {
|
||||||
|
when(pages.authenticatedActor("member@example.test"))
|
||||||
|
.thenReturn(new ProjectActorView(20L, "INTERN"));
|
||||||
|
when(pages.listVisible(20L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
mvc.perform(get("/projects"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
|
||||||
|
.string(not(containsString("Create Project"))));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "member@example.test")
|
@WithMockUser(username = "member@example.test")
|
||||||
void guessedProjectIdReturnsTheSameNotFoundResponseAsAMissingProject() throws Exception {
|
void guessedProjectIdReturnsTheSameNotFoundResponseAsAMissingProject() throws Exception {
|
||||||
|
|||||||
Reference in New Issue
Block a user