Skip to content

Commit 498dc81

Browse files
fhennigchaoran-chen
authored andcommitted
test(backend): more backend testing for file sharing (#4043)
resolves #3991 - Add public policy to testcontainer bucket, so tagged files get made public. - Add a new test, that checks whether approved files are actually public. - Add a new test that checks whether the presigned URLs returned by 'extract-...-data' are accessible. 🚀 Preview: Add `preview` label to enable
1 parent 598e91f commit 498dc81

7 files changed

Lines changed: 145 additions & 7 deletions

File tree

backend/src/main/kotlin/org/loculus/backend/api/FileTypes.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ data class FileIdAndUrl(
2121
data class FileIdAndName(val fileId: FileId, val name: String)
2222

2323
data class FileIdAndNameAndUrl(val fileId: FileId, val name: String, val url: String)
24+
25+
/**
26+
* Strip the URL from the object.
27+
*/
28+
fun FileIdAndNameAndUrl.toFileIdAndName(): FileIdAndName = FileIdAndName(fileId, name)

backend/src/main/kotlin/org/loculus/backend/service/files/S3Service.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class S3Service(private val s3Config: S3Config) {
5151
return "${config.endpoint}/${config.bucket}/${getFileName(fileId)}"
5252
}
5353

54+
/**
55+
* Sets the 'public=true' tag on the given file ID.
56+
* The bucket should have a policy that files with this tag are publicly accessible.
57+
*/
5458
fun setFileToPublic(fileId: FileId) {
5559
val config = getS3BucketConfig()
5660
getInternalClient().setObjectTags(

backend/src/test/kotlin/org/loculus/backend/controller/EndpointTestExtension.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.loculus.backend.controller
22

33
import io.minio.MakeBucketArgs
44
import io.minio.MinioClient
5+
import io.minio.SetBucketPolicyArgs
56
import mu.KotlinLogging
67
import org.junit.jupiter.api.extension.BeforeEachCallback
78
import org.junit.jupiter.api.extension.ExtendWith
@@ -254,4 +255,24 @@ private fun createBucket(container: MinIOContainer, region: String, bucket: Stri
254255
.bucket(bucket)
255256
.build(),
256257
)
258+
val policy = """
259+
{
260+
"Version":"2012-10-17",
261+
"Statement":[
262+
{
263+
"Effect":"Allow",
264+
"Principal":"*",
265+
"Action":"s3:GetObject",
266+
"Resource":["arn:aws:s3:::$bucket/*"],
267+
"Condition":{
268+
"StringEquals":{
269+
"s3:ExistingObjectTag/public":"true"
270+
}
271+
}
272+
}
273+
]
274+
}
275+
""".trimIndent()
276+
277+
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).region(region).config(policy).build())
257278
}

backend/src/test/kotlin/org/loculus/backend/controller/files/FilesClient.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.loculus.backend.controller.files
22

33
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
4+
import org.loculus.backend.api.FileIdAndUrl
45
import org.loculus.backend.controller.jwtForDefaultUser
56
import org.loculus.backend.controller.withAuth
67
import org.loculus.backend.service.files.FileId
@@ -27,3 +28,11 @@ fun ResultActions.andGetFileIds(): List<FileId> = andReturn()
2728
val responseJson = jacksonObjectMapper().readTree(it)
2829
responseJson.map { UUID.fromString(it.get("fileId").textValue()) }
2930
}
31+
32+
fun ResultActions.andGetFileIdsAndUrls(): List<FileIdAndUrl> = andReturn()
33+
.response
34+
.contentAsString
35+
.let {
36+
val responseJson = jacksonObjectMapper().readTree(it)
37+
responseJson.map { FileIdAndUrl(UUID.fromString(it.get("fileId").textValue()), it.get("url").textValue()) }
38+
}

backend/src/test/kotlin/org/loculus/backend/controller/submission/ApproveProcessedDataEndpointTest.kt

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.loculus.backend.controller.submission
22

3+
import com.fasterxml.jackson.databind.ObjectMapper
34
import org.hamcrest.MatcherAssert.assertThat
45
import org.hamcrest.Matchers.containsString
56
import org.hamcrest.Matchers.hasSize
@@ -11,11 +12,15 @@ import org.loculus.backend.api.ApproveDataScope.WITHOUT_WARNINGS
1112
import org.loculus.backend.api.Status.APPROVED_FOR_RELEASE
1213
import org.loculus.backend.api.Status.IN_PROCESSING
1314
import org.loculus.backend.api.Status.PROCESSED
15+
import org.loculus.backend.api.SubmittedProcessedData
16+
import org.loculus.backend.api.toFileIdAndName
17+
import org.loculus.backend.config.BackendSpringProperty
1418
import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_USER_NAME
1519
import org.loculus.backend.controller.DEFAULT_ORGANISM
1620
import org.loculus.backend.controller.DEFAULT_USER_NAME
1721
import org.loculus.backend.controller.EndpointTest
1822
import org.loculus.backend.controller.OTHER_ORGANISM
23+
import org.loculus.backend.controller.S3_CONFIG
1924
import org.loculus.backend.controller.assertHasError
2025
import org.loculus.backend.controller.assertStatusIs
2126
import org.loculus.backend.controller.expectUnauthorizedResponse
@@ -29,11 +34,18 @@ import org.springframework.http.MediaType
2934
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
3035
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
3136
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
32-
33-
@EndpointTest
37+
import java.net.URI
38+
import java.net.http.HttpClient
39+
import java.net.http.HttpRequest
40+
import java.net.http.HttpResponse
41+
42+
@EndpointTest(
43+
properties = ["${BackendSpringProperty.BACKEND_CONFIG_PATH}=$S3_CONFIG" ],
44+
)
3445
class ApproveProcessedDataEndpointTest(
3546
@Autowired val client: SubmissionControllerClient,
3647
@Autowired val convenienceClient: SubmissionConvenienceClient,
48+
@Autowired val objectMapper: ObjectMapper,
3749
) {
3850

3951
@Test
@@ -404,4 +416,37 @@ class ApproveProcessedDataEndpointTest(
404416

405417
convenienceClient.getSequenceEntry(accessionVersions.first()).assertStatusIs(APPROVED_FOR_RELEASE)
406418
}
419+
420+
@Test
421+
fun `WHEN entries with extra files are approved THEN the files become public`() {
422+
convenienceClient.submitDefaultFiles(includeFileMapping = true)
423+
val unprocessedData = convenienceClient.extractUnprocessedData()
424+
// create submittable data with the file IDs included
425+
val submittableData: List<SubmittedProcessedData> = unprocessedData.map {
426+
SubmittedProcessedData(
427+
accession = it.accession,
428+
version = it.version,
429+
data = defaultProcessedData.copy(
430+
files = it.data.files!!.map {
431+
it.key to it.value.map { x -> x.toFileIdAndName() }
432+
}.toMap(),
433+
),
434+
)
435+
}
436+
convenienceClient.submitProcessedData(submittableData)
437+
client.approveProcessedSequenceEntries(ALL)
438+
val releasedData = convenienceClient.getReleasedData()
439+
440+
val tree = objectMapper.readTree(releasedData[0].metadata["fileField"]!!.asText())
441+
442+
val fileUrl = tree.get(0).get("url").asText()
443+
444+
val client = HttpClient.newHttpClient()
445+
val request = HttpRequest.newBuilder()
446+
.uri(URI.create(fileUrl))
447+
.build()
448+
449+
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
450+
assertThat(response.statusCode(), `is`(200))
451+
}
407452
}

backend/src/test/kotlin/org/loculus/backend/controller/submission/ExtractUnprocessedDataEndpointTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ import org.springframework.http.HttpHeaders.ETAG
4242
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
4343
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
4444
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
45+
import java.net.URI
46+
import java.net.http.HttpClient
47+
import java.net.http.HttpRequest
48+
import java.net.http.HttpResponse
4549

4650
@EndpointTest(
4751
properties = [
@@ -254,4 +258,24 @@ class ExtractUnprocessedDataEndpointTest(
254258
),
255259
)
256260
}
261+
262+
@Test
263+
fun `GIVEN returns file mapping THEN the URLs are valid`() {
264+
convenienceClient.submitDefaultFiles(includeFileMapping = true)
265+
266+
val result = client.extractUnprocessedData(
267+
numberOfSequenceEntries = DefaultFiles.NUMBER_OF_SEQUENCES,
268+
)
269+
val responseBody = result.expectNdjsonAndGetContent<UnprocessedData>()
270+
val url = responseBody.first().data.files!!["fileField"]!!.first().url
271+
272+
val client = HttpClient.newHttpClient()
273+
val request = HttpRequest.newBuilder()
274+
.uri(URI.create(url))
275+
.build()
276+
277+
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
278+
assertThat(response.statusCode(), `is`(200))
279+
assertThat(response.body(), `is`("Hello, world!"))
280+
}
257281
}

backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import org.loculus.backend.controller.ORGANISM_WITHOUT_CONSENSUS_SEQUENCES
3131
import org.loculus.backend.controller.OTHER_ORGANISM
3232
import org.loculus.backend.controller.expectNdjsonAndGetContent
3333
import org.loculus.backend.controller.files.FilesClient
34-
import org.loculus.backend.controller.files.andGetFileIds
34+
import org.loculus.backend.controller.files.andGetFileIdsAndUrls
3535
import org.loculus.backend.controller.generateJwtFor
3636
import org.loculus.backend.controller.getAccessionVersions
3737
import org.loculus.backend.controller.groupmanagement.GroupManagementControllerClient
@@ -43,6 +43,10 @@ import org.springframework.http.MediaType
4343
import org.springframework.test.web.servlet.ResultActions
4444
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
4545
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
46+
import java.net.URI
47+
import java.net.http.HttpClient
48+
import java.net.http.HttpRequest
49+
import java.net.http.HttpResponse
4650

4751
data class SubmissionResult(val submissionIdMappings: List<SubmissionIdMapping>, val groupId: Int)
4852

@@ -79,9 +83,28 @@ class SubmissionConvenienceClient(
7983

8084
var fileMapping: SubmissionIdFilesMap? = null
8185
if (includeFileMapping) {
82-
val fileId = filesClient.requestUploads(groupIdToSubmitFor, jwt = jwt).andGetFileIds()[0]
83-
fileMapping = DefaultFiles.submissionIds.associateWith {
84-
mapOf("fileField" to listOf(FileIdAndName(fileId, "foo.txt")))
86+
val fileIdsAndUrls = filesClient.requestUploads(
87+
groupIdToSubmitFor,
88+
DefaultFiles.submissionIds.size,
89+
jwt = jwt,
90+
).andGetFileIdsAndUrls()
91+
92+
fileMapping = mutableMapOf()
93+
94+
val client = HttpClient.newBuilder().build()
95+
val fileContent = "Hello, world!".toByteArray()
96+
97+
DefaultFiles.submissionIds.forEachIndexed { i, submissionId ->
98+
99+
val request = HttpRequest.newBuilder()
100+
.uri(URI.create(fileIdsAndUrls[i].url))
101+
.PUT(HttpRequest.BodyPublishers.ofByteArray(fileContent))
102+
.build()
103+
104+
client.send(request, HttpResponse.BodyHandlers.ofString())
105+
106+
fileMapping[submissionId] =
107+
mapOf("fileField" to listOf(FileIdAndName(fileIdsAndUrls[i].fileId, "hello.txt")))
85108
}
86109
}
87110

@@ -112,8 +135,15 @@ class SubmissionConvenienceClient(
112135
username: String = DEFAULT_USER_NAME,
113136
groupId: Int? = null,
114137
dataUseTerms: DataUseTerms = DataUseTerms.Open,
138+
includeFileMapping: Boolean = false,
115139
): List<AccessionVersionInterface> {
116-
submitDefaultFiles(organism = organism, username = username, groupId = groupId, dataUseTerms = dataUseTerms)
140+
submitDefaultFiles(
141+
organism = organism,
142+
username = username,
143+
groupId = groupId,
144+
dataUseTerms = dataUseTerms,
145+
includeFileMapping = includeFileMapping,
146+
)
117147
return extractUnprocessedData(organism = organism).getAccessionVersions()
118148
}
119149

0 commit comments

Comments
 (0)