Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ internal abstract class ReposiliteRunner {
.mapValues { (_, repositoryConfiguration) ->
repositoryConfiguration.copy(
redeployment = true,
storageProvider = _storageProvider!!,
storageProvider = useTargetStorageSettings(),
)
}
.toMutableMap()
Expand All @@ -140,6 +140,11 @@ internal abstract class ReposiliteRunner {
return reposilite.launch()
}

protected inline fun <reified T : StorageProviderSettings> useTargetStorageSettings(): T =
checkNotNull(_storageProvider) {
"Missing storage configuration for test target"
} as T

protected open fun overrideParameters(parameters: ReposiliteParameters) { }

protected open fun overrideLocalConfiguration(localConfiguration: LocalConfiguration) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ package com.reposilite.maven

import com.reposilite.RecommendedLocalSpecificationJunitExtension
import com.reposilite.RecommendedRemoteSpecificationJunitExtension
import com.reposilite.ReposiliteObjectMapper.DEFAULT_OBJECT_MAPPER
import com.reposilite.configuration.local.LocalConfiguration
import com.reposilite.configuration.shared.SharedConfigurationFacade
import com.reposilite.maven.application.MavenSettings
import com.reposilite.maven.application.RepositorySettings
import com.reposilite.maven.specification.MavenIntegrationSpecification
import com.reposilite.shared.ErrorResponse
import com.reposilite.shared.extensions.maxAge
import com.reposilite.storage.api.DocumentInfo
import com.reposilite.storage.s3.S3StorageProviderSettings
import io.javalin.http.HttpStatus.NOT_FOUND
import io.javalin.http.HttpStatus.UNAUTHORIZED
import java.util.concurrent.CompletableFuture
Expand All @@ -43,10 +48,95 @@ import org.junit.jupiter.api.extension.ExtendWith
internal class LocalMavenIntegrationTest : MavenIntegrationTest()

@ExtendWith(RecommendedRemoteSpecificationJunitExtension::class)
internal class RemoteMavenIntegrationTest : MavenIntegrationTest()
internal class RemoteMavenIntegrationTest : MavenIntegrationTest() {

@Test
fun `should preserve invalid repository settings and recover after correction`() {
// given: a configuration containing an invalid repository name
val storage = useTargetStorageSettings<S3StorageProviderSettings>()
val configuration = MavenSettings(repositories = listOf(
RepositorySettings(id = " invalid", storageProvider = storage),
RepositorySettings(id = "second", storageProvider = storage),
))
val (name, secret) = useDefaultManagementToken()
val facade = useFacade<SharedConfigurationFacade>()

// when: the invalid configuration is saved through the settings API
val update = put("$base/api/settings/domain/maven").basicAuth(name, secret).body(configuration).asEmpty()
val saved = get("$base/api/settings/domain/maven").basicAuth(name, secret).asObject(MavenSettings::class.java)
val persisted = DEFAULT_OBJECT_MAPPER.readTree(facade.fetchConfiguration()).get("maven")

// then: the full configuration is retained and only the valid repository loads
assertThat(update.isSuccess).isTrue()
assertThat(saved.body).isEqualTo(configuration)
assertThat(DEFAULT_OBJECT_MAPPER.treeToValue(persisted, MavenSettings::class.java)).isEqualTo(configuration)
assertThat(mavenFacade.getRepositories().map { it.name }).containsExactly("second")

// when: the invalid name is corrected
val corrected = configuration.copy(repositories = configuration.repositories.map {
if (it.id == " invalid") it.copy(id = "first") else it
})
val correction = put("$base/api/settings/domain/maven").basicAuth(name, secret).body(corrected).asEmpty()

// then: repositories become available without restarting the instance
assertThat(correction.isSuccess).isTrue()
assertThat(mavenFacade.getRepositories().map { it.name }).containsExactlyInAnyOrder("first", "second")
}

}

internal abstract class MavenIntegrationTest : MavenIntegrationSpecification() {

@Test
fun `should preserve stored files across repository reloads`() {
// given: an artifact and the current storage provider
val (repository, gav, file, content) = useDocument("releases", "gav", "artifact.jar", "content", true)
val previous = mavenFacade.getRepository(repository)!!.storageProvider
val settings = useFacade<SharedConfigurationFacade>().getDomainSettings<MavenSettings>()

// when: repository settings are reloaded twice
repeat(2) { settings.update { it.copy() } }
val response = get("$base/$repository/$gav/$file").asString()

// then: replacement storage providers can reuse the same storage and read its files
assertThat(mavenFacade.getRepository(repository)!!.storageProvider).isNotSameAs(previous)
assertThat(response.isSuccess).isTrue
assertThat(response.body).isEqualTo(content)
}

@Test
fun `should skip invalid repository names when reloading settings`() {
// given: invalid and valid repository definitions in the same configuration
val settings = useFacade<SharedConfigurationFacade>().getDomainSettings<MavenSettings>()
val configuration = settings.get().copy(
repositories = settings.get().repositories + listOf(
RepositorySettings(id = " invalid", storageProvider = useTargetStorageSettings()),
RepositorySettings(id = "valid-after-invalid", storageProvider = useTargetStorageSettings()),
),
)

// when: the configuration is reloaded
settings.update { configuration }

// then: only the invalid repository is skipped
assertThat(mavenFacade.getRepository(" invalid")).isNull()
assertThat(mavenFacade.getRepository("valid-after-invalid")).isNotNull()
assertThat(mavenFacade.getRepository("releases")).isNotNull()
}

@Test
fun `should browse repository root`() {
// given: an artifact in a repository directory
useDocument("releases", "gav", "artifact.jar", "content", true)

// when: the repository root is requested
val response = get("$base/releases").asString()

// then: the index contains the artifact directory
assertThat(response.isSuccess).isTrue
assertThat(response.body).contains("gav")
}

@Test
fun `should support head requests`() {
// given: the details about an existing in repository file
Expand Down Expand Up @@ -168,4 +258,4 @@ internal abstract class MavenIntegrationTest : MavenIntegrationSpecification() {
assertThat(response.body).contains("Reposilite - 404 Not Found")
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ import com.reposilite.RecommendedRemoteSpecificationJunitExtension
import com.reposilite.maven.api.LookupRequest
import com.reposilite.maven.specification.MavenIntegrationSpecification
import com.reposilite.storage.api.toLocation
import io.javalin.Javalin
import java.util.concurrent.Callable
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
Expand All @@ -44,9 +42,9 @@ internal class RemoteMavenMirrorsIntegrationTest : MavenMirrorsIntegrationTest()
internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecification() {

@Test
fun `should proxy remote file`() = runBlocking {
fun `should proxy remote file`() {
// given: a remote server and artifact
useProxiedHost("releases", "com/reposilite/remote.jar", "content") { gav, content ->
useProxiedHost("com/reposilite/remote.jar", "content") { gav, content ->
// when: non-existing file is requested
val notFoundResponse = get("$base/proxied/not/found.jar").asString()

Expand All @@ -63,9 +61,9 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
}

@Test
fun `should not proxy file with forbidden extension`() = runBlocking {
fun `should not proxy file with forbidden extension`() {
// given: a remote server and artifact
useProxiedHost("releases", "com/reposilite/remote.file", "content") { gav, _ ->
useProxiedHost("com/reposilite/remote.file", "content") { gav, _ ->
// when: file that exists in remote repository is requested
val response = get("$base/proxied/$gav").asString()
// then: service responds with 404 status page as .file extension is not allowed
Expand All @@ -77,14 +75,10 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
fun `should not respond with 200 and empty body when upstream HEAD succeeds but GET fails`() {
// given: an upstream where HEAD reports the artifact exists but GET fails (network blip / 5xx mid-fetch)
val gav = "com/reposilite/broken.jar"
val started = CountDownLatch(1)
val upstream = Javalin.start { config ->
config.jetty.port = reposilite.parameters.port + 1
config.events.serverStarted { started.countDown() }
val upstream = startProxiedHost { config ->
config.routes.head("/releases/$gav") { ctx -> ctx.contentType("application/java-archive").header("Content-Length", "100").status(200) }
config.routes.get("/releases/$gav") { ctx -> ctx.status(500).result("upstream temporarily unavailable") }
}
assertThat(started.await(10, TimeUnit.SECONDS)).isTrue

try {
// when: client requests the artifact through the proxy
Expand All @@ -103,10 +97,7 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
val gav = "com/reposilite/concurrent.jar"
val content = "concurrent-content"
val upstreamHits = AtomicInteger(0)
val started = CountDownLatch(1)
val upstream = Javalin.start { config ->
config.jetty.port = reposilite.parameters.port + 1
config.events.serverStarted { started.countDown() }
val upstream = startProxiedHost { config ->
config.routes.head("/releases/$gav") { ctx ->
ctx.contentType("application/java-archive").header("Content-Length", content.length.toString()).status(200)
}
Expand All @@ -116,7 +107,6 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
ctx.result(content)
}
}
assertThat(started.await(10, TimeUnit.SECONDS)).isTrue

try {
// when: five clients request the same artifact in parallel through a storing mirror
Expand All @@ -143,7 +133,7 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
@Test
fun `should prioritize upstream metadata file over local copy`() = runBlocking {
// given: a remote server and artifact
useProxiedHost("releases", "com/reposilite/maven-metadata.xml", "upstream") { gav, _ ->
useProxiedHost("com/reposilite/maven-metadata.xml", "upstream") { gav, _ ->
// and: local repository with cached metadata file
useDocument("proxied", "com/reposilite", "maven-metadata.xml", "local", true)

Expand All @@ -167,4 +157,4 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,56 @@
package com.reposilite.maven.specification

import com.reposilite.ReposiliteSpecification
import com.reposilite.configuration.shared.SharedConfigurationFacade
import com.reposilite.maven.application.MavenSettings
import io.javalin.Javalin
import kotlinx.coroutines.Job
import io.javalin.config.JavalinConfig

internal abstract class MavenIntegrationSpecification : ReposiliteSpecification() {

protected suspend fun useProxiedHost(
repository: String,
protected fun useProxiedHost(
gav: String,
content: String,
block: (String, String) -> Unit
) {
val serverStartedJob = Job()
val application = startProxiedHost { config ->
config.routes.head("/releases/$gav") { ctx -> ctx.result(content) }
config.routes.get("/releases/$gav") { ctx -> ctx.result(content) }
}

val application = Javalin.create { config ->
config.events.serverStarted { serverStartedJob.complete() }
config.routes.head("/$repository/$gav") { ctx -> ctx.result(content) }
config.routes.get("/$repository/$gav") { ctx -> ctx.result(content) }
}.start(reposilite.parameters.port + 1)
try {
block(gav, content)
} finally {
application.stop()
}
}

protected fun startProxiedHost(configure: (JavalinConfig) -> Unit): Javalin {
val application = Javalin.create { config -> configure(config) }.start(0)

try {
val reference = "http://localhost:${application.port()}/releases"
useFacade<SharedConfigurationFacade>()
.getDomainSettings<MavenSettings>()
.update { settings ->
settings.copy(
repositories = settings.repositories.map { repository ->
if (repository.id == "proxied" || repository.id == "proxied-stored") {
repository.copy(
proxied = repository.proxied.map { mirror -> mirror.copy(reference = reference) }
)
} else {
repository
}
}
)
}
} catch (exception: Exception) {
application.stop()
throw exception
}

serverStartedJob.join()
block(gav, content)
application.stop()
return application
}

}
}
Loading
Loading