Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9ba4a40
test localstack update
Ian-Nara Jan 9, 2026
1df6f92
[CI Pipeline] Released Snapshot version: 4.1.1-alpha-74-SNAPSHOT
Jan 9, 2026
94c0668
update image
Ian-Nara Jan 9, 2026
1a5ef7f
Merge branch 'ian-UID2-6154-optout-e2e-update-sqs' of github.com:IABT…
Ian-Nara Jan 9, 2026
23b9afe
change env var
Ian-Nara Jan 9, 2026
8d1e736
debug
Ian-Nara Jan 9, 2026
a09326d
debug
Ian-Nara Jan 9, 2026
0fb1a47
config update
Ian-Nara Jan 9, 2026
73a938c
test
Ian-Nara Jan 13, 2026
edc91b7
[CI Pipeline] Released Snapshot version: 4.1.2-alpha-75-SNAPSHOT
Jan 13, 2026
104d682
debugging
Ian-Nara Jan 13, 2026
42797ec
[CI Pipeline] Released Snapshot version: 4.1.3-alpha-76-SNAPSHOT
Jan 13, 2026
a614c3a
Merge branch 'ian-UID2-6154-optout-e2e-update-sqs' of github.com:IABT…
Ian-Nara Jan 13, 2026
82ef261
[CI Pipeline] Released Snapshot version: 4.1.4-alpha-77-SNAPSHOT
Jan 13, 2026
6010d70
debug
Ian-Nara Jan 13, 2026
44712e5
default value
Ian-Nara Jan 13, 2026
4396858
[CI Pipeline] Released Snapshot version: 4.1.5-alpha-79-SNAPSHOT
Jan 13, 2026
c4b0ef3
debug
Ian-Nara Jan 15, 2026
9d5e462
Merge branch 'ian-UID2-6154-optout-e2e-update-sqs' of github.com:IABT…
Ian-Nara Jan 15, 2026
d970559
debug
Ian-Nara Jan 15, 2026
ad2f7b7
debug
Ian-Nara Jan 15, 2026
a93d738
[CI Pipeline] Released Snapshot version: 4.1.6-alpha-80-SNAPSHOT
Jan 15, 2026
326e3bd
debug
Ian-Nara Jan 15, 2026
6f77411
debug
Ian-Nara Jan 15, 2026
9c68e77
debug
Ian-Nara Jan 15, 2026
0f1e3e3
debug
Ian-Nara Jan 15, 2026
32bb5bb
debug
Ian-Nara Jan 15, 2026
228e69f
debug
Ian-Nara Jan 15, 2026
0530c37
debug
Ian-Nara Jan 15, 2026
cc2f71e
debug
Ian-Nara Jan 15, 2026
1db0561
[CI Pipeline] Released Snapshot version: 4.1.7-alpha-81-SNAPSHOT
Jan 15, 2026
2f06380
debug
Ian-Nara Jan 15, 2026
3c49aaa
debug
Ian-Nara Jan 15, 2026
3965756
test
Ian-Nara Jan 15, 2026
83cea3e
test
Ian-Nara Jan 15, 2026
86ea7cc
test
Ian-Nara Jan 15, 2026
2ee87da
testtest
Ian-Nara Jan 15, 2026
fad0e21
disable
Ian-Nara Jan 15, 2026
7510951
[CI Pipeline] Released Snapshot version: 4.1.8-alpha-82-SNAPSHOT
Jan 15, 2026
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
3 changes: 3 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# List any vulnerability that are to be accepted
# See https://aquasecurity.github.io/trivy/v0.35/docs/vulnerability/examples/filter/
# for more details

#
CVE-2025-68973
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ENV E2E_PHONE_SUPPORT ""

ENV UID2_CORE_E2E_OPERATOR_API_KEY ""
ENV UID2_CORE_E2E_OPTOUT_API_KEY ""
ENV UID2_CORE_E2E_OPTOUT_INTERNAL_API_KEY "test-optout-internal-key"
ENV UID2_CORE_E2E_CORE_URL ""
ENV UID2_CORE_E2E_OPTOUT_URL ""

Expand Down
22 changes: 17 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: "3.8"
services:
localstack:
container_name: localstack
image: localstack/localstack:1.3.0
image: localstack/localstack:3.0.0
ports:
- "127.0.0.1:5001:5001"
volumes:
Expand All @@ -13,14 +13,20 @@ services:
- "./docker/uid2-optout/src/init-aws.sh:/etc/localstack/init/ready.d/init-aws-optout.sh"
- "./docker/uid2-optout/src/s3/optout:/s3/optout"
environment:
- EDGE_PORT=5001
- GATEWAY_LISTEN=0.0.0.0:5001
- KMS_PROVIDER=local-kms
- LOCALSTACK_HOST=localstack:5001
- SERVICES=s3,sqs,kms
- DEFAULT_REGION=us-east-1
- AWS_DEFAULT_REGION=us-east-1
- SQS_ENDPOINT_STRATEGY=path
healthcheck:
test: awslocal s3api wait bucket-exists --bucket test-core-bucket
&& awslocal s3api wait bucket-exists --bucket test-optout-bucket
&& awslocal sqs get-queue-url --queue-name optout-queue
interval: 5s
timeout: 5s
retries: 3
timeout: 10s
retries: 6
networks:
- e2e_default

Expand Down Expand Up @@ -49,17 +55,23 @@ services:
image: ghcr.io/iabtechlab/uid2-optout:latest
ports:
- "127.0.0.1:8081:8081"
- "127.0.0.1:8082:8082"
- "127.0.0.1:5090:5005"
volumes:
- ./docker/uid2-optout/conf/default-config.json:/app/conf/default-config.json
- ./docker/uid2-optout/conf/local-e2e-docker-config.json:/app/conf/local-config.json
- ./docker/uid2-optout/mount/:/opt/uid2/optout/
depends_on:
localstack:
condition: service_healthy
core:
condition: service_healthy
healthcheck:
test: wget --tries=1 --spider http://localhost:8081/ops/healthcheck || exit 1
test: wget --tries=1 --spider http://localhost:8081/ops/healthcheck
&& wget --tries=1 --spider http://localhost:8082/ops/healthcheck || exit 1
interval: 5s
timeout: 10s
retries: 12
networks:
- e2e_default

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.uid2</groupId>
<artifactId>uid2-e2e</artifactId>
<version>4.1.0</version>
<version>4.1.8-alpha-82-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
Expand Down
110 changes: 110 additions & 0 deletions src/test/java/app/component/Optout.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package app.component;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.uid2.shared.util.Mapper;
import common.Const;
import common.EnvUtil;
import common.HttpClient;

/**
* Component for interacting with the UID2 Optout service.
*/
public class Optout extends App {
private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance();

// The SQS delta producer runs on port 8082 (8081 + 1)
private static final int DELTA_PRODUCER_PORT_OFFSET = 1;

// Loaded lazily to avoid crashing when env var is missing
private String optoutInternalApiKey;

public Optout(String host, Integer port, String name) {
super(host, port, name);
// Load API key lazily - only fail when actually used
this.optoutInternalApiKey = EnvUtil.getEnv(Const.Config.Core.OPTOUT_INTERNAL_API_KEY, false);
}

public Optout(String host, String name) {
super(host, null, name);
this.optoutInternalApiKey = EnvUtil.getEnv(Const.Config.Core.OPTOUT_INTERNAL_API_KEY, false);
}

private String getOptoutInternalApiKey() {
if (optoutInternalApiKey == null || optoutInternalApiKey.isEmpty()) {
throw new IllegalStateException("Missing environment variable: " + Const.Config.Core.OPTOUT_INTERNAL_API_KEY);
}
return optoutInternalApiKey;
}

/**
* Triggers delta production on the optout service.
* This reads from the SQS queue and produces delta files.
* The endpoint is on port 8082 (optout port + 1).
*
* @return JsonNode with response, or null if job already running (409)
*/
public JsonNode triggerDeltaProduce() throws Exception {
String deltaProduceUrl = getDeltaProducerBaseUrl() + "/optout/deltaproduce";
try {
String response = HttpClient.post(deltaProduceUrl, "", getOptoutInternalApiKey());
return OBJECT_MAPPER.readTree(response);
} catch (HttpClient.HttpException e) {
if (e.getCode() == 409) {
// Job already running - this is fine, we'll just wait for it
return null;
}
throw e;
}
}

/**
* Gets the status of the current delta production job.
*/
public JsonNode getDeltaProduceStatus() throws Exception {
String statusUrl = getDeltaProducerBaseUrl() + "/optout/deltaproduce/status";
String response = HttpClient.get(statusUrl, getOptoutInternalApiKey());
return OBJECT_MAPPER.readTree(response);
}

/**
* Triggers delta production and waits for it to complete.
* If a job is already running, waits for that job instead.
* @param maxWaitSeconds Maximum time to wait for completion
* @return true if delta production completed successfully
*/
public boolean triggerDeltaProduceAndWait(int maxWaitSeconds) throws Exception {
// Try to trigger - will return null if job already running (409)
triggerDeltaProduce();

long startTime = System.currentTimeMillis();
long maxWaitMs = maxWaitSeconds * 1000L;

while (System.currentTimeMillis() - startTime < maxWaitMs) {
Thread.sleep(2000); // Poll every 2 seconds

JsonNode status = getDeltaProduceStatus();
String state = status.path("state").asText();

if ("completed".equalsIgnoreCase(state) || "failed".equalsIgnoreCase(state)) {
return "completed".equalsIgnoreCase(state);
}

// If idle (no job), try to trigger again
if ("idle".equalsIgnoreCase(state) || "none".equalsIgnoreCase(state) || state.isEmpty()) {
triggerDeltaProduce();
}
}

return false; // Timed out
}

private String getDeltaProducerBaseUrl() {
// Delta producer runs on optout port + 1
if (getPort() != null) {
return "http://" + getHost() + ":" + (getPort() + DELTA_PRODUCER_PORT_OFFSET);
}
// If port not specified, assume default optout port (8081) + 1
return "http://" + getHost() + ":8082";
}
}
1 change: 1 addition & 0 deletions src/test/java/common/Const.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static final class Config {
public static final class Core {
public static final String OPERATOR_API_KEY = "UID2_CORE_E2E_OPERATOR_API_KEY";
public static final String OPTOUT_API_KEY = "UID2_CORE_E2E_OPTOUT_API_KEY";
public static final String OPTOUT_INTERNAL_API_KEY = "UID2_CORE_E2E_OPTOUT_INTERNAL_API_KEY";
public static final String CORE_URL = "UID2_CORE_E2E_CORE_URL";
public static final String OPTOUT_URL = "UID2_CORE_E2E_OPTOUT_URL";
}
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/suite/core/CoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.uid2.shared.attest.JwtService;
import com.uid2.shared.attest.JwtValidationResponse;
import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
Expand All @@ -29,6 +30,24 @@ public void testAttest_EmptyAttestationRequest(Core core) {
assertEquals("Unsuccessful POST request - URL: " + coreUrl + "/attest - Code: 400 Bad Request - Response body: {\"status\":\"no attestation_request attached\"}", exception.getMessage());
}

/**
* Tests valid attestation request with JWT signing.
*
* DISABLED: This test requires KMS RSA signing which doesn't work properly on LocalStack 3.x.
*
* To fix this test for LocalStack 4.x+:
* 1. Upgrade LocalStack to 4.x (KMS_PROVIDER=local-kms was removed in 3.x)
* 2. Create KMS key dynamically via AWS CLI in init-aws.sh:
* awslocal kms create-key --key-usage SIGN_VERIFY --key-spec RSA_2048
* awslocal kms create-alias --alias-name alias/jwt-signing-key --target-key-id $KEY_ID
* 3. Update uid2-core to use the alias: aws_kms_jwt_signing_key_id: "alias/jwt-signing-key"
* 4. Modify uid2-core to fetch public key from KMS using GetPublicKey API instead of
* using hardcoded aws_kms_jwt_signing_public_keys config
* 5. Update this test to fetch the public key dynamically from KMS for JWT validation
*
* See: https://docs.localstack.cloud/aws/services/kms/
*/
@Disabled("LocalStack 3.x KMS doesn't support RSA signing - see Javadoc for fix instructions")
@ParameterizedTest(name = "/attest - {0}")
@MethodSource({
"suite.core.TestData#baseArgs"
Expand Down
32 changes: 30 additions & 2 deletions src/test/java/suite/optout/OptoutTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package suite.optout;

import app.component.Operator;
import app.component.Optout;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.uid2.client.IdentityTokens;
Expand All @@ -9,6 +10,8 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.util.HashSet;
Expand All @@ -23,19 +26,23 @@
@SuppressWarnings("unused")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OptoutTest {
// TODO: Test failure case
private static final Logger LOGGER = LoggerFactory.getLogger(OptoutTest.class);

private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance();
private static final int OPTOUT_DELAY_MS = 1000;
private static final int OPTOUT_WAIT_SECONDS = 300;
private static final int DELTA_PRODUCE_WAIT_SECONDS = 120;

private static Set<Arguments> outputArgs;
private static Set<Arguments> outputAdvertisingIdArgs;
private static Optout optoutService;

@BeforeAll
public static void setupAll() {
outputArgs = new HashSet<>();
outputAdvertisingIdArgs = new HashSet<>();
// Initialize optout service component for delta production
optoutService = new Optout("optout", 8081, "Optout Service");
}

@ParameterizedTest(name = "/v2/token/logout with /v2/token/generate - {0} - {2}")
Expand Down Expand Up @@ -78,7 +85,28 @@ public void testV2LogoutWithV2IdentityMap(String label, Operator operator, Strin
outputAdvertisingIdArgs.add(Arguments.of(label, operator, operatorName, rawUID, toOptOut, beforeOptOutTimestamp));
}

/**
* Triggers delta production on the optout service after all logout requests.
* This reads the opt-out requests from SQS and produces delta files that
* the operator will sync to reflect the opt-outs.
*/
@Test
@Order(4)
public void triggerDeltaProduction() throws Exception {
LOGGER.info("Triggering delta production on optout service");

// Trigger delta production and wait for completion
// This handles 409 (job already running) gracefully
boolean success = optoutService.triggerDeltaProduceAndWait(DELTA_PRODUCE_WAIT_SECONDS);

// Get final status
JsonNode status = optoutService.getDeltaProduceStatus();
LOGGER.info("Delta production completed with status: {}", status);

assertThat(success).as("Delta production should complete successfully").isTrue();
}

@Order(5)
@ParameterizedTest(name = "/v2/token/refresh after {2} generate and {3} logout - {0} - {1}")
@MethodSource({
"afterOptoutTokenArgs"
Expand All @@ -89,7 +117,7 @@ public void testV2TokenRefreshAfterOptOut(String label, Operator operator, Strin
with().pollInterval(5, TimeUnit.SECONDS).await("Get V2 Token Response").atMost(OPTOUT_WAIT_SECONDS, TimeUnit.SECONDS).until(() -> operator.v2TokenRefresh(refreshToken, refreshResponseKey).equals(OBJECT_MAPPER.readTree("{\"status\":\"optout\"}")));
}

@Order(5)
@Order(6)
@ParameterizedTest(name = "/v2/optout/status after v2/identity/map and v2/token/logout - DII {0} - expecting {4} - {2}")
@MethodSource({"afterOptoutAdvertisingIdArgs"})
public void testV2OptOutStatus(String label, Operator operator, String operatorName, String rawUID,
Expand Down