Skip to content

Commit c7afb80

Browse files
committed
test(profile): define credential isolation contracts
1 parent adcf9c1 commit c7afb80

1 file changed

Lines changed: 146 additions & 7 deletions

File tree

Lines changed: 146 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,173 @@
11
package io.github.easy4j.hermes.security;
22

3-
import io.github.easy4j.hermes.HermesCliConfig;
43
import io.github.easy4j.hermes.HermesClient;
4+
import io.github.easy4j.hermes.HermesClientConfig;
5+
import io.github.easy4j.hermes.HermesCliConfig;
56
import io.github.easy4j.hermes.HermesHttpClientConfig;
7+
import io.github.easy4j.hermes.api.model.ChatRequest;
8+
import okhttp3.mockwebserver.MockResponse;
69
import okhttp3.mockwebserver.MockWebServer;
10+
import okhttp3.mockwebserver.RecordedRequest;
711
import org.junit.jupiter.api.Test;
812

13+
import java.util.Collections;
14+
import java.util.concurrent.TimeUnit;
15+
import java.util.concurrent.atomic.AtomicReference;
16+
917
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertFalse;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
1020
import static org.junit.jupiter.api.Assertions.assertThrows;
1121

1222
class ProfileAuthenticationContractTest {
1323

1424
@Test
15-
void missingNamedProfileCredentialDoesNotBorrowRootApiKey() throws Exception {
25+
void independentProfilesUseTheirOwnCredentials() throws Exception {
26+
try (MockWebServer server = new MockWebServer()) {
27+
server.enqueue(okHealth());
28+
server.enqueue(okHealth());
29+
server.start();
30+
31+
try (HermesClient root = new HermesClient(http(server), disabledCli())) {
32+
ProfileBinding teamA = ProfileBinding.of(
33+
"team-a", "credential-a",
34+
identity -> CredentialSnapshot.of("token-a", "generation-a"));
35+
ProfileBinding teamB = ProfileBinding.of(
36+
"team-b", "credential-b",
37+
identity -> CredentialSnapshot.of("token-b", "generation-b"));
38+
39+
root.forProfile(teamA).health();
40+
root.forProfile(teamB).health();
41+
42+
RecordedRequest first = server.takeRequest(3, TimeUnit.SECONDS);
43+
RecordedRequest second = server.takeRequest(3, TimeUnit.SECONDS);
44+
assertNotNull(first);
45+
assertNotNull(second);
46+
assertEquals("/p/team-a/health", first.getPath());
47+
assertEquals("Bearer token-a", first.getHeader("Authorization"));
48+
assertEquals("/p/team-b/health", second.getPath());
49+
assertEquals("Bearer token-b", second.getHeader("Authorization"));
50+
}
51+
}
52+
}
53+
54+
@Test
55+
void configuredResolverProvidesNamedProfileCredentials() throws Exception {
1656
try (MockWebServer server = new MockWebServer()) {
57+
server.enqueue(okHealth());
1758
server.start();
1859

19-
HermesHttpClientConfig http = new HermesHttpClientConfig()
60+
HermesClientConfig config = new HermesClientConfig();
61+
config.getHttp()
2062
.setEndpointPolicy(EndpointPolicy.trustedLocal("127.0.0.1", server.getPort()))
2163
.setBaseUrl("http://127.0.0.1:" + server.getPort());
22-
http.setApiKey("root-secret");
64+
config.getCli().setEnabled(false);
65+
config.setProfileCredentialResolver(profileId -> ProfileBinding.of(
66+
profileId, "resolver-" + profileId,
67+
identity -> CredentialSnapshot.of("resolved-token", "1")));
68+
69+
try (HermesClient root = new HermesClient(config)) {
70+
root.forProfile("team-a").health();
71+
RecordedRequest request = server.takeRequest(3, TimeUnit.SECONDS);
72+
assertNotNull(request);
73+
assertEquals("/p/team-a/health", request.getPath());
74+
assertEquals("Bearer resolved-token", request.getHeader("Authorization"));
75+
}
76+
}
77+
}
78+
79+
@Test
80+
void missingNamedProfileCredentialDoesNotBorrowRootApiKey() throws Exception {
81+
try (MockWebServer server = new MockWebServer()) {
82+
server.start();
2383

24-
HermesCliConfig cli = new HermesCliConfig();
25-
cli.setEnabled(false);
84+
HermesHttpClientConfig http = http(server);
85+
http.setApiKey("root-secret");
2686

27-
try (HermesClient root = new HermesClient(http, cli)) {
87+
try (HermesClient root = new HermesClient(http, disabledCli())) {
2888
assertThrows(IllegalStateException.class, () -> root.forProfile("team-a"));
2989
assertEquals(0, server.getRequestCount(),
3090
"missing profile credentials must fail before any request is sent");
3191
}
3292
}
3393
}
94+
95+
@Test
96+
void credentialProviderIsResolvedForEveryNewRequest() throws Exception {
97+
try (MockWebServer server = new MockWebServer()) {
98+
server.enqueue(okHealth());
99+
server.enqueue(okHealth());
100+
server.start();
101+
102+
AtomicReference<CredentialSnapshot> credential =
103+
new AtomicReference<>(CredentialSnapshot.of("token-a", "generation-a"));
104+
ProfileBinding binding = ProfileBinding.of(
105+
"team-a", "credential-a", identity -> credential.get());
106+
107+
try (HermesClient root = new HermesClient(http(server), disabledCli())) {
108+
HermesClient profile = root.forProfile(binding);
109+
profile.health();
110+
111+
credential.set(CredentialSnapshot.of("token-b", "generation-b"));
112+
profile.health();
113+
114+
RecordedRequest first = server.takeRequest(3, TimeUnit.SECONDS);
115+
RecordedRequest second = server.takeRequest(3, TimeUnit.SECONDS);
116+
assertNotNull(first);
117+
assertNotNull(second);
118+
assertEquals("Bearer token-a", first.getHeader("Authorization"));
119+
assertEquals("Bearer token-b", second.getHeader("Authorization"));
120+
}
121+
}
122+
}
123+
124+
@Test
125+
void credentialSnapshotDoesNotExposeSecretInToString() {
126+
CredentialSnapshot snapshot = CredentialSnapshot.of("super-secret-token", "generation-a");
127+
assertFalse(snapshot.toString().contains("super-secret-token"));
128+
}
129+
130+
@Test
131+
void businessHeadersCannotOverrideBoundAuthorization() throws Exception {
132+
try (MockWebServer server = new MockWebServer()) {
133+
server.start();
134+
135+
ProfileBinding binding = ProfileBinding.of(
136+
"team-a", "credential-a",
137+
identity -> CredentialSnapshot.of("profile-token", "1"));
138+
139+
try (HermesClient root = new HermesClient(http(server), disabledCli())) {
140+
HermesClient profile = root.forProfile(binding);
141+
ChatRequest request = new ChatRequest();
142+
request.setMessages(Collections.singletonList(
143+
new ChatRequest.Message("user", "hello")));
144+
145+
assertThrows(IllegalArgumentException.class,
146+
() -> profile.chatCompletion(
147+
request,
148+
Collections.singletonMap("Authorization", "Bearer attacker-token")));
149+
assertEquals(0, server.getRequestCount(),
150+
"identity override must be rejected before network I/O");
151+
}
152+
}
153+
}
154+
155+
private static HermesHttpClientConfig http(MockWebServer server) {
156+
return new HermesHttpClientConfig()
157+
.setEndpointPolicy(EndpointPolicy.trustedLocal("127.0.0.1", server.getPort()))
158+
.setBaseUrl("http://127.0.0.1:" + server.getPort());
159+
}
160+
161+
private static HermesCliConfig disabledCli() {
162+
HermesCliConfig cli = new HermesCliConfig();
163+
cli.setEnabled(false);
164+
return cli;
165+
}
166+
167+
private static MockResponse okHealth() {
168+
return new MockResponse()
169+
.setResponseCode(200)
170+
.addHeader("Content-Type", "application/json")
171+
.setBody("{\"status\":\"ok\"}");
172+
}
34173
}

0 commit comments

Comments
 (0)