feat(project): lock task mutation context
This commit is contained in:
+12
-3
@@ -97,7 +97,12 @@ public class ProjectQueryService {
|
|||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ProjectTaskContext taskContext(long actorUserId, long projectId) {
|
public ProjectTaskContext taskContext(long actorUserId, long projectId) {
|
||||||
var project = visibleProject(actorUserId, projectId);
|
var project = projects.findById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
||||||
|
return taskContext(actorUserId, project);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectTaskContext taskContext(long actorUserId, ProjectEntity project) {
|
||||||
|
requireVisibleProject(actorUserId, project);
|
||||||
var activeMembers = project.memberships().stream()
|
var activeMembers = project.memberships().stream()
|
||||||
.filter(membership -> membership.isCurrent() && isEligibleIntern(membership.internUserId()))
|
.filter(membership -> membership.isCurrent() && isEligibleIntern(membership.internUserId()))
|
||||||
.map(membership -> new ProjectTaskMemberView(
|
.map(membership -> new ProjectTaskMemberView(
|
||||||
@@ -142,15 +147,19 @@ public class ProjectQueryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ProjectEntity visibleProject(long actorUserId, long projectId) {
|
private ProjectEntity visibleProject(long actorUserId, long projectId) {
|
||||||
var actor = activeActor(actorUserId);
|
|
||||||
var project = projects.findById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
var project = projects.findById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
||||||
|
requireVisibleProject(actorUserId, project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireVisibleProject(long actorUserId, ProjectEntity project) {
|
||||||
|
var actor = activeActor(actorUserId);
|
||||||
var visible = "ADMIN".equals(actor.role().name())
|
var visible = "ADMIN".equals(actor.role().name())
|
||||||
|| ("MENTOR".equals(actor.role().name()) && project.mentorUserId() == actorUserId)
|
|| ("MENTOR".equals(actor.role().name()) && project.mentorUserId() == actorUserId)
|
||||||
|| ("INTERN".equals(actor.role().name()) && project.hasEverHadMember(actorUserId));
|
|| ("INTERN".equals(actor.role().name()) && project.hasEverHadMember(actorUserId));
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
throw new ProjectAccessDeniedException();
|
throw new ProjectAccessDeniedException();
|
||||||
}
|
}
|
||||||
return project;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ProjectEntity> visibleProjects(AccountIdentity actor, long actorUserId) {
|
private List<ProjectEntity> visibleProjects(AccountIdentity actor, long actorUserId) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.lab.labtimesheet.feature.project.exception.ProjectAccessDeniedExcepti
|
|||||||
import com.lab.labtimesheet.feature.account.service.AccountService;
|
import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||||
import com.lab.labtimesheet.feature.project.model.ProjectInternEligibility;
|
import com.lab.labtimesheet.feature.project.model.ProjectInternEligibility;
|
||||||
import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateCommand;
|
import com.lab.labtimesheet.feature.project.model.dto.ProjectCreateCommand;
|
||||||
|
import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskContext;
|
||||||
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
|
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
|
||||||
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
|
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
@@ -15,14 +16,17 @@ public class ProjectService {
|
|||||||
|
|
||||||
private final ProjectRepository projects;
|
private final ProjectRepository projects;
|
||||||
private final AccountService accounts;
|
private final AccountService accounts;
|
||||||
|
private final ProjectQueryService queries;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
public ProjectService(
|
public ProjectService(
|
||||||
ProjectRepository projects,
|
ProjectRepository projects,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
|
ProjectQueryService queries,
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
this.projects = projects;
|
this.projects = projects;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
|
this.queries = queries;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +65,11 @@ public class ProjectService {
|
|||||||
projects.flush();
|
projects.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ProjectTaskContext taskMutationContext(long actorUserId, long projectId) {
|
||||||
|
return queries.taskContext(actorUserId, lockedProject(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
private ProjectEntity lockedProject(long projectId) {
|
private ProjectEntity lockedProject(long projectId) {
|
||||||
return projects.findLockedById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
return projects.findLockedById(projectId).orElseThrow(ProjectAccessDeniedException::new);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -144,11 +144,14 @@ class ProjectServiceIntegrationTest {
|
|||||||
assertEquals(List.of(), projectPages.listVisible(unrelatedId));
|
assertEquals(List.of(), projectPages.listVisible(unrelatedId));
|
||||||
assertEquals(projectId, projectPages.detail(memberId, projectId).id());
|
assertEquals(projectId, projectPages.detail(memberId, projectId).id());
|
||||||
assertEquals("INTERN", projectPages.authenticatedActor("member-view@example.test").role());
|
assertEquals("INTERN", projectPages.authenticatedActor("member-view@example.test").role());
|
||||||
var taskContext = projectPages.taskContext(memberId, projectId);
|
var taskContext = projectService.taskMutationContext(memberId, projectId);
|
||||||
assertEquals(mentorId, taskContext.mentorUserId());
|
assertEquals(mentorId, taskContext.mentorUserId());
|
||||||
assertEquals("PLANNED", taskContext.status());
|
assertEquals("PLANNED", taskContext.status());
|
||||||
assertEquals(2, taskContext.activeMembers().size());
|
assertEquals(2, taskContext.activeMembers().size());
|
||||||
assertEquals(membershipId(projectId, leaderId), taskContext.currentLeaderMembershipId());
|
assertEquals(membershipId(projectId, leaderId), taskContext.currentLeaderMembershipId());
|
||||||
|
assertEquals(taskContext, projectPages.taskContext(memberId, projectId));
|
||||||
|
assertThrows(ProjectAccessDeniedException.class,
|
||||||
|
() -> projectService.taskMutationContext(otherMentorId, projectId));
|
||||||
jdbc.update("""
|
jdbc.update("""
|
||||||
update projects set status = 'ACTIVE', activated_at = ?, updated_at = ? where id = ?
|
update projects set status = 'ACTIVE', activated_at = ?, updated_at = ? where id = ?
|
||||||
""", dbTime(NOW.plusSeconds(30)), dbTime(NOW.plusSeconds(30)), projectId);
|
""", dbTime(NOW.plusSeconds(30)), dbTime(NOW.plusSeconds(30)), projectId);
|
||||||
@@ -169,7 +172,7 @@ class ProjectServiceIntegrationTest {
|
|||||||
entityManager.clear();
|
entityManager.clear();
|
||||||
|
|
||||||
assertEquals(projectId, projectPages.detail(memberId, projectId).id());
|
assertEquals(projectId, projectPages.detail(memberId, projectId).id());
|
||||||
assertTrue(projectPages.taskContext(memberId, projectId).activeMembers().stream()
|
assertTrue(projectService.taskMutationContext(memberId, projectId).activeMembers().stream()
|
||||||
.noneMatch(member -> member.userId() == memberId));
|
.noneMatch(member -> member.userId() == memberId));
|
||||||
assertEquals(0, projectPages.dashboardSummary(memberId).activeProjectCount());
|
assertEquals(0, projectPages.dashboardSummary(memberId).activeProjectCount());
|
||||||
assertEquals(1, projectPages.dashboardSummary(mentorId).distinctActiveMemberCount());
|
assertEquals(1, projectPages.dashboardSummary(mentorId).distinctActiveMemberCount());
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package com.lab.labtimesheet.feature.project.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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 java.time.Clock;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ProjectTaskMutationContextTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProjectRepository projects;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AccountService accounts;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProjectQueryService queries;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProjectEntity project;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsTheProjectForUpdateBeforeBuildingTheTaskMutationContext() {
|
||||||
|
long actorUserId = 20L;
|
||||||
|
long projectId = 30L;
|
||||||
|
var expected = new ProjectTaskContext(
|
||||||
|
projectId,
|
||||||
|
10L,
|
||||||
|
"ACTIVE",
|
||||||
|
LocalDate.of(2026, 8, 15),
|
||||||
|
LocalDate.of(2026, 9, 30),
|
||||||
|
40L,
|
||||||
|
List.of());
|
||||||
|
var service = new ProjectService(projects, accounts, queries, Clock.systemUTC());
|
||||||
|
when(projects.findLockedById(projectId)).thenReturn(Optional.of(project));
|
||||||
|
when(queries.taskContext(actorUserId, project)).thenReturn(expected);
|
||||||
|
|
||||||
|
var actual = service.taskMutationContext(actorUserId, projectId);
|
||||||
|
|
||||||
|
assertSame(expected, actual);
|
||||||
|
verify(projects).findLockedById(projectId);
|
||||||
|
verify(projects, never()).findById(projectId);
|
||||||
|
verify(queries).taskContext(actorUserId, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user