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"); + } +}