diff --git a/settings.gradle.kts b/settings.gradle.kts index 32072f8..5bd628e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { versionCatalogs { create("libs") { version("micronaut", "5.0.2") - version("vulpes.model", "1.8.3") + version("vulpes.model", "2.0.0") version("uuid.creator", "6.1.1") version("datafaker", "2.7.0") version("jetbrains.annotation", "26.1.0") @@ -48,7 +48,7 @@ dependencyResolutionManagement { library("vulpes.api", "net.onelitefeather", "vulpes-model").versionRef("vulpes.model") library("jetbrains.annotation", "org.jetbrains", "annotations").versionRef("jetbrains.annotation") library("datafaker", "net.datafaker", "datafaker").versionRef("datafaker") - library("testcontainers.junit", "org.testcontainers", "junit-jupiter").withoutVersion() + library("testcontainers.junit", "org.testcontainers", "testcontainers-junit-jupiter").withoutVersion() library("hibernate.validator", "org.hibernate.validator", "hibernate-validator").versionRef("hibernate.validator") library("jakarta.validation", "jakarta.validation", "jakarta.validation-api").versionRef("jakarta.validation") diff --git a/src/main/java/net/onelitefeather/vulpes/backend/controller/AttributeController.java b/src/main/java/net/onelitefeather/vulpes/backend/controller/AttributeController.java index 98fc28f..effa610 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/controller/AttributeController.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/controller/AttributeController.java @@ -26,7 +26,7 @@ import java.util.List; import java.util.UUID; -@Controller("/attribute") +@Controller("/project/{projectId}/attribute") public class AttributeController { private final AttributeService attributeService; @@ -39,7 +39,7 @@ public AttributeController(AttributeService attributeService) { @Operation( summary = "Add a new attribute", operationId = "addAttribute", - description = "Adds a new attribute to the database. The attribute is created with the given properties.", + description = "Adds a new attribute to the given project.", tags = {"Attribute"} ) @ApiResponse( @@ -51,8 +51,8 @@ public AttributeController(AttributeService attributeService) { ) ) @ApiResponse( - responseCode = "500", - description = "The attribute could not be added to the database.", + responseCode = "404", + description = "The project was not found.", content = @Content( mediaType = "application/json", schema = @Schema(implementation = AttributeModelResponseDTO.AttributeModelErrorDTO.class) @@ -60,15 +60,18 @@ public AttributeController(AttributeService attributeService) { ) @Post @Validated(groups = ValidationGroup.Create.class) - public HttpResponse add(@Body AttributeModelDTO model) { - AttributeModelResponseDTO.AttributeModelDTO createdAttribute = attributeService.create(model); - return HttpResponse.ok(createdAttribute); + public HttpResponse add(@PathVariable UUID projectId, @Body AttributeModelDTO model) { + AttributeModelResponseDTO result = attributeService.create(projectId, model); + if (result instanceof AttributeModelResponseDTO.AttributeModelErrorDTO) { + return HttpResponse.notFound(result); + } + return HttpResponse.ok(result); } @Operation( summary = "Update an attribute", operationId = "updateAttribute", - description = "Returns the attribute with the given ID.", + description = "Updates an attribute owned by the given project.", tags = {"Attribute"} ) @ApiResponse( @@ -81,7 +84,7 @@ public HttpResponse add(@Body AttributeModelDTO model ) @ApiResponse( responseCode = "404", - description = "The attribute was not found.", + description = "The attribute was not found, or does not belong to the given project.", content = @Content( mediaType = "application/json", schema = @Schema(implementation = AttributeModelResponseDTO.AttributeModelErrorDTO.class) @@ -89,8 +92,8 @@ public HttpResponse add(@Body AttributeModelDTO model ) @Post("/update") @Validated(groups = ValidationGroup.Update.class) - public HttpResponse update(@Body AttributeModelDTO model) { - AttributeModelResponseDTO result = attributeService.update(model); + public HttpResponse update(@PathVariable UUID projectId, @Body AttributeModelDTO model) { + AttributeModelResponseDTO result = attributeService.update(projectId, model); if (result instanceof AttributeModelResponseDTO.AttributeModelErrorDTO) { return HttpResponse.notFound(result); } @@ -100,7 +103,7 @@ public HttpResponse update(@Body AttributeModelDTO mo @Operation( summary = "Delete an attribute by ID", operationId = "deleteAttributeById", - description = "Deletes the attribute with the given ID.", + description = "Deletes an attribute owned by the given project.", tags = {"Attribute"} ) @ApiResponse( @@ -113,15 +116,15 @@ public HttpResponse update(@Body AttributeModelDTO mo ) @ApiResponse( responseCode = "404", - description = "The attribute was not found.", + description = "The attribute was not found, or does not belong to the given project.", content = @Content( mediaType = "application/json", schema = @Schema(implementation = AttributeModelResponseDTO.AttributeModelErrorDTO.class) ) ) @Delete("/delete/{id}") - public HttpResponse delete(@PathVariable UUID id) { - AttributeModelResponseDTO result = attributeService.delete(id); + public HttpResponse delete(@PathVariable UUID projectId, @PathVariable UUID id) { + AttributeModelResponseDTO result = attributeService.delete(projectId, id); if (result instanceof AttributeModelResponseDTO.AttributeModelErrorDTO) { return HttpResponse.notFound(result); } @@ -129,14 +132,14 @@ public HttpResponse delete(@PathVariable UUID id) { } /** - * Deletes all [AttributeModel] from the database. + * Deletes all [AttributeModel] belonging to the given project. * - * @return a list with all [AttributeModel] mapped in a [HttpResponse] + * @return a list with all deleted [AttributeModel] mapped in a [HttpResponse] */ @Operation( summary = "Delete all attributes", operationId = "deleteAllAttributes", - description = "Deletes all attributes from the database.", + description = "Deletes all attributes belonging to the given project.", tags = {"Attribute"} ) @ApiResponse( @@ -148,20 +151,20 @@ public HttpResponse delete(@PathVariable UUID id) { ) ) @Delete("/delete") - public HttpResponse> deleteAll() { - List result = attributeService.deleteAll(); + public HttpResponse> deleteAll(@PathVariable UUID projectId) { + List result = attributeService.deleteAll(projectId); return HttpResponse.ok(result); } /** - * Returns all [AttributeModel] which are currently persists in the database. + * Returns all [AttributeModel] belonging to the given project. * * @return a list with all [AttributeModel] mapped in a [HttpResponse] */ @Operation( summary = "Get all attributes", operationId = "getAllAttributes", - description = "Gets all attributes from the database.", + description = "Gets all attributes belonging to the given project.", tags = {"Attribute"} ) @ApiResponse( @@ -177,8 +180,8 @@ public HttpResponse> deleteAll() { ) @Produces(MediaType.APPLICATION_JSON) @Get(uris = {"/"}) - public HttpResponse> getAll(Pageable pageable) { - Page models = attributeService.getAll(pageable); + public HttpResponse> getAll(@PathVariable UUID projectId, Pageable pageable) { + Page models = attributeService.getAll(projectId, pageable); return HttpResponse.ok(models); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/controller/NotificationController.java b/src/main/java/net/onelitefeather/vulpes/backend/controller/NotificationController.java index a509475..74d4439 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/controller/NotificationController.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/controller/NotificationController.java @@ -30,7 +30,7 @@ * @version 1.0.0 * @since 1.0.0 */ -@Controller("/notification") +@Controller("/project/{projectId}/notification") public class NotificationController { private final NotificationService notificationService; @@ -40,16 +40,10 @@ public NotificationController(NotificationService notificationService) { this.notificationService = notificationService; } - /** - * Adds a new notification. - * - * @param model the notification model to be added - * @return HttpResponse containing the added notification - */ @Operation( summary = "Add a new notification", operationId = "addNotification", - description = "Adds a new notification to the database.", + description = "Adds a new notification to the given project.", tags = {"Notification"} ) @ApiResponse( @@ -61,8 +55,8 @@ public NotificationController(NotificationService notificationService) { ) ) @ApiResponse( - responseCode = "500", - description = "The notification could not be added to the database.", + responseCode = "404", + description = "The project was not found.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NotificationModelResponseDTO.NotificationModelErrorDTO.class) @@ -71,23 +65,18 @@ public NotificationController(NotificationService notificationService) { @Post @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Create.class) - public HttpResponse add( - @Body NotificationModelDTO model - ) { - NotificationModelResponseDTO.NotificationModelDTO result = notificationService.create(model); + public HttpResponse add(@PathVariable UUID projectId, @Body NotificationModelDTO model) { + NotificationModelResponseDTO result = notificationService.create(projectId, model); + if (result instanceof NotificationModelResponseDTO.NotificationModelErrorDTO) { + return HttpResponse.notFound(result); + } return HttpResponse.ok(result); } - /** - * Retrieves a notification by its ID. - * - * @param id the ID of the notification to retrieve - * @return HttpResponse containing the notification if found, or not found response - */ @Operation( summary = "Get a notification by ID", operationId = "getNotificationById", - description = "Retrieves a notification from the database by its ID.", + description = "Retrieves a notification owned by the given project by its ID.", tags = {"Notification"} ) @ApiResponse( @@ -100,7 +89,7 @@ public HttpResponse add( ) @ApiResponse( responseCode = "404", - description = "The notification was not found in the database.", + description = "The notification was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NotificationModelResponseDTO.NotificationModelErrorDTO.class) @@ -108,24 +97,18 @@ public HttpResponse add( ) @Get("/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse getById(@PathVariable UUID id) { - Optional model = notificationService.findById(id); + public HttpResponse getById(@PathVariable UUID projectId, @PathVariable UUID id) { + Optional model = notificationService.findById(projectId, id); if (model.isPresent()) { return HttpResponse.ok(NotificationModelResponseDTO.NotificationModelDTO.createDTO(model.get())); } return HttpResponse.notFound(new NotificationModelResponseDTO.NotificationModelErrorDTO("Notification not found")); } - /** - * Removes a notification by its ID. - * - * @param id the ID of the notification to remove - * @return HttpResponse containing the removed notification if found, or not found response - */ @Operation( summary = "Remove a notification by ID", operationId = "removeNotificationById", - description = "Removes a notification from the database by its ID.", + description = "Removes a notification owned by the given project by its ID.", tags = {"Notification"} ) @ApiResponse( @@ -138,7 +121,7 @@ public HttpResponse getById(@PathVariable UUID id) ) @ApiResponse( responseCode = "404", - description = "The notification was not found in the database.", + description = "The notification was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NotificationModelResponseDTO.NotificationModelErrorDTO.class) @@ -146,23 +129,18 @@ public HttpResponse getById(@PathVariable UUID id) ) @Delete("/delete/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse remove(@PathVariable UUID id) { - NotificationModelResponseDTO result = notificationService.delete(id); + public HttpResponse remove(@PathVariable UUID projectId, @PathVariable UUID id) { + NotificationModelResponseDTO result = notificationService.delete(projectId, id); if (result instanceof NotificationModelResponseDTO.NotificationModelErrorDTO) { return HttpResponse.notFound(result); } return HttpResponse.ok(result); } - /** - * Retrieves all notifications. - * - * @return HttpResponse containing a list of all notifications - */ @Operation( summary = "Get all notifications", operationId = "getAllNotifications", - description = "Retrieves all notifications from the database.", + description = "Retrieves all notifications belonging to the given project.", tags = {"Notification"} ) @ApiResponse( @@ -176,30 +154,17 @@ public HttpResponse remove(@PathVariable UUID id) ) ) ) - @ApiResponse( - responseCode = "404", - description = "No notifications were found in the database.", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = NotificationModelResponseDTO.NotificationModelErrorDTO.class) - ) - ) @Get(uris = {"/"}) @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> getAll(Pageable pageable) { - Page list = notificationService.getAll(pageable); + public HttpResponse> getAll(@PathVariable UUID projectId, Pageable pageable) { + Page list = notificationService.getAll(projectId, pageable); return HttpResponse.ok(list); } - /** - * Deletes all notifications. - * - * @return HttpResponse containing an empty list - */ @Operation( summary = "Delete all notifications", operationId = "deleteAllNotifications", - description = "Deletes all notifications from the database.", + description = "Deletes all notifications belonging to the given project.", tags = {"Notification"} ) @ApiResponse( @@ -212,21 +177,15 @@ public HttpResponse> get ) @Delete("/delete/") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> deleteAll() { - List result = notificationService.deleteAll(); + public HttpResponse> deleteAll(@PathVariable UUID projectId) { + List result = notificationService.deleteAll(projectId); return HttpResponse.ok(result); } - /** - * Updates an existing notification. - * - * @param model the notification model to update - * @return HttpResponse containing the updated notification - */ @Operation( summary = "Update a notification", operationId = "updateNotification", - description = "Updates an existing notification in the database.", + description = "Updates a notification owned by the given project.", tags = {"Notification"} ) @ApiResponse( @@ -239,7 +198,7 @@ public HttpResponse> deleteAll() { ) @ApiResponse( responseCode = "404", - description = "The notification was not found in the database.", + description = "The notification was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NotificationModelResponseDTO.NotificationModelErrorDTO.class) @@ -248,8 +207,8 @@ public HttpResponse> deleteAll() { @Post("/update") @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Update.class) - public HttpResponse update(@Body NotificationModelDTO model) { - NotificationModelResponseDTO result = notificationService.update(model); + public HttpResponse update(@PathVariable UUID projectId, @Body NotificationModelDTO model) { + NotificationModelResponseDTO result = notificationService.update(projectId, model); if (result instanceof NotificationModelResponseDTO.NotificationModelErrorDTO) { return HttpResponse.notFound(result); } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/controller/font/FontController.java b/src/main/java/net/onelitefeather/vulpes/backend/controller/font/FontController.java index 9f756cd..9cbc355 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/controller/font/FontController.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/controller/font/FontController.java @@ -30,7 +30,7 @@ import static net.onelitefeather.vulpes.backend.domain.font.FontModelResponseDTO.*; -@Controller("/font") +@Controller("/project/{projectId}/font") public class FontController { private final FontService fontService; @@ -43,7 +43,7 @@ public FontController(FontService fontService) { @Operation( summary = "Add a new font", operationId = "addFont", - description = "Adds a new font to the database. The font is created with the given properties.", + description = "Adds a new font to the given project.", tags = {"Font"} ) @ApiResponse( @@ -55,8 +55,8 @@ public FontController(FontService fontService) { ) ) @ApiResponse( - responseCode = "500", - description = "The font could not be added to the database.", + responseCode = "404", + description = "The project was not found.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = FontModelErrorDTO.class) @@ -65,17 +65,18 @@ public FontController(FontService fontService) { @Post @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Create.class) - public HttpResponse add( - @Body FontModelDTO item - ) { - FontModelResponseDTO.FontModelDTO result = fontService.create(item); + public HttpResponse add(@PathVariable UUID projectId, @Body FontModelDTO item) { + FontModelResponseDTO result = fontService.create(projectId, item); + if (result instanceof FontModelErrorDTO) { + return HttpResponse.notFound(result); + } return HttpResponse.ok(result); } @Operation( summary = "Get a font by ID", operationId = "getFontById", - description = "Gets a font by ID from the database.", + description = "Gets a font owned by the given project by ID.", tags = {"Font"} ) @ApiResponse( @@ -88,7 +89,7 @@ public HttpResponse add( ) @ApiResponse( responseCode = "404", - description = "The font was not found in the database.", + description = "The font was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = FontModelErrorDTO.class) @@ -96,8 +97,8 @@ public HttpResponse add( ) @Get("/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse getById(@PathVariable UUID id) { - Optional model = fontService.findById(id); + public HttpResponse getById(@PathVariable UUID projectId, @PathVariable UUID id) { + Optional model = fontService.findById(projectId, id); if (model.isPresent()) { FontEntity fontModel = model.get(); FontModelResponseDTO.FontModelDTO dto = FontModelResponseDTO.FontModelDTO.createDTO(fontModel); @@ -109,7 +110,7 @@ public HttpResponse getById(@PathVariable UUID id) { @Operation( summary = "Remove a font by ID", operationId = "deleteFont", - description = "Removes a font by ID from the database.", + description = "Removes a font owned by the given project by ID.", tags = {"Font"} ) @ApiResponse( @@ -122,7 +123,7 @@ public HttpResponse getById(@PathVariable UUID id) { ) @ApiResponse( responseCode = "404", - description = "The font was not found in the database.", + description = "The font was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = FontModelErrorDTO.class) @@ -130,8 +131,8 @@ public HttpResponse getById(@PathVariable UUID id) { ) @Delete("/delete/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse remove(@PathVariable UUID id) { - FontModelResponseDTO result = fontService.delete(id); + public HttpResponse remove(@PathVariable UUID projectId, @PathVariable UUID id) { + FontModelResponseDTO result = fontService.delete(projectId, id); if (result instanceof FontModelResponseDTO.FontModelErrorDTO) { return HttpResponse.notFound(result); } @@ -141,7 +142,7 @@ public HttpResponse remove(@PathVariable UUID id) { @Operation( summary = "Get all fonts", operationId = "getAllFonts", - description = "Gets all fonts from the database.", + description = "Gets all fonts belonging to the given project.", tags = {"Font"} ) @ApiResponse( @@ -157,15 +158,15 @@ public HttpResponse remove(@PathVariable UUID id) { ) @Get(uris = {"/"}) @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> getAll(Pageable pageable) { - Page models = fontService.getAll(pageable); + public HttpResponse> getAll(@PathVariable UUID projectId, Pageable pageable) { + Page models = fontService.getAll(projectId, pageable); return HttpResponse.ok(models); } @Operation( summary = "Delete all fonts", operationId = "deleteAllFonts", - description = "Deletes all fonts from the database.", + description = "Deletes all fonts belonging to the given project.", tags = {"Font"} ) @ApiResponse( @@ -178,8 +179,8 @@ public HttpResponse> getAll(Pageable pag ) @Delete("delete") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> deleteAll() { - List result = fontService.deleteAll(); + public HttpResponse> deleteAll(@PathVariable UUID projectId) { + List result = fontService.deleteAll(projectId); return HttpResponse.ok(result); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/controller/item/ItemController.java b/src/main/java/net/onelitefeather/vulpes/backend/controller/item/ItemController.java index 6ea3a35..0e117c2 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/controller/item/ItemController.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/controller/item/ItemController.java @@ -32,7 +32,7 @@ * REST controller for item resources. * Provides CRUD operations and nested resource management (enchantments, lore, flags). */ -@Controller("/item") +@Controller("/project/{projectId}/item") public class ItemController { private final ItemService itemService; @@ -45,7 +45,7 @@ public ItemController(ItemService itemService) { @Operation( summary = "Create a new item", operationId = "addItem", - description = "Creates a new item with the provided properties and stores it in the database.", + description = "Creates a new item in the given project and stores it in the database.", tags = {"Item"} ) @ApiResponse( @@ -57,8 +57,8 @@ public ItemController(ItemService itemService) { ) ) @ApiResponse( - responseCode = "500", - description = "Item could not be created due to an internal error.", + responseCode = "404", + description = "The project was not found.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ItemModelResponseDTO.ItemModelErrorDTO.class) @@ -68,16 +68,20 @@ public ItemController(ItemService itemService) { @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Create.class) public HttpResponse add( - @Body ItemModelDTO itemModel + @PathVariable UUID projectId, + @Body ItemModelDTO itemModel ) { - ItemModelResponseDTO.ItemModelDTO createdItem = itemService.create(itemModel); - return HttpResponse.ok(createdItem); + ItemModelResponseDTO result = itemService.create(projectId, itemModel); + if (result instanceof ItemModelResponseDTO.ItemModelErrorDTO) { + return HttpResponse.notFound(result); + } + return HttpResponse.ok(result); } @Operation( summary = "Get an item by ID", operationId = "getItemById", - description = "Retrieves a single item from the database by its unique ID (itemId).", + description = "Retrieves a single item owned by the given project by its unique ID (itemId).", tags = {"Item"} ) @ApiResponse( @@ -90,7 +94,7 @@ public HttpResponse add( ) @ApiResponse( responseCode = "404", - description = "Item with the given ID was not found.", + description = "Item with the given ID was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ItemModelResponseDTO.ItemModelErrorDTO.class) @@ -99,9 +103,10 @@ public HttpResponse add( @Get("/{itemId}") @Produces(MediaType.APPLICATION_JSON) public HttpResponse getById( + @PathVariable UUID projectId, @PathVariable("itemId") UUID itemId ) { - Optional foundItemOpt = itemService.findById(itemId); + Optional foundItemOpt = itemService.findById(projectId, itemId); if (foundItemOpt.isPresent()) { var foundItem = foundItemOpt.get(); return HttpResponse.ok(ItemModelResponseDTO.ItemModelDTO.createDTO(foundItem)); @@ -112,7 +117,7 @@ public HttpResponse getById( @Operation( summary = "Get all items", operationId = "getAllItems", - description = "Retrieves a pageable list of all items. Supports standard Micronaut pagination (page, size, sort).", + description = "Retrieves a pageable list of all items belonging to the given project. Supports standard Micronaut pagination (page, size, sort).", tags = {"Item"} ) @ApiResponse( @@ -128,15 +133,15 @@ public HttpResponse getById( ) @Get @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> getAll(Pageable pageable) { - Page itemsPage = itemService.getAll(pageable); + public HttpResponse> getAll(@PathVariable UUID projectId, Pageable pageable) { + Page itemsPage = itemService.getAll(projectId, pageable); return HttpResponse.ok(itemsPage); } @Operation( summary = "Update an item", operationId = "updateItem", - description = "Updates an existing item in the database.", + description = "Updates an existing item owned by the given project.", tags = {"Item"} ) @ApiResponse( @@ -149,7 +154,7 @@ public HttpResponse> getAll(Pageable pag ) @ApiResponse( responseCode = "404", - description = "Item was not found and could not be updated.", + description = "Item was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ItemModelResponseDTO.ItemModelErrorDTO.class) @@ -159,9 +164,10 @@ public HttpResponse> getAll(Pageable pag @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Update.class) public HttpResponse update( + @PathVariable UUID projectId, @Body ItemModelDTO itemModel ) { - ItemModelResponseDTO updateResult = itemService.update(itemModel); + ItemModelResponseDTO updateResult = itemService.update(projectId, itemModel); if (updateResult instanceof ItemModelResponseDTO.ItemModelErrorDTO) { return HttpResponse.notFound(updateResult); } @@ -171,7 +177,7 @@ public HttpResponse update( @Operation( summary = "Remove an item by ID", operationId = "removeItemById", - description = "Deletes an item from the database by its unique ID (itemId).", + description = "Deletes an item owned by the given project by its unique ID (itemId).", tags = {"Item"} ) @ApiResponse( @@ -184,7 +190,7 @@ public HttpResponse update( ) @ApiResponse( responseCode = "404", - description = "Item with the given ID was not found.", + description = "Item with the given ID was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ItemModelResponseDTO.ItemModelErrorDTO.class) @@ -192,8 +198,8 @@ public HttpResponse update( ) @Delete("/delete/{itemId}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse delete(@PathVariable("itemId") UUID itemId) { - ItemModelResponseDTO deleteResult = itemService.delete(itemId); + public HttpResponse delete(@PathVariable UUID projectId, @PathVariable("itemId") UUID itemId) { + ItemModelResponseDTO deleteResult = itemService.delete(projectId, itemId); if (deleteResult instanceof ItemModelResponseDTO.ItemModelErrorDTO) { return HttpResponse.notFound(deleteResult); } @@ -202,7 +208,7 @@ public HttpResponse delete(@PathVariable("itemId") UUID it @Operation( summary = "Delete all items", - description = "Deletes all items from the database.", + description = "Deletes all items belonging to the given project.", tags = {"Item"} ) @ApiResponse( @@ -217,8 +223,8 @@ public HttpResponse delete(@PathVariable("itemId") UUID it ) @Delete("/deleteAll") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> deleteAll() { - List deleteResults = itemService.deleteAll(); + public HttpResponse> deleteAll(@PathVariable UUID projectId) { + List deleteResults = itemService.deleteAll(projectId); return HttpResponse.ok(deleteResults); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/controller/sound/SoundController.java b/src/main/java/net/onelitefeather/vulpes/backend/controller/sound/SoundController.java index c381a99..b67f380 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/controller/sound/SoundController.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/controller/sound/SoundController.java @@ -33,17 +33,12 @@ * @version 1.0.0 * @since 0.1.0 */ -@Controller("/sound") +@Controller("/project/{projectId}/sound") public class SoundController { private static final String GENERIC_ERROR = "Sound event not found"; private final SoundService soundService; - /** - * Constructs a new {@link SoundController} with the specified {@link SoundService}. - * - * @param soundService the service to manage sound events - */ @Inject public SoundController(SoundService soundService) { this.soundService = soundService; @@ -52,7 +47,7 @@ public SoundController(SoundService soundService) { @Operation( summary = "Add a new sound event", operationId = "addSoundEvent", - description = "Adds a new sound event to the database. The sound event is created with the given properties.", + description = "Adds a new sound event to the given project.", tags = {"Sound"} ) @ApiResponse( @@ -64,8 +59,8 @@ public SoundController(SoundService soundService) { ) ) @ApiResponse( - responseCode = "500", - description = "The sound event could not be added to the database.", + responseCode = "404", + description = "The project was not found.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SoundResponseDTO.SoundErrorDTO.class) @@ -74,10 +69,8 @@ public SoundController(SoundService soundService) { @Post @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Create.class) - public HttpResponse add( - @Body SoundEventDTO dtoModel - ) { - SoundResponseDTO result = soundService.create(dtoModel); + public HttpResponse add(@PathVariable UUID projectId, @Body SoundEventDTO dtoModel) { + SoundResponseDTO result = soundService.create(projectId, dtoModel); if (result instanceof SoundResponseDTO.SoundErrorDTO) { return HttpResponse.badRequest(result); } @@ -87,7 +80,7 @@ public HttpResponse add( @Operation( summary = "Get a sound by its ID", operationId = "getSoundById", - description = "Retrieves a sound from the database by its ID.", + description = "Retrieves a sound owned by the given project by its ID.", tags = {"Sound"} ) @ApiResponse( @@ -100,7 +93,7 @@ public HttpResponse add( ) @ApiResponse( responseCode = "404", - description = "The sound was not found in the database.", + description = "The sound was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SoundResponseDTO.SoundErrorDTO.class) @@ -108,8 +101,8 @@ public HttpResponse add( ) @Get("/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse getById(@PathVariable UUID id) { - var soundEvent = soundService.findById(id); + public HttpResponse getById(@PathVariable UUID projectId, @PathVariable UUID id) { + var soundEvent = soundService.findById(projectId, id); if (soundEvent.isPresent()) { return HttpResponse.ok(SoundResponseDTO.SoundModelDTO.createDTO(soundEvent.get())); } @@ -119,7 +112,7 @@ public HttpResponse getById(@PathVariable UUID id) { @Operation( summary = "Remove a sound event by ID", operationId = "removeSoundEventById", - description = "Removes a sound event from the database by its ID.", + description = "Removes a sound event owned by the given project by its ID.", tags = {"Sound"} ) @ApiResponse( @@ -132,7 +125,7 @@ public HttpResponse getById(@PathVariable UUID id) { ) @ApiResponse( responseCode = "404", - description = "The sound event was not found in the database.", + description = "The sound event was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SoundResponseDTO.SoundErrorDTO.class) @@ -140,8 +133,8 @@ public HttpResponse getById(@PathVariable UUID id) { ) @Delete("/delete/{id}") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse remove(@PathVariable UUID id) { - SoundResponseDTO result = soundService.delete(id); + public HttpResponse remove(@PathVariable UUID projectId, @PathVariable UUID id) { + SoundResponseDTO result = soundService.delete(projectId, id); if (result instanceof SoundResponseDTO.SoundErrorDTO) { return HttpResponse.notFound(result); } @@ -151,7 +144,7 @@ public HttpResponse remove(@PathVariable UUID id) { @Operation( summary = "Get all sound events", operationId = "getAllSoundEvents", - description = "Retrieves all sound events from the database.", + description = "Retrieves all sound events belonging to the given project.", tags = {"Sound"} ) @ApiResponse( @@ -165,25 +158,17 @@ public HttpResponse remove(@PathVariable UUID id) { ) ) ) - @ApiResponse( - responseCode = "404", - description = "No sound events were found in the database.", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = SoundResponseDTO.SoundErrorDTO.class) - ) - ) @Get("/") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> getAll(Pageable pageable) { - Page returnValues = soundService.getAll(pageable); + public HttpResponse> getAll(@PathVariable UUID projectId, Pageable pageable) { + Page returnValues = soundService.getAll(projectId, pageable); return HttpResponse.ok(returnValues); } @Operation( summary = "Delete all sound events", operationId = "deleteAllSoundEvents", - description = "Deletes all sound events from the database.", + description = "Deletes all sound events belonging to the given project.", tags = {"Sound"} ) @ApiResponse( @@ -196,15 +181,15 @@ public HttpResponse> getAll(Pageable pageab ) @Delete("/delete/") @Produces(MediaType.APPLICATION_JSON) - public HttpResponse> deleteAll() { - List results = soundService.deleteAll(); + public HttpResponse> deleteAll(@PathVariable UUID projectId) { + List results = soundService.deleteAll(projectId); return HttpResponse.ok(results); } @Operation( summary = "Update a sound event", operationId = "updateSoundEvent", - description = "Updates an existing sound event in the database.", + description = "Updates a sound event owned by the given project.", tags = {"Sound"} ) @ApiResponse( @@ -217,7 +202,7 @@ public HttpResponse> deleteAll() { ) @ApiResponse( responseCode = "404", - description = "The sound event was not found in the database.", + description = "The sound event was not found, or does not belong to the given project.", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SoundResponseDTO.SoundErrorDTO.class) @@ -226,8 +211,8 @@ public HttpResponse> deleteAll() { @Post("/update") @Produces(MediaType.APPLICATION_JSON) @Validated(groups = ValidationGroup.Update.class) - public HttpResponse update(@Body SoundEventDTO model) { - SoundResponseDTO result = soundService.update(model); + public HttpResponse update(@PathVariable UUID projectId, @Body SoundEventDTO model) { + SoundResponseDTO result = soundService.update(projectId, model); if (result instanceof SoundResponseDTO.SoundErrorDTO) { return HttpResponse.notFound(result); } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelDTO.java index ebac51f..b08f560 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelDTO.java @@ -9,6 +9,7 @@ import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import net.onelitefeather.vulpes.api.model.AttributeEntity; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; import java.util.UUID; @@ -37,9 +38,10 @@ public record AttributeModelDTO( /** * Converts the dto class to a {@link AttributeEntity}. * + * @param project the project this attribute belongs to * @return the created entity */ - public @NotNull AttributeEntity toAttributeModel() { - return new AttributeEntity(id, uiName, variableName, defaultValue, maximumValue); + public @NotNull AttributeEntity toAttributeModel(ProjectEntity project) { + return new AttributeEntity(id, uiName, variableName, defaultValue, maximumValue, project); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelResponseDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelResponseDTO.java index f5344fc..c40f594 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelResponseDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/attribute/AttributeModelResponseDTO.java @@ -11,15 +11,6 @@ @Serdeable public interface AttributeModelResponseDTO { - /** - * The {@link AttributeModelDTO} is used to represent an attribute model in the system. - * - * @param id the unique identifier of the attribute model - * @param uiName the name to display in the UI - * @param variableName the name used for variable generation - * @param defaultValue the default value of the attribute - * @param maximumValue the maximum value of the attribute - */ @Schema( name = "ResponseAttributeModelDTO", description = "Attribute Model Data" @@ -30,7 +21,8 @@ record AttributeModelDTO( @Schema(description = "The name for the ui") String uiName, @Schema(description = "The name which represents the variable after the generation") String variableName, @Schema(description = "Default value of the attribute") double defaultValue, - @Schema(description = "Maximum value of the attribute") double maximumValue + @Schema(description = "Maximum value of the attribute") double maximumValue, + @Schema(description = "ID of the project this attribute belongs to") UUID projectId ) implements AttributeModelResponseDTO { /** @@ -45,7 +37,8 @@ public static AttributeModelDTO create(AttributeEntity model) { model.getUiName(), model.getVariableName(), model.getDefaultValue(), - model.getMaximumValue() + model.getMaximumValue(), + model.getProject().getId() ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelDTO.java index f444e3d..29a87fa 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelDTO.java @@ -8,6 +8,7 @@ import jakarta.validation.constraints.Null; import jakarta.validation.constraints.PositiveOrZero; import net.onelitefeather.vulpes.api.model.FontEntity; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; import net.onelitefeather.vulpes.backend.validation.ValidationGroup.Create; import net.onelitefeather.vulpes.backend.validation.ValidationGroup.Update; @@ -51,9 +52,10 @@ public record FontModelDTO( /** * Converts a {@link FontModelDTO} to a {@link FontEntity}. * + * @param project the project this font belongs to * @return the converted entity */ - public @NotNull FontEntity toFontModel() { + public @NotNull FontEntity toFontModel(ProjectEntity project) { return new FontEntity( id, uiName, @@ -63,7 +65,8 @@ public record FontModelDTO( comment, height, ascent, - List.of() + List.of(), + project ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelResponseDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelResponseDTO.java index c5b64fd..fc95e97 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelResponseDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/font/FontModelResponseDTO.java @@ -22,6 +22,7 @@ public sealed interface FontModelResponseDTO { * @param comment an example comment for the font model * @param ascent the ascent value of the font model * @param height the height of the font model + * @param projectId the ID of the project this font belongs to */ @Schema(name = "ResponseFontModelDTO", description = "Font model data") @Serdeable @@ -34,7 +35,8 @@ record FontModelDTO( @Schema(description = "Example comment", requiredMode = Schema.RequiredMode.REQUIRED) String texturePath, @Schema(description = "Example comment", requiredMode = Schema.RequiredMode.REQUIRED) String comment, @Schema(description = "Example comment", requiredMode = Schema.RequiredMode.REQUIRED) int ascent, - @Schema(description = "Example comment", requiredMode = Schema.RequiredMode.REQUIRED) int height + @Schema(description = "Example comment", requiredMode = Schema.RequiredMode.REQUIRED) int height, + @Schema(description = "ID of the project this font belongs to", requiredMode = Schema.RequiredMode.REQUIRED) UUID projectId ) implements FontModelResponseDTO { /** @@ -53,7 +55,8 @@ public static FontModelDTO createDTO(FontEntity fontModel) { fontModel.getTexturePath(), fontModel.getComment(), fontModel.getAscent(), - fontModel.getHeight() + fontModel.getHeight(), + fontModel.getProject().getId() ); } @@ -64,17 +67,7 @@ public static FontModelDTO createDTO(FontEntity fontModel) { * @return a new dto instance with characters */ public static FontModelDTO createDTOWithChars(FontEntity fontModel) { - return new FontModelDTO( - fontModel.getId(), - fontModel.getUiName(), - fontModel.getVariableName(), - fontModel.getProvider(), - fontModel.getMapper(), - fontModel.getTexturePath(), - fontModel.getComment(), - fontModel.getAscent(), - fontModel.getHeight() - ); + return createDTO(fontModel); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelDTO.java index 9934f4e..a7344b8 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelDTO.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import net.onelitefeather.vulpes.api.model.ItemEntity; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; import net.onelitefeather.vulpes.backend.validation.ValidationGroup; import java.util.List; @@ -56,18 +57,21 @@ public record ItemModelDTO( String groupName, @Schema(description = "Integer which refers to the customModelData index", requiredMode = Schema.RequiredMode.REQUIRED) @PositiveOrZero - int customModelData, + @Nullable + Integer customModelData, @Schema(description = "The amount of the item", requiredMode = Schema.RequiredMode.REQUIRED) @Positive - int amount + @Nullable + Integer amount ) { /** * Converts this DTO to an {@link ItemEntity}. * + * @param project the project this item belongs to * @return a new {@link ItemEntity} instance with the data from this DTO */ - public @NotNull ItemEntity toItemEntity() { + public @NotNull ItemEntity toItemEntity(ProjectEntity project) { return new ItemEntity( this.id, uiName, @@ -76,11 +80,12 @@ public record ItemModelDTO( displayName, material, groupName, - customModelData, - amount, + customModelData != null ? customModelData : 0, + amount != null ? amount : 1, + List.of(), List.of(), List.of(), - List.of() + project ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelResponseDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelResponseDTO.java index 06e70fc..74053a9 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelResponseDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/item/ItemModelResponseDTO.java @@ -32,6 +32,7 @@ public sealed interface ItemModelResponseDTO { * @param enchantments the map of enchantment names and their levels * @param lore the list of text lines displayed in the item tooltip * @param flags the list of item flags that modify item behavior + * @param projectId the ID of the project this item belongs to */ @Schema( name = "ResponseItemModelDTO", @@ -50,7 +51,8 @@ record ItemModelDTO( @Schema(description = "Quantity of the item") int amount, @Schema(description = "Map of enchantment names and their levels") Map enchantments, @Schema(description = "List of text lines displayed in the item tooltip") List lore, - @Schema(description = "List of item flags that modify item behavior") List flags + @Schema(description = "List of item flags that modify item behavior") List flags, + @Schema(description = "ID of the project this item belongs to") UUID projectId ) implements ItemModelResponseDTO { /** @@ -72,7 +74,8 @@ public static ItemModelDTO createDTO(@NotNull ItemEntity itemEntity) { itemEntity.getAmount(), Collections.emptyMap(), Collections.emptyList(), - Collections.emptyList() + Collections.emptyList(), + itemEntity.getProject().getId() ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelDTO.java index 94b7b5b..139b59a 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelDTO.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; import net.onelitefeather.vulpes.api.model.NotificationEntity; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; import net.onelitefeather.vulpes.backend.validation.ValidationGroup; import static net.onelitefeather.vulpes.backend.validation.ValidationGroup.*; @@ -52,9 +53,10 @@ public record NotificationModelDTO( /** * Converts this DTO to a {@link NotificationEntity}. * + * @param project the project this notification belongs to * @return a new {@link NotificationEntity} instance with the data from this DTO */ - public @NotNull NotificationEntity toNotificationModel() { + public @NotNull NotificationEntity toNotificationModel(ProjectEntity project) { return new NotificationEntity( this.id, uiName, @@ -62,7 +64,8 @@ public record NotificationModelDTO( comment, material, frameType, - title + title, + project ); } } \ No newline at end of file diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelResponseDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelResponseDTO.java index db30d8e..59a1f16 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelResponseDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/notification/NotificationModelResponseDTO.java @@ -21,6 +21,7 @@ public sealed interface NotificationModelResponseDTO { * @param material the material type of the notification * @param frameType the frame type of the notification * @param title the title of the notification + * @param projectId the ID of the project this notification belongs to */ @Schema( name = "ResponseNotificationModelDTO", @@ -34,7 +35,8 @@ record NotificationModelDTO( @Schema(description = "Description of the Notification") String comment, @Schema(description = "Material type of the Notification") String material, @Schema(description = "Frame type of the Notification") String frameType, - @Schema(description = "Title of the Notification") String title + @Schema(description = "Title of the Notification") String title, + @Schema(description = "ID of the project this notification belongs to") UUID projectId ) implements NotificationModelResponseDTO { /** @@ -51,7 +53,8 @@ public static NotificationModelDTO createDTO(NotificationEntity notificationMode notificationModel.getComment(), notificationModel.getMaterial(), notificationModel.getFrameType(), - notificationModel.getTitle() + notificationModel.getTitle(), + notificationModel.getProject().getId() ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundEventDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundEventDTO.java index 1626f40..15332da 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundEventDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundEventDTO.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; import net.onelitefeather.vulpes.api.model.sound.SoundEventEntity; import net.onelitefeather.vulpes.backend.validation.ValidationGroup; @@ -61,9 +62,10 @@ public record SoundEventDTO( /** * Converts this DTO to a {@link SoundEventEntity}. * + * @param project the project this sound event belongs to * @return a new {@link SoundEventEntity} instance with the data from this DTO */ - public @NotNull SoundEventEntity toEntity() { + public @NotNull SoundEventEntity toEntity(ProjectEntity project) { return new SoundEventEntity( id, uiName, @@ -71,7 +73,8 @@ public record SoundEventDTO( keyName, false, subTitle, - List.of() + List.of(), + project ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundResponseDTO.java b/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundResponseDTO.java index ebd05ac..a991503 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundResponseDTO.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/domain/sound/SoundResponseDTO.java @@ -51,6 +51,7 @@ public static SoundFileSourceDTO createDTO(SoundFileSource source) { * @param variableName the name used for variable generation * @param keyName the key of the sound * @param subTitle the subtitle displayed when the sound is played + * @param projectId the unique identifier of the project this sound event belongs to */ @Schema( name = "ResponseSoundModelDTO", @@ -62,7 +63,8 @@ record SoundModelDTO( @Schema(description = "Name to display it in the ui") String uiName, @Schema(description = "The name which is used for the variable generation") String variableName, @Schema(description = "They key of the sound") String keyName, - @Schema(description = "The subtitle which is display when the sound is played") String subTitle + @Schema(description = "The subtitle which is display when the sound is played") String subTitle, + @Schema(description = "ID of the project this sound event belongs to") UUID projectId ) implements SoundResponseDTO { /** @@ -77,7 +79,8 @@ public static SoundModelDTO createDTO(SoundEventEntity event) { event.getUiName(), event.getVariableName(), event.getKeyName(), - event.getSubTitle() + event.getSubTitle(), + event.getProject().getId() ); } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/CrudService.java b/src/main/java/net/onelitefeather/vulpes/backend/service/CrudService.java index b45b1cd..8411602 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/CrudService.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/CrudService.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; /** * Generic CRUD service interface providing common persistence operations. @@ -63,4 +64,57 @@ public interface CrudService { * @return an optional containing the entity if present */ Optional findById(ID id); + + /** + * Creates a new entity from the given request DTO, scoped to a project. + * + * @param projectId the identifier of the owning project + * @param dto the request DTO + * @return the created entity mapped to the success DTO, or an error DTO if the project does not exist + */ + RES create(UUID projectId, REQ dto); + + /** + * Updates an existing entity with the data from the given request DTO, scoped to a project. + * + * @param projectId the identifier of the owning project + * @param dto the request DTO + * @return the updated entity mapped to the success DTO, or an error DTO if not found or not owned by the project + */ + RES update(UUID projectId, REQ dto); + + /** + * Deletes an entity by its identifier, scoped to a project. + * + * @param projectId the identifier of the owning project + * @param id the identifier of the entity to delete + * @return the deleted entity mapped to the success DTO, or an error DTO if not found or not owned by the project + */ + RES delete(UUID projectId, ID id); + + /** + * Deletes all entities belonging to a project. + * + * @param projectId the identifier of the owning project + * @return an empty list (matching the existing unscoped {@link #deleteAll()} convention) + */ + List deleteAll(UUID projectId); + + /** + * Retrieves all entities belonging to a project, with pagination support. + * + * @param projectId the identifier of the owning project + * @param pageable pagination details + * @return a page of success DTOs belonging to the project + */ + Page getAll(UUID projectId, Pageable pageable); + + /** + * Finds an entity by its identifier, scoped to a project. + * + * @param projectId the identifier of the owning project + * @param id the identifier to look for + * @return an optional containing the entity if present and owned by the project + */ + Optional findById(UUID projectId, ID id); } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudService.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudService.java index 6f9d5dd..143a839 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudService.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudService.java @@ -3,10 +3,14 @@ import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.repository.PageableRepository; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.backend.service.CrudService; import java.util.List; import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; import java.util.function.Function; /** @@ -29,6 +33,12 @@ public abstract class AbstractCrudService protected final Function errorMapper; protected final String entityName; + protected final boolean projectScoped; + protected final ProjectRepository projectRepository; + protected final BiFunction scopedEntityMapper; + protected final Function entityProjectIdExtractor; + protected final BiFunction> findByProjectFn; + /** * Constructs a new AbstractCrudService with identical mapping for single and list representations. * @@ -77,6 +87,94 @@ protected AbstractCrudService( this.idMapper = idMapper; this.errorMapper = errorMapper; this.entityName = entityName; + this.projectScoped = false; + this.projectRepository = null; + this.scopedEntityMapper = null; + this.entityProjectIdExtractor = null; + this.findByProjectFn = null; + } + + /** + * Constructs a new project-scoped AbstractCrudService with identical mapping for single and list representations. + * + * @param repository the pageable repository + * @param projectRepository to resolve the owning {@link ProjectEntity} for a given project id + * @param scopedEntityMapper to convert a request DTO plus the resolved project to an entity + * @param dtoMapper to convert an entity to a success DTO + * @param entityProjectIdExtractor to read the owning project's id off an entity + * @param findByProjectFn to look up a page of entities belonging to a project + * @param idMapper to extract the ID from a request DTO + * @param errorMapper to create an error response DTO from an error message + * @param entityName the human-readable entity name for error messages + */ + protected AbstractCrudService( + PageableRepository repository, + ProjectRepository projectRepository, + BiFunction scopedEntityMapper, + Function dtoMapper, + Function entityProjectIdExtractor, + BiFunction> findByProjectFn, + Function idMapper, + Function errorMapper, + String entityName + ) { + this( + repository, projectRepository, scopedEntityMapper, dtoMapper, dtoMapper, + entityProjectIdExtractor, findByProjectFn, idMapper, errorMapper, entityName + ); + } + + /** + * Constructs a new project-scoped AbstractCrudService with custom mapping for single and list representations. + * + * @param repository the pageable repository + * @param projectRepository to resolve the owning {@link ProjectEntity} for a given project id + * @param scopedEntityMapper to convert a request DTO plus the resolved project to an entity + * @param dtoMapper to convert an entity to a single success DTO + * @param dtoListMapper to convert an entity to a list success DTO + * @param entityProjectIdExtractor to read the owning project's id off an entity + * @param findByProjectFn to look up a page of entities belonging to a project + * @param idMapper to extract the ID from a request DTO + * @param errorMapper to create an error response DTO from an error message + * @param entityName the human-readable entity name for error messages + */ + protected AbstractCrudService( + PageableRepository repository, + ProjectRepository projectRepository, + BiFunction scopedEntityMapper, + Function dtoMapper, + Function dtoListMapper, + Function entityProjectIdExtractor, + BiFunction> findByProjectFn, + Function idMapper, + Function errorMapper, + String entityName + ) { + this.repository = repository; + this.entityMapper = null; + this.dtoMapper = dtoMapper; + this.dtoListMapper = dtoListMapper; + this.idMapper = idMapper; + this.errorMapper = errorMapper; + this.entityName = entityName; + this.projectScoped = true; + this.projectRepository = projectRepository; + this.scopedEntityMapper = scopedEntityMapper; + this.entityProjectIdExtractor = entityProjectIdExtractor; + this.findByProjectFn = findByProjectFn; + } + + private void requireProjectScoped() { + if (!projectScoped) { + throw new UnsupportedOperationException(entityName + " is not project-scoped."); + } + } + + private void requireNotProjectScoped() { + if (projectScoped) { + throw new UnsupportedOperationException( + entityName + " is project-scoped; use the projectId-taking overload instead."); + } } /** @@ -84,6 +182,7 @@ protected AbstractCrudService( */ @Override public SUCCESS create(REQ dto) { + requireNotProjectScoped(); E entity = entityMapper.apply(dto); E saved = repository.save(entity); return dtoMapper.apply(saved); @@ -94,6 +193,7 @@ public SUCCESS create(REQ dto) { */ @Override public RES update(REQ dto) { + requireNotProjectScoped(); ID id = idMapper.apply(dto); if (id == null || repository.findById(id).isEmpty()) { return errorMapper.apply(entityName + " not found"); @@ -108,6 +208,7 @@ public RES update(REQ dto) { */ @Override public RES delete(ID id) { + requireNotProjectScoped(); Optional existing = repository.findById(id); if (existing.isPresent()) { repository.deleteById(id); @@ -121,6 +222,7 @@ public RES delete(ID id) { */ @Override public List deleteAll() { + requireNotProjectScoped(); repository.deleteAll(); return List.of(); } @@ -130,6 +232,7 @@ public List deleteAll() { */ @Override public Page getAll(Pageable pageable) { + requireNotProjectScoped(); return repository.findAll(pageable).map(dtoListMapper); } @@ -138,6 +241,89 @@ public Page getAll(Pageable pageable) { */ @Override public Optional findById(ID id) { + requireNotProjectScoped(); return repository.findById(id); } + + /** + * {@inheritDoc} + */ + @Override + public RES create(UUID projectId, REQ dto) { + requireProjectScoped(); + Optional project = projectRepository.findById(projectId); + if (project.isEmpty()) { + return errorMapper.apply("Project not found"); + } + E entity = scopedEntityMapper.apply(dto, project.get()); + E saved = repository.save(entity); + return dtoMapper.apply(saved); + } + + /** + * {@inheritDoc} + */ + @Override + public RES update(UUID projectId, REQ dto) { + requireProjectScoped(); + ID id = idMapper.apply(dto); + if (id == null) { + return errorMapper.apply(entityName + " not found"); + } + Optional existing = repository.findById(id); + if (existing.isEmpty() || !projectId.equals(entityProjectIdExtractor.apply(existing.get()))) { + return errorMapper.apply(entityName + " not found"); + } + Optional project = projectRepository.findById(projectId); + if (project.isEmpty()) { + return errorMapper.apply("Project not found"); + } + E entity = scopedEntityMapper.apply(dto, project.get()); + E updated = repository.update(entity); + return dtoMapper.apply(updated); + } + + /** + * {@inheritDoc} + */ + @Override + public RES delete(UUID projectId, ID id) { + requireProjectScoped(); + Optional existing = repository.findById(id); + if (existing.isEmpty() || !projectId.equals(entityProjectIdExtractor.apply(existing.get()))) { + return errorMapper.apply(entityName + " not found"); + } + repository.deleteById(id); + return dtoMapper.apply(existing.get()); + } + + /** + * {@inheritDoc} + */ + @Override + public List deleteAll(UUID projectId) { + requireProjectScoped(); + List toDelete = findByProjectFn.apply(projectId, Pageable.unpaged()).getContent(); + repository.deleteAll(toDelete); + return List.of(); + } + + /** + * {@inheritDoc} + */ + @Override + public Page getAll(UUID projectId, Pageable pageable) { + requireProjectScoped(); + return findByProjectFn.apply(projectId, pageable).map(dtoListMapper); + } + + /** + * {@inheritDoc} + */ + @Override + public Optional findById(UUID projectId, ID id) { + requireProjectScoped(); + return repository.findById(id) + .filter(entity -> projectId.equals(entityProjectIdExtractor.apply(entity))); + } } diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AttributeServiceImpl.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AttributeServiceImpl.java index b282d87..1a29295 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AttributeServiceImpl.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/AttributeServiceImpl.java @@ -4,6 +4,7 @@ import jakarta.inject.Singleton; import net.onelitefeather.vulpes.api.model.AttributeEntity; import net.onelitefeather.vulpes.api.repository.AttributeRepository; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelDTO; import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelResponseDTO; import net.onelitefeather.vulpes.backend.service.AttributeService; @@ -19,11 +20,14 @@ public class AttributeServiceImpl implements AttributeService { @Inject - public AttributeServiceImpl(AttributeRepository attributeRepository) { + public AttributeServiceImpl(AttributeRepository attributeRepository, ProjectRepository projectRepository) { super( attributeRepository, + projectRepository, AttributeModelDTO::toAttributeModel, AttributeModelResponseDTO.AttributeModelDTO::create, + entity -> entity.getProject().getId(), + attributeRepository::findByProjectId, AttributeModelDTO::id, AttributeModelResponseDTO.AttributeModelErrorDTO::new, "Attribute" diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/FontServiceImpl.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/FontServiceImpl.java index 1d50fe0..75bb452 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/FontServiceImpl.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/FontServiceImpl.java @@ -7,6 +7,7 @@ import jakarta.transaction.Transactional; import net.onelitefeather.vulpes.api.model.FontEntity; import net.onelitefeather.vulpes.api.repository.FontRepository; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.api.repository.font.FontStringRepository; import net.onelitefeather.vulpes.backend.domain.font.FontModelDTO; import net.onelitefeather.vulpes.backend.domain.font.FontModelResponseDTO; @@ -28,12 +29,15 @@ public class FontServiceImpl private final FontStringRepository fontStringRepository; @Inject - public FontServiceImpl(FontRepository fontRepository, FontStringRepository fontStringRepository) { + public FontServiceImpl(FontRepository fontRepository, FontStringRepository fontStringRepository, ProjectRepository projectRepository) { super( fontRepository, + projectRepository, FontModelDTO::toFontModel, FontModelResponseDTO.FontModelDTO::createDTOWithChars, FontModelResponseDTO.FontModelDTO::createDTO, + entity -> entity.getProject().getId(), + fontRepository::findByProjectId, FontModelDTO::id, FontModelResponseDTO.FontModelErrorDTO::new, "Font" diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/ItemServiceImpl.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/ItemServiceImpl.java index 00299fc..7ff5613 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/ItemServiceImpl.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/ItemServiceImpl.java @@ -9,6 +9,7 @@ import net.onelitefeather.vulpes.api.model.item.ItemEnchantmentEntity; import net.onelitefeather.vulpes.api.model.item.ItemLoreEntity; import net.onelitefeather.vulpes.api.repository.ItemRepository; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.api.repository.item.ItemEnchantmentRepository; import net.onelitefeather.vulpes.api.repository.item.ItemFlagRepository; import net.onelitefeather.vulpes.api.repository.item.ItemLoreRepository; @@ -43,11 +44,15 @@ public class ItemServiceImpl public ItemServiceImpl(ItemRepository itemRepository, ItemEnchantmentRepository itemEnchantmentRepository, ItemLoreRepository itemLoreRepository, - ItemFlagRepository itemFlagRepository) { + ItemFlagRepository itemFlagRepository, + ProjectRepository projectRepository) { super( itemRepository, + projectRepository, ItemModelDTO::toItemEntity, ItemModelResponseDTO.ItemModelDTO::createDTO, + entity -> entity.getProject().getId(), + itemRepository::findByProjectId, ItemModelDTO::id, ItemModelResponseDTO.ItemModelErrorDTO::new, "Item" diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/NotificationServiceImpl.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/NotificationServiceImpl.java index 83293e2..33e4b8e 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/NotificationServiceImpl.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/NotificationServiceImpl.java @@ -4,6 +4,7 @@ import jakarta.inject.Singleton; import net.onelitefeather.vulpes.api.model.NotificationEntity; import net.onelitefeather.vulpes.api.repository.NotificationRepository; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.backend.domain.notification.NotificationModelDTO; import net.onelitefeather.vulpes.backend.domain.notification.NotificationModelResponseDTO; import net.onelitefeather.vulpes.backend.service.NotificationService; @@ -19,11 +20,14 @@ public class NotificationServiceImpl implements NotificationService { @Inject - public NotificationServiceImpl(NotificationRepository notificationRepository) { + public NotificationServiceImpl(NotificationRepository notificationRepository, ProjectRepository projectRepository) { super( notificationRepository, + projectRepository, NotificationModelDTO::toNotificationModel, NotificationModelResponseDTO.NotificationModelDTO::createDTO, + entity -> entity.getProject().getId(), + notificationRepository::findByProjectId, NotificationModelDTO::id, NotificationModelResponseDTO.NotificationModelErrorDTO::new, "Notification" diff --git a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/SoundServiceImpl.java b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/SoundServiceImpl.java index 9bb73e1..cc777bf 100644 --- a/src/main/java/net/onelitefeather/vulpes/backend/service/impl/SoundServiceImpl.java +++ b/src/main/java/net/onelitefeather/vulpes/backend/service/impl/SoundServiceImpl.java @@ -6,6 +6,7 @@ import jakarta.inject.Singleton; import jakarta.transaction.Transactional; import net.onelitefeather.vulpes.api.model.sound.SoundEventEntity; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; import net.onelitefeather.vulpes.api.repository.SoundFileSourceRepository; import net.onelitefeather.vulpes.api.repository.SoundRepository; import net.onelitefeather.vulpes.backend.domain.sound.SoundEventDTO; @@ -32,13 +33,17 @@ public class SoundServiceImpl * * @param soundRepository the repository to manage sound events * @param soundFileSourceRepository the repository to manage sound file sources + * @param projectRepository the repository to manage projects */ @Inject - public SoundServiceImpl(SoundRepository soundRepository, SoundFileSourceRepository soundFileSourceRepository) { + public SoundServiceImpl(SoundRepository soundRepository, SoundFileSourceRepository soundFileSourceRepository, ProjectRepository projectRepository) { super( soundRepository, + projectRepository, SoundEventDTO::toEntity, SoundResponseDTO.SoundModelDTO::createDTO, + entity -> entity.getProject().getId(), + soundRepository::findByProjectId, SoundEventDTO::id, SoundResponseDTO.SoundErrorDTO::new, "Sound event" diff --git a/src/test/java/net/onelitefeather/vulpes/backend/controller/AttributeControllerTest.java b/src/test/java/net/onelitefeather/vulpes/backend/controller/AttributeControllerTest.java new file mode 100644 index 0000000..0d636ce --- /dev/null +++ b/src/test/java/net/onelitefeather/vulpes/backend/controller/AttributeControllerTest.java @@ -0,0 +1,136 @@ +package net.onelitefeather.vulpes.backend.controller; + +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.http.HttpResponse; +import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelDTO; +import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelResponseDTO; +import net.onelitefeather.vulpes.backend.service.AttributeService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Unit tests for AttributeController (project-scoped)") +class AttributeControllerTest { + + private static class StubAttributeService implements AttributeService { + AttributeModelResponseDTO response; + Page page; + + @Override + public AttributeModelResponseDTO.AttributeModelDTO create(AttributeModelDTO dto) { + return (AttributeModelResponseDTO.AttributeModelDTO) response; + } + + @Override + public AttributeModelResponseDTO update(AttributeModelDTO dto) { + return response; + } + + @Override + public AttributeModelResponseDTO delete(UUID id) { + return response; + } + + @Override + public List deleteAll() { + return List.of(); + } + + @Override + public Page getAll(Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID id) { + return Optional.empty(); + } + + @Override + public AttributeModelResponseDTO create(UUID projectId, AttributeModelDTO dto) { + return response; + } + + @Override + public AttributeModelResponseDTO update(UUID projectId, AttributeModelDTO dto) { + return response; + } + + @Override + public AttributeModelResponseDTO delete(UUID projectId, UUID id) { + return response; + } + + @Override + public List deleteAll(UUID projectId) { + return List.of(); + } + + @Override + public Page getAll(UUID projectId, Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID projectId, UUID id) { + return Optional.empty(); + } + } + + @Test + void add_success_returnsOk() { + StubAttributeService stub = new StubAttributeService(); + UUID projectId = UUID.randomUUID(); + AttributeModelDTO dto = new AttributeModelDTO(null, "UI", "var", 1.0, 10.0); + stub.response = new AttributeModelResponseDTO.AttributeModelDTO(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectId); + AttributeController controller = new AttributeController(stub); + + HttpResponse resp = controller.add(projectId, dto); + + assertEquals(200, resp.getStatus().getCode()); + assertInstanceOf(AttributeModelResponseDTO.AttributeModelDTO.class, resp.body()); + } + + @Test + void add_unknownProject_returns404() { + StubAttributeService stub = new StubAttributeService(); + stub.response = new AttributeModelResponseDTO.AttributeModelErrorDTO("Project not found"); + AttributeController controller = new AttributeController(stub); + AttributeModelDTO dto = new AttributeModelDTO(null, "UI", "var", 1.0, 10.0); + + HttpResponse resp = controller.add(UUID.randomUUID(), dto); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void delete_crossProject_returns404() { + StubAttributeService stub = new StubAttributeService(); + stub.response = new AttributeModelResponseDTO.AttributeModelErrorDTO("Attribute not found"); + AttributeController controller = new AttributeController(stub); + + HttpResponse resp = controller.delete(UUID.randomUUID(), UUID.randomUUID()); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getAll_returnsScopedPage() { + StubAttributeService stub = new StubAttributeService(); + UUID projectId = UUID.randomUUID(); + var dto = new AttributeModelResponseDTO.AttributeModelDTO(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectId); + stub.page = Page.of(List.of(dto), Pageable.from(0, 10), 1L); + AttributeController controller = new AttributeController(stub); + + HttpResponse> resp = controller.getAll(projectId, Pageable.from(0, 10)); + + assertEquals(200, resp.getStatus().getCode()); + assertEquals(1, resp.body().getTotalSize()); + } +} diff --git a/src/test/java/net/onelitefeather/vulpes/backend/controller/NotificationControllerTest.java b/src/test/java/net/onelitefeather/vulpes/backend/controller/NotificationControllerTest.java new file mode 100644 index 0000000..ce76d94 --- /dev/null +++ b/src/test/java/net/onelitefeather/vulpes/backend/controller/NotificationControllerTest.java @@ -0,0 +1,149 @@ +package net.onelitefeather.vulpes.backend.controller; + +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.http.HttpResponse; +import net.onelitefeather.vulpes.api.model.NotificationEntity; +import net.onelitefeather.vulpes.backend.domain.notification.NotificationModelDTO; +import net.onelitefeather.vulpes.backend.domain.notification.NotificationModelResponseDTO; +import net.onelitefeather.vulpes.backend.service.NotificationService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Unit tests for NotificationController (project-scoped)") +class NotificationControllerTest { + + private static class StubNotificationService implements NotificationService { + NotificationModelResponseDTO response; + Page page; + Optional findByIdResponse = Optional.empty(); + + @Override + public NotificationModelResponseDTO.NotificationModelDTO create(NotificationModelDTO dto) { + return (NotificationModelResponseDTO.NotificationModelDTO) response; + } + + @Override + public NotificationModelResponseDTO update(NotificationModelDTO dto) { + return response; + } + + @Override + public NotificationModelResponseDTO delete(UUID id) { + return response; + } + + @Override + public List deleteAll() { + return List.of(); + } + + @Override + public Page getAll(Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID id) { + return findByIdResponse; + } + + @Override + public NotificationModelResponseDTO create(UUID projectId, NotificationModelDTO dto) { + return response; + } + + @Override + public NotificationModelResponseDTO update(UUID projectId, NotificationModelDTO dto) { + return response; + } + + @Override + public NotificationModelResponseDTO delete(UUID projectId, UUID id) { + return response; + } + + @Override + public List deleteAll(UUID projectId) { + return List.of(); + } + + @Override + public Page getAll(UUID projectId, Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID projectId, UUID id) { + return findByIdResponse; + } + } + + @Test + void add_success_returnsOk() { + StubNotificationService stub = new StubNotificationService(); + UUID projectId = UUID.randomUUID(); + NotificationModelDTO dto = new NotificationModelDTO(null, "UI", "var", "comment", "STONE", "frame", "title"); + stub.response = new NotificationModelResponseDTO.NotificationModelDTO(UUID.randomUUID(), "UI", "var", "comment", "STONE", "frame", "title", projectId); + NotificationController controller = new NotificationController(stub); + + HttpResponse resp = controller.add(projectId, dto); + + assertEquals(200, resp.getStatus().getCode()); + assertInstanceOf(NotificationModelResponseDTO.NotificationModelDTO.class, resp.body()); + } + + @Test + void add_unknownProject_returns404() { + StubNotificationService stub = new StubNotificationService(); + stub.response = new NotificationModelResponseDTO.NotificationModelErrorDTO("Project not found"); + NotificationController controller = new NotificationController(stub); + NotificationModelDTO dto = new NotificationModelDTO(null, "UI", "var", "comment", "STONE", "frame", "title"); + + HttpResponse resp = controller.add(UUID.randomUUID(), dto); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getById_crossProject_returns404() { + StubNotificationService stub = new StubNotificationService(); + stub.findByIdResponse = Optional.empty(); + NotificationController controller = new NotificationController(stub); + + HttpResponse resp = controller.getById(UUID.randomUUID(), UUID.randomUUID()); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void delete_crossProject_returns404() { + StubNotificationService stub = new StubNotificationService(); + stub.response = new NotificationModelResponseDTO.NotificationModelErrorDTO("Notification not found"); + NotificationController controller = new NotificationController(stub); + + HttpResponse resp = controller.remove(UUID.randomUUID(), UUID.randomUUID()); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getAll_returnsScopedPage() { + StubNotificationService stub = new StubNotificationService(); + UUID projectId = UUID.randomUUID(); + var dto = new NotificationModelResponseDTO.NotificationModelDTO(UUID.randomUUID(), "UI", "var", "comment", "STONE", "frame", "title", projectId); + stub.page = Page.of(List.of(dto), Pageable.from(0, 10), 1L); + NotificationController controller = new NotificationController(stub); + + HttpResponse> resp = controller.getAll(projectId, Pageable.from(0, 10)); + + assertEquals(200, resp.getStatus().getCode()); + assertEquals(1, resp.body().getTotalSize()); + } +} diff --git a/src/test/java/net/onelitefeather/vulpes/backend/controller/SoundControllerTest.java b/src/test/java/net/onelitefeather/vulpes/backend/controller/SoundControllerTest.java index 1a55676..63c4083 100644 --- a/src/test/java/net/onelitefeather/vulpes/backend/controller/SoundControllerTest.java +++ b/src/test/java/net/onelitefeather/vulpes/backend/controller/SoundControllerTest.java @@ -62,6 +62,36 @@ public Optional findById(UUID id) { return findByIdResponse; } + @Override + public SoundResponseDTO create(UUID projectId, SoundEventDTO soundEventDTO) { + return response; + } + + @Override + public SoundResponseDTO update(UUID projectId, SoundEventDTO soundEventDTO) { + return response; + } + + @Override + public SoundResponseDTO delete(UUID projectId, UUID id) { + return response; + } + + @Override + public List deleteAll(UUID projectId) { + return List.of(); + } + + @Override + public Page getAll(UUID projectId, Pageable pageable) { + return modelDtoPage; + } + + @Override + public Optional findById(UUID projectId, UUID id) { + return findByIdResponse; + } + @Override public Page getSoundSourcesById(UUID id, Pageable pageable) { return sourcesPage; @@ -83,6 +113,10 @@ public SoundResponseDTO.SoundFileSourceDTO deleteLinkedSource(UUID soundEventId, } } + private static net.onelitefeather.vulpes.api.model.project.ProjectEntity sampleProject(UUID id) { + return new net.onelitefeather.vulpes.api.model.project.ProjectEntity(id, "Test Project", "test-project", null, null, null, false); + } + private static SoundEventDTO sampleEventDTO(UUID id) { String uiName = FAKER.rockBand().name(); String varName = FAKER.internet().slug(); @@ -94,13 +128,14 @@ private static SoundEventDTO sampleEventDTO(UUID id) { @Test void testAdd_returnsOk() { StubSoundService stub = new StubSoundService(); + UUID projectId = UUID.randomUUID(); UUID id = UUID.randomUUID(); SoundEventDTO dto = sampleEventDTO(id); - SoundResponseDTO.SoundModelDTO expected = SoundResponseDTO.SoundModelDTO.createDTO(dto.toEntity()); + SoundResponseDTO.SoundModelDTO expected = SoundResponseDTO.SoundModelDTO.createDTO(dto.toEntity(sampleProject(projectId))); stub.response = expected; SoundController controller = new SoundController(stub); - HttpResponse resp = controller.add(dto); + HttpResponse resp = controller.add(projectId, dto); assertEquals(200, resp.getStatus().getCode()); assertInstanceOf(SoundResponseDTO.SoundModelDTO.class, resp.body()); @@ -112,12 +147,13 @@ void testAdd_returnsOk() { @Test void testGetById_found_returnsOk() { StubSoundService stub = new StubSoundService(); + UUID projectId = UUID.randomUUID(); UUID id = UUID.randomUUID(); - SoundEventEntity entity = sampleEventDTO(id).toEntity(); + SoundEventEntity entity = sampleEventDTO(id).toEntity(sampleProject(projectId)); stub.findByIdResponse = Optional.of(entity); SoundController controller = new SoundController(stub); - HttpResponse resp = controller.getById(id); + HttpResponse resp = controller.getById(projectId, id); assertEquals(200, resp.getStatus().getCode()); assertInstanceOf(SoundResponseDTO.SoundModelDTO.class, resp.body()); SoundResponseDTO.SoundModelDTO body = (SoundResponseDTO.SoundModelDTO) resp.body(); @@ -130,7 +166,7 @@ void testGetById_notFound_returns404() { stub.findByIdResponse = Optional.empty(); SoundController controller = new SoundController(stub); - HttpResponse resp = controller.getById(UUID.randomUUID()); + HttpResponse resp = controller.getById(UUID.randomUUID(), UUID.randomUUID()); assertEquals(404, resp.getStatus().getCode()); assertInstanceOf(SoundResponseDTO.SoundErrorDTO.class, resp.body()); } @@ -141,7 +177,7 @@ void testRemove_notFound_returns404() { stub.response = new SoundResponseDTO.SoundErrorDTO("Sound event not found"); SoundController controller = new SoundController(stub); - HttpResponse resp = controller.remove(UUID.randomUUID()); + HttpResponse resp = controller.remove(UUID.randomUUID(), UUID.randomUUID()); assertEquals(404, resp.getStatus().getCode()); } diff --git a/src/test/java/net/onelitefeather/vulpes/backend/controller/font/FontControllerTest.java b/src/test/java/net/onelitefeather/vulpes/backend/controller/font/FontControllerTest.java new file mode 100644 index 0000000..5c211c7 --- /dev/null +++ b/src/test/java/net/onelitefeather/vulpes/backend/controller/font/FontControllerTest.java @@ -0,0 +1,170 @@ +package net.onelitefeather.vulpes.backend.controller.font; + +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.http.HttpResponse; +import net.onelitefeather.vulpes.api.model.FontEntity; +import net.onelitefeather.vulpes.backend.domain.font.FontModelDTO; +import net.onelitefeather.vulpes.backend.domain.font.FontModelResponseDTO; +import net.onelitefeather.vulpes.backend.domain.font.FontStringDTO; +import net.onelitefeather.vulpes.backend.domain.font.FontStringResponseDTO; +import net.onelitefeather.vulpes.backend.service.FontService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Unit tests for FontController (project-scoped)") +class FontControllerTest { + + private static class StubFontService implements FontService { + FontModelResponseDTO response; + Page page; + Optional findByIdResponse = Optional.empty(); + + @Override + public FontModelResponseDTO.FontModelDTO create(FontModelDTO dto) { + return (FontModelResponseDTO.FontModelDTO) response; + } + + @Override + public FontModelResponseDTO update(FontModelDTO dto) { + return response; + } + + @Override + public FontModelResponseDTO delete(UUID id) { + return response; + } + + @Override + public List deleteAll() { + return List.of(); + } + + @Override + public Page getAll(Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID id) { + return findByIdResponse; + } + + @Override + public FontModelResponseDTO create(UUID projectId, FontModelDTO dto) { + return response; + } + + @Override + public FontModelResponseDTO update(UUID projectId, FontModelDTO dto) { + return response; + } + + @Override + public FontModelResponseDTO delete(UUID projectId, UUID id) { + return response; + } + + @Override + public List deleteAll(UUID projectId) { + return List.of(); + } + + @Override + public Page getAll(UUID projectId, Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID projectId, UUID id) { + return findByIdResponse; + } + + @Override + public Page findCharsByFontId(UUID id, Pageable pageable) { + return Page.empty(); + } + + @Override + public FontStringResponseDTO updateCharByFontId(UUID id, FontStringDTO charModel) { + return null; + } + + @Override + public FontStringResponseDTO createCharByFontId(UUID id, FontStringDTO charModel) { + return null; + } + + @Override + public FontStringResponseDTO deleteCharByFontId(UUID fontId, UUID charId) { + return null; + } + + @Override + public List deleteAllCharByFontId(UUID fontId) { + return List.of(); + } + } + + private static FontModelDTO sampleDTO(UUID id) { + return new FontModelDTO(id, "UI", "var", "provider", "mapper", "texture", "comment", 1, 1); + } + + private static FontModelResponseDTO.FontModelDTO sampleResponse(UUID id, UUID projectId) { + return new FontModelResponseDTO.FontModelDTO(id, "UI", "var", "provider", "mapper", "texture", "comment", 1, 1, projectId); + } + + @Test + void add_success_returnsOk() { + StubFontService stub = new StubFontService(); + UUID projectId = UUID.randomUUID(); + stub.response = sampleResponse(UUID.randomUUID(), projectId); + FontController controller = new FontController(stub); + + HttpResponse resp = controller.add(projectId, sampleDTO(null)); + + assertEquals(200, resp.getStatus().getCode()); + assertInstanceOf(FontModelResponseDTO.FontModelDTO.class, resp.body()); + } + + @Test + void add_unknownProject_returns404() { + StubFontService stub = new StubFontService(); + stub.response = new FontModelResponseDTO.FontModelErrorDTO("Project not found"); + FontController controller = new FontController(stub); + + HttpResponse resp = controller.add(UUID.randomUUID(), sampleDTO(null)); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getById_crossProject_returns404() { + StubFontService stub = new StubFontService(); + stub.findByIdResponse = Optional.empty(); + FontController controller = new FontController(stub); + + HttpResponse resp = controller.getById(UUID.randomUUID(), UUID.randomUUID()); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getAll_returnsScopedPage() { + StubFontService stub = new StubFontService(); + UUID projectId = UUID.randomUUID(); + stub.page = Page.of(List.of(sampleResponse(UUID.randomUUID(), projectId)), Pageable.from(0, 10), 1L); + FontController controller = new FontController(stub); + + HttpResponse> resp = controller.getAll(projectId, Pageable.from(0, 10)); + + assertEquals(200, resp.getStatus().getCode()); + assertEquals(1, resp.body().getTotalSize()); + } +} diff --git a/src/test/java/net/onelitefeather/vulpes/backend/controller/item/ItemControllerTest.java b/src/test/java/net/onelitefeather/vulpes/backend/controller/item/ItemControllerTest.java new file mode 100644 index 0000000..5473052 --- /dev/null +++ b/src/test/java/net/onelitefeather/vulpes/backend/controller/item/ItemControllerTest.java @@ -0,0 +1,233 @@ +package net.onelitefeather.vulpes.backend.controller.item; + +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.http.HttpResponse; +import net.onelitefeather.vulpes.api.model.ItemEntity; +import net.onelitefeather.vulpes.backend.domain.item.ItemEnchantmentDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemEnchantmentResponseDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemFlagDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemFlagResponseDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemLoreDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemLoreResponseDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemModelDTO; +import net.onelitefeather.vulpes.backend.domain.item.ItemModelResponseDTO; +import net.onelitefeather.vulpes.backend.service.ItemService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Unit tests for ItemController (project-scoped)") +class ItemControllerTest { + + private static class StubItemService implements ItemService { + ItemModelResponseDTO response; + Page page; + Optional findByIdResponse = Optional.empty(); + + @Override + public ItemModelResponseDTO.ItemModelDTO create(ItemModelDTO dto) { + return (ItemModelResponseDTO.ItemModelDTO) response; + } + + @Override + public ItemModelResponseDTO update(ItemModelDTO dto) { + return response; + } + + @Override + public ItemModelResponseDTO delete(UUID id) { + return response; + } + + @Override + public List deleteAll() { + return List.of(); + } + + @Override + public Page getAll(Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID id) { + return findByIdResponse; + } + + @Override + public ItemModelResponseDTO create(UUID projectId, ItemModelDTO dto) { + return response; + } + + @Override + public ItemModelResponseDTO update(UUID projectId, ItemModelDTO dto) { + return response; + } + + @Override + public ItemModelResponseDTO delete(UUID projectId, UUID id) { + return response; + } + + @Override + public List deleteAll(UUID projectId) { + return List.of(); + } + + @Override + public Page getAll(UUID projectId, Pageable pageable) { + return page; + } + + @Override + public Optional findById(UUID projectId, UUID id) { + return findByIdResponse; + } + + @Override + public Page findFlagsById(UUID id, Pageable pageable) { + return Page.empty(); + } + + @Override + public ItemFlagResponseDTO createFlagById(UUID id, ItemFlagDTO itemFlagDTO) { + return null; + } + + @Override + public ItemFlagResponseDTO deleteFlagById(UUID id, UUID flagId) { + return null; + } + + @Override + public List deleteAllFlagsById(UUID id) { + return List.of(); + } + + @Override + public ItemFlagResponseDTO updateFlagById(UUID id, ItemFlagDTO flag) { + return null; + } + + @Override + public Page findEnchantmentsById(UUID id, Pageable pageable) { + return Page.empty(); + } + + @Override + public ItemEnchantmentResponseDTO updateEnchantmentById(UUID id, ItemEnchantmentDTO enchantment) { + return null; + } + + @Override + public ItemEnchantmentResponseDTO createEnchantmentById(UUID id, ItemEnchantmentDTO enchantment) { + return null; + } + + @Override + public ItemEnchantmentResponseDTO deleteEnchantmentById(UUID id, UUID enchantment) { + return null; + } + + @Override + public List deleteAllEnchantmentsById(UUID id) { + return List.of(); + } + + @Override + public Page findLoreById(UUID id, Pageable pageable) { + return Page.empty(); + } + + @Override + public ItemLoreResponseDTO updateLoreById(UUID id, ItemLoreDTO loreDto) { + return null; + } + + @Override + public ItemLoreResponseDTO createLoreById(UUID id, ItemLoreDTO loreDto) { + return null; + } + + @Override + public ItemLoreResponseDTO deleteLoreById(UUID id, UUID loreId) { + return null; + } + + @Override + public ItemLoreResponseDTO reorderLoreById(UUID id, UUID entryId, int newIndex) { + return null; + } + + @Override + public List deleteAllLoreById(UUID id) { + return List.of(); + } + } + + private static ItemModelDTO sampleDTO(UUID id) { + return new ItemModelDTO(id, "UI", "var", "comment", "display", "STONE", "group", 0, 1); + } + + private static ItemModelResponseDTO.ItemModelDTO sampleResponse(UUID id, UUID projectId) { + return new ItemModelResponseDTO.ItemModelDTO( + id, "UI", "var", "comment", "display", "STONE", "group", 0, 1, + Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), projectId + ); + } + + @Test + void add_success_returnsOk() { + StubItemService stub = new StubItemService(); + UUID projectId = UUID.randomUUID(); + stub.response = sampleResponse(UUID.randomUUID(), projectId); + ItemController controller = new ItemController(stub); + + HttpResponse resp = controller.add(projectId, sampleDTO(null)); + + assertEquals(200, resp.getStatus().getCode()); + assertInstanceOf(ItemModelResponseDTO.ItemModelDTO.class, resp.body()); + } + + @Test + void add_unknownProject_returns404() { + StubItemService stub = new StubItemService(); + stub.response = new ItemModelResponseDTO.ItemModelErrorDTO("Project not found"); + ItemController controller = new ItemController(stub); + + HttpResponse resp = controller.add(UUID.randomUUID(), sampleDTO(null)); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getById_crossProject_returns404() { + StubItemService stub = new StubItemService(); + stub.findByIdResponse = Optional.empty(); + ItemController controller = new ItemController(stub); + + HttpResponse resp = controller.getById(UUID.randomUUID(), UUID.randomUUID()); + + assertEquals(404, resp.getStatus().getCode()); + } + + @Test + void getAll_returnsScopedPage() { + StubItemService stub = new StubItemService(); + UUID projectId = UUID.randomUUID(); + stub.page = Page.of(List.of(sampleResponse(UUID.randomUUID(), projectId)), Pageable.from(0, 10), 1L); + ItemController controller = new ItemController(stub); + + HttpResponse> resp = controller.getAll(projectId, Pageable.from(0, 10)); + + assertEquals(200, resp.getStatus().getCode()); + assertEquals(1, resp.body().getTotalSize()); + } +} diff --git a/src/test/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudServiceProjectScopedTest.java b/src/test/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudServiceProjectScopedTest.java new file mode 100644 index 0000000..747c2ea --- /dev/null +++ b/src/test/java/net/onelitefeather/vulpes/backend/service/impl/AbstractCrudServiceProjectScopedTest.java @@ -0,0 +1,289 @@ +package net.onelitefeather.vulpes.backend.service.impl; + +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.model.Sort; +import io.micronaut.data.repository.PageableRepository; +import net.onelitefeather.vulpes.api.model.AttributeEntity; +import net.onelitefeather.vulpes.api.model.project.ProjectEntity; +import net.onelitefeather.vulpes.api.repository.ProjectRepository; +import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelDTO; +import net.onelitefeather.vulpes.backend.domain.attribute.AttributeModelResponseDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Unit tests for AbstractCrudService's project-scoped overloads") +class AbstractCrudServiceProjectScopedTest { + + /** + * Minimal in-memory {@link PageableRepository} fake, keyed by a caller-supplied id extractor. + * No database, no Micronaut context — just enough surface for AbstractCrudService to drive. + */ + private static class FakePageableRepository implements PageableRepository { + final Map store = new LinkedHashMap<>(); + final Function idOf; + + FakePageableRepository(Function idOf) { + this.idOf = idOf; + } + + @Override + public S save(S entity) { + store.put(idOf.apply(entity), entity); + return entity; + } + + @Override + public List saveAll(Iterable entities) { + List list = new ArrayList<>(); + for (S entity : entities) { + list.add(save(entity)); + } + return list; + } + + @Override + public S insert(S entity) { + return save(entity); + } + + @Override + public List insertAll(Iterable entities) { + return saveAll(entities); + } + + @Override + public Optional findById(ID id) { + return Optional.ofNullable(store.get(id)); + } + + @Override + public boolean existsById(ID id) { + return store.containsKey(id); + } + + @Override + public List findAll() { + return new ArrayList<>(store.values()); + } + + @Override + public long count() { + return store.size(); + } + + @Override + public S update(S entity) { + store.put(idOf.apply(entity), entity); + return entity; + } + + @Override + public List updateAll(Iterable entities) { + List list = new ArrayList<>(); + for (S entity : entities) { + list.add(update(entity)); + } + return list; + } + + @Override + public void deleteById(ID id) { + store.remove(id); + } + + @Override + public void delete(E entity) { + store.remove(idOf.apply(entity)); + } + + @Override + public void deleteAll(Iterable entities) { + entities.forEach(e -> store.remove(idOf.apply(e))); + } + + @Override + public void deleteAll() { + store.clear(); + } + + @Override + public List findAll(Sort sort) { + return findAll(); + } + + @Override + public Page findAll(Pageable pageable) { + List all = new ArrayList<>(store.values()); + return Page.of(all, pageable, (long) all.size()); + } + } + + private static class FakeProjectRepository extends FakePageableRepository implements ProjectRepository { + FakeProjectRepository() { + super(ProjectEntity::getId); + } + } + + private static class TestAttributeService + extends AbstractCrudService { + + TestAttributeService( + PageableRepository repository, + ProjectRepository projectRepository, + BiFunction> findByProjectFn + ) { + super( + repository, + projectRepository, + AttributeModelDTO::toAttributeModel, + AttributeModelResponseDTO.AttributeModelDTO::create, + e -> e.getProject().getId(), + findByProjectFn, + AttributeModelDTO::id, + AttributeModelResponseDTO.AttributeModelErrorDTO::new, + "Attribute" + ); + } + } + + private FakePageableRepository attributeRepository; + private FakeProjectRepository projectRepository; + private TestAttributeService service; + private ProjectEntity projectA; + private ProjectEntity projectB; + + private Page findByProject(UUID projectId, Pageable pageable) { + List matching = attributeRepository.store.values().stream() + .filter(e -> e.getProject().getId().equals(projectId)) + .toList(); + return Page.of(matching, pageable, (long) matching.size()); + } + + @BeforeEach + void setUp() { + attributeRepository = new FakePageableRepository<>(AttributeEntity::getId); + projectRepository = new FakeProjectRepository(); + service = new TestAttributeService(attributeRepository, projectRepository, this::findByProject); + + projectA = new ProjectEntity(UUID.randomUUID(), "Project A", "project-a", null, null, null, false); + projectB = new ProjectEntity(UUID.randomUUID(), "Project B", "project-b", null, null, null, false); + projectRepository.save(projectA); + projectRepository.save(projectB); + } + + @Test + @DisplayName("create() with a known projectId resolves the project and saves the entity") + void create_knownProject_savesEntity() { + AttributeModelDTO dto = new AttributeModelDTO(null, "UI", "var", 1.0, 10.0); + + AttributeModelResponseDTO result = service.create(projectA.getId(), dto); + + assertInstanceOf(AttributeModelResponseDTO.AttributeModelDTO.class, result); + var success = (AttributeModelResponseDTO.AttributeModelDTO) result; + assertEquals(projectA.getId(), success.projectId()); + } + + @Test + @DisplayName("create() with an unknown projectId returns an error DTO") + void create_unknownProject_returnsError() { + AttributeModelDTO dto = new AttributeModelDTO(null, "UI", "var", 1.0, 10.0); + + AttributeModelResponseDTO result = service.create(UUID.randomUUID(), dto); + + assertInstanceOf(AttributeModelResponseDTO.AttributeModelErrorDTO.class, result); + } + + @Test + @DisplayName("update() on an entity owned by a different project returns not-found") + void update_crossProject_returnsNotFound() { + AttributeEntity existing = new AttributeEntity(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectA); + attributeRepository.save(existing); + AttributeModelDTO updateDto = new AttributeModelDTO(existing.getId(), "UI2", "var2", 2.0, 20.0); + + AttributeModelResponseDTO result = service.update(projectB.getId(), updateDto); + + assertInstanceOf(AttributeModelResponseDTO.AttributeModelErrorDTO.class, result); + } + + @Test + @DisplayName("delete() on an entity owned by a different project returns not-found and does not delete it") + void delete_crossProject_returnsNotFoundAndKeepsEntity() { + AttributeEntity existing = new AttributeEntity(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectA); + attributeRepository.save(existing); + + AttributeModelResponseDTO result = service.delete(projectB.getId(), existing.getId()); + + assertInstanceOf(AttributeModelResponseDTO.AttributeModelErrorDTO.class, result); + assertTrue(attributeRepository.findById(existing.getId()).isPresent()); + } + + @Test + @DisplayName("delete() on an entity owned by the matching project succeeds and removes it") + void delete_sameProject_succeeds() { + AttributeEntity existing = new AttributeEntity(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectA); + attributeRepository.save(existing); + + AttributeModelResponseDTO result = service.delete(projectA.getId(), existing.getId()); + + assertInstanceOf(AttributeModelResponseDTO.AttributeModelDTO.class, result); + assertTrue(attributeRepository.findById(existing.getId()).isEmpty()); + } + + @Test + @DisplayName("deleteAll() only deletes entities belonging to the given project") + void deleteAll_scopesToProject() { + AttributeEntity a1 = new AttributeEntity(UUID.randomUUID(), "A1", "a1", 1.0, 10.0, projectA); + AttributeEntity b1 = new AttributeEntity(UUID.randomUUID(), "B1", "b1", 1.0, 10.0, projectB); + attributeRepository.save(a1); + attributeRepository.save(b1); + + service.deleteAll(projectA.getId()); + + assertTrue(attributeRepository.findById(a1.getId()).isEmpty()); + assertTrue(attributeRepository.findById(b1.getId()).isPresent()); + } + + @Test + @DisplayName("getAll() only returns entities belonging to the given project") + void getAll_scopesToProject() { + attributeRepository.save(new AttributeEntity(UUID.randomUUID(), "A1", "a1", 1.0, 10.0, projectA)); + attributeRepository.save(new AttributeEntity(UUID.randomUUID(), "A2", "a2", 1.0, 10.0, projectA)); + attributeRepository.save(new AttributeEntity(UUID.randomUUID(), "B1", "b1", 1.0, 10.0, projectB)); + + Page page = service.getAll(projectA.getId(), Pageable.from(0, 10)); + + assertEquals(2, page.getTotalSize()); + assertTrue(page.getContent().stream().allMatch(dto -> dto.projectId().equals(projectA.getId()))); + } + + @Test + @DisplayName("findById() returns empty when the entity belongs to a different project") + void findById_crossProject_returnsEmpty() { + AttributeEntity existing = new AttributeEntity(UUID.randomUUID(), "UI", "var", 1.0, 10.0, projectA); + attributeRepository.save(existing); + + Optional result = service.findById(projectB.getId(), existing.getId()); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("the unscoped create(dto) still throws for a project-scoped service") + void unscopedCreate_throwsUnsupported() { + AttributeModelDTO dto = new AttributeModelDTO(null, "UI", "var", 1.0, 10.0); + assertThrows(UnsupportedOperationException.class, () -> service.create(dto)); + } +}