diff --git a/.gitignore b/.gitignore index 29fdf3fc..549c37fc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ out/ ### VS Code ### .vscode/ + +### Static Content +static/ diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ErrorController.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ErrorController.kt index c5099fd3..6f16fab2 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ErrorController.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ErrorController.kt @@ -12,12 +12,12 @@ import org.springframework.security.access.AccessDeniedException import org.springframework.security.core.AuthenticationException import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.multipart.MaxUploadSizeExceededException import org.springframework.web.multipart.support.MissingServletRequestPartException +import org.springframework.web.servlet.NoHandlerFoundException import pt.up.fe.ni.website.backend.config.Logging data class SimpleError( @@ -32,7 +32,7 @@ data class CustomError(val errors: List) @RestControllerAdvice class ErrorController(private val objectMapper: ObjectMapper) : ErrorController, Logging { - @RequestMapping("/**") + @ExceptionHandler(NoHandlerFoundException::class) @ResponseStatus(HttpStatus.NOT_FOUND) fun endpointNotFound(): CustomError = wrapSimpleError("invalid endpoint") diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/EventController.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/EventController.kt index 0a837ff2..7f9fe91c 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/EventController.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/EventController.kt @@ -74,4 +74,18 @@ class EventController(private val service: EventService) { @PathVariable idEvent: Long, @PathVariable idAccount: Long ) = service.removeTeamMemberById(idEvent, idAccount) + + @PutMapping("/{idEvent}/gallery", consumes = ["multipart/form-data"]) + fun addGalleryImage( + @PathVariable idEvent: Long, + @RequestParam + @ValidImage + image: MultipartFile + ) = service.addGalleryImage(idEvent, image, EventService.IMAGE_FOLDER) + + @DeleteMapping("/{idEvent}/gallery") + fun removeGalleryImage( + @PathVariable idEvent: Long, + @RequestPart imageUrl: String + ) = service.removeGalleryImage(idEvent, imageUrl) } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ProjectController.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ProjectController.kt index ab4a2c06..71014234 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ProjectController.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/controller/ProjectController.kt @@ -88,4 +88,18 @@ class ProjectController(private val service: ProjectService) { @PathVariable idProject: Long, @PathVariable idAccount: Long ) = service.removeHallOfFameMemberById(idProject, idAccount) + + @PutMapping("/{idProject}/gallery", consumes = ["multipart/form-data"]) + fun addGalleryImage( + @PathVariable idProject: Long, + @RequestParam + @ValidImage + image: MultipartFile + ) = service.addGalleryImage(idProject, image, ProjectService.IMAGE_FOLDER) + + @DeleteMapping("/{idProject}/gallery") + fun removeGalleryImage( + @PathVariable idProject: Long, + @RequestPart imageUrl: String + ) = service.removeGalleryImage(idProject, imageUrl) } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ActivityDto.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ActivityDto.kt index c9d34c29..8b1c6325 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ActivityDto.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ActivityDto.kt @@ -8,7 +8,7 @@ abstract class ActivityDto( val title: String, val description: String, val teamMembersIds: List?, - val slug: String?, + val slug: String, var image: String?, @JsonIgnore var imageFile: MultipartFile? = null diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/EventDto.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/EventDto.kt index 19fe4673..3edca148 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/EventDto.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/EventDto.kt @@ -7,7 +7,7 @@ class EventDto( title: String, description: String, teamMembersIds: List?, - slug: String?, + slug: String, image: String?, val registerUrl: String?, diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/PostDto.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/PostDto.kt index bbc32568..0a03f542 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/PostDto.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/PostDto.kt @@ -6,5 +6,5 @@ class PostDto( val title: String, val body: String, val thumbnailPath: String, - val slug: String? + val slug: String ) : EntityDto() diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ProjectDto.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ProjectDto.kt index bebe2626..c2230206 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ProjectDto.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/dto/entity/ProjectDto.kt @@ -6,7 +6,7 @@ class ProjectDto( title: String, description: String, teamMembersIds: List?, - slug: String?, + slug: String, image: String?, val isArchived: Boolean = false, diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt index 3b2b81c0..d8e50b6f 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import jakarta.persistence.CascadeType import jakarta.persistence.Column +import jakarta.persistence.ElementCollection import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.GeneratedValue @@ -38,11 +39,14 @@ abstract class Activity( @Column(unique = true) @field:Size(min = Constants.Slug.minSize, max = Constants.Slug.maxSize) - open val slug: String? = null, + open val slug: String, @field:NotBlank open var image: String, + @ElementCollection(fetch = FetchType.EAGER) + open val gallery: MutableList = mutableListOf(), + @Id @GeneratedValue open val id: Long? = null diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt index 38135f16..0e548594 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Event.kt @@ -15,8 +15,9 @@ class Event( description: String, teamMembers: MutableList = mutableListOf(), associatedRoles: MutableList = mutableListOf(), - slug: String? = null, + slug: String, image: String, + gallery: MutableList = mutableListOf(), @field:NullOrNotBlank @field:URL @@ -33,4 +34,4 @@ class Event( val category: String?, id: Long? = null -) : Activity(title, description, teamMembers, associatedRoles, slug, image, id) +) : Activity(title, description, teamMembers, associatedRoles, slug, image, gallery, id) diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt index 58be5866..4cab516f 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt @@ -44,5 +44,5 @@ class Post( @Column(unique = true) @field:Size(min = Constants.Slug.minSize, max = Constants.Slug.maxSize) - val slug: String? = null + val slug: String ) diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Project.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Project.kt index 962b77bb..879b2c03 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/model/Project.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/model/Project.kt @@ -18,8 +18,9 @@ class Project( description: String, teamMembers: MutableList = mutableListOf(), associatedRoles: MutableList = mutableListOf(), - slug: String? = null, + slug: String, image: String, + gallery: MutableList = mutableListOf(), var isArchived: Boolean = false, @@ -48,4 +49,4 @@ class Project( val timeline: List<@Valid TimelineEvent> = emptyList(), id: Long? = null -) : Activity(title, description, teamMembers, associatedRoles, slug, image, id) +) : Activity(title, description, teamMembers, associatedRoles, slug, image, gallery, id) diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/repository/ActivityRepository.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/repository/ActivityRepository.kt index 370f356c..6c5bd1c9 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/repository/ActivityRepository.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/repository/ActivityRepository.kt @@ -6,5 +6,5 @@ import pt.up.fe.ni.website.backend.model.Activity @Repository interface ActivityRepository : CrudRepository { - fun findBySlug(slug: String?): T? + fun findBySlug(slug: String): T? } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/repository/PostRepository.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/repository/PostRepository.kt index 6bbb9138..846544f8 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/repository/PostRepository.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/repository/PostRepository.kt @@ -6,5 +6,5 @@ import pt.up.fe.ni.website.backend.model.Post @Repository interface PostRepository : JpaRepository { - fun findBySlug(slug: String?): Post? + fun findBySlug(slug: String): Post? } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/service/ErrorMessages.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/service/ErrorMessages.kt index 86f9032f..55b06061 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/service/ErrorMessages.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/service/ErrorMessages.kt @@ -46,6 +46,8 @@ object ErrorMessages { fun roleNotFound(id: Long): String = "role not found with id $id" + fun fileNotFound(fileName: String): String = "file not found with name $fileName" + fun userAlreadyHasRole(roleId: Long, userId: Long): String = "user $userId already has role $roleId" fun userNotInRole(roleId: Long, userId: Long): String = "user $userId doesn't have role $roleId" diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/service/activity/AbstractActivityService.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/service/activity/AbstractActivityService.kt index 3f5bd097..549b22db 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/service/activity/AbstractActivityService.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/service/activity/AbstractActivityService.kt @@ -2,6 +2,7 @@ package pt.up.fe.ni.website.backend.service.activity import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile import pt.up.fe.ni.website.backend.dto.entity.ActivityDto import pt.up.fe.ni.website.backend.model.Activity import pt.up.fe.ni.website.backend.repository.ActivityRepository @@ -26,7 +27,7 @@ abstract class AbstractActivityService( } dto.imageFile?.let { - val fileName = fileUploader.buildFileName(it, dto.title) + val fileName = fileUploader.buildFileName(it, dto.slug) dto.image = fileUploader.uploadImage(imageFolder, fileName, it.bytes) } @@ -51,7 +52,7 @@ abstract class AbstractActivityService( if (imageFile == null) { dto.image = activity.image } else { - val fileName = fileUploader.buildFileName(imageFile, dto.title) + val fileName = fileUploader.buildFileName(imageFile, dto.slug) dto.image = fileUploader.uploadImage(imageFolder, fileName, imageFile.bytes) } @@ -76,13 +77,33 @@ abstract class AbstractActivityService( fun removeTeamMemberById(idActivity: Long, idAccount: Long): T { val activity = getActivityById(idActivity) if (!accountService.doesAccountExist(idAccount)) { - throw NoSuchElementException( - ErrorMessages.accountNotFound( - idAccount - ) - ) + throw NoSuchElementException(ErrorMessages.accountNotFound(idAccount)) } activity.teamMembers.removeIf { it.id == idAccount } return repository.save(activity) } + + fun addGalleryImage(activityId: Long, image: MultipartFile, imageFolder: String): Activity { + val activity = getActivityById(activityId) + + val fileName = fileUploader.buildFileName(image, activity.slug) + val imageName = fileUploader.uploadImage("$imageFolder/gallery", fileName, image.bytes) + + activity.gallery.add(imageName) + + return repository.save(activity) + } + + fun removeGalleryImage(activityId: Long, imageName: String): Activity { + val activity = getActivityById(activityId) + + if (activity.gallery.contains(imageName)) { + fileUploader.deleteImage(imageName) + activity.gallery.remove(imageName) + } else { + throw NoSuchElementException(ErrorMessages.fileNotFound(imageName)) + } + + return repository.save(activity) + } } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/CloudinaryFileUploader.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/CloudinaryFileUploader.kt index 64cf99e1..9f3c066f 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/CloudinaryFileUploader.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/CloudinaryFileUploader.kt @@ -20,4 +20,12 @@ class CloudinaryFileUploader(private val basePath: String, private val cloudinar return result["url"]?.toString() ?: "" } + + override fun deleteImage(fileName: String) { + // Here, fileName is the publicId in Cloudinary, which would be the path to the file + cloudinary.uploader().destroy( + fileName, + null + ) + } } diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/FileUploader.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/FileUploader.kt index 665c5a48..597cc938 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/FileUploader.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/FileUploader.kt @@ -6,6 +6,7 @@ import pt.up.fe.ni.website.backend.utils.extensions.filenameExtension abstract class FileUploader { abstract fun uploadImage(folder: String, fileName: String, image: ByteArray): String + abstract fun deleteImage(fileName: String) fun buildFileName(photoFile: MultipartFile, prefix: String = ""): String { val limitedPrefix = prefix.take(100) // File name length has a limit of 256 characters diff --git a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/StaticFileUploader.kt b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/StaticFileUploader.kt index 9db9039c..ecc0df8a 100644 --- a/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/StaticFileUploader.kt +++ b/src/main/kotlin/pt/up/fe/ni/website/backend/service/upload/StaticFileUploader.kt @@ -11,4 +11,11 @@ class StaticFileUploader(private val storePath: String, private val servePath: S return "$servePath/$folder/$fileName" } + + override fun deleteImage(fileName: String) { + val storedFileName = fileName.replace(servePath, storePath) + val file = File(storedFileName) + + if (file.exists()) file.delete() + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9c2b6289..113211b4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -32,9 +32,14 @@ upload.provider=static upload.cloudinary-base-path=website upload.cloudinary-url=GET_YOURS_AT_CLOUDINARY_DASHBOARD # Folder in which files will be stored -upload.static-path=classpath:static +upload.static-path=file:static # URL that will serve static content -upload.static-serve=http://localhost:3000/static +upload.static-serve=http://localhost:8080/static + +# Files are served in the following path +spring.mvc.static-path-pattern=/static/** +# Add NoHandlerFoundException as an exception +spring.mvc.throw-exception-if-no-handler-found=true # Cors Origin cors.allow-origin = http://localhost:3000 diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/AuthControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/AuthControllerTest.kt index 5919e29e..59d4ec44 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/AuthControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/AuthControllerTest.kt @@ -244,8 +244,8 @@ class AuthControllerTest @Autowired constructor( inner class CheckPermissions { private val testPermissions = listOf(Permission.CREATE_ACCOUNT, Permission.CREATE_ACTIVITY) private val testActivity = Project( - "Test Activity", "Test Description", mutableListOf(), mutableListOf(), "test slug", "test image", false, - emptyList(), null, "test target audience" + "Test Activity", "Test Description", mutableListOf(), mutableListOf(), "test slug", "test image", + mutableListOf(), false, emptyList(), null, "test target audience" ) private val testRole = Role("MEMBER", Permissions(testPermissions), false) private val testPerActivityRole = PerActivityRole(Permissions(listOf(Permission.EDIT_ACTIVITY))) diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/EventControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/EventControllerTest.kt index 1104958e..6f91bb48 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/EventControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/EventControllerTest.kt @@ -70,6 +70,7 @@ internal class EventControllerTest @Autowired constructor( mutableListOf(), "great-event", "cool-image.png", + mutableListOf(), "https://docs.google.com/forms", DateInterval( TestUtils.createDate(2022, Calendar.JULY, 28, 10, 30), @@ -124,6 +125,7 @@ internal class EventControllerTest @Autowired constructor( mutableListOf(), "bloat", "waldo.jpeg", + mutableListOf(), null, DateInterval( TestUtils.createDate(2022, Calendar.JANUARY, 15), @@ -139,6 +141,7 @@ internal class EventControllerTest @Autowired constructor( mutableListOf(), "ni", "ni.png", + mutableListOf(), null, DateInterval( TestUtils.createDate(2022, Calendar.SEPTEMBER, 11), @@ -271,12 +274,95 @@ internal class EventControllerTest @Autowired constructor( } } + @NestedTest + @DisplayName("GET events/category/{category}") + inner class GetEventsByCategory { + private val testEvents = listOf( + testEvent, + Event( + "Bad event", + "This event was a failure", + mutableListOf(testAccount), + mutableListOf(), + "bad-event", + "bad-image.png", + mutableListOf(), + null, + DateInterval( + TestUtils.createDate(2021, Calendar.OCTOBER, 27), + null + ), + null, + null + ), + Event( + "Mid event", + "This event was ok", + mutableListOf(), + mutableListOf(), + "mid-event", + "mid-image.png", + mutableListOf(), + null, + DateInterval( + TestUtils.createDate(2022, Calendar.JANUARY, 15), + null + ), + null, + "Other category" + ), + Event( + "Cool event", + "This event was a awesome", + mutableListOf(testAccount), + mutableListOf(), + "cool-event", + "cool-image.png", + mutableListOf(), + null, + DateInterval( + TestUtils.createDate(2022, Calendar.SEPTEMBER, 11), + null + ), + null, + "Great Events" + ) + ) + + private val parameters = listOf(parameterWithName("category").description("Category of the events to retrieve")) + + @BeforeEach + fun addToRepositories() { + accountRepository.save(testAccount) + for (event in testEvents) repository.save(event) + } + + @Test + fun `should return all events of the category`() { + mockMvc.perform(get("/events/category/{category}", testEvent.category)) + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.length()").value(2), + jsonPath("$[0].category").value(testEvent.category), + jsonPath("$[1].category").value(testEvent.category) + ) + .andDocument( + documentation.getModelDocumentationArray(), + "Get events by category", + "This endpoint allows the retrieval of events labeled with a given category. " + + "It might be used to filter events in the event page.", + urlParameters = parameters + ) + } + } + @NestedTest @DisplayName("POST /events") inner class CreateEvent { private val uuid: UUID = UUID.randomUUID() private val mockedSettings = Mockito.mockStatic(UUID::class.java) - private val expectedImagePath = "${uploadConfigProperties.staticServe}/events/${testEvent.title}-$uuid.jpeg" + private val expectedImagePath = "${uploadConfigProperties.staticServe}/events/${testEvent.slug}-$uuid.jpeg" @BeforeEach fun addAccount() { @@ -344,6 +430,7 @@ internal class EventControllerTest @Autowired constructor( mutableListOf(), testEvent.slug, "duplicated-slug.png", + mutableListOf(), "https://docs.google.com/forms", DateInterval( TestUtils.createDate(2022, Calendar.AUGUST, 28), @@ -725,6 +812,195 @@ internal class EventControllerTest @Autowired constructor( } } + @NestedTest + @DisplayName("PUT /events/{idEvent}/gallery") + inner class AddGalleryImage { + + private val uuid: UUID = UUID.randomUUID() + private val mockedSettings = Mockito.mockStatic(UUID::class.java) + + @BeforeAll + fun setupMocks() { + Mockito.`when`(UUID.randomUUID()).thenReturn(uuid) + } + + @AfterAll + fun cleanup() { + mockedSettings.close() + } + + @BeforeEach + fun addToRepositories() { + accountRepository.save(testAccount) + repository.save(testEvent) + } + + private val parameters = listOf( + parameterWithName("idEvent").description("The id of the event being targeted") + ) + + @Test + fun `should add an image`() { + val expectedImagePath = "${uploadConfigProperties.staticServe}/events/gallery/${testEvent.slug}-$uuid.jpeg" + + mockMvc.multipartBuilder("/events/${testEvent.id}/gallery") + .asPutMethod() + .addFile("image", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.title").value(testEvent.title), + jsonPath("$.description").value(testEvent.description), + jsonPath("$.teamMembers.length()").value(testEvent.teamMembers.size), + jsonPath("$.gallery.length()").value(1), + jsonPath("$.gallery[0]").value(expectedImagePath), + jsonPath("$.registerUrl").value(testEvent.registerUrl), + jsonPath("$.dateInterval.startDate").value(testEvent.dateInterval.startDate.toJson()), + jsonPath("$.dateInterval.endDate").value(testEvent.dateInterval.endDate.toJson()), + jsonPath("$.location").value(testEvent.location), + jsonPath("$.category").value(testEvent.category), + jsonPath("$.slug").value(testEvent.slug), + jsonPath("$.image").value(testEvent.image) + ) + /* + .andDocument( + documentation, + "Adds an image to the gallery of the selected event", + "This endpoint adds an image to the gallery of an event, returning the event's updated data.", + urlParameters = parameters + ) + */ + } + + @Test + fun `should fail if event does not exist`() { + val nonexistentID = 5 + + mockMvc.multipartBuilder("/events/$nonexistentID/gallery") + .asPutMethod() + .addFile("image", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("activity not found with id $nonexistentID") + ).andDocumentErrorResponse(documentation) + } + + @Test + fun `should fail if image in wrong format`() { + mockMvc.multipartBuilder("/events/${testEvent.id}/gallery") + .asPutMethod() + .addFile("image", filename = "image.gif", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isBadRequest, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("invalid image type (png, jpg, jpeg or webp)") + ).andDocumentErrorResponse(documentation) + } + } + + @Nested + @NestedTest + @DisplayName("DELETE /events/{idEvent}/gallery") + inner class RemoveGalleryImage { + + private val uuid: UUID = UUID.randomUUID() + private val mockedSettings = Mockito.mockStatic(UUID::class.java) + private val mockImageUrl = "${uploadConfigProperties.staticServe}/events/gallery/${testEvent.title}-$uuid.jpeg" + + @BeforeAll + fun setupMocks() { + Mockito.`when`(UUID.randomUUID()).thenReturn(uuid) + + testEvent.gallery.add(mockImageUrl) + } + + @AfterAll + fun cleanup() { + mockedSettings.close() + } + + @BeforeEach + fun addToRepositories() { + accountRepository.save(testAccount) + + repository.save(testEvent) + } + + private val parameters = listOf( + parameterWithName("idEvent").description("The id of the event being targeted") + ) + + @Test + fun `should remove an image`() { + mockMvc.multipartBuilder("/events/${testEvent.id}/gallery") + .asDeleteMethod() + .addPart("imageUrl", mockImageUrl) + .perform() + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.title").value(testEvent.title), + jsonPath("$.description").value(testEvent.description), + jsonPath("$.teamMembers.length()").value(testEvent.teamMembers.size), + jsonPath("$.gallery.length()").value(0), + jsonPath("$.registerUrl").value(testEvent.registerUrl), + jsonPath("$.dateInterval.startDate").value(testEvent.dateInterval.startDate.toJson()), + jsonPath("$.dateInterval.endDate").value(testEvent.dateInterval.endDate.toJson()), + jsonPath("$.location").value(testEvent.location), + jsonPath("$.category").value(testEvent.category), + jsonPath("$.slug").value(testEvent.slug), + jsonPath("$.image").value(testEvent.image) + ) + /* + .andDocument( + documentation, + "Removes an image from the gallery of a selected event", + "This endpoint removes an image to the gallery of an event, returning the event's updated data.", + urlParameters = parameters + ) + */ + } + + @Test + fun `should fail if event does not exist`() { + val nonexistentID = 5 + + mockMvc.multipartBuilder("/events/$nonexistentID/gallery") + .asDeleteMethod() + .addPart("imageUrl", mockImageUrl) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("activity not found with id $nonexistentID") + ).andDocumentErrorResponse(documentation) + } + + @Test + fun `should fail if image does not exist`() { + val wrongImageUrl = + "${uploadConfigProperties.staticServe}/gallery/events/Another${testEvent.title}-$uuid.jpeg" + + mockMvc.multipartBuilder("/events/${testEvent.id}/gallery") + .asDeleteMethod() + .addPart("imageUrl", wrongImageUrl) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("file not found with name $wrongImageUrl") + ).andDocumentErrorResponse(documentation) + } + } + @NestedTest @DisplayName("PUT /events/{eventId}") inner class UpdateEvent { @@ -832,7 +1108,7 @@ internal class EventControllerTest @Autowired constructor( @Test fun `should update the event with same slug`() { - eventPart["slug"] = testEvent.slug!! + eventPart["slug"] = testEvent.slug mockMvc.multipartBuilder("/events/${testEvent.id}") .asPutMethod() .addPart("event", objectMapper.writeValueAsString(eventPart)) @@ -866,7 +1142,8 @@ internal class EventControllerTest @Autowired constructor( location = newLocation, category = newCategory, image = "image.png", - slug = newSlug + slug = newSlug, + gallery = mutableListOf() ) repository.save(otherEvent) @@ -885,7 +1162,7 @@ internal class EventControllerTest @Autowired constructor( @Test fun `should update the event with image`() { - val expectedImagePath = "${uploadConfigProperties.staticServe}/events/$newTitle-$uuid.jpeg" + val expectedImagePath = "${uploadConfigProperties.staticServe}/events/$newSlug-$uuid.jpeg" mockMvc.multipartBuilder("/events/${testEvent.id}") .asPutMethod() @@ -927,11 +1204,12 @@ internal class EventControllerTest @Autowired constructor( } @Test - fun `should fail if the event does not exist`() { + fun`should fail if the event does not exist`() { val eventPart = objectMapper.writeValueAsString( mapOf( "title" to "New Title", "description" to "New Description", + "slug" to "new-slug", "dateInterval" to DateInterval(TestUtils.createDate(2022, Calendar.DECEMBER, 1), null), "associatedRoles" to testEvent.associatedRoles ) @@ -1012,6 +1290,7 @@ internal class EventControllerTest @Autowired constructor( requiredFields = mapOf( "title" to testEvent.title, "description" to testEvent.description, + "slug" to testEvent.slug, "dateInterval" to testEvent.dateInterval, "image" to testEvent.image ) @@ -1063,6 +1342,29 @@ internal class EventControllerTest @Autowired constructor( ) } + @NestedTest + @DisplayName("slug") + inner class SlugValidation { + @BeforeAll + fun setParam() { + validationTester.param = "slug" + } + + @Test + fun `should be required`() = validationTester.isRequired() + + @Test + @DisplayName( + "size should be between ${ActivityConstants.Slug.minSize}" + + " and ${ActivityConstants.Slug.maxSize}()" + ) + fun size() = + validationTester.hasSizeBetween( + ActivityConstants.Slug.minSize, + ActivityConstants.Slug.maxSize + ) + } + @NestedTest @DisplayName("registerUrl") inner class UrlValidation { diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/GenerationControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/GenerationControllerTest.kt index 0b9c98ab..06c8bfba 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/GenerationControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/GenerationControllerTest.kt @@ -1059,9 +1059,11 @@ class GenerationControllerTest @Autowired constructor( Project( "NIJobs", "cool project", + slug = "ni-jobs", image = "cool-image.png", targetAudience = "students", - github = "https://github.com/NIAEFEUP/nijobs-be" + github = "https://github.com/NIAEFEUP/nijobs-be", + gallery = mutableListOf() ) ) ) @@ -1081,10 +1083,12 @@ class GenerationControllerTest @Autowired constructor( Event( title = "SINF", description = "cool event", + slug = "sinf", dateInterval = DateInterval(TestUtils.createDate(2023, 9, 10)), location = null, category = null, - image = "cool-image.png" + image = "cool-image.png", + gallery = mutableListOf() ) ) ) diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/PostControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/PostControllerTest.kt index a21edf3e..9021f56d 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/PostControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/PostControllerTest.kt @@ -56,7 +56,8 @@ internal class PostControllerTest @Autowired constructor( Post( "NIAEFEUP gets a new president", "New president promised to buy new chairs", - "https://thumbnails/pres.png" + "https://thumbnails/pres.png", + slug = "new-president" ) ) @@ -255,6 +256,7 @@ internal class PostControllerTest @Autowired constructor( requiredFields = mapOf( "title" to testPost.title, "body" to testPost.body, + "slug" to testPost.slug, "thumbnailPath" to testPost.thumbnailPath ) ) @@ -314,6 +316,9 @@ internal class PostControllerTest @Autowired constructor( validationTester.param = "slug" } + @Test + fun `should be required`() = validationTester.isRequired() + @Test @DisplayName("size should be between ${Constants.Slug.minSize} and ${Constants.Slug.maxSize}()") fun size() = validationTester.hasSizeBetween(Constants.Slug.minSize, Constants.Slug.maxSize) @@ -392,7 +397,7 @@ internal class PostControllerTest @Autowired constructor( ) @Test - fun `should update the post without the slug`() { + fun `should update the post without changing the slug`() { val newTitle = "New Title" val newBody = "New Body of the post" val newThumbnailPath = "https://thumbnails/new.png" @@ -405,7 +410,8 @@ internal class PostControllerTest @Autowired constructor( mapOf( "title" to newTitle, "body" to newBody, - "thumbnailPath" to newThumbnailPath + "thumbnailPath" to newThumbnailPath, + "slug" to testPost.slug ) ) ) @@ -436,7 +442,7 @@ internal class PostControllerTest @Autowired constructor( } @Test - fun `should update the post with the slug`() { + fun `should update the post changing the slug`() { val newTitle = "New Title" val newBody = "New Body of the post" val newThumbnailPath = "https://thumbnails/new.png" @@ -491,7 +497,8 @@ internal class PostControllerTest @Autowired constructor( mapOf( "title" to "New Title", "body" to "New Body of the post", - "thumbnailPath" to "thumbnails/new.png" + "thumbnailPath" to "thumbnails/new.png", + "slug" to "new-slug" ) ) ) @@ -562,7 +569,8 @@ internal class PostControllerTest @Autowired constructor( requiredFields = mapOf( "title" to testPost.title, "body" to testPost.body, - "thumbnailPath" to testPost.thumbnailPath + "thumbnailPath" to testPost.thumbnailPath, + "slug" to testPost.slug ) ) @@ -621,6 +629,9 @@ internal class PostControllerTest @Autowired constructor( validationTester.param = "slug" } + @Test + fun `should be required`() = validationTester.isRequired() + @Test @DisplayName("size should be between ${Constants.Slug.minSize} and ${Constants.Slug.maxSize}()") fun size() = validationTester.hasSizeBetween(Constants.Slug.minSize, Constants.Slug.maxSize) diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/ProjectControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/ProjectControllerTest.kt index 1ab9e633..8b2df3c0 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/ProjectControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/ProjectControllerTest.kt @@ -85,6 +85,7 @@ internal class ProjectControllerTest @Autowired constructor( mutableListOf(), "awesome-project", "cool-image.png", + mutableListOf(), false, listOf("Java", "Kotlin", "Spring"), "Nice one", @@ -113,8 +114,9 @@ internal class ProjectControllerTest @Autowired constructor( "Job platform for students", mutableListOf(), mutableListOf(), - null, + "ni-jobs", "cool-image.png", + mutableListOf(), false, listOf("ExpressJS", "React"), "Nice one", @@ -278,7 +280,7 @@ internal class ProjectControllerTest @Autowired constructor( inner class CreateProject { private val uuid: UUID = UUID.randomUUID() private val mockedSettings = Mockito.mockStatic(UUID::class.java) - private val expectedImagePath = "${uploadConfigProperties.staticServe}/projects/${testProject.title}-$uuid.jpeg" + private val expectedImagePath = "${uploadConfigProperties.staticServe}/projects/${testProject.slug}-$uuid.jpeg" @BeforeEach fun addToRepositories() { @@ -359,6 +361,7 @@ internal class ProjectControllerTest @Autowired constructor( mutableListOf(), testProject.slug, "cool-project.png", + mutableListOf(), false, listOf("Java", "Kotlin", "Spring"), "Nice project", @@ -447,6 +450,7 @@ internal class ProjectControllerTest @Autowired constructor( requiredFields = mapOf( "title" to testProject.title, "description" to testProject.description, + "slug" to testProject.slug, "targetAudience" to testProject.targetAudience ) ) @@ -501,6 +505,9 @@ internal class ProjectControllerTest @Autowired constructor( validationTester.param = "slug" } + @Test + fun `should be required`() = validationTester.isRequired() + @Test @DisplayName( "size should be between ${ActivityConstants.Slug.minSize} and ${ActivityConstants.Slug.maxSize}()" @@ -754,7 +761,7 @@ internal class ProjectControllerTest @Autowired constructor( @Test fun `should update the project with the same slug`() { - projectPart["slug"] = testProject.slug!! + projectPart["slug"] = testProject.slug mockMvc.multipartBuilder("/projects/${testProject.id}") .asPutMethod() .addPart("project", objectMapper.writeValueAsString(projectPart)) @@ -800,6 +807,7 @@ internal class ProjectControllerTest @Autowired constructor( description = newDescription, teamMembers = mutableListOf(), image = "image.png", + gallery = mutableListOf(), slug = newSlug, slogan = newSlogan, targetAudience = newTargetAudience, @@ -824,7 +832,7 @@ internal class ProjectControllerTest @Autowired constructor( @Test fun `should update the project with image`() { - val expectedImagePath = "${uploadConfigProperties.staticServe}/projects/$newTitle-$uuid.jpeg" + val expectedImagePath = "${uploadConfigProperties.staticServe}/projects/$newSlug-$uuid.jpeg" mockMvc.multipartBuilder("/projects/${testProject.id}") .asPutMethod() @@ -929,6 +937,7 @@ internal class ProjectControllerTest @Autowired constructor( requiredFields = mapOf( "title" to testProject.title, "description" to testProject.description, + "slug" to testProject.slug, "targetAudience" to testProject.targetAudience ) ) @@ -983,6 +992,9 @@ internal class ProjectControllerTest @Autowired constructor( validationTester.param = "slug" } + @Test + fun `should be required`() = validationTester.isRequired() + @Test @DisplayName( "size should be between ${ActivityConstants.Slug.minSize} and ${ActivityConstants.Slug.maxSize}()" @@ -1046,6 +1058,7 @@ internal class ProjectControllerTest @Autowired constructor( mapOf( "title" to testProject.title, "description" to testProject.description, + "slug" to testProject.slug, "targetAudience" to testProject.targetAudience, "links" to listOf(params) ) @@ -1138,6 +1151,7 @@ internal class ProjectControllerTest @Autowired constructor( mapOf( "title" to testProject.title, "description" to testProject.description, + "slug" to testProject.slug, "targetAudience" to testProject.targetAudience, "timeline" to listOf(params) ) @@ -1244,8 +1258,9 @@ internal class ProjectControllerTest @Autowired constructor( "very cool project", mutableListOf(), mutableListOf(), - null, + "proj1", "cool-image.png", + mutableListOf(), true, listOf("React", "TailwindCSS"), "Nice one", @@ -1556,6 +1571,198 @@ internal class ProjectControllerTest @Autowired constructor( } } + @NestedTest + @DisplayName("PUT /projects/{idProject}/gallery") + inner class AddGalleryImage { + + private val uuid: UUID = UUID.randomUUID() + private val mockedSettings = Mockito.mockStatic(UUID::class.java) + + @BeforeAll + fun setupMocks() { + Mockito.`when`(UUID.randomUUID()).thenReturn(uuid) + } + + @AfterAll + fun cleanup() { + mockedSettings.close() + } + + @BeforeEach + fun addToRepositories() { + accountRepository.save(testAccount) + repository.save(testProject) + } + + private val parameters = listOf( + parameterWithName("idProject").description("The id of the project being targeted") + ) + + @Test + fun `should add an image`() { + val expectedImagePath = + "${uploadConfigProperties.staticServe}/projects/gallery/${testProject.slug}-$uuid.jpeg" + + mockMvc.multipartBuilder("/projects/${testProject.id}/gallery") + .asPutMethod() + .addFile("image", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.title").value(testProject.title), + jsonPath("$.description").value(testProject.description), + jsonPath("$.teamMembers.length()").value(testProject.teamMembers.size), + jsonPath("$.isArchived").value(testProject.isArchived), + jsonPath("$.slug").value(testProject.slug), + jsonPath("$.slogan").value(testProject.slogan), + jsonPath("$.targetAudience").value(testProject.targetAudience), + jsonPath("$.github").value(testProject.github), + jsonPath("$.links.length()").value(testProject.links.size), + jsonPath("$.timeline.length()").value(testProject.timeline.size), + jsonPath("$.image").value(testProject.image), + jsonPath("$.gallery.length()").value(1), + jsonPath("$.gallery[0]").value(expectedImagePath) + ) + /* + .andDocument( + documentation, + "Adds an image to the gallery of the selected project", + "This endpoint adds an image to the gallery of an project, returning the project's updated data.", + urlParameters = parameters + ) + */ + } + + @Test + fun `should fail if project does not exist`() { + val nonexistentID = 5 + + mockMvc.multipartBuilder("/projects/$nonexistentID/gallery") + .asPutMethod() + .addFile("image", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("activity not found with id $nonexistentID") + ).andDocumentErrorResponse(documentation) + } + + @Test + fun `should fail if image in wrong format`() { + mockMvc.multipartBuilder("/projects/${testProject.id}/gallery") + .asPutMethod() + .addFile("image", filename = "image.gif", contentType = MediaType.IMAGE_JPEG_VALUE) + .perform() + .andExpectAll( + status().isBadRequest, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("invalid image type (png, jpg, jpeg or webp)") + ).andDocumentErrorResponse(documentation) + } + } + + @NestedTest + @DisplayName("DELETE /projects/{idProject}/gallery") + inner class RemoveGalleryImage { + + private val uuid: UUID = UUID.randomUUID() + private val mockedSettings = Mockito.mockStatic(UUID::class.java) + private val mockImageUrl = + "${uploadConfigProperties.staticServe}/projects/gallery/${testProject.title}-$uuid.jpeg" + + @BeforeAll + fun setupMocks() { + Mockito.`when`(UUID.randomUUID()).thenReturn(uuid) + + testProject.gallery.add(mockImageUrl) + } + + @AfterAll + fun cleanup() { + mockedSettings.close() + } + + @BeforeEach + fun addToRepositories() { + accountRepository.save(testAccount) + + repository.save(testProject) + } + + private val parameters = listOf( + parameterWithName("idProject").description("The id of the project being targeted") + ) + + @Test + fun `should remove an image`() { + mockMvc.multipartBuilder("/projects/${testProject.id}/gallery") + .asDeleteMethod() + .addPart("imageUrl", mockImageUrl) + .perform() + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.title").value(testProject.title), + jsonPath("$.description").value(testProject.description), + jsonPath("$.teamMembers.length()").value(testProject.teamMembers.size), + jsonPath("$.isArchived").value(testProject.isArchived), + jsonPath("$.slug").value(testProject.slug), + jsonPath("$.slogan").value(testProject.slogan), + jsonPath("$.targetAudience").value(testProject.targetAudience), + jsonPath("$.github").value(testProject.github), + jsonPath("$.links.length()").value(testProject.links.size), + jsonPath("$.timeline.length()").value(testProject.timeline.size), + jsonPath("$.image").value(testProject.image), + jsonPath("$.gallery.length()").value(0) + ) + /* + .andDocument( + documentation, + "Removes an image from the gallery of a selected project", + "This endpoint removes an image to the gallery of a project, returning the project's updated data.", + urlParameters = parameters + ) + */ + } + + @Test + fun `should fail if project does not exist`() { + val nonexistentID = 5 + + mockMvc.multipartBuilder("/projects/$nonexistentID/gallery") + .asDeleteMethod() + .addPart("imageUrl", mockImageUrl) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("activity not found with id $nonexistentID") + ).andDocumentErrorResponse(documentation) + } + + @Test + fun `should fail if image does not exist`() { + val wrongImageUrl = + "${uploadConfigProperties.staticServe}/gallery/projects/Another${testProject.title}-$uuid.jpeg" + + mockMvc.multipartBuilder("/projects/${testProject.id}/gallery") + .asDeleteMethod() + .addPart("imageUrl", wrongImageUrl) + .perform() + .andExpectAll( + status().isNotFound, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.errors.length()").value(1), + jsonPath("$.errors[0].message").value("file not found with name $wrongImageUrl") + ).andDocumentErrorResponse(documentation) + } + } + fun Date?.toJson(): String { val quotedDate = objectMapper.writeValueAsString(this) // objectMapper adds quotes to the date, so remove them diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/RoleControllerTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/RoleControllerTest.kt index 41ec65d7..a71c15b4 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/controller/RoleControllerTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/controller/RoleControllerTest.kt @@ -76,6 +76,7 @@ internal class RoleControllerTest @Autowired constructor( val project = Project( "UNI", "Melhor app", + slug = "uni-app", image = "image.png", targetAudience = "Estudantes" ) @@ -738,7 +739,13 @@ internal class RoleControllerTest @Autowired constructor( @BeforeEach fun addAll() { - project = Project("test project", "test", image = "image.png", targetAudience = "estudantes") + project = Project( + "test project", + "test", + slug = "test-proj", + image = "image.png", + targetAudience = "estudantes" + ) roleRepository.save(testRole) projectRepository.save(project) val perActivityRole = PerActivityRole(Permissions(listOf(Permission.EDIT_ACTIVITY))) diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/model/AccountTest.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/model/AccountTest.kt index dd2967d7..252e98d7 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/model/AccountTest.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/model/AccountTest.kt @@ -32,6 +32,7 @@ class AccountTest { val websiteProject = Project( "NI Website", "NI's website is where everything about NI is shown to the public", + slug = "ni-website", image = "cool-image.jpg", targetAudience = "Everyone" diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadActivity.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadActivity.kt index b46da124..d1b594e6 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadActivity.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadActivity.kt @@ -13,6 +13,11 @@ class PayloadActivity { DocumentedJSONField("id", "Id of the activity", JsonFieldType.NUMBER), DocumentedJSONField("title", "Title of the activity", JsonFieldType.STRING), DocumentedJSONField("description", "Description of the activity", JsonFieldType.STRING), + DocumentedJSONField( + "slug", + "Short and friendly textual post identifier", + JsonFieldType.STRING + ), DocumentedJSONField( "hallOfFame", "Array of members that were once associated with the project", @@ -89,6 +94,13 @@ class PayloadActivity { "Description of the event", JsonFieldType.STRING, optional = true + ), + DocumentedJSONField( + "gallery[]", + "Array of paths for the images associated with the activity", + JsonFieldType.ARRAY, + isInRequest = false, + optional = true ) ).addFieldsBeneathPath( "dateInterval", diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadEvent.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadEvent.kt index e952163d..1a6e4527 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadEvent.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadEvent.kt @@ -20,8 +20,7 @@ class PayloadEvent : ModelDocumentation( DocumentedJSONField( "slug", "Short and friendly textual event identifier", - JsonFieldType.STRING, - optional = true + JsonFieldType.STRING ), DocumentedJSONField("id", "Event ID", JsonFieldType.NUMBER, isInRequest = false), DocumentedJSONField( @@ -42,6 +41,13 @@ class PayloadEvent : ModelDocumentation( JsonFieldType.NUMBER, isInResponse = false, optional = true + ), + DocumentedJSONField( + "gallery[]", + "Array of paths for the images associated with the event", + JsonFieldType.ARRAY, + isInRequest = false, + optional = true ) ).addFieldsBeneathPath( "teamMembers[]", diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadPost.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadPost.kt index a7e45775..702dbc0f 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadPost.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadPost.kt @@ -15,8 +15,7 @@ class PayloadPost : ModelDocumentation( DocumentedJSONField( "slug", "Short and friendly textual post identifier", - JsonFieldType.STRING, - optional = true + JsonFieldType.STRING ), DocumentedJSONField("id", "Post ID", JsonFieldType.NUMBER, isInRequest = false), DocumentedJSONField( diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadProject.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadProject.kt index 3a6204a5..8a7d0a82 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadProject.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation/payloadschemas/model/PayloadProject.kt @@ -23,14 +23,20 @@ class PayloadProject : ModelDocumentation( DocumentedJSONField( "slug", "Short and friendly textual event identifier", - JsonFieldType.STRING, - optional = true + JsonFieldType.STRING ), DocumentedJSONField( "image", "Path to the image", JsonFieldType.STRING ), + DocumentedJSONField( + "gallery[]", + "Array of paths for the images associated with the project", + JsonFieldType.ARRAY, + isInRequest = false, + optional = true + ), DocumentedJSONField( "targetAudience", "Information about the target audience", diff --git a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/mockmvc/MockMvcMultipartBuilder.kt b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/mockmvc/MockMvcMultipartBuilder.kt index 240ed891..f8f3d635 100644 --- a/src/test/kotlin/pt/up/fe/ni/website/backend/utils/mockmvc/MockMvcMultipartBuilder.kt +++ b/src/test/kotlin/pt/up/fe/ni/website/backend/utils/mockmvc/MockMvcMultipartBuilder.kt @@ -49,6 +49,13 @@ class MockMvcMultipartBuilder( return this } + fun asDeleteMethod(): MockMvcMultipartBuilder { + multipart.with { + it.method = "DELETE" + it + } + return this + } fun perform(): ResultActions { return mockMvc.perform(multipart) }