From ac9f5b3362a72ca7770376541d813a4fc06b8906 Mon Sep 17 00:00:00 2001 From: neo1027144 Date: Wed, 1 Apr 2026 21:10:16 +0800 Subject: [PATCH] test: add unit tests for DeviceCodeOAuthFlow Test cases: - Construction with all fields populated - Null refreshUrl (optional field) - Empty scopes map with full field assertions - Null validation for required fields (deviceAuthorizationUrl, tokenUrl, scopes) - Record equality, hashCode consistency, and inequality edge cases - Scopes map immutability (defensive copy via Map.copyOf) Also fix: add Map.copyOf() defensive copy in DeviceCodeOAuthFlow compact constructor Fixes #607 --- .../java/io/a2a/spec/DeviceCodeOAuthFlow.java | 1 + .../io/a2a/spec/DeviceCodeOAuthFlowTest.java | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 spec/src/test/java/io/a2a/spec/DeviceCodeOAuthFlowTest.java diff --git a/spec/src/main/java/io/a2a/spec/DeviceCodeOAuthFlow.java b/spec/src/main/java/io/a2a/spec/DeviceCodeOAuthFlow.java index d2d25c047..22823352d 100644 --- a/spec/src/main/java/io/a2a/spec/DeviceCodeOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/DeviceCodeOAuthFlow.java @@ -33,5 +33,6 @@ public record DeviceCodeOAuthFlow(String deviceAuthorizationUrl, String tokenUrl Assert.checkNotNullParam("deviceAuthorizationUrl", deviceAuthorizationUrl); Assert.checkNotNullParam("tokenUrl", tokenUrl); Assert.checkNotNullParam("scopes", scopes); + scopes = Map.copyOf(scopes); } } diff --git a/spec/src/test/java/io/a2a/spec/DeviceCodeOAuthFlowTest.java b/spec/src/test/java/io/a2a/spec/DeviceCodeOAuthFlowTest.java new file mode 100644 index 000000000..bcb35e931 --- /dev/null +++ b/spec/src/test/java/io/a2a/spec/DeviceCodeOAuthFlowTest.java @@ -0,0 +1,107 @@ +package io.a2a.spec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link DeviceCodeOAuthFlow}. + *

+ * Tests cover construction with valid parameters, null validation of required + * fields, and handling of the optional {@code refreshUrl} field. + * + * @see DeviceCodeOAuthFlow + */ +class DeviceCodeOAuthFlowTest { + + private static final String DEVICE_AUTH_URL = "https://auth.example.com/device/code"; + private static final String TOKEN_URL = "https://auth.example.com/token"; + private static final String REFRESH_URL = "https://auth.example.com/refresh"; + private static final Map SCOPES = Map.of("read", "Read access", "write", "Write access"); + + @Test + void testConstruction_withAllFields() { + DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + + assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); + assertEquals(TOKEN_URL, flow.tokenUrl()); + assertEquals(REFRESH_URL, flow.refreshUrl()); + assertEquals(SCOPES, flow.scopes()); + } + + @Test + void testConstruction_withNullRefreshUrl() { + DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + + assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); + assertEquals(TOKEN_URL, flow.tokenUrl()); + assertNull(flow.refreshUrl()); + assertEquals(SCOPES, flow.scopes()); + } + + @Test + void testConstruction_withEmptyScopes() { + Map emptyScopes = Map.of(); + DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, emptyScopes); + + assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); + assertEquals(TOKEN_URL, flow.tokenUrl()); + assertNull(flow.refreshUrl()); + assertEquals(emptyScopes, flow.scopes()); + } + + @Test + void testConstruction_nullDeviceAuthorizationUrl_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> new DeviceCodeOAuthFlow(null, TOKEN_URL, REFRESH_URL, SCOPES)); + } + + @Test + void testConstruction_nullTokenUrl_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, null, REFRESH_URL, SCOPES)); + } + + @Test + void testConstruction_nullScopes_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, null)); + } + + @Test + void testEqualityAndHashCode() { + DeviceCodeOAuthFlow flow1 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + DeviceCodeOAuthFlow flow2 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + DeviceCodeOAuthFlow flow3 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + DeviceCodeOAuthFlow flow4 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + + // Test for equality and hashCode consistency + assertEquals(flow1, flow2); + assertEquals(flow1.hashCode(), flow2.hashCode()); + assertEquals(flow3, flow4); + assertEquals(flow3.hashCode(), flow4.hashCode()); + + // Test for inequality with different field values + assertNotEquals(flow1, flow3); + assertNotEquals(flow1, new DeviceCodeOAuthFlow("https://other.com", TOKEN_URL, REFRESH_URL, SCOPES)); + assertNotEquals(flow1, null); + assertNotEquals(flow1, "not a flow"); + } + + @Test + void testScopesImmutability() { + Map mutableScopes = new java.util.HashMap<>(); + mutableScopes.put("read", "Read access"); + DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, mutableScopes); + + // Modifying the original map should not affect the record + mutableScopes.put("write", "Write access"); + assertNotEquals(mutableScopes.size(), flow.scopes().size(), + "Record should be immutable and perform a defensive copy of the scopes map"); + } +}