mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:29:36 +01:00
feat(inventory): GET Endpoint für einzelnen Lagerort (StorageLocation by ID)
GetStorageLocation Use Case mit AuthorizationPort, GET /{id} Endpoint im Controller.
6 Unit Tests und 5 Integrationstests für alle Edge Cases.
This commit is contained in:
parent
df1d1dfdd3
commit
42c9ca9d19
5 changed files with 264 additions and 2 deletions
|
|
@ -0,0 +1,40 @@
|
||||||
|
package de.effigenix.application.inventory;
|
||||||
|
|
||||||
|
import de.effigenix.domain.inventory.InventoryAction;
|
||||||
|
import de.effigenix.domain.inventory.StorageLocation;
|
||||||
|
import de.effigenix.domain.inventory.StorageLocationError;
|
||||||
|
import de.effigenix.domain.inventory.StorageLocationId;
|
||||||
|
import de.effigenix.domain.inventory.StorageLocationRepository;
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
|
import de.effigenix.shared.security.AuthorizationPort;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class GetStorageLocation {
|
||||||
|
|
||||||
|
private final StorageLocationRepository storageLocationRepository;
|
||||||
|
private final AuthorizationPort authPort;
|
||||||
|
|
||||||
|
public GetStorageLocation(StorageLocationRepository storageLocationRepository, AuthorizationPort authPort) {
|
||||||
|
this.storageLocationRepository = storageLocationRepository;
|
||||||
|
this.authPort = authPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<StorageLocationError, StorageLocation> execute(String storageLocationId, ActorId performedBy) {
|
||||||
|
if (!authPort.can(performedBy, InventoryAction.STOCK_READ)) {
|
||||||
|
return Result.failure(new StorageLocationError.Unauthorized("Not authorized to view storage location"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageLocationId == null || storageLocationId.isBlank()) {
|
||||||
|
return Result.failure(new StorageLocationError.StorageLocationNotFound(storageLocationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (storageLocationRepository.findById(StorageLocationId.of(storageLocationId))) {
|
||||||
|
case Result.Failure(var err) -> Result.failure(new StorageLocationError.RepositoryFailure(err.message()));
|
||||||
|
case Result.Success(var opt) -> opt
|
||||||
|
.<Result<StorageLocationError, StorageLocation>>map(Result::success)
|
||||||
|
.orElseGet(() -> Result.failure(new StorageLocationError.StorageLocationNotFound(storageLocationId)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import de.effigenix.application.inventory.RemoveStockBatch;
|
||||||
import de.effigenix.application.inventory.UnblockStockBatch;
|
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||||
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
||||||
|
import de.effigenix.application.inventory.GetStorageLocation;
|
||||||
import de.effigenix.application.inventory.ListStorageLocations;
|
import de.effigenix.application.inventory.ListStorageLocations;
|
||||||
import de.effigenix.application.inventory.UpdateStorageLocation;
|
import de.effigenix.application.inventory.UpdateStorageLocation;
|
||||||
import de.effigenix.application.usermanagement.AuditLogger;
|
import de.effigenix.application.usermanagement.AuditLogger;
|
||||||
|
|
@ -38,8 +39,8 @@ public class InventoryUseCaseConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository) {
|
public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository) {
|
||||||
return new DeactivateStorageLocation(storageLocationRepository);
|
return new DeactivateStorageLocation(storageLocationRepository, stockRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
@ -47,6 +48,11 @@ public class InventoryUseCaseConfiguration {
|
||||||
return new ActivateStorageLocation(storageLocationRepository);
|
return new ActivateStorageLocation(storageLocationRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GetStorageLocation getStorageLocation(StorageLocationRepository storageLocationRepository, AuthorizationPort authorizationPort) {
|
||||||
|
return new GetStorageLocation(storageLocationRepository, authorizationPort);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ListStorageLocations listStorageLocations(StorageLocationRepository storageLocationRepository) {
|
public ListStorageLocations listStorageLocations(StorageLocationRepository storageLocationRepository) {
|
||||||
return new ListStorageLocations(storageLocationRepository);
|
return new ListStorageLocations(storageLocationRepository);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package de.effigenix.infrastructure.inventory.web.controller;
|
||||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||||
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
||||||
|
import de.effigenix.application.inventory.GetStorageLocation;
|
||||||
import de.effigenix.application.inventory.ListStorageLocations;
|
import de.effigenix.application.inventory.ListStorageLocations;
|
||||||
import de.effigenix.application.inventory.UpdateStorageLocation;
|
import de.effigenix.application.inventory.UpdateStorageLocation;
|
||||||
import de.effigenix.application.inventory.command.CreateStorageLocationCommand;
|
import de.effigenix.application.inventory.command.CreateStorageLocationCommand;
|
||||||
|
|
@ -34,6 +35,7 @@ public class StorageLocationController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(StorageLocationController.class);
|
private static final Logger logger = LoggerFactory.getLogger(StorageLocationController.class);
|
||||||
|
|
||||||
private final CreateStorageLocation createStorageLocation;
|
private final CreateStorageLocation createStorageLocation;
|
||||||
|
private final GetStorageLocation getStorageLocation;
|
||||||
private final UpdateStorageLocation updateStorageLocation;
|
private final UpdateStorageLocation updateStorageLocation;
|
||||||
private final DeactivateStorageLocation deactivateStorageLocation;
|
private final DeactivateStorageLocation deactivateStorageLocation;
|
||||||
private final ActivateStorageLocation activateStorageLocation;
|
private final ActivateStorageLocation activateStorageLocation;
|
||||||
|
|
@ -41,12 +43,14 @@ public class StorageLocationController {
|
||||||
|
|
||||||
public StorageLocationController(
|
public StorageLocationController(
|
||||||
CreateStorageLocation createStorageLocation,
|
CreateStorageLocation createStorageLocation,
|
||||||
|
GetStorageLocation getStorageLocation,
|
||||||
UpdateStorageLocation updateStorageLocation,
|
UpdateStorageLocation updateStorageLocation,
|
||||||
DeactivateStorageLocation deactivateStorageLocation,
|
DeactivateStorageLocation deactivateStorageLocation,
|
||||||
ActivateStorageLocation activateStorageLocation,
|
ActivateStorageLocation activateStorageLocation,
|
||||||
ListStorageLocations listStorageLocations
|
ListStorageLocations listStorageLocations
|
||||||
) {
|
) {
|
||||||
this.createStorageLocation = createStorageLocation;
|
this.createStorageLocation = createStorageLocation;
|
||||||
|
this.getStorageLocation = getStorageLocation;
|
||||||
this.updateStorageLocation = updateStorageLocation;
|
this.updateStorageLocation = updateStorageLocation;
|
||||||
this.deactivateStorageLocation = deactivateStorageLocation;
|
this.deactivateStorageLocation = deactivateStorageLocation;
|
||||||
this.activateStorageLocation = activateStorageLocation;
|
this.activateStorageLocation = activateStorageLocation;
|
||||||
|
|
@ -71,6 +75,22 @@ public class StorageLocationController {
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@PreAuthorize("hasAuthority('STOCK_READ') or hasAuthority('STOCK_WRITE')")
|
||||||
|
public ResponseEntity<StorageLocationResponse> getStorageLocation(
|
||||||
|
@PathVariable("id") String storageLocationId,
|
||||||
|
Authentication authentication
|
||||||
|
) {
|
||||||
|
var actorId = extractActorId(authentication);
|
||||||
|
var result = getStorageLocation.execute(storageLocationId, actorId);
|
||||||
|
|
||||||
|
if (result.isFailure()) {
|
||||||
|
throw new StorageLocationDomainErrorException(result.unsafeGetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
||||||
public ResponseEntity<StorageLocationResponse> createStorageLocation(
|
public ResponseEntity<StorageLocationResponse> createStorageLocation(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package de.effigenix.application.inventory;
|
||||||
|
|
||||||
|
import de.effigenix.domain.inventory.*;
|
||||||
|
import de.effigenix.shared.common.RepositoryError;
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
|
import de.effigenix.shared.security.AuthorizationPort;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("GetStorageLocation Use Case")
|
||||||
|
class GetStorageLocationTest {
|
||||||
|
|
||||||
|
@Mock private StorageLocationRepository storageLocationRepository;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
|
private GetStorageLocation getStorageLocation;
|
||||||
|
private ActorId performedBy;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
getStorageLocation = new GetStorageLocation(storageLocationRepository, authPort);
|
||||||
|
performedBy = ActorId.of("admin-user");
|
||||||
|
}
|
||||||
|
|
||||||
|
private StorageLocation existingLocation(String id) {
|
||||||
|
return StorageLocation.reconstitute(
|
||||||
|
StorageLocationId.of(id),
|
||||||
|
new StorageLocationName("Kühlraum 1"),
|
||||||
|
StorageType.COLD_ROOM,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should return storage location when found")
|
||||||
|
void shouldReturnStorageLocationWhenFound() {
|
||||||
|
var locationId = "location-1";
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(true);
|
||||||
|
when(storageLocationRepository.findById(StorageLocationId.of(locationId)))
|
||||||
|
.thenReturn(Result.success(Optional.of(existingLocation(locationId))));
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute(locationId, performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().id().value()).isEqualTo(locationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail with StorageLocationNotFound when not found")
|
||||||
|
void shouldFailWhenNotFound() {
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(true);
|
||||||
|
when(storageLocationRepository.findById(StorageLocationId.of("nonexistent")))
|
||||||
|
.thenReturn(Result.success(Optional.empty()));
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute("nonexistent", performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.StorageLocationNotFound.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||||
|
void shouldFailWhenUnauthorized() {
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(false);
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute("location-1", performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.Unauthorized.class);
|
||||||
|
verify(storageLocationRepository, never()).findById(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail with RepositoryFailure when repository fails")
|
||||||
|
void shouldFailWhenRepositoryFails() {
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(true);
|
||||||
|
when(storageLocationRepository.findById(StorageLocationId.of("location-1")))
|
||||||
|
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute("location-1", performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail with StorageLocationNotFound when id is null")
|
||||||
|
void shouldFailWhenIdIsNull() {
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(true);
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute(null, performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.StorageLocationNotFound.class);
|
||||||
|
verify(storageLocationRepository, never()).findById(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail with StorageLocationNotFound when id is blank")
|
||||||
|
void shouldFailWhenIdIsBlank() {
|
||||||
|
when(authPort.can(performedBy, InventoryAction.STOCK_READ)).thenReturn(true);
|
||||||
|
|
||||||
|
var result = getStorageLocation.execute(" ", performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.StorageLocationNotFound.class);
|
||||||
|
verify(storageLocationRepository, never()).findById(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package de.effigenix.infrastructure.inventory.web;
|
||||||
import de.effigenix.domain.usermanagement.RoleName;
|
import de.effigenix.domain.usermanagement.RoleName;
|
||||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||||
import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest;
|
import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest;
|
||||||
|
import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest;
|
||||||
import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest;
|
import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest;
|
||||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||||
|
|
@ -225,6 +226,60 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Einzelnen Lagerort abfragen ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerort per ID abfragen → 200")
|
||||||
|
void getStorageLocation_existing_returns200() throws Exception {
|
||||||
|
String id = createAndReturnId("Detail Test", "COLD_ROOM", "-2", "8");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations/" + id)
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id").value(id))
|
||||||
|
.andExpect(jsonPath("$.name").value("Detail Test"))
|
||||||
|
.andExpect(jsonPath("$.storageType").value("COLD_ROOM"))
|
||||||
|
.andExpect(jsonPath("$.temperatureRange.minTemperature").value(-2))
|
||||||
|
.andExpect(jsonPath("$.temperatureRange.maxTemperature").value(8))
|
||||||
|
.andExpect(jsonPath("$.active").value(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Nicht existierenden Lagerort per ID abfragen → 404")
|
||||||
|
void getStorageLocation_notFound_returns404() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations/" + UUID.randomUUID())
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isNotFound())
|
||||||
|
.andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerort per ID abfragen mit STOCK_READ → 200")
|
||||||
|
void getStorageLocation_withReaderToken_returns200() throws Exception {
|
||||||
|
String id = createAndReturnId("Reader Detail", "DRY_STORAGE", null, null);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations/" + id)
|
||||||
|
.header("Authorization", "Bearer " + readerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.id").value(id))
|
||||||
|
.andExpect(jsonPath("$.name").value("Reader Detail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerort per ID abfragen ohne passende Berechtigung → 403")
|
||||||
|
void getStorageLocation_withViewerToken_returns403() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations/" + UUID.randomUUID())
|
||||||
|
.header("Authorization", "Bearer " + viewerToken))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerort per ID abfragen ohne Token → 401")
|
||||||
|
void getStorageLocation_withoutToken_returns401() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations/" + UUID.randomUUID()))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Lagerort bearbeiten (Story 1.2) ====================
|
// ==================== Lagerort bearbeiten (Story 1.2) ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -346,6 +401,26 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
|
||||||
.andExpect(jsonPath("$.active").value(false));
|
.andExpect(jsonPath("$.active").value(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerort mit Bestand deaktivieren → 409")
|
||||||
|
void deactivateStorageLocation_withStock_returns409() throws Exception {
|
||||||
|
String id = createAndReturnId("Stock Location", "DRY_STORAGE", null, null);
|
||||||
|
|
||||||
|
// Stock an diesem Lagerort anlegen
|
||||||
|
var stockRequest = new CreateStockRequest(
|
||||||
|
UUID.randomUUID().toString(), id, null, null, null);
|
||||||
|
mockMvc.perform(post("/api/inventory/stocks")
|
||||||
|
.header("Authorization", "Bearer " + adminToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(stockRequest)))
|
||||||
|
.andExpect(status().isCreated());
|
||||||
|
|
||||||
|
mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/deactivate")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isConflict())
|
||||||
|
.andExpect(jsonPath("$.code").value("STOCK_EXISTS_AT_LOCATION"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Bereits inaktiven Lagerort deaktivieren → 409")
|
@DisplayName("Bereits inaktiven Lagerort deaktivieren → 409")
|
||||||
void deactivateStorageLocation_alreadyInactive_returns409() throws Exception {
|
void deactivateStorageLocation_alreadyInactive_returns409() throws Exception {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue