Merge commit '2a9a1495203830ed0434649c153ee75e812ffe51' into work/reports-ui
# Conflicts: # src/main/resources/templates/projects/detail.html
This commit is contained in:
+48
@@ -150,6 +150,54 @@ class ProjectControllerTest {
|
||||
.andExpect(redirectedUrl("/projects/30"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "mentor@example.test")
|
||||
void owningMentorCanActivateAPlannedProject() throws Exception {
|
||||
when(pages.authenticatedUserId("mentor@example.test")).thenReturn(10L);
|
||||
|
||||
mvc.perform(post("/projects/30/activate").with(csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/projects/30"));
|
||||
|
||||
verify(projects).activate(10L, 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "mentor@example.test")
|
||||
void plannedProjectDetailShowsActivationOnlyToTheOwningMentor() throws Exception {
|
||||
when(pages.authenticatedUserId("mentor@example.test")).thenReturn(10L);
|
||||
when(pages.detail(10L, 30L)).thenReturn(new ProjectDetail(
|
||||
30L,
|
||||
"Intern Portal Refresh",
|
||||
null,
|
||||
"PLANNED",
|
||||
LocalDate.of(2026, 8, 15),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
"Mentor",
|
||||
"Leader",
|
||||
true));
|
||||
|
||||
mvc.perform(get("/projects/30"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
|
||||
.string(containsString(">Activate<")));
|
||||
|
||||
when(pages.detail(10L, 30L)).thenReturn(new ProjectDetail(
|
||||
30L,
|
||||
"Intern Portal Refresh",
|
||||
null,
|
||||
"PLANNED",
|
||||
LocalDate.of(2026, 8, 15),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
"Mentor",
|
||||
"Leader",
|
||||
false));
|
||||
mvc.perform(get("/projects/30"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content()
|
||||
.string(not(containsString(">Activate<"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "mentor@example.test")
|
||||
void invalidCreateSubmissionStaysOnSafeFormWithoutMutation() throws Exception {
|
||||
|
||||
+9
-4
@@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.project.model.ProjectInternEligibility;
|
||||
import com.lab.labtimesheet.feature.project.model.ProjectStatus;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ProjectEntityTest {
|
||||
@@ -114,16 +115,20 @@ class ProjectEntityTest {
|
||||
var project = plannedProject();
|
||||
|
||||
assertThrows(ProjectAccessDeniedException.class,
|
||||
() -> project.activate(11L, true, CREATED_AT.plusSeconds(60)));
|
||||
() -> project.activate(11L, Set.of(20L), true, CREATED_AT.plusSeconds(60)));
|
||||
assertThrows(ProjectRuleViolationException.class,
|
||||
() -> project.activate(10L, false, CREATED_AT.plusSeconds(60)));
|
||||
() -> project.activate(10L, Set.of(), true, CREATED_AT.plusSeconds(60)));
|
||||
assertThrows(ProjectRuleViolationException.class,
|
||||
() -> project.activate(10L, Set.of(21L), true, CREATED_AT.plusSeconds(60)));
|
||||
assertThrows(ProjectRuleViolationException.class,
|
||||
() -> project.activate(10L, Set.of(20L), false, CREATED_AT.plusSeconds(60)));
|
||||
|
||||
project.activate(10L, true, CREATED_AT.plusSeconds(60));
|
||||
project.activate(10L, Set.of(20L), true, CREATED_AT.plusSeconds(60));
|
||||
|
||||
assertEquals(ProjectStatus.ACTIVE, project.status());
|
||||
assertEquals(CREATED_AT.plusSeconds(60), project.activatedAt());
|
||||
assertThrows(ProjectRuleViolationException.class,
|
||||
() -> project.activate(10L, true, CREATED_AT.plusSeconds(120)));
|
||||
() -> project.activate(10L, Set.of(20L), true, CREATED_AT.plusSeconds(120)));
|
||||
}
|
||||
|
||||
private static ProjectEntity plannedProject() {
|
||||
|
||||
+40
@@ -215,6 +215,46 @@ class ProjectServiceIntegrationTest {
|
||||
assertTrue(members.stream().noneMatch(member -> member.currentLeader()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ownerActivatesAPlannedProjectWhenCurrentMemberAndTaskAssigneeGuardsPass() {
|
||||
long mentorId = user("mentor-activate@example.test", "MENTOR");
|
||||
long leaderId = intern("leader-activate@example.test", "I014");
|
||||
long projectId = createProject(mentorId, leaderId, "Ready to activate");
|
||||
|
||||
projectService.activate(mentorId, projectId);
|
||||
|
||||
assertEquals("ACTIVE", text("select status from projects where id = ?", projectId));
|
||||
assertEquals(1, count("select count(*) from projects where id = ? and activated_at is not null", projectId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activationRejectsATaskAssignedToAFormerMemberWithoutPartialMutation() {
|
||||
long mentorId = user("mentor-guard@example.test", "MENTOR");
|
||||
long leaderId = intern("leader-guard@example.test", "I015");
|
||||
long formerMemberId = intern("former-assignee@example.test", "I016");
|
||||
long projectId = createProject(mentorId, leaderId, "Assignee guard");
|
||||
projectService.addMember(mentorId, projectId, formerMemberId);
|
||||
long leaderMembershipId = membershipId(projectId, leaderId);
|
||||
long formerMembershipId = membershipId(projectId, formerMemberId);
|
||||
jdbc.update("""
|
||||
insert into tasks (
|
||||
project_id, assignee_membership_id, title,
|
||||
created_by_membership_id, assigned_by_membership_id)
|
||||
values (?, ?, 'Former assignee', ?, ?)
|
||||
""", projectId, formerMembershipId, leaderMembershipId, leaderMembershipId);
|
||||
jdbc.update("""
|
||||
update project_memberships
|
||||
set left_at = ?, removed_by_mentor_user_id = ?, updated_at = ?
|
||||
where id = ?
|
||||
""", dbTime(NOW.plusSeconds(60)), mentorId, dbTime(NOW.plusSeconds(60)), formerMembershipId);
|
||||
entityManager.clear();
|
||||
|
||||
assertThrows(ProjectRuleViolationException.class, () -> projectService.activate(mentorId, projectId));
|
||||
|
||||
assertEquals("PLANNED", text("select status from projects where id = ?", projectId));
|
||||
assertEquals(1, count("select count(*) from tasks where project_id = ? and deleted_at is null", projectId));
|
||||
}
|
||||
|
||||
private long createProject(long mentorId, long leaderId, String name) {
|
||||
return projectService.create(
|
||||
mentorId,
|
||||
|
||||
+5
-1
@@ -9,6 +9,7 @@ import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||
import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskContext;
|
||||
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
|
||||
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
|
||||
import com.lab.labtimesheet.feature.task.service.TaskQueryService;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
@@ -30,6 +31,9 @@ class ProjectTaskMutationContextTest {
|
||||
@Mock
|
||||
private ProjectQueryService queries;
|
||||
|
||||
@Mock
|
||||
private TaskQueryService taskQueries;
|
||||
|
||||
@Mock
|
||||
private ProjectEntity project;
|
||||
|
||||
@@ -45,7 +49,7 @@ class ProjectTaskMutationContextTest {
|
||||
LocalDate.of(2026, 9, 30),
|
||||
40L,
|
||||
List.of());
|
||||
var service = new ProjectService(projects, accounts, queries, Clock.systemUTC());
|
||||
var service = new ProjectService(projects, accounts, queries, taskQueries, Clock.systemUTC());
|
||||
when(projects.findLockedById(projectId)).thenReturn(Optional.of(project));
|
||||
when(queries.taskContext(actorUserId, project)).thenReturn(expected);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user