diff --git a/backend/pom.xml b/backend/pom.xml index 2d8463657b..ace13685c7 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -71,8 +71,8 @@ com.tngtech.archunit - archunit - 1.0.1 + archunit-junit5 + 1.2.0 test diff --git a/backend/src/main/java/ch/puzzle/okr/controller/TeamController.java b/backend/src/main/java/ch/puzzle/okr/controller/TeamController.java index 11dacc3707..a465d59e24 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/TeamController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/TeamController.java @@ -35,7 +35,7 @@ public TeamController(TeamAuthorizationService teamAuthorizationService, TeamMap @Content(mediaType = "application/json", schema = @Schema(implementation = TeamDto.class)) }), }) @GetMapping public List getAllTeams(@RequestParam(value = "quarterId", required = false) Long quarterId) { - return teamAuthorizationService.getEntities().stream().map(team -> teamMapper.toDto(team, quarterId)).toList(); + return teamAuthorizationService.getAllTeams().stream().map(team -> teamMapper.toDto(team, quarterId)).toList(); } @Operation(summary = "Create Team", description = "Create a new Team") diff --git a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java index 67083fed9d..2a45432e3c 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java @@ -2,7 +2,7 @@ import ch.puzzle.okr.dto.UserDto; import ch.puzzle.okr.mapper.UserMapper; -import ch.puzzle.okr.service.business.UserBusinessService; +import ch.puzzle.okr.service.authorization.UserAuthorizationService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -18,11 +18,11 @@ @RequestMapping("api/v1/users") public class UserController { - private final UserBusinessService userBusinessService; + private final UserAuthorizationService userAuthorizationService; private final UserMapper userMapper; - public UserController(UserBusinessService userBusinessService, UserMapper userMapper) { - this.userBusinessService = userBusinessService; + public UserController(UserAuthorizationService userAuthorizationService, UserMapper userMapper) { + this.userAuthorizationService = userAuthorizationService; this.userMapper = userMapper; } @@ -31,7 +31,7 @@ public UserController(UserBusinessService userBusinessService, UserMapper userMa @Content(mediaType = "application/json", schema = @Schema(implementation = UserDto.class)) }), }) @GetMapping public List getAllUsers() { - return userBusinessService.getAllUsers().stream().map(userMapper::toDto).toList(); + return userAuthorizationService.getAllUsers().stream().map(userMapper::toDto).toList(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/converter/JwtConverterFactory.java b/backend/src/main/java/ch/puzzle/okr/converter/JwtConverterFactory.java new file mode 100644 index 0000000000..febe219644 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/converter/JwtConverterFactory.java @@ -0,0 +1,35 @@ +package ch.puzzle.okr.converter; + +import ch.puzzle.okr.models.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class JwtConverterFactory { + + @Autowired + private ApplicationContext appContext; + private Converter> jwtOrganisationConverter; + private Converter jwtUserConverter; + + public synchronized Converter> getJwtOrganisationConverter() { + if (jwtOrganisationConverter == null) { + // place to load configured converter instead of default converter + jwtOrganisationConverter = appContext.getBean(JwtOrganisationConverter.class); + } + return jwtOrganisationConverter; + } + + public synchronized Converter getJwtUserConverter() { + if (jwtUserConverter == null) { + // place to load configured converter instead of default converter + jwtUserConverter = appContext.getBean(JwtUserConverter.class); + } + return jwtUserConverter; + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/converter/JwtOrganisationConverter.java b/backend/src/main/java/ch/puzzle/okr/converter/JwtOrganisationConverter.java index eeaa0d394b..4ef9d275f8 100644 --- a/backend/src/main/java/ch/puzzle/okr/converter/JwtOrganisationConverter.java +++ b/backend/src/main/java/ch/puzzle/okr/converter/JwtOrganisationConverter.java @@ -34,6 +34,7 @@ public List convert(Jwt token) { return organisations.stream().filter(o -> o.startsWith(organisationNamePrefix)).toList(); } } + logger.warn("empty list of realms or organisations {}", realmAccess); return List.of(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/converter/JwtUserConverter.java b/backend/src/main/java/ch/puzzle/okr/converter/JwtUserConverter.java index 7da4734cd1..05da5c023b 100644 --- a/backend/src/main/java/ch/puzzle/okr/converter/JwtUserConverter.java +++ b/backend/src/main/java/ch/puzzle/okr/converter/JwtUserConverter.java @@ -37,7 +37,7 @@ public User convert(Jwt token) { .withFirstname(claims.get(firstname).toString()).withLastname(claims.get(lastname).toString()) .withEmail(claims.get(email).toString()).build(); } catch (Exception e) { - logger.error("can not convert user from claims {}", claims); + logger.warn("can not convert user from claims {}", claims); throw new ResponseStatusException(BAD_REQUEST, "can not convert user from token"); } } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/TeamDto.java b/backend/src/main/java/ch/puzzle/okr/dto/TeamDto.java index 3ab618d1a7..fd9a338034 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/TeamDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/TeamDto.java @@ -2,5 +2,5 @@ import java.util.List; -public record TeamDto(Long id, String name, List organisations) { +public record TeamDto(Long id, int version, String name, List organisations) { } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/UserDto.java b/backend/src/main/java/ch/puzzle/okr/dto/UserDto.java index 78a1bfede2..3d25fc02eb 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/UserDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/UserDto.java @@ -1,4 +1,5 @@ package ch.puzzle.okr.dto; -public record UserDto(Long id, String username, String firstname, String lastname, String email) { +public record UserDto(Long id, int version, String username, String firstname, String lastname, String email, + boolean isWriteable) { } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/overview/OverviewTeamDto.java b/backend/src/main/java/ch/puzzle/okr/dto/overview/OverviewTeamDto.java index 41a46bfdcc..4a1b7f65cb 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/overview/OverviewTeamDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/overview/OverviewTeamDto.java @@ -1,4 +1,4 @@ package ch.puzzle.okr.dto.overview; -public record OverviewTeamDto(Long id, String name, boolean writable, boolean hasInActiveOrganisations) { +public record OverviewTeamDto(Long id, int version, String name, boolean writable, boolean hasInActiveOrganisations) { } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/OverviewMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/OverviewMapper.java index bc01cf187c..35b09a2870 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/OverviewMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/OverviewMapper.java @@ -67,8 +67,8 @@ private OverviewDto createOverviewDto(Overview overview) { objectives.add(createObjectiveDto(overview)); } return new OverviewDto( - new OverviewTeamDto(overview.getOverviewId().getTeamId(), overview.getTeamName(), - overview.isWriteable(), + new OverviewTeamDto(overview.getOverviewId().getTeamId(), overview.getTeamVersion(), + overview.getTeamName(), overview.isWriteable(), organisationBusinessService.teamHasInActiveOrganisations(overview.getOverviewId().getTeamId())), objectives); } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/TeamMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/TeamMapper.java index 8389a980ad..a439542aff 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/TeamMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/TeamMapper.java @@ -28,13 +28,13 @@ public TeamDto toDto(Team team, Long quarterId) { long chosenQuarterId = quarterId == null ? quarterBusinessService.getCurrentQuarter().getId() : quarterId; List organisationDTOs = team.getAuthorizationOrganisation().stream() .map(organisationMapper::toDto).toList(); - return new TeamDto(team.getId(), team.getName(), organisationDTOs); + return new TeamDto(team.getId(), team.getVersion(), team.getName(), organisationDTOs); } public Team toTeam(TeamDto teamDto) { List organisations = teamDto.organisations().stream().map(organisationMapper::toOrganisation) .toList(); - return Team.Builder.builder().withId(teamDto.id()).withName(teamDto.name()) + return Team.Builder.builder().withId(teamDto.id()).withVersion(teamDto.version()).withName(teamDto.name()) .withAuthorizationOrganisation(organisations).build(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java index 6af7624f13..b35e2b6a36 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java @@ -7,6 +7,7 @@ @Component public class UserMapper { public UserDto toDto(User user) { - return new UserDto(user.getId(), user.getUsername(), user.getFirstname(), user.getLastname(), user.getEmail()); + return new UserDto(user.getId(), user.getVersion(), user.getUsername(), user.getFirstname(), user.getLastname(), + user.getEmail(), user.isWriteable()); } } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/RoleMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/role/DefaultRoleMapper.java similarity index 89% rename from backend/src/main/java/ch/puzzle/okr/mapper/RoleMapper.java rename to backend/src/main/java/ch/puzzle/okr/mapper/role/DefaultRoleMapper.java index 3ce825024d..64d567f68a 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/RoleMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/role/DefaultRoleMapper.java @@ -1,4 +1,4 @@ -package ch.puzzle.okr.mapper; +package ch.puzzle.okr.mapper.role; import ch.puzzle.okr.models.User; import ch.puzzle.okr.models.authorization.AuthorizationRole; @@ -13,7 +13,7 @@ import static ch.puzzle.okr.models.authorization.AuthorizationRole.*; @Component -public class RoleMapper { +public class DefaultRoleMapper implements RoleMapper { private static final String DELIMITER = ","; @Value("${okr.organisation.name.1stLevel}") @@ -23,7 +23,8 @@ public class RoleMapper { @Value("${okr.user.champion.usernames}") private String okrChampionUsernames; - public List mapOrganisationNames(List organisationNames, User user) { + @Override + public List mapAuthorizationRoles(List organisationNames, User user) { List roles = new ArrayList<>(); if (hasFirstLevelOrganisationName(organisationNames) || isOkrChampion(user)) { roles.addAll(List.of(READ_ALL_DRAFT, WRITE_ALL)); diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapper.java new file mode 100644 index 0000000000..6517342c06 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapper.java @@ -0,0 +1,10 @@ +package ch.puzzle.okr.mapper.role; + +import ch.puzzle.okr.models.User; +import ch.puzzle.okr.models.authorization.AuthorizationRole; + +import java.util.List; + +public interface RoleMapper { + List mapAuthorizationRoles(List organisationNames, User user); +} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapperFactory.java b/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapperFactory.java new file mode 100644 index 0000000000..f3b31e2bef --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/mapper/role/RoleMapperFactory.java @@ -0,0 +1,20 @@ +package ch.puzzle.okr.mapper.role; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class RoleMapperFactory { + @Autowired + private ApplicationContext appContext; + private RoleMapper roleMapper; + + public synchronized RoleMapper getRoleMapper() { + if (roleMapper == null) { + // place to load configured role mapper instead of default role mapper + roleMapper = appContext.getBean(DefaultRoleMapper.class); + } + return roleMapper; + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/models/Team.java b/backend/src/main/java/ch/puzzle/okr/models/Team.java index 46cd91387a..27ae00e149 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/Team.java +++ b/backend/src/main/java/ch/puzzle/okr/models/Team.java @@ -8,11 +8,14 @@ import java.util.Objects; @Entity -public class Team { +public class Team implements WriteableInterface { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence_team") private Long id; + @Version + private int version; + @NotBlank(message = "Missing attribute name when saving team") @NotNull(message = "Attribute name can not be null when saving team") @Size(min = 2, max = 250, message = "Attribute name must have size between 2 and 250 characters when saving team") @@ -22,11 +25,14 @@ public class Team { @JoinTable(name = "team_organisation", joinColumns = @JoinColumn(name = "team_id"), inverseJoinColumns = @JoinColumn(name = "organisation_id")) private List authorizationOrganisation; + private transient boolean writeable; + public Team() { } private Team(Builder builder) { id = builder.id; + version = builder.version; setName(builder.name); setAuthorizationOrganisation(builder.authorizationOrganisation); } @@ -35,6 +41,10 @@ public Long getId() { return id; } + public int getVersion() { + return version; + } + public String getName() { return name; } @@ -51,9 +61,19 @@ public void setAuthorizationOrganisation(List authorizationOrganis this.authorizationOrganisation = authorizationOrganisation; } + @Override + public boolean isWriteable() { + return writeable; + } + + @Override + public void setWriteable(boolean writeable) { + this.writeable = writeable; + } + @Override public String toString() { - return "Team{" + "id=" + id + ", name='" + name + '}'; + return "Team{" + "id=" + id + ", version=" + version + ", name='" + name + ", writeable=" + writeable + '}'; } @Override @@ -63,16 +83,18 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Team team = (Team) o; - return Objects.equals(id, team.id) && Objects.equals(name, team.name); + return Objects.equals(id, team.id) && Objects.equals(version, team.version) && Objects.equals(name, team.name) + && Objects.equals(writeable, team.writeable); } @Override public int hashCode() { - return Objects.hash(id, name); + return Objects.hash(id, version, name, writeable); } public static final class Builder { private Long id; + private int version; private String name; private List authorizationOrganisation; @@ -89,6 +111,11 @@ public Builder withId(Long id) { return this; } + public Builder withVersion(int version) { + this.version = version; + return this; + } + public Builder withName(String name) { this.name = name; return this; diff --git a/backend/src/main/java/ch/puzzle/okr/models/User.java b/backend/src/main/java/ch/puzzle/okr/models/User.java index 75092cfe11..3ff112f9af 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/User.java +++ b/backend/src/main/java/ch/puzzle/okr/models/User.java @@ -10,11 +10,14 @@ @Entity // table cannot be named "user" since it is a reserved keyword of Postgres @Table(name = "person") -public class User { +public class User implements WriteableInterface { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence_person") private Long id; + @Version + private int version; + @Column(unique = true) @NotBlank(message = "Missing attribute username when saving user") @NotNull(message = "Attribute username can not be null when saving user") @@ -37,11 +40,14 @@ public class User { @Size(min = 2, max = 250, message = "Attribute email must have size between 2 and 250 characters when saving user") private String email; + private transient boolean writeable; + public User() { } private User(Builder builder) { id = builder.id; + version = builder.version; setUsername(builder.username); setFirstname(builder.firstname); setLastname(builder.lastname); @@ -52,6 +58,10 @@ public Long getId() { return id; } + public int getVersion() { + return version; + } + public String getUsername() { return username; } @@ -84,10 +94,21 @@ public void setEmail(String email) { this.email = email; } + @Override + public boolean isWriteable() { + return writeable; + } + + @Override + public void setWriteable(boolean writeable) { + this.writeable = writeable; + } + @Override public String toString() { - return "User{" + "id=" + id + ", username='" + username + '\'' + ", firstname='" + firstname + '\'' - + ", lastname='" + lastname + '\'' + ", email='" + email + '\'' + '}'; + return "User{" + "id=" + id + ", version=" + version + ", username='" + username + '\'' + ", firstname='" + + firstname + '\'' + ", lastname='" + lastname + '\'' + ", email='" + email + '\'' + ", writeable=" + + writeable + '}'; } @Override @@ -97,18 +118,20 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; User user = (User) o; - return Objects.equals(id, user.id) && Objects.equals(username, user.username) - && Objects.equals(firstname, user.firstname) && Objects.equals(lastname, user.lastname) - && Objects.equals(email, user.email); + return Objects.equals(id, user.id) && Objects.equals(version, user.version) + && Objects.equals(username, user.username) && Objects.equals(firstname, user.firstname) + && Objects.equals(lastname, user.lastname) && Objects.equals(email, user.email) + && Objects.equals(writeable, user.writeable); } @Override public int hashCode() { - return Objects.hash(id, username, firstname, lastname, email); + return Objects.hash(id, version, username, firstname, lastname, email, writeable); } public static final class Builder { private Long id; + private int version; private String username; private String firstname; private String lastname; @@ -126,6 +149,11 @@ public Builder withId(Long id) { return this; } + public Builder withVersion(int version) { + this.version = version; + return this; + } + public Builder withUsername(String username) { this.username = username; return this; diff --git a/backend/src/main/java/ch/puzzle/okr/models/overview/Overview.java b/backend/src/main/java/ch/puzzle/okr/models/overview/Overview.java index ae5a5b7489..65707048b8 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/overview/Overview.java +++ b/backend/src/main/java/ch/puzzle/okr/models/overview/Overview.java @@ -17,6 +17,7 @@ public class Overview implements WriteableInterface { @EmbeddedId private OverviewId overviewId; private String teamName; + private int teamVersion; private String objectiveTitle; @Enumerated(EnumType.STRING) private State objectiveState; @@ -43,6 +44,7 @@ public Overview() { private Overview(Builder builder) { overviewId = builder.overviewId; + teamVersion = builder.teamVersion; teamName = builder.teamName; objectiveTitle = builder.objectiveTitle; objectiveState = builder.objectiveState; @@ -67,6 +69,10 @@ public OverviewId getOverviewId() { return overviewId; } + public int getTeamVersion() { + return teamVersion; + } + public String getTeamName() { return teamName; } @@ -151,12 +157,12 @@ public void setWriteable(boolean writeable) { @Override public String toString() { - return "Overview{" + "overviewId=" + overviewId + ", teamName='" + teamName + '\'' + ", objectiveTitle='" - + objectiveTitle + '\'' + ", objectiveState=" + objectiveState + ", objectiveCreatedOn=" - + objectiveCreatedOn + ", quarterId=" + quarterId + ", quarterLabel='" + quarterLabel + '\'' - + ", keyResultTitle='" + keyResultTitle + '\'' + ", keyResultType='" + keyResultType + '\'' - + ", baseline=" + baseline + ", stretchGoal=" + stretchGoal + ", unit='" + unit + '\'' - + ", commitZone='" + commitZone + '\'' + ", targetZone='" + targetZone + '\'' + ", stretchZone='" + return "Overview{" + "overviewId=" + overviewId + ", teamVersion='" + teamVersion + ", teamName='" + teamName + + '\'' + ", objectiveTitle='" + objectiveTitle + '\'' + ", objectiveState=" + objectiveState + + ", objectiveCreatedOn=" + objectiveCreatedOn + ", quarterId=" + quarterId + ", quarterLabel='" + + quarterLabel + '\'' + ", keyResultTitle='" + keyResultTitle + '\'' + ", keyResultType='" + + keyResultType + '\'' + ", baseline=" + baseline + ", stretchGoal=" + stretchGoal + ", unit='" + unit + + '\'' + ", commitZone='" + commitZone + '\'' + ", targetZone='" + targetZone + '\'' + ", stretchZone='" + stretchZone + '\'' + ", checkInValue=" + checkInValue + ", checkInZone='" + checkInZone + '\'' + ", confidence=" + confidence + ", createdOn=" + checkInCreatedOn + ", writeable=" + writeable + '\'' + '}'; @@ -164,6 +170,7 @@ public String toString() { public static final class Builder { private OverviewId overviewId; + private int teamVersion; private String teamName; private String objectiveTitle; private State objectiveState; @@ -195,6 +202,11 @@ public Builder withOverviewId(OverviewId overviewId) { return this; } + public Builder withTeamVersion(int teamVersion) { + this.teamVersion = teamVersion; + return this; + } + public Builder withTeamName(String teamName) { this.teamName = teamName; return this; diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationService.java index f79b461712..8f95370b2c 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationService.java @@ -1,7 +1,7 @@ package ch.puzzle.okr.service.authorization; -import ch.puzzle.okr.converter.JwtOrganisationConverter; -import ch.puzzle.okr.mapper.RoleMapper; +import ch.puzzle.okr.converter.JwtConverterFactory; +import ch.puzzle.okr.mapper.role.RoleMapperFactory; import ch.puzzle.okr.models.User; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.UserBusinessService; @@ -23,23 +23,24 @@ public class AuthorizationRegistrationService { private final UserBusinessService userBusinessService; private final TeamPersistenceService teamPersistenceService; - private final JwtOrganisationConverter jwtOrganisationConverter; - private final RoleMapper roleMapper; + private final JwtConverterFactory jwtConverterFactory; + private final RoleMapperFactory roleMapperFactory; public AuthorizationRegistrationService(UserBusinessService userBusinessService, - TeamPersistenceService teamPersistenceService, JwtOrganisationConverter jwtOrganisationConverter, - RoleMapper roleMapper) { + TeamPersistenceService teamPersistenceService, JwtConverterFactory jwtConverterFactory, + RoleMapperFactory roleMapperFactory) { this.userBusinessService = userBusinessService; this.teamPersistenceService = teamPersistenceService; - this.jwtOrganisationConverter = jwtOrganisationConverter; - this.roleMapper = roleMapper; + this.jwtConverterFactory = jwtConverterFactory; + this.roleMapperFactory = roleMapperFactory; } @Cacheable(value = AUTHORIZATION_USER_CACHE, key = "#user.username") public AuthorizationUser registerAuthorizationUser(User user, Jwt token) { - List organisationNames = jwtOrganisationConverter.convert(token); + List organisationNames = jwtConverterFactory.getJwtOrganisationConverter().convert(token); return new AuthorizationUser(userBusinessService.getOrCreateUser(user), getTeamIds(organisationNames), - getFirstLevelTeamIds(), roleMapper.mapOrganisationNames(organisationNames, user)); + getFirstLevelTeamIds(), + roleMapperFactory.getRoleMapper().mapAuthorizationRoles(organisationNames, user)); } private List getTeamIds(List organisationNames) { diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationService.java index 2312ef4cd7..b264e03750 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/AuthorizationService.java @@ -1,6 +1,6 @@ package ch.puzzle.okr.service.authorization; -import ch.puzzle.okr.converter.JwtUserConverter; +import ch.puzzle.okr.converter.JwtConverterFactory; import ch.puzzle.okr.models.Action; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.Team; @@ -35,15 +35,15 @@ public class AuthorizationService { private final AuthorizationRegistrationService authorizationRegistrationService; private final ObjectivePersistenceService objectivePersistenceService; private final ActionPersistenceService actionPersistenceService; - private final JwtUserConverter jwtUserConverter; + private final JwtConverterFactory jwtConverterFactory; public AuthorizationService(AuthorizationRegistrationService authorizationRegistrationService, ObjectivePersistenceService objectivePersistenceService, ActionPersistenceService actionPersistenceService, - JwtUserConverter jwtUserConverter) { + JwtConverterFactory jwtConverterFactory) { this.authorizationRegistrationService = authorizationRegistrationService; this.actionPersistenceService = actionPersistenceService; this.objectivePersistenceService = objectivePersistenceService; - this.jwtUserConverter = jwtUserConverter; + this.jwtConverterFactory = jwtConverterFactory; } public static boolean hasRoleReadTeamsDraft(AuthorizationUser user) { @@ -78,7 +78,7 @@ public AuthorizationUser getAuthorizationUser() { SecurityContext securityContext = SecurityContextHolder.getContext(); Authentication authentication = securityContext.getAuthentication(); Jwt token = (Jwt) authentication.getPrincipal(); - User user = jwtUserConverter.convert(token); + User user = jwtConverterFactory.getJwtUserConverter().convert(token); return authorizationRegistrationService.registerAuthorizationUser(user, token); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/TeamAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/TeamAuthorizationService.java index 76cf2cb8c5..c0101804f3 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/TeamAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/TeamAuthorizationService.java @@ -24,12 +24,16 @@ public TeamAuthorizationService(TeamBusinessService teamBusinessService, public Team createEntity(Team entity) { checkUserAuthorization("not authorized to create team"); - return teamBusinessService.createTeam(entity); + Team savedTeam = teamBusinessService.createTeam(entity); + savedTeam.setWriteable(true); + return savedTeam; } public Team updateEntity(Team entity, Long id) { checkUserAuthorization("not authorized to update team"); - return teamBusinessService.updateTeam(entity, id); + Team updatedTeam = teamBusinessService.updateTeam(entity, id); + updatedTeam.setWriteable(true); + return updatedTeam; } public void deleteEntity(Long id) { @@ -37,14 +41,18 @@ public void deleteEntity(Long id) { teamBusinessService.deleteTeam(id); } - public void checkUserAuthorization(String message) { + private void checkUserAuthorization(String message) { AuthorizationUser authorizationUser = authorizationService.getAuthorizationUser(); if (!hasRoleWriteAll(authorizationUser)) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, message); } } - public List getEntities() { - return teamBusinessService.getAllTeams(); + public List getAllTeams() { + AuthorizationUser authorizationUser = authorizationService.getAuthorizationUser(); + boolean isWritable = hasRoleWriteAll(authorizationUser); + List allTeams = teamBusinessService.getAllTeams(); + allTeams.forEach(team -> team.setWriteable(isWritable)); + return allTeams; } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java new file mode 100644 index 0000000000..24daa70404 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java @@ -0,0 +1,30 @@ +package ch.puzzle.okr.service.authorization; + +import ch.puzzle.okr.models.User; +import ch.puzzle.okr.models.authorization.AuthorizationUser; +import ch.puzzle.okr.service.business.UserBusinessService; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static ch.puzzle.okr.service.authorization.AuthorizationService.hasRoleWriteAll; + +@Service +public class UserAuthorizationService { + private final UserBusinessService userBusinessService; + private final AuthorizationService authorizationService; + + public UserAuthorizationService(UserBusinessService userBusinessService, + AuthorizationService authorizationService) { + this.userBusinessService = userBusinessService; + this.authorizationService = authorizationService; + } + + public List getAllUsers() { + AuthorizationUser authorizationUser = authorizationService.getAuthorizationUser(); + boolean isWritable = hasRoleWriteAll(authorizationUser); + List allUsers = userBusinessService.getAllUsers(); + allUsers.forEach(user -> user.setWriteable(isWritable)); + return allUsers; + } +} diff --git a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql index acb632e652..bbaff68591 100644 --- a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql +++ b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql @@ -176,35 +176,8 @@ values (21, 1, insert into quarter (id, label, start_date, end_date) values (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'); -insert into organisation (id, version, org_name, state) -values (1, 1, 'org_bl', 'ACTIVE'), - (2, 1, 'org_de', 'ACTIVE'), - (3, 1, 'org_gl', 'ACTIVE'), - (4, 1, 'org_pl', 'ACTIVE'), - (5, 1, 'org_pv', 'ACTIVE'), - (6, 1, 'org_ux', 'ACTIVE'), - (7, 1, 'org_zh', 'ACTIVE'), - (8, 1, 'org_sys', 'ACTIVE'), - (9, 1, 'org_azubi', 'ACTIVE'), - (10, 1, 'org_de_gl', 'ACTIVE'), - (11, 1, 'org_de_gs', 'ACTIVE'), - (12, 1, 'org_devtre', 'ACTIVE'), - (13, 1, 'org_racoon', 'ACTIVE'), - (14, 1, 'org_tqm_qm', 'ACTIVE'), - (15, 1, 'org_devruby', 'ACTIVE'), - (16, 1, 'org_midcicd', 'ACTIVE'), - (17, 1, 'org_verkauf', 'ACTIVE'), - (18, 1, 'org_finanzen', 'ACTIVE'), - (19, 1, 'org_mobility', 'ACTIVE'), - (20, 1, 'org_personal', 'ACTIVE'), - (21, 1, 'org_security', 'ACTIVE'), - (22, 1, 'org_marketing', 'ACTIVE'), - (23, 1, 'org_openshift', 'ACTIVE'), - (24, 1, 'org_ausbildung', 'ACTIVE'), - (25, 1, 'org_backoffice', 'ACTIVE'), - (26, 1, 'org_branch_sec', 'ACTIVE'), - (27, 1, 'org_standort_zh', 'ACTIVE'), - (28, 1, 'org_midcontainer', 'ACTIVE'); +delete +from team_organisation; insert into team_organisation (team_id, organisation_id) values (5, 3), diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 8c558326a5..db4d5b0077 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -12,24 +12,24 @@ truncate table action; SET REFERENTIAL_INTEGRITY TRUE; -ALTER SEQUENCE sequence_team RESTART WITH 200; -ALTER SEQUENCE sequence_person RESTART WITH 200; -ALTER SEQUENCE sequence_quarter RESTART WITH 200; -ALTER SEQUENCE sequence_objective RESTART WITH 200; -ALTER SEQUENCE sequence_key_result RESTART WITH 200; -ALTER SEQUENCE sequence_check_in RESTART WITH 200; +ALTER SEQUENCE sequence_action RESTART WITH 200; ALTER SEQUENCE sequence_alignment RESTART WITH 200; +ALTER SEQUENCE sequence_check_in RESTART WITH 200; ALTER SEQUENCE sequence_completed RESTART WITH 200; +ALTER SEQUENCE sequence_key_result RESTART WITH 200; +ALTER SEQUENCE sequence_objective RESTART WITH 200; ALTER SEQUENCE sequence_organisation RESTART WITH 200; -ALTER SEQUENCE sequence_action RESTART WITH 200; +ALTER SEQUENCE sequence_person RESTART WITH 200; +ALTER SEQUENCE sequence_quarter RESTART WITH 200; +ALTER SEQUENCE sequence_team RESTART WITH 200; -insert into person (id, email, firstname, lastname, username) -values (1, 'peggimann@puzzle.ch', 'Paco', 'Eggimann', 'peggimann'), - (11, 'wunderland@puzzle.ch', 'Alice', 'Wunderland', 'alice'), - (21, 'baumeister@puzzle.ch', 'Bob', 'Baumeister', 'bob'), - (31, 'peterson@puzzle.ch', 'Findus', 'Peterson', 'findus'), - (41, 'egiman@puzzle.ch', 'Paco', 'Egiman', 'paco'), - (51, 'papierer@puzzle.ch', 'Robin', 'Papierer', 'robin'); +insert into person (id, version, email, firstname, lastname, username) +values (1, 1, 'peggimann@puzzle.ch', 'Paco', 'Eggimann', 'peggimann'), + (11, 1, 'wunderland@puzzle.ch', 'Alice', 'Wunderland', 'alice'), + (21, 1, 'baumeister@puzzle.ch', 'Bob', 'Baumeister', 'bob'), + (31, 1, 'peterson@puzzle.ch', 'Findus', 'Peterson', 'findus'), + (41, 1, 'egiman@puzzle.ch', 'Paco', 'Egiman', 'paco'), + (51, 1, 'papierer@puzzle.ch', 'Robin', 'Papierer', 'robin'); insert into quarter (id, label, start_date, end_date) values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), @@ -41,11 +41,11 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), (7, 'GJ 23/24-Q2', '2023-10-01', '2023-12-31'), (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'); -insert into team (id, name) -values (4, '/BBT'), - (8, 'we are cube.³'), - (5, 'Puzzle ITC'), - (6, 'LoremIpsum'); +insert into team (id, version, name) +values (4, 1, '/BBT'), + (8, 1, 'we are cube.³'), + (5, 1, 'Puzzle ITC'), + (6, 1, 'LoremIpsum'); insert into objective (id, version, description, modified_on, progress, title, created_by_id, quarter_id, team_id, state, modified_by_id, created_on) diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 963c4546f8..f581f26742 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -12,6 +12,7 @@ create sequence if not exists sequence_action; create table if not exists person ( id bigint not null, + version int not null, email varchar(250) not null, firstname varchar(50) not null, lastname varchar(50) not null, @@ -36,8 +37,9 @@ create table if not exists quarter create table if not exists team ( - id bigint not null, - name varchar(250) not null, + id bigint not null, + version int not null, + name varchar(250) not null, primary key (id) ); @@ -144,7 +146,7 @@ create table action ( id bigint not null primary key, - version int not null, + version int not null, action varchar(4096) not null, priority integer not null, is_checked boolean not null, @@ -159,6 +161,7 @@ create index if not exists idx_completed_objective DROP VIEW IF EXISTS OVERVIEW; CREATE VIEW OVERVIEW AS SELECT TQ.TEAM_ID AS "TEAM_ID", + TQ.TEAM_VERSION AS "TEAM_VERSION", TQ.NAME AS "TEAM_NAME", TQ.QUARTER_ID AS "QUARTER_ID", TQ.LABEL AS "QUARTER_LABEL", @@ -180,7 +183,7 @@ SELECT TQ.TEAM_ID AS "TEAM_ID", C.ZONE AS "CHECK_IN_ZONE", C.CONFIDENCE, C.CREATED_ON AS "CHECK_IN_CREATED_ON" -FROM (SELECT T.ID AS TEAM_ID, T.NAME, Q.ID AS QUARTER_ID, Q.LABEL +FROM (SELECT T.ID AS TEAM_ID, T.VERSION AS TEAM_VERSION, T.NAME, Q.ID AS QUARTER_ID, Q.LABEL FROM TEAM T, QUARTER Q) TQ LEFT JOIN OBJECTIVE O ON TQ.TEAM_ID = O.TEAM_ID AND TQ.QUARTER_ID = O.QUARTER_ID diff --git a/backend/src/main/resources/db/migration/V2_0_10__createActionTable.sql b/backend/src/main/resources/db/migration/V2_0_10__createActionTable.sql index c0624cef81..a8a0bf7d1d 100644 --- a/backend/src/main/resources/db/migration/V2_0_10__createActionTable.sql +++ b/backend/src/main/resources/db/migration/V2_0_10__createActionTable.sql @@ -1,5 +1,5 @@ create sequence if not exists sequence_action; -ALTER SEQUENCE sequence_action RESTART WITH 500; +ALTER SEQUENCE sequence_action RESTART WITH 1000; create table if not exists action ( id bigint not null primary key, diff --git a/backend/src/main/resources/db/migration/V2_0_11__updateSequences.sql b/backend/src/main/resources/db/migration/V2_0_11__updateSequences.sql new file mode 100644 index 0000000000..ce392b322b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_0_11__updateSequences.sql @@ -0,0 +1,10 @@ +ALTER SEQUENCE sequence_action RESTART WITH 1000; +ALTER SEQUENCE sequence_alignment RESTART WITH 1000; +ALTER SEQUENCE sequence_check_in RESTART WITH 1000; +ALTER SEQUENCE sequence_completed RESTART WITH 1000; +ALTER SEQUENCE sequence_key_result RESTART WITH 1000; +ALTER SEQUENCE sequence_objective RESTART WITH 1000; +ALTER SEQUENCE sequence_organisation RESTART WITH 1000; +ALTER SEQUENCE sequence_person RESTART WITH 1000; +ALTER SEQUENCE sequence_quarter RESTART WITH 1000; +ALTER SEQUENCE sequence_team RESTART WITH 1000; diff --git a/backend/src/main/resources/db/migration/V2_0_5__createCompletedTable.sql b/backend/src/main/resources/db/migration/V2_0_5__createCompletedTable.sql index ee7fb73a9d..6eaf6d3670 100644 --- a/backend/src/main/resources/db/migration/V2_0_5__createCompletedTable.sql +++ b/backend/src/main/resources/db/migration/V2_0_5__createCompletedTable.sql @@ -1,5 +1,5 @@ create sequence if not exists sequence_completed; -alter sequence sequence_completed restart with 500; +alter sequence sequence_completed restart with 1000; create table if not exists completed ( diff --git a/backend/src/main/resources/db/migration/V2_0_7__createAuthorizationOrganisation.sql b/backend/src/main/resources/db/migration/V2_0_7__createAuthorizationOrganisation.sql index 8e3438ef8c..b309489d78 100644 --- a/backend/src/main/resources/db/migration/V2_0_7__createAuthorizationOrganisation.sql +++ b/backend/src/main/resources/db/migration/V2_0_7__createAuthorizationOrganisation.sql @@ -1,5 +1,5 @@ create sequence if not exists sequence_organisation; -Alter SEQUENCE sequence_organisation RESTART WITH 200; +Alter SEQUENCE sequence_organisation RESTART WITH 1000; create table if not exists organisation ( @@ -22,3 +22,48 @@ create table if not exists team_organisation create index if not exists idx_team_organisation_team on team_organisation (team_id); + +DO +$$ + BEGIN + IF NOT EXISTS(SELECT * FROM organisation) + THEN + insert into organisation (id, org_name, state) + values (1, 'org_bl', 'ACTIVE'), + (2, 'org_de', 'ACTIVE'), + (3, 'org_gl', 'ACTIVE'), + (4, 'org_pl', 'ACTIVE'), + (5, 'org_pv', 'ACTIVE'), + (6, 'org_ux', 'ACTIVE'), + (7, 'org_zh', 'ACTIVE'), + (8, 'org_sys', 'ACTIVE'), + (9, 'org_azubi', 'ACTIVE'), + (10, 'org_de_gl', 'ACTIVE'), + (11, 'org_de_gs', 'ACTIVE'), + (12, 'org_devtre', 'ACTIVE'), + (13, 'org_racoon', 'ACTIVE'), + (14, 'org_tqm_qm', 'ACTIVE'), + (15, 'org_devruby', 'ACTIVE'), + (16, 'org_midcicd', 'ACTIVE'), + (17, 'org_verkauf', 'ACTIVE'), + (18, 'org_finanzen', 'ACTIVE'), + (19, 'org_mobility', 'ACTIVE'), + (20, 'org_personal', 'ACTIVE'), + (21, 'org_security', 'ACTIVE'), + (22, 'org_marketing', 'ACTIVE'), + (23, 'org_openshift', 'ACTIVE'), + (24, 'org_ausbildung', 'ACTIVE'), + (25, 'org_backoffice', 'ACTIVE'), + (26, 'org_branch_sec', 'ACTIVE'), + (27, 'org_standort_zh', 'ACTIVE'), + (28, 'org_midcontainer', 'ACTIVE'); + + END IF; + IF NOT EXISTS(SELECT * FROM team_organisation) + THEN + insert into team_organisation (team_id, organisation_id) + select t.id, 3 + from team t; + END IF; + END; +$$; diff --git a/backend/src/main/resources/db/migration/V2_0_9__addOptimisticLocking.sql b/backend/src/main/resources/db/migration/V2_0_9__addOptimisticLocking.sql index 46d8dd2a37..1400dfa6a0 100644 --- a/backend/src/main/resources/db/migration/V2_0_9__addOptimisticLocking.sql +++ b/backend/src/main/resources/db/migration/V2_0_9__addOptimisticLocking.sql @@ -10,6 +10,10 @@ alter table objective add column if not exists version int; alter table organisation add column if not exists version int; +alter table person + add column if not exists version int; +alter table team + add column if not exists version int; update alignment set version = 1 @@ -29,6 +33,12 @@ where version is null; update organisation set version = 1 where version is null; +update person +set version = 1 +where version is null; +update team +set version = 1 +where version is null; alter table alignment alter column version set not null; @@ -42,3 +52,7 @@ alter table objective alter column version set not null; alter table organisation alter column version set not null; +alter table person + alter column version set not null; +alter table team + alter column version set not null; diff --git a/backend/src/main/resources/db/migration/V2_1_0__createOverviewView.sql b/backend/src/main/resources/db/migration/V2_1_0__createOverviewView.sql index 8779d187f2..43bae53589 100644 --- a/backend/src/main/resources/db/migration/V2_1_0__createOverviewView.sql +++ b/backend/src/main/resources/db/migration/V2_1_0__createOverviewView.sql @@ -1,6 +1,7 @@ DROP VIEW IF EXISTS OVERVIEW; CREATE VIEW OVERVIEW AS SELECT tq.team_id AS "team_id", + tq.team_version AS "team_version", tq.name AS "team_name", tq.quater_id AS "quarter_id", tq.label AS "quarter_label", @@ -22,7 +23,7 @@ SELECT tq.team_id AS "team_id", c.zone AS "check_in_zone", c.confidence, c.created_on AS "check_in_created_on" -FROM (select t.id as team_id, t.name, q.id as quater_id, q.label +FROM (select t.id as team_id, t.version as team_version, t.name, q.id as quater_id, q.label from team t, quarter q) tq LEFT JOIN OBJECTIVE O ON TQ.TEAM_ID = O.TEAM_ID AND TQ.QUATER_ID = O.QUARTER_ID diff --git a/backend/src/test/java/ch/puzzle/okr/controller/TeamControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/TeamControllerIT.java index bf2fd1bd4c..3984536208 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/TeamControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/TeamControllerIT.java @@ -44,8 +44,8 @@ class TeamControllerIT { static Team teamPuzzle = Team.Builder.builder().withId(5L).withName(PUZZLE).build(); static Team teamOKR = Team.Builder.builder().withId(7L).withName("OKR").build(); static List teamList = Arrays.asList(teamPuzzle, teamOKR); - static TeamDto teamPuzzleDto = new TeamDto(5L, PUZZLE, new ArrayList<>()); - static TeamDto teamOkrDto = new TeamDto(7L, "OKR", new ArrayList<>()); + static TeamDto teamPuzzleDto = new TeamDto(5L, 3, PUZZLE, new ArrayList<>()); + static TeamDto teamOkrDto = new TeamDto(7L, 4, "OKR", new ArrayList<>()); private static final String CREATE_NEW_TEAM = """ { @@ -58,7 +58,7 @@ class TeamControllerIT { } """; private static final String RESPONSE_NEW_TEAM = """ - {"id":7,"name":"OKR","organisations":[]}"""; + {"id":7,"version":4,"name":"OKR","organisations":[]}"""; private static final String UPDATE_TEAM = """ { @@ -84,7 +84,7 @@ void setUp() { @Test void shouldGetAllTeams() throws Exception { - BDDMockito.given(teamAuthorizationService.getEntities()).willReturn(teamList); + BDDMockito.given(teamAuthorizationService.getAllTeams()).willReturn(teamList); mvc.perform(get("/api/v2/teams?quarterId=1").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(2))) @@ -94,7 +94,7 @@ void shouldGetAllTeams() throws Exception { @Test void shouldGetAllTeamsWhenNoQuarterParamIsPassed() throws Exception { - BDDMockito.given(teamAuthorizationService.getEntities()).willReturn(teamList); + BDDMockito.given(teamAuthorizationService.getAllTeams()).willReturn(teamList); mvc.perform(get(BASE_URL).contentType(MediaType.APPLICATION_JSON)).andExpectAll(); BDDMockito.verify(teamMapper).toDto(teamOKR, null); BDDMockito.verify(teamMapper).toDto(teamPuzzle, null); @@ -102,7 +102,7 @@ void shouldGetAllTeamsWhenNoQuarterParamIsPassed() throws Exception { @Test void shouldGetAllTeamsIfTeamModelIsNull() throws Exception { - BDDMockito.given(teamAuthorizationService.getEntities()).willReturn(Collections.emptyList()); + BDDMockito.given(teamAuthorizationService.getAllTeams()).willReturn(Collections.emptyList()); mvc.perform(get("/api/v2/teams?quarterId=1").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(0))); @@ -130,7 +130,7 @@ void shouldReturnResponseStatusExceptionWhenCreatingObjectiveWithNullValues() th @Test void shouldReturnUpdatedTeam() throws Exception { - TeamDto teamDto = new TeamDto(1L, "OKR-Team", new ArrayList<>()); + TeamDto teamDto = new TeamDto(1L, 0, "OKR-Team", new ArrayList<>()); Team team = Team.Builder.builder().withId(1L).withName("OKR-Team") .withAuthorizationOrganisation(new ArrayList<>()).build(); @@ -140,6 +140,7 @@ void shouldReturnUpdatedTeam() throws Exception { mvc.perform(put(URL_TEAM_1).contentType(MediaType.APPLICATION_JSON).content(UPDATE_TEAM) .with(SecurityMockMvcRequestPostProcessors.csrf())).andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(jsonPath("$.id", Is.is(teamDto.id().intValue()))) + .andExpect(jsonPath("$.version", Is.is(teamDto.version()))) .andExpect(jsonPath("$.name", Is.is(teamDto.name()))); } @@ -176,4 +177,4 @@ void throwExceptionWhenOTeamWithIdCantBeFoundWhileDeleting() throws Exception { mvc.perform(delete(URL_TEAM_1).with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isNotFound()); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java index fd66977e4f..10511c2346 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/UserControllerIT.java @@ -3,7 +3,7 @@ import ch.puzzle.okr.dto.UserDto; import ch.puzzle.okr.mapper.UserMapper; import ch.puzzle.okr.models.User; -import ch.puzzle.okr.service.business.UserBusinessService; +import ch.puzzle.okr.service.authorization.UserAuthorizationService; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.jupiter.api.BeforeEach; @@ -43,12 +43,12 @@ class UserControllerIT { static User userBob = User.Builder.builder().withId(9L).withUsername(USERNAME_2).withFirstname(FIRSTNAME_2) .withLastname(LASTNAME_2).withEmail(EMAIL_2).build(); static List userList = Arrays.asList(userAlice, userBob); - static UserDto userAliceDto = new UserDto(2L, USERNAME_1, FIRSTNAME_1, LASTNAME_1, EMAIL_1); - static UserDto userBobDto = new UserDto(9L, USERNAME_2, FIRSTNAME_2, LASTNAME_2, EMAIL_2); + static UserDto userAliceDto = new UserDto(2L, 3, USERNAME_1, FIRSTNAME_1, LASTNAME_1, EMAIL_1, true); + static UserDto userBobDto = new UserDto(9L, 4, USERNAME_2, FIRSTNAME_2, LASTNAME_2, EMAIL_2, false); @Autowired private MockMvc mvc; @MockBean - private UserBusinessService userBusinessService; + private UserAuthorizationService userAuthorizationService; @MockBean private UserMapper userMapper; @@ -60,7 +60,7 @@ void setUp() { @Test void shouldGetAllUsers() throws Exception { - BDDMockito.given(userBusinessService.getAllUsers()).willReturn(userList); + BDDMockito.given(userAuthorizationService.getAllUsers()).willReturn(userList); mvc.perform(get("/api/v1/users").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(2))) @@ -76,7 +76,7 @@ void shouldGetAllUsers() throws Exception { @Test void shouldGetAllUsersIfNoUserExists() throws Exception { - BDDMockito.given(userBusinessService.getAllUsers()).willReturn(Collections.emptyList()); + BDDMockito.given(userAuthorizationService.getAllUsers()).willReturn(Collections.emptyList()); mvc.perform(get("/api/v1/users").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(0))); diff --git a/backend/src/test/java/ch/puzzle/okr/converter/JwtConverterFactoryIT.java b/backend/src/test/java/ch/puzzle/okr/converter/JwtConverterFactoryIT.java new file mode 100644 index 0000000000..47e7a5f51b --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/converter/JwtConverterFactoryIT.java @@ -0,0 +1,34 @@ +package ch.puzzle.okr.converter; + +import ch.puzzle.okr.models.User; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@SpringIntegrationTest +public class JwtConverterFactoryIT { + + @Autowired + private JwtConverterFactory jwtConverterFactory; + + @Test + void getJwtOrganisationConverter() { + Converter> converter = jwtConverterFactory.getJwtOrganisationConverter(); + assertNotNull(converter); + assertSame(converter, jwtConverterFactory.getJwtOrganisationConverter()); + } + + @Test + void getJwtUserConverter() { + Converter converter = jwtConverterFactory.getJwtUserConverter(); + assertNotNull(converter); + assertSame(converter, jwtConverterFactory.getJwtUserConverter()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/role/RoleMapperFactoryIT.java b/backend/src/test/java/ch/puzzle/okr/mapper/role/RoleMapperFactoryIT.java new file mode 100644 index 0000000000..d5848d8fad --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/mapper/role/RoleMapperFactoryIT.java @@ -0,0 +1,22 @@ +package ch.puzzle.okr.mapper.role; + +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@SpringIntegrationTest +public class RoleMapperFactoryIT { + + @Autowired + private RoleMapperFactory roleMapperFactory; + + @Test + void getRoleMapper() { + RoleMapper mapper = roleMapperFactory.getRoleMapper(); + assertNotNull(mapper); + assertSame(mapper, roleMapperFactory.getRoleMapper()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java index b7b3ac2d37..d191420e8c 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.converter.JwtConverterFactory; import ch.puzzle.okr.converter.JwtUserConverter; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.Team; @@ -43,6 +44,8 @@ class AuthorizationServiceTest { @Mock ObjectivePersistenceService objectivePersistenceService; @Mock + JwtConverterFactory jwtConverterFactory; + @Mock JwtUserConverter jwtUserConverter; private final User user = defaultUser(null); @@ -138,6 +141,7 @@ void getAuthorizationUserShouldReturnAuthorizationUser() { AuthorizationUser authorizationUser = defaultAuthorizationUser(); setSecurityContext(token); + when(jwtConverterFactory.getJwtUserConverter()).thenReturn(jwtUserConverter); when(jwtUserConverter.convert(token)).thenReturn(user); when(authorizationRegistrationService.registerAuthorizationUser(user, token)).thenReturn(authorizationUser); diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/OverviewAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/OverviewAuthorizationServiceTest.java index da152b99cc..12090b33c3 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/OverviewAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/OverviewAuthorizationServiceTest.java @@ -14,7 +14,8 @@ import java.util.List; -import static ch.puzzle.okr.TestHelper.defaultAuthorizationUser; +import static ch.puzzle.okr.TestHelper.*; +import static ch.puzzle.okr.models.authorization.AuthorizationRole.READ_ALL_PUBLISHED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -66,4 +67,17 @@ void getFilteredOverviewShouldReturnEmptyListWhenNotAuthorized() { List overviews = overviewAuthorizationService.getFilteredOverview(1L, List.of(5L), ""); assertThat(List.of()).hasSameElementsAs(overviews); } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void hasWriteAllAccessShouldReturnHasRoleWriteAll(boolean hasRoleWriteAll) { + if (hasRoleWriteAll) { + when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); + } else { + when(authorizationService.getAuthorizationUser()) + .thenReturn(mockAuthorizationUser(defaultUser(5L), List.of(), 7L, List.of(READ_ALL_PUBLISHED))); + } + + assertEquals(hasRoleWriteAll, overviewAuthorizationService.hasWriteAllAccess()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java index a513c2890c..a736e3cd9b 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java @@ -5,6 +5,8 @@ import ch.puzzle.okr.service.business.TeamBusinessService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -37,7 +39,7 @@ class TeamAuthorizationServiceTest { @Test void createEntityShouldReturnTeamWhenAuthorized() { when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); - when(teamAuthorizationService.createEntity(newTeam)).thenReturn(newTeam); + when(teamBusinessService.createTeam(newTeam)).thenReturn(newTeam); Team team = teamAuthorizationService.createEntity(newTeam); assertEquals(newTeam, team); @@ -93,12 +95,19 @@ void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { assertEquals(reason, exception.getReason()); } - @Test - void getEntitiesShouldReturnTeams() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getAllTeamsShouldReturnAllTeams(boolean isWriteable) { List teamList = List.of(newTeam, newTeam); + if (isWriteable) { + when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); + } else { + when(authorizationService.getAuthorizationUser()).thenReturn(userWithoutWriteAllRole()); + } when(teamBusinessService.getAllTeams()).thenReturn(teamList); - List teams = teamAuthorizationService.getEntities(); + List teams = teamAuthorizationService.getAllTeams(); assertEquals(teamList, teams); + teams.forEach(team -> assertEquals(isWriteable, team.isWriteable())); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java new file mode 100644 index 0000000000..dadb590a4b --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java @@ -0,0 +1,48 @@ +package ch.puzzle.okr.service.authorization; + +import ch.puzzle.okr.models.User; +import ch.puzzle.okr.models.authorization.AuthorizationUser; +import ch.puzzle.okr.service.business.UserBusinessService; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static ch.puzzle.okr.TestHelper.defaultAuthorizationUser; +import static ch.puzzle.okr.TestHelper.userWithoutWriteAllRole; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UserAuthorizationServiceTest { + @Mock + UserBusinessService userBusinessService; + @Mock + AuthorizationService authorizationService; + @InjectMocks + private UserAuthorizationService userAuthorizationService; + + private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); + User user = User.Builder.builder().withId(5L).withFirstname("firstname").withLastname("lastname") + .withUsername("username").withEmail("lastname@puzzle.ch").build(); + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void getAllUsersShouldReturnAllUsers(boolean isWriteable) { + List userList = List.of(user, user); + if (isWriteable) { + when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); + } else { + when(authorizationService.getAuthorizationUser()).thenReturn(userWithoutWriteAllRole()); + } + when(userBusinessService.getAllUsers()).thenReturn(userList); + + List users = userAuthorizationService.getAllUsers(); + assertEquals(userList, users); + users.forEach(user -> assertEquals(isWriteable, user.isWriteable())); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index d268668286..eb16d3b652 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -23,6 +23,7 @@ import static ch.puzzle.okr.TestHelper.defaultAuthorizationUser; import static ch.puzzle.okr.models.State.DRAFT; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -57,15 +58,25 @@ class ObjectiveBusinessServiceTest { @Test void getOneObjective() { - Mockito.when(objectivePersistenceService.findById(5L)).thenReturn(objective); + when(objectivePersistenceService.findById(5L)).thenReturn(objective); + Objective realObjective = objectiveBusinessService.getEntityById(5L); assertEquals("Objective 1", realObjective.getTitle()); } + @Test + void getEntitiesByTeamId() { + when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective)); + + List entities = objectiveBusinessService.getEntitiesByTeamId(5L); + + assertThat(entities).hasSameElementsAs(List.of(objective)); + } + @Test void shouldNotFindTheObjective() { - Mockito.when(objectivePersistenceService.findById(6L)) + when(objectivePersistenceService.findById(6L)) .thenThrow(new ResponseStatusException(NOT_FOUND, "Objective with id 6 not found")); ResponseStatusException exception = assertThrows(ResponseStatusException.class, @@ -94,7 +105,7 @@ void shouldSaveANewObjective() { void shouldNotThrowResponseStatusExceptionWhenPuttingNullId() { Objective objective1 = Objective.Builder.builder().withId(null).withTitle("Title") .withDescription("Description").withModifiedOn(LocalDateTime.now()).build(); - Mockito.when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjective); + when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjective); Objective savedObjective = objectiveBusinessService.createEntity(objective1, authorizationUser); assertNull(savedObjective.getId()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ActionPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ActionPersistenceServiceIT.java index d350b8895f..881516f55a 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ActionPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ActionPersistenceServiceIT.java @@ -11,8 +11,8 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; @SpringIntegrationTest class ActionPersistenceServiceIT { @@ -21,7 +21,12 @@ class ActionPersistenceServiceIT { private ActionPersistenceService actionPersistenceService; private static Action createAction(Long id) { - return Action.Builder.builder().withId(id).withAction("Neue Katze").withPriority(0).withIsChecked(false) + return createAction(id, 1); + } + + private static Action createAction(Long id, int version) { + return Action.Builder.builder().withId(id).withVersion(version).withAction("Neue Katze").withPriority(0) + .withIsChecked(false) .withKeyResult(KeyResultMetric.Builder.builder().withBaseline(1.0).withStretchGoal(13.0).withId(8L) .withObjective(Objective.Builder.builder().withId(1L).build()).build()) .build(); @@ -68,6 +73,18 @@ void updateActionShouldUpdateAction() { assertEquals(4, updateAction.getPriority()); } + @Test + void updateActionShouldThrowExceptionWhenAlreadyUpdated() { + createdAction = actionPersistenceService.save(createAction(null)); + Action changedAction = createAction(createdAction.getId(), 0); + changedAction.setAction("Updated Action"); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> actionPersistenceService.save(changedAction)); + assertEquals(UNPROCESSABLE_ENTITY, exception.getStatus()); + assertTrue(exception.getReason().contains("updated or deleted by another user")); + } + @Test void getAllActionsShouldReturnListOfAllActions() { List actions = actionPersistenceService.findAll(); diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java index 2b21ba53bb..f1cfc68165 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; @SpringIntegrationTest class TeamPersistenceServiceIT { @@ -92,6 +93,19 @@ void shouldUpdateTeamProperly() { assertEquals("Updated Team", returnedTeam.getName()); } + @Test + void updateTeamShouldThrowExceptionWhenAlreadyUpdated() { + Team team = Team.Builder.builder().withVersion(1).withName("New Team").build(); + createdTeam = teamPersistenceService.save(team); + Team changedTeam = Team.Builder.builder().withId(createdTeam.getId()).withVersion(0).withName("Changed Team") + .build(); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> teamPersistenceService.save(changedTeam)); + assertEquals(UNPROCESSABLE_ENTITY, exception.getStatus()); + assertTrue(exception.getReason().contains("updated or deleted by another user")); + } + @Test void shouldDeleteTeam() { Team team = Team.Builder.builder().withName("New Team").build(); diff --git a/backend/src/test/resources/logback.xml b/backend/src/test/resources/logback.xml new file mode 100644 index 0000000000..0f596d4a21 --- /dev/null +++ b/backend/src/test/resources/logback.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index aa6a5b7565..480fc9cfd4 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -53,8 +53,8 @@ const quarterService = { const teamService = { getAllTeams(): Observable { return of([ - { id: 1, name: teamMin1.name, writeable: true, organisations: [] }, - { id: 4, name: 'team2', writeable: true, organisations: [] }, + { id: 1, version: 2, name: teamMin1.name, writeable: true, organisations: [] }, + { id: 4, version: 5, name: 'team2', writeable: true, organisations: [] }, ]); }, }; diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index d700eb6322..6e998ae231 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -52,6 +52,7 @@ export class ObjectiveFormComponent implements OnInit { objective: { objectiveId?: number; teamId?: number; + teamVersion?: number; }; }, ) {} diff --git a/frontend/src/app/shared/dialog/team-management/team-management.component.spec.ts b/frontend/src/app/shared/dialog/team-management/team-management.component.spec.ts index c5ad9de7cb..e4e5819190 100644 --- a/frontend/src/app/shared/dialog/team-management/team-management.component.spec.ts +++ b/frontend/src/app/shared/dialog/team-management/team-management.component.spec.ts @@ -137,7 +137,11 @@ describe('TeamManagementComponent', () => { fixture.detectChanges(); component.saveTeam(); expect(teamServiceMock.updateTeam).toHaveBeenCalled(); - expect(teamServiceMock.updateTeam).toHaveBeenCalledWith({ ...teamFormObject, id: teamMin1.id } as Team); + expect(teamServiceMock.updateTeam).toHaveBeenCalledWith({ + ...teamFormObject, + id: teamMin1.id, + version: teamMin1.version, + } as Team); }); it('should call service method to delete team', async () => { diff --git a/frontend/src/app/shared/dialog/team-management/team-management.component.ts b/frontend/src/app/shared/dialog/team-management/team-management.component.ts index c9565a61f4..04db6ff091 100644 --- a/frontend/src/app/shared/dialog/team-management/team-management.component.ts +++ b/frontend/src/app/shared/dialog/team-management/team-management.component.ts @@ -33,7 +33,10 @@ export class TeamManagementComponent implements OnInit { private dialog: MatDialog, private organisationService: OrganisationService, private teamService: TeamService, - @Inject(MAT_DIALOG_DATA) public data: { team: TeamMin }, + @Inject(MAT_DIALOG_DATA) + public data: { + team: TeamMin; + }, ) {} ngOnInit(): void { @@ -60,7 +63,11 @@ export class TeamManagementComponent implements OnInit { this.dialogRef.close(result); }); } else { - let updatedTeam: Team = { ...this.teamForm.value, id: this.data.team.id } as Team; + let updatedTeam: Team = { + ...this.teamForm.value, + id: this.data.team.id, + version: this.data.team.version, + } as Team; this.teamService.updateTeam(updatedTeam).subscribe((result) => { this.dialogRef.close(result); }); @@ -117,6 +124,4 @@ export class TeamManagementComponent implements OnInit { compareWithFunc(a: Organisation, b: Organisation) { return a.orgName === b.orgName; } - - protected readonly OrganisationState = OrganisationState; } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 8d18988959..2bf7cf8d0d 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -43,28 +43,33 @@ export const teamFormObject = { export const teamMin1: TeamMin = { id: 1, + version: 2, name: 'Marketing Team', writable: true, } as TeamMin; export const teamMin2: TeamMin = { id: 1, + version: 3, name: 'Marketing Team', writable: false, } as TeamMin; export const team1: Team = { id: 1, + version: 2, name: 'Team2', } as Team; export const team2: Team = { id: 2, + version: 3, name: 'Team2', } as Team; export const team3: Team = { id: 3, + version: 4, name: 'Team3', } as Team; diff --git a/frontend/src/app/shared/types/model/Team.ts b/frontend/src/app/shared/types/model/Team.ts index 0fec92d6f5..4a196320b9 100644 --- a/frontend/src/app/shared/types/model/Team.ts +++ b/frontend/src/app/shared/types/model/Team.ts @@ -2,6 +2,7 @@ import { Organisation } from './Organisation'; export interface Team { id: number; + version: number; name: string; organisations: Organisation[]; } diff --git a/frontend/src/app/shared/types/model/TeamMin.ts b/frontend/src/app/shared/types/model/TeamMin.ts index cc3abf978f..6743ff678e 100644 --- a/frontend/src/app/shared/types/model/TeamMin.ts +++ b/frontend/src/app/shared/types/model/TeamMin.ts @@ -1,5 +1,6 @@ export interface TeamMin { id: number; + version: number; name: string; writable: boolean; hasInActiveOrganisations: boolean; diff --git a/frontend/src/app/team/team.component.ts b/frontend/src/app/team/team.component.ts index b633578d3f..4e4692467e 100644 --- a/frontend/src/app/team/team.component.ts +++ b/frontend/src/app/team/team.component.ts @@ -39,7 +39,12 @@ export class TeamComponent { createObjective() { const matDialogRef = this.dialog.open(ObjectiveFormComponent, { - data: { objective: { teamId: this.overviewEntity.value.team.id } }, + data: { + objective: { + teamId: this.overviewEntity.value.team.id, + teamVersion: this.overviewEntity.value.team.version, + }, + }, width: '45em', }); matDialogRef.afterClosed().subscribe((result) => {