Skip to content

Commit

Permalink
#32591 use reverse proxy in tests to get rid of LocalhostHttpClient
Browse files Browse the repository at this point in the history
  • Loading branch information
Lesrac committed Nov 4, 2024
1 parent 2616f23 commit 784a9a7
Show file tree
Hide file tree
Showing 17 changed files with 184 additions and 210 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ Following environment variables need to be set:
* cdrClient.localFolder=~/Documents/cdr/inflight
* cdrClient.targetFolder=~/Documents/cdr/target
* cdrClient.sourceFolder=~/Documents/cdr/source
* CDR_B2C_TENANT_ID=some-tenant-id
* CDR_CLIENT_ID=my-own-client-id
* CDR_CLIENT_SECRET=my-own-client-secret
* CDR_B2C_TENANT_ID=some-cdr-azure-ad-tenant
* CDR_CLIENT_ID=oauth2-client-id
* CDR_CLIENT_SECRET=oauth2-client-secret

## Application Plugin
To create scripts to run the application locally one needs to run following gradle cmd: ```gradlew installDist```
Expand Down
42 changes: 42 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.springframework.boot.gradle.tasks.bundling.BootJar
import java.net.URI
import java.time.Duration

group = "com.swisscom.health.des.cdr"
version = "3.1.3-SNAPSHOT"
Expand All @@ -23,6 +24,7 @@ val jvmVersion: String by project
val outputDir: Provider<Directory> = layout.buildDirectory.dir(".")

plugins {
id("com.avast.gradle.docker-compose") version "0.17.8"
id("org.springframework.boot")
id("io.spring.dependency-management")
id("io.gitlab.arturbosch.detekt")
Expand Down Expand Up @@ -115,6 +117,12 @@ kotlin {
}
}

tasks.test {
useJUnitPlatform {
excludeTags(Constants.INTEGRATION_TEST_TAG)
}
}

val jacocoTestCoverageVerification = tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
violationRules {
/**
Expand Down Expand Up @@ -153,6 +161,11 @@ tasks.withType<Test> {
includeEngines("junit-jupiter")
}
finalizedBy(jacocoTestReport)

jvmArgs(
// tests_hosts is used to redirect msal4j, which insists on talking to the Mothership, to our docker compose setup
"-Djdk.net.hosts.file=${layout.projectDirectory.file("src/test/resources/test_hosts").asFile.absolutePath}"
)
}

jacoco {
Expand Down Expand Up @@ -227,3 +240,32 @@ publishing {
}
}
}

/***********************
* Integration Testing *
***********************/
object Constants {
const val TASK_GROUP_VERIFICATION = "verification"
const val INTEGRATION_TEST_TAG = "integration-test"
}

tasks.register<Test>("integrationTest") {
group = Constants.TASK_GROUP_VERIFICATION
useJUnitPlatform {
includeTags(Constants.INTEGRATION_TEST_TAG)
}
shouldRunAfter(tasks.test)
// Ensure latest images get pulled
dependsOn(tasks.composePull)
}

dockerCompose {
dockerComposeWorkingDirectory.set(File("${rootProject.projectDir}/docker-compose"))
dockerComposeStopTimeout.set(Duration.ofSeconds(5)) // time before docker-compose sends SIGTERM to the running containers after the composeDown task has been started
ignorePullFailure.set(true)
isRequiredBy(tasks.getByName("integrationTest"))
}

/***************************
* END Integration Testing *
***************************/
12 changes: 12 additions & 0 deletions docker-compose/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
http_port 8080
https_port 8443
}
localhost {
reverse_proxy /isalive mock-oauth2-server:8080
}
login.microsoftonline.com {
tls internal
reverse_proxy /common/discovery/* wiremock:8080
reverse_proxy /test-tenant-id/* mock-oauth2-server:8080
}
3 changes: 3 additions & 0 deletions docker-compose/caddy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM caddy:2.8.4
RUN apk update && apk upgrade && apk add curl
COPY ./Caddyfile /etc/caddy/Caddyfile
40 changes: 33 additions & 7 deletions docker-compose/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,35 +1,61 @@
services:
wiremock:
image: local.wiremock.cdrapi
container_name: wiremock-cdr-client
build: ./wiremock
mem_limit: 256m
healthcheck:
# use a mapped URL for health checking; the response is a 200 OK with a string body "OK"
test: curl --fail http://localhost:8080/health || exit 1
interval: 5s
retries: 5
start_period: 5s
timeout: 10s
ports:
- "9090:8080"
- "8443:8443"
- "9090:8080"
environment:
- TZ=Europe/Zurich
command: ["--verbose", "--https-port", "8443", "--global-response-templating"]
networks:
- cdr_client_net

mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:2.1.9
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
container_name: mock-oauth2-server-cdr-client
environment:
- TZ=Europe/Zurich
- LOG_LEVEL=DEBUG
- JSON_CONFIG_PATH=/app/config.json
- SERVER_PORT=8443
ports:
- "8444:8443"
- SERVER_PORT=8080
volumes:
- ./mockOAuth2Server/config.json:/app/config.json
- ./mockOAuth2Server/mockoauth2server.p12:/app/mockoauth2server.p12
networks:
- cdr_client_net

caddy:
depends_on:
wiremock:
condition: service_healthy
image: local.caddy.cdrappmgr
container_name: caddy-app-mgr
build: ./caddy
mem_limit: 256m
ports:
- "8443:8443"
healthcheck:
# checks the mock-oauth2-server's health endpoint to prove that both caddy and the mock-oauth2-server are up and running
test: curl --insecure --fail https://localhost:8443/isalive || exit 1
interval: 5s
retries: 5
start_period: 120s
timeout: 10s
networks:
- cdr_client_net

networks:
cdr_client_net:
name: cdr_client_net
ipam:
config:
- subnet: 10.112.0.0/16
- subnet: 10.113.0.0/16
18 changes: 5 additions & 13 deletions docker-compose/mockOAuth2Server/config.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
{
"interactiveLogin": false,
"httpServer": {
"type": "NettyWrapper",
"ssl": {
"keyPassword": "changeit",
"keystoreFile": "/app/mockoauth2server.p12",
"keystoreType": "PKCS12",
"keystorePassword": "changeit"
}
"type": "NettyWrapper"
},
"tokenProvider" : {
"keyProvider" : {
Expand All @@ -16,19 +10,17 @@
},
"tokenCallbacks": [
{
"issuerId": "mock-issuer",
"issuerId": "test-tenant-id/oauth2/v2.0",
"tokenExpiry":360,
"requestMappings": [
{
"requestParam": "scope",
"requestParam": "client_id",
"match": "*",
"claims": {
"sub": "${clientId}",
"aud": [
"CDR.ReadWrite"
],
"roles": [
"CDR.ReadWrite.OwnedBy"
"Application.ReadWrite.OwnedBy",
"AppRoleAssignment.ReadWrite.All"
]
}
}
Expand Down
4 changes: 3 additions & 1 deletion docker-compose/wiremock/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
FROM wiremock/wiremock:3.6.0-alpine
FROM wiremock/wiremock:3.9.2-1-alpine

RUN apk update && apk upgrade && apk add curl

COPY ./mappings/* /home/wiremock/mappings/
4 changes: 2 additions & 2 deletions docker-compose/wiremock/mappings/discovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"response": {
"status": 200,
"headers": {
"Content-Type": "*/*",
"Content-Type": "application/json",
"client-request-id": "{{request.headers.client-request-id}}"
},
"body": "{\"tenant_discovery_endpoint\":\"https://localhost:8444/common/.well-known/openid-configuration\",\"api-version\":\"1.1\",\"metadata\":[{\"preferred_network\":\"localhost:8444\",\"preferred_cache\":\"login.windows.net\",\"aliases\":[\"localhost:8444\"]}]}"
"body": "{\"tenant_discovery_endpoint\":\"https://login.microsoftonline.com:8443/common/.well-known/openid-configuration\",\"api-version\":\"1.1\",\"metadata\":[{\"preferred_network\":\"login.microsoftonline.com\",\"preferred_cache\":\"login.microsoftonline.com\",\"aliases\":[\"login.microsoftonline.com\"]}]}"
}
}
13 changes: 13 additions & 0 deletions docker-compose/wiremock/mappings/health.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"request": {
"urlPattern": "/health",
"method": "GET"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "text/plain"
},
"body": "OK"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.microsoft.aad.msal4j.ClientCredentialFactory
import com.microsoft.aad.msal4j.ClientCredentialParameters
import com.microsoft.aad.msal4j.ConfidentialClientApplication
import com.microsoft.aad.msal4j.IConfidentialClientApplication
import com.swisscom.health.des.cdr.clientvm.msal4j.LocalhostHttpClient
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
Expand All @@ -17,7 +16,6 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.retry.support.RetryTemplate
import java.io.IOException
import java.nio.file.Path
Expand Down Expand Up @@ -89,17 +87,6 @@ class CdrClientContext {
}

@Bean
@Profile("dev", "test")
fun localhostConfidentialClientApp(config: CdrClientConfig): IConfidentialClientApplication =
ConfidentialClientApplication.builder(
config.idpCredentials.clientId,
ClientCredentialFactory.createFromSecret(config.idpCredentials.clientSecret)
).authority(config.idpEndpoint.toString())
.httpClient(LocalhostHttpClient())
.build()

@Bean
@Profile("!dev & !test")
fun confidentialClientApp(config: CdrClientConfig): IConfidentialClientApplication =
ConfidentialClientApplication.builder(
config.idpCredentials.clientId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,20 @@ abstract class FileHandlingBase(
protected fun buildBaseHeaders(connectorId: String, mode: CdrClientConfig.Mode): Headers {
val traceId = tracer.currentSpan()?.context()?.traceId() ?: ""
// TODO: Remove this check once the token is required
val accessToken = if(cdrClientConfig.idpCredentials.tenantId != DEFAULT_TENANT_ID) {
getAccessToken().getOrNull()
val accessToken = if (cdrClientConfig.idpCredentials.tenantId != NO_TOKEN_TENANT_ID) {
getAccessToken()
} else {
null
}
if(accessToken != null) {
return Headers.Builder().run {
this[CONNECTOR_ID_HEADER] = connectorId
this[FUNCTION_KEY_HEADER] = cdrClientConfig.functionKey
this[CDR_PROCESSING_MODE_HEADER] = mode.value
this[AZURE_TRACE_ID_HEADER] = traceId
return Headers.Builder().run {
this[CONNECTOR_ID_HEADER] = connectorId
this[FUNCTION_KEY_HEADER] = cdrClientConfig.functionKey
this[CDR_PROCESSING_MODE_HEADER] = mode.value
this[AZURE_TRACE_ID_HEADER] = traceId
if (accessToken != null) {
this[HttpHeaders.AUTHORIZATION] = "Bearer $accessToken"
this.build()
}
} else {
return Headers.Builder().run {
this[CONNECTOR_ID_HEADER] = connectorId
this[FUNCTION_KEY_HEADER] = cdrClientConfig.functionKey
this[CDR_PROCESSING_MODE_HEADER] = mode.value
this[AZURE_TRACE_ID_HEADER] = traceId
this.build()
}
this.build()
}
}

Expand All @@ -113,18 +105,22 @@ abstract class FileHandlingBase(
}
}

private fun getAccessToken(): Result<String> = runCatching {
private fun getAccessToken(): String = runCatching {
retryIoErrorsThrice.execute<String, Exception> { _ ->
val authResult: IAuthenticationResult = securedApp.acquireToken(clientCredentialParams).get()
logger.debug { "Token taken from ${authResult.metadata().tokenSource()}" }
authResult.accessToken()
}
}.onFailure { e ->
logger.error(e) { "Failed to get access token: ${e.message}" }
}
}.fold(
onSuccess = { token: String -> token },
onFailure = { e ->
logger.error(e) { "Failed to get access token: ${e.message}" }
""
}
)

companion object {
private const val DEFAULT_TENANT_ID = "\$CDR_B2C_TENANT_ID"
private const val NO_TOKEN_TENANT_ID = "no-token"
}

}
Loading

0 comments on commit 784a9a7

Please sign in to comment.