From 4e31e03bb1054d8ec7f0b7cea8895ca6fb0c8691 Mon Sep 17 00:00:00 2001 From: Hanna Kurhuzenkava Date: Tue, 14 Jan 2025 23:12:57 +0300 Subject: [PATCH 1/2] DOKY-179 Create a temporary token to download file Add JWT-based document download token generation Implemented functionality to generate and manage JWT tokens for authorizing document downloads. Added new endpoints, service methods, database entities, and repository support for this feature. Updated migrations to include download token handling in both MySQL and SQL Server schemas. --- app-server/doky-front/webpack.config.js | 2 +- .../hkurh/doky/documents/DocumentFacade.kt | 8 ++++ .../hkurh/doky/documents/DocumentService.kt | 10 +++++ .../hkurh/doky/documents/api/DocumentApi.kt | 35 +++++++++++---- .../doky/documents/api/DocumentController.kt | 6 +++ .../documents/api/DownloadTokenResponse.kt | 8 ++++ .../documents/impl/DefaultDocumentFacade.kt | 32 ++++++++------ .../documents/impl/DefaultDocumentService.kt | 19 ++++++++ .../org/hkurh/doky/security/JwtProvider.kt | 22 ++++++++++ .../impl/DefaultDocumentFacadeTest.kt | 4 +- .../db/DownloadDocumentTokenEntity.kt | 43 +++++++++++++++++++ .../DownloadDocumentTokenEntityRepository.kt | 19 ++++++++ .../V1_5__add_document_download_tokens.sql | 23 ++++++++++ .../V1_5__add_document_download_tokens.sql | 28 ++++++++++++ 14 files changed, 235 insertions(+), 24 deletions(-) create mode 100644 app-server/src/main/kotlin/org/hkurh/doky/documents/api/DownloadTokenResponse.kt create mode 100644 persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntity.kt create mode 100644 persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntityRepository.kt create mode 100644 persistence/src/main/resources/migration/mysql/V1_5__add_document_download_tokens.sql create mode 100644 persistence/src/main/resources/migration/sqlserver/V1_5__add_document_download_tokens.sql diff --git a/app-server/doky-front/webpack.config.js b/app-server/doky-front/webpack.config.js index 476b3ac0..db96bea8 100644 --- a/app-server/doky-front/webpack.config.js +++ b/app-server/doky-front/webpack.config.js @@ -27,7 +27,7 @@ module.exports = (env, argv) => { }, devServer: { historyApiFallback: true, - port: 10001 + port: 10010 }, resolve: { alias: { diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentFacade.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentFacade.kt index feb86fc1..665c5cdc 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentFacade.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentFacade.kt @@ -62,4 +62,12 @@ interface DocumentFacade { */ @Throws(IOException::class) fun getFile(id: String): Resource? + + /** + * Generates a download token for the specified document ID and current user. + * + * @param id The ID of the document for which the download token is being generated. + * @return A string representing the generated download token. + */ + fun generateDownloadToken(id: String): String } diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentService.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentService.kt index ed80733c..937e1aac 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentService.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/DocumentService.kt @@ -1,6 +1,7 @@ package org.hkurh.doky.documents import org.hkurh.doky.documents.db.DocumentEntity +import org.hkurh.doky.users.db.UserEntity /** * Represents a service for managing documents. @@ -36,4 +37,13 @@ interface DocumentService { * @param document The [DocumentEntity] object to be saved. */ fun save(document: DocumentEntity) + + /** + * Generates a download token for a specified document and user. + * + * @param user The [UserEntity] representing the user requesting the download token. + * @param document The [DocumentEntity] representing the document for which the token is generated. + * @return A string representing the generated download token. + */ + fun generateDownloadToken(user: UserEntity, document: DocumentEntity): String } diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentApi.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentApi.kt index e8a7866a..9cf2211c 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentApi.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentApi.kt @@ -21,16 +21,30 @@ import java.io.IOException interface DocumentApi { @Operation(summary = "Upload file to document entry") @ApiResponses( - ApiResponse(responseCode = "200", description = "File is uploaded and attached to document"), - ApiResponse(responseCode = "404", description = "Document with provided id does not exist")) + ApiResponse(responseCode = "200", description = "File is uploaded and attached to document"), + ApiResponse(responseCode = "404", description = "Document with provided id does not exist") + ) fun uploadFile(@RequestBody file: MultipartFile, @PathVariable id: String): ResponseEntity<*>? + @Operation(summary = "Get token to authorize file download") + @ApiResponses( + ApiResponse(responseCode = "200", description = "Token is generated and provided"), + ApiResponse(responseCode = "404", description = "Document with provided id does not exist") + ) + fun getDownloadToken(@PathVariable id: String): ResponseEntity? + @Operation(summary = "Download file attached to document entry") @ApiResponses( - ApiResponse(responseCode = "200", description = "File is sent to client and can be downloaded", - content = [Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE)], - headers = [Header(name = "attachment; filename=...")]), - ApiResponse(responseCode = "404", description = "No document with requested id, or no attached file for document")) + ApiResponse( + responseCode = "200", description = "File is sent to client and can be downloaded", + content = [Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE)], + headers = [Header(name = "attachment; filename=...")] + ), + ApiResponse( + responseCode = "404", + description = "No document with requested id, or no attached file for document" + ) + ) @Throws(IOException::class) fun downloadFile(@PathVariable id: String): ResponseEntity<*>? @@ -47,9 +61,12 @@ interface DocumentApi { @Operation(summary = "Get metadata for document") @ApiResponses( - ApiResponse(responseCode = "200", description = "Document information is retrieved successfully", - content = [Content(schema = Schema(implementation = DocumentResponse::class))]), - ApiResponse(responseCode = "404", description = "No document with provided id")) + ApiResponse( + responseCode = "200", description = "Document information is retrieved successfully", + content = [Content(schema = Schema(implementation = DocumentResponse::class))] + ), + ApiResponse(responseCode = "404", description = "No document with provided id") + ) operator fun get(@PathVariable id: String): ResponseEntity<*>? @ApiResponses( diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentController.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentController.kt index c9d95909..1aa8a52c 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentController.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DocumentController.kt @@ -29,6 +29,12 @@ class DocumentController(private val documentFacade: DocumentFacade) : DocumentA return ResponseEntity.ok(null) } + @GetMapping("/{id}/download/token") + override fun getDownloadToken(@PathVariable id: String): ResponseEntity? { + val token = documentFacade.generateDownloadToken(id) + return ResponseEntity.ok(DownloadTokenResponse(token)) + } + @GetMapping("/{id}/download") @Trace(operationName = "document.download") @Throws(IOException::class) diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DownloadTokenResponse.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DownloadTokenResponse.kt new file mode 100644 index 00000000..dbf63e08 --- /dev/null +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/api/DownloadTokenResponse.kt @@ -0,0 +1,8 @@ +package org.hkurh.doky.documents.api + +/** + * Represents the response containing a token used for authorizing a file download. + * + * @property token The token string used to authorize the download operation. + */ +class DownloadTokenResponse(val token: String = "") diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacade.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacade.kt index 5532bb84..bed7d4e1 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacade.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacade.kt @@ -9,6 +9,7 @@ import org.hkurh.doky.documents.api.DocumentResponse import org.hkurh.doky.errorhandling.DokyNotFoundException import org.hkurh.doky.filestorage.FileStorageService import org.hkurh.doky.toDto +import org.hkurh.doky.users.UserService import org.springframework.core.io.Resource import org.springframework.core.io.UrlResource import org.springframework.stereotype.Component @@ -20,7 +21,8 @@ import java.io.IOException @Component class DefaultDocumentFacade( private val documentService: DocumentService, - private val fileStorageService: FileStorageService + private val fileStorageService: FileStorageService, + private val userService: UserService ) : DocumentFacade { override fun createDocument(name: String, description: String?): DocumentResponse? { val documentEntity = documentService.create(name, description) @@ -48,18 +50,14 @@ class DefaultDocumentFacade( @Transactional(propagation = Propagation.REQUIRED) override fun saveFile(file: MultipartFile, id: String) { - val document = documentService.find(id) - if (document != null) { - try { - val path = fileStorageService.store(file, document.filePath) - document.filePath = path - document.fileName = file.originalFilename - documentService.save(document) - } catch (e: IOException) { - throw RuntimeException(e) - } - } else { - throw DokyNotFoundException("Document with id [$id] not found") + val document = documentService.find(id) ?: throw DokyNotFoundException("Document with id [$id] not found") + try { + val path = fileStorageService.store(file, document.filePath) + document.filePath = path + document.fileName = file.originalFilename + documentService.save(document) + } catch (e: IOException) { + throw RuntimeException(e) } } @@ -83,6 +81,14 @@ class DefaultDocumentFacade( } } + override fun generateDownloadToken(id: String): String { + val user = userService.getCurrentUser() + val document = documentService.find(id) ?: throw DokyNotFoundException("Document with id [$id] not found") + LOG.debug { "Generate token for user [${user.id}] and document [$id]" } + val token = documentService.generateDownloadToken(user, document) + return token + } + companion object { private val LOG = KotlinLogging.logger {} } diff --git a/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentService.kt b/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentService.kt index 634171d4..6f0fb915 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentService.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentService.kt @@ -4,13 +4,19 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.hkurh.doky.documents.DocumentService import org.hkurh.doky.documents.db.DocumentEntity import org.hkurh.doky.documents.db.DocumentEntityRepository +import org.hkurh.doky.documents.db.DownloadDocumentTokenEntity +import org.hkurh.doky.documents.db.DownloadDocumentTokenEntityRepository +import org.hkurh.doky.security.JwtProvider import org.hkurh.doky.users.UserService +import org.hkurh.doky.users.db.UserEntity import org.springframework.stereotype.Service @Service class DefaultDocumentService( private val documentEntityRepository: DocumentEntityRepository, + private val downloadDocumentTokenEntityRepository: DownloadDocumentTokenEntityRepository, private val userService: UserService, + private val jwtProvider: JwtProvider ) : DocumentService { override fun create(name: String, description: String?): DocumentEntity { @@ -39,6 +45,19 @@ class DefaultDocumentService( documentEntityRepository.save(document) } + override fun generateDownloadToken(user: UserEntity, document: DocumentEntity): String { + val token = jwtProvider.generateDownloadToken(user) + val tokenEntity = + downloadDocumentTokenEntityRepository.findByUserAndDocument(user, document) ?: DownloadDocumentTokenEntity() + tokenEntity.apply { + this.user = user + this.document = document + this.token = token + } + downloadDocumentTokenEntityRepository.save(tokenEntity) + return token + } + companion object { private val LOG = KotlinLogging.logger {} } diff --git a/app-server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt b/app-server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt index 40167c4d..ff7eb245 100644 --- a/app-server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt +++ b/app-server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt @@ -3,6 +3,7 @@ package org.hkurh.doky.security import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import org.hkurh.doky.DokyApplication +import org.hkurh.doky.users.db.UserEntity import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.springframework.stereotype.Component @@ -36,6 +37,27 @@ object JwtProvider { .compact() } + /** + * Generates a download token for a given user and document. The token is a JWT + * with an expiration time of 10 minutes and includes claims containing the user ID + * and document ID. + * + * @param user the user for whom the token is being generated + * @return a JWT download token as a string + */ + fun generateDownloadToken(user: UserEntity): String { + val currentTime = DateTime(DateTimeZone.getDefault()) + val expireTokenTime = currentTime.plusMinutes(10) + val claims = mapOf("user" to user.id) + return Jwts.builder() + .setId("dokyDownloadToken") + .setClaims(claims) + .setIssuedAt(currentTime.toDate()) + .setExpiration(expireTokenTime.toDate()) + .signWith(DokyApplication.SECRET_KEY_SPEC, SignatureAlgorithm.HS256) + .compact() + } + /** * Retrieves the username from the provided token. * diff --git a/app-server/src/test/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacadeTest.kt b/app-server/src/test/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacadeTest.kt index d643b56a..bf255761 100644 --- a/app-server/src/test/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacadeTest.kt +++ b/app-server/src/test/kotlin/org/hkurh/doky/documents/impl/DefaultDocumentFacadeTest.kt @@ -6,6 +6,7 @@ import org.hkurh.doky.documents.api.DocumentRequest import org.hkurh.doky.documents.db.DocumentEntity import org.hkurh.doky.errorhandling.DokyNotFoundException import org.hkurh.doky.filestorage.FileStorageService +import org.hkurh.doky.users.UserService import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -23,7 +24,8 @@ class DefaultDocumentFacadeTest : DokyUnitTest { private var documentService: DocumentService = mock() private var fileStorageService: FileStorageService = mock() - private var documentFacade = DefaultDocumentFacade(documentService, fileStorageService) + private var userService: UserService = mock() + private var documentFacade = DefaultDocumentFacade(documentService, fileStorageService, userService) @BeforeEach fun setUp() { diff --git a/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntity.kt b/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntity.kt new file mode 100644 index 00000000..a2942ce0 --- /dev/null +++ b/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntity.kt @@ -0,0 +1,43 @@ +package org.hkurh.doky.documents.db + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Index +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint +import org.hkurh.doky.users.db.UserEntity + +@Entity +@Table( + name = "download_document_tokens", + indexes = [Index(name = "idx_download_document_tokens_token", columnList = "token")], + uniqueConstraints = [ + UniqueConstraint(name = "uc_download_document_tokens_token", columnNames = ["token"]), + UniqueConstraint( + name = "uc_download_document_tokens_app_user_document", + columnNames = ["app_user", "document"] + )] +) +class DownloadDocumentTokenEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false, updatable = false) + var id: Long? = null + + @OneToOne + @JoinColumn(name = "app_user", nullable = false, unique = true) + lateinit var user: UserEntity + + @OneToOne + @JoinColumn(name = "document", nullable = false, unique = true) + lateinit var document: DocumentEntity + + @Column(name = "token", nullable = false, unique = true) + lateinit var token: String +} diff --git a/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntityRepository.kt b/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntityRepository.kt new file mode 100644 index 00000000..2c73c991 --- /dev/null +++ b/persistence/src/main/kotlin/org/hkurh/doky/documents/db/DownloadDocumentTokenEntityRepository.kt @@ -0,0 +1,19 @@ +package org.hkurh.doky.documents.db + +import org.hkurh.doky.users.db.UserEntity +import org.springframework.data.jpa.repository.JpaSpecificationExecutor +import org.springframework.data.repository.CrudRepository + + +/** + * Repository interface for managing [DownloadDocumentTokenEntity] objects. + * Provides methods for querying and persisting [DownloadDocumentTokenEntity] instances in the database. + * Inherits functionality from [CrudRepository] for common CRUD operations and [JpaSpecificationExecutor] + * for specification-based queries. + */ +interface DownloadDocumentTokenEntityRepository : CrudRepository, + JpaSpecificationExecutor { + + fun findByUserAndDocument(user: UserEntity, document: DocumentEntity): DownloadDocumentTokenEntity? + +} diff --git a/persistence/src/main/resources/migration/mysql/V1_5__add_document_download_tokens.sql b/persistence/src/main/resources/migration/mysql/V1_5__add_document_download_tokens.sql new file mode 100644 index 00000000..ff1175da --- /dev/null +++ b/persistence/src/main/resources/migration/mysql/V1_5__add_document_download_tokens.sql @@ -0,0 +1,23 @@ +CREATE TABLE download_document_tokens +( + id BIGINT AUTO_INCREMENT NOT NULL, + app_user BIGINT NOT NULL, + document BIGINT NOT NULL, + token VARCHAR(255) NOT NULL, + CONSTRAINT pk_download_document_tokens PRIMARY KEY (id) +); + +ALTER TABLE download_document_tokens + ADD CONSTRAINT uc_download_document_tokens_app_user_document UNIQUE (app_user, document); + +ALTER TABLE download_document_tokens + ADD CONSTRAINT uc_download_document_tokens_token UNIQUE (token); + +CREATE INDEX idx_download_document_tokens_token ON download_document_tokens (token); + +ALTER TABLE download_document_tokens + ADD CONSTRAINT FK_DOWNLOAD_DOCUMENT_TOKENS_ON_APP_USER FOREIGN KEY (app_user) REFERENCES users (id); + +ALTER TABLE download_document_tokens + ADD CONSTRAINT FK_DOWNLOAD_DOCUMENT_TOKENS_ON_DOCUMENT FOREIGN KEY (document) REFERENCES documents (id); + diff --git a/persistence/src/main/resources/migration/sqlserver/V1_5__add_document_download_tokens.sql b/persistence/src/main/resources/migration/sqlserver/V1_5__add_document_download_tokens.sql new file mode 100644 index 00000000..9c6b8a89 --- /dev/null +++ b/persistence/src/main/resources/migration/sqlserver/V1_5__add_document_download_tokens.sql @@ -0,0 +1,28 @@ +CREATE TABLE download_document_tokens +( + id bigint IDENTITY (1, 1) NOT NULL, + app_user bigint NOT NULL, + document bigint NOT NULL, + token varchar(255) NOT NULL, + CONSTRAINT pk_download_document_tokens PRIMARY KEY (id) +) +GO + +ALTER TABLE download_document_tokens + ADD CONSTRAINT uc_download_document_tokens_app_user_document UNIQUE (app_user, document) +GO + +ALTER TABLE download_document_tokens + ADD CONSTRAINT uc_download_document_tokens_token UNIQUE (token) +GO + +CREATE NONCLUSTERED INDEX idx_download_document_tokens_token ON download_document_tokens (token) +GO + +ALTER TABLE download_document_tokens + ADD CONSTRAINT FK_DOWNLOAD_DOCUMENT_TOKENS_ON_APP_USER FOREIGN KEY (app_user) REFERENCES users (id) +GO + +ALTER TABLE download_document_tokens + ADD CONSTRAINT FK_DOWNLOAD_DOCUMENT_TOKENS_ON_DOCUMENT FOREIGN KEY (document) REFERENCES documents (id) +GO From 84b7bede8acb8a208137455595c2135dfa77403d Mon Sep 17 00:00:00 2001 From: Hanna Kurhuzenkava Date: Fri, 31 Jan 2025 18:22:24 +0300 Subject: [PATCH 2/2] Add download token generation and related tests Implemented download token generation endpoints and added tests to validate functionality, including handling non-existing documents. Enhanced cleanup script to clear download tokens and refined JDBC queries for better error handling. Updated build configuration to include Maven repository. --- .../kotlin/org/hkurh/doky/DocumentSpec.kt | 77 ++++++++++++++++++- .../resources/sql/cleanup_base_test_data.sql | 3 +- build.gradle.kts | 3 + 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/app-server/src/apiTest/kotlin/org/hkurh/doky/DocumentSpec.kt b/app-server/src/apiTest/kotlin/org/hkurh/doky/DocumentSpec.kt index 0eeb3f40..db7fb7fd 100644 --- a/app-server/src/apiTest/kotlin/org/hkurh/doky/DocumentSpec.kt +++ b/app-server/src/apiTest/kotlin/org/hkurh/doky/DocumentSpec.kt @@ -22,10 +22,12 @@ import java.sql.Types class DocumentSpec : RestSpec() { val endpoint = "$restPrefix/documents" val endpointSingle = "$endpoint/{id}" + val endpointDownloadToken = "$endpoint/{id}/download/token" val endpointUpload = "$endpointSingle/upload" val documentIdProperty = "id" val documentNameProperty = "name" val documentFileNameProperty = "fileName" + val tokenProperty = "token" val existedDocumentNameFirst = "Test_1" val existedDocumentFileNameFirst = "test.txt" val existedDocumentNameThird = "Test_3" @@ -128,7 +130,7 @@ class DocumentSpec : RestSpec() { @Sql(scripts = ["classpath:sql/DocumentSpec/setup.sql"], executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) fun shouldReturnErrorWhenUploadFileToNonExistingDocument() { // given - val docId = getDocumentId(existedDocumentNameThird) as Long + val docId = getDocumentId(existedDocumentNameThird) val requestSpec = prepareRequestSpecWithLogin() .addPathParam(documentIdProperty, docId) .addMultiPart("file", createFileToUpload(), "text/plain") @@ -160,11 +162,72 @@ class DocumentSpec : RestSpec() { response.then().statusCode(HttpStatus.BAD_REQUEST.value()) } - fun getDocumentId(docName: String): Long? { + @Test + @DisplayName("Should generate download token") + @Sql(scripts = ["classpath:sql/DocumentSpec/setup.sql"], executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + fun shouldGenerateDownloadToken() { + // given + val documentId = getDocumentId(existedDocumentNameFirst) + val requestSpec = prepareRequestSpecWithLogin() + .addPathParam(documentIdProperty, documentId) + .build() + + // when + val response = given(requestSpec).get(endpointDownloadToken) + + // then + response.then().statusCode(HttpStatus.OK.value()) + val token: String = response.path(tokenProperty) + val tokenRow = getDownloadToken(token) + val userId = getUserId(validUserUid) + assertEquals(tokenRow.app_user, userId) + assertEquals(tokenRow.document, documentId) + } + + @Test + @DisplayName("Should return 404 when generate download token for non existing document") + @Sql(scripts = ["classpath:sql/DocumentSpec/setup.sql"], executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + fun shouldReturn404WhenGenerateDownloadTokenForNonExistingDocument() { + // given + val documentId = getDocumentId(existedDocumentNameThird) + val requestSpec = prepareRequestSpecWithLogin() + .addPathParam(documentIdProperty, documentId) + .build() + + // when + val response = given(requestSpec).get(endpointDownloadToken) + + // then + response.then().statusCode(HttpStatus.NOT_FOUND.value()) + } + + fun getDocumentId(documentName: String): Long { val existedDocumentQuery = "select d.id from documents d where d.name = ?" - val args = arrayOf(docName) + val args = arrayOf(documentName) + val argTypes = intArrayOf(Types.VARCHAR) + val documentId = jdbcTemplate.queryForObject(existedDocumentQuery, args, argTypes, Long::class.java) + return documentId ?: throw IllegalArgumentException("Document not found with name: $documentName") + } + + fun getUserId(userUid: String): Long { + val existedDocumentQuery = "select u.id from users u where u.uid = ?" + val args = arrayOf(userUid) val argTypes = intArrayOf(Types.VARCHAR) - return jdbcTemplate.queryForObject(existedDocumentQuery, args, argTypes, Long::class.java) + val userId = jdbcTemplate.queryForObject(existedDocumentQuery, args, argTypes, Long::class.java) + return userId ?: throw IllegalArgumentException("User not found with UID: $userUid") + } + + fun getDownloadToken(token: String): DownloadDocumentTokenRow { + val downloadTokenQuery = "select * from download_document_tokens dt where dt.token = ?" + val args = arrayOf(token) + val argTypes = intArrayOf(Types.VARCHAR) + return jdbcTemplate.queryForObject(downloadTokenQuery, args, argTypes) { rs, _ -> + DownloadDocumentTokenRow( + token = rs.getString("token"), + document = rs.getLong("document"), + app_user = rs.getLong("app_user") + ) + } ?: throw IllegalArgumentException("Download token not found for token: $token") } fun createFileToUpload(): File? { @@ -175,3 +238,9 @@ class DocumentSpec : RestSpec() { return file } } + +data class DownloadDocumentTokenRow( + val token: String, + val document: Long, + val app_user: Long +) diff --git a/app-server/src/apiTest/resources/sql/cleanup_base_test_data.sql b/app-server/src/apiTest/resources/sql/cleanup_base_test_data.sql index 608b840f..0fdc6d8f 100644 --- a/app-server/src/apiTest/resources/sql/cleanup_base_test_data.sql +++ b/app-server/src/apiTest/resources/sql/cleanup_base_test_data.sql @@ -1,3 +1,5 @@ +DELETE FROM download_document_tokens; + DELETE FROM documents; DELETE FROM user_authorities; @@ -5,4 +7,3 @@ DELETE FROM user_authorities; DELETE FROM reset_password_tokens; DELETE FROM users; - diff --git a/build.gradle.kts b/build.gradle.kts index a6181a9f..6dacdbe1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,9 @@ import java.time.format.DateTimeFormatter allprojects { repositories { + maven { + url = uri("https://repo.maven.apache.org/maven2") + } mavenLocal() mavenCentral() }