Skip to content

Commit 63bf69a

Browse files
committed
refactor: Delete hand-rolled ProblemDetailRenderer, migrate SecurityFilter to ProblemDetail
1 parent 7b2e648 commit 63bf69a

5 files changed

Lines changed: 49 additions & 135 deletions

File tree

src/main/java/com/retailsvc/http/OpenApiServer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ record HandlerConfig(
103103
spec.securitySchemes(),
104104
spec.security(),
105105
handlerConfig.securityValidators(),
106-
handlerConfig.externalAuth()));
106+
handlerConfig.externalAuth(),
107+
bodyMappers.get(JSON)));
107108
ctx.setHandler(
108109
new DispatchHandler(
109110
handlerConfig.handlers(),

src/main/java/com/retailsvc/http/internal/ProblemDetailRenderer.java

Lines changed: 0 additions & 93 deletions
This file was deleted.

src/main/java/com/retailsvc/http/internal/SecurityFilter.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55

66
import com.retailsvc.http.Request;
77
import com.retailsvc.http.SchemeValidator;
8+
import com.retailsvc.http.TypeMapper;
89
import com.retailsvc.http.spec.Operation;
910
import com.retailsvc.http.spec.security.SecurityRequirement;
1011
import com.retailsvc.http.spec.security.SecurityScheme;
1112
import com.sun.net.httpserver.Filter;
1213
import com.sun.net.httpserver.HttpExchange;
1314
import java.io.IOException;
14-
import java.nio.charset.StandardCharsets;
1515
import java.util.ArrayList;
1616
import java.util.Comparator;
1717
import java.util.LinkedHashMap;
1818
import java.util.LinkedHashSet;
1919
import java.util.List;
2020
import java.util.Locale;
2121
import java.util.Map;
22+
import java.util.Objects;
2223
import java.util.Optional;
2324

2425
public final class SecurityFilter extends Filter {
@@ -28,18 +29,21 @@ public final class SecurityFilter extends Filter {
2829
private final List<SecurityRequirement> rootSecurity;
2930
private final Map<String, SchemeValidator> validators;
3031
private final boolean externalAuth;
32+
private final TypeMapper jsonMapper;
3133

3234
public SecurityFilter(
3335
Map<String, Operation> operationsById,
3436
Map<String, SecurityScheme> schemes,
3537
List<SecurityRequirement> rootSecurity,
3638
Map<String, SchemeValidator> validators,
37-
boolean externalAuth) {
39+
boolean externalAuth,
40+
TypeMapper jsonMapper) {
3841
this.operationsById = Map.copyOf(operationsById);
3942
this.schemes = Map.copyOf(schemes);
4043
this.rootSecurity = List.copyOf(rootSecurity);
4144
this.validators = Map.copyOf(validators);
4245
this.externalAuth = externalAuth;
46+
this.jsonMapper = Objects.requireNonNull(jsonMapper, "jsonMapper must not be null");
4347
}
4448

4549
@Override
@@ -119,8 +123,9 @@ private void renderRejection(HttpExchange exchange, List<GroupOutcome.Failed> fa
119123
failures.stream().max(Comparator.comparing(GroupOutcome.Failed::kind)).orElseThrow();
120124
String detail = describe(pick);
121125

122-
byte[] body =
123-
ProblemDetailRenderer.render(status, title, detail).getBytes(StandardCharsets.UTF_8);
126+
ProblemDetail problemDetail =
127+
new ProblemDetail("about:blank", title, status, detail, null, null);
128+
byte[] body = jsonMapper.writeTo(problemDetail);
124129
exchange.getResponseHeaders().add("Content-Type", "application/problem+json");
125130
if (!anyDenied) {
126131
LinkedHashSet<String> attempted = new LinkedHashSet<>();

src/test/java/com/retailsvc/http/internal/ProblemDetailRendererTest.java

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/test/java/com/retailsvc/http/internal/SecurityFilterTest.java

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import com.retailsvc.http.Request;
1313
import com.retailsvc.http.SchemeValidator;
14+
import com.retailsvc.http.TypeMapper;
1415
import com.retailsvc.http.spec.HttpMethod;
1516
import com.retailsvc.http.spec.Operation;
1617
import com.retailsvc.http.spec.security.SecurityRequirement;
@@ -23,6 +24,7 @@
2324
import com.sun.net.httpserver.HttpExchange;
2425
import java.io.ByteArrayOutputStream;
2526
import java.net.URI;
27+
import java.nio.charset.StandardCharsets;
2628
import java.util.List;
2729
import java.util.Map;
2830
import java.util.Optional;
@@ -49,7 +51,8 @@ void allowsRequestWhenValidatorReturnsPrincipal() throws Exception {
4951
Map.of("bearerAuth", (req, cred) -> Optional.of("user-1"));
5052

5153
SecurityFilter filter =
52-
new SecurityFilter(Map.of("getX", op), schemes, List.of(), validators, false);
54+
new SecurityFilter(
55+
Map.of("getX", op), schemes, List.of(), validators, false, mockJsonMapper());
5356

5457
HttpExchange ex = mock(HttpExchange.class);
5558
Headers headers = new Headers();
@@ -88,7 +91,8 @@ void passesThroughWhenOperationHasNoSecurity() throws Exception {
8891
Optional.empty()); // inherits root, root is empty
8992

9093
SecurityFilter filter =
91-
new SecurityFilter(Map.of("getY", op), Map.of(), List.of(), Map.of(), false);
94+
new SecurityFilter(
95+
Map.of("getY", op), Map.of(), List.of(), Map.of(), false, mockJsonMapper());
9296

9397
HttpExchange ex = mock(HttpExchange.class);
9498
Chain chain = mock(Chain.class);
@@ -117,7 +121,8 @@ void missingCredentialReturns401WithBearerChallenge() throws Exception {
117121
Map.of("bearerAuth", new HttpBearer(Optional.empty())),
118122
List.of(),
119123
Map.of("bearerAuth", (req, cred) -> Optional.of("never-called")),
120-
false);
124+
false,
125+
mockJsonMapper());
121126

122127
HttpExchange ex = mock(HttpExchange.class);
123128
Headers headers = new Headers();
@@ -159,7 +164,8 @@ void deniedValidatorReturns403WithoutChallenge() throws Exception {
159164
Map.of("bearerAuth", new HttpBearer(Optional.empty())),
160165
List.of(),
161166
Map.of("bearerAuth", (req, cred) -> Optional.empty()),
162-
false);
167+
false,
168+
mockJsonMapper());
163169

164170
HttpExchange ex = mock(HttpExchange.class);
165171
Headers headers = new Headers();
@@ -196,7 +202,8 @@ void apiKeyMissingReturnsApiKeyChallengeHeader() throws Exception {
196202
Map.of("apiKeyAuth", new ApiKey("X-API-Key", Location.HEADER)),
197203
List.of(),
198204
Map.of("apiKeyAuth", (req, cred) -> Optional.of("ok")),
199-
false);
205+
false,
206+
mockJsonMapper());
200207

201208
HttpExchange ex = mock(HttpExchange.class);
202209
when(ex.getRequestHeaders()).thenReturn(new Headers());
@@ -241,7 +248,8 @@ void andGroupRequiresAllSchemesToSucceed() throws Exception {
241248
"bearerAuth", (req, cred) -> Optional.of("bearer-principal"));
242249

243250
SecurityFilter filter =
244-
new SecurityFilter(Map.of("getX", op), schemes, List.of(), validators, false);
251+
new SecurityFilter(
252+
Map.of("getX", op), schemes, List.of(), validators, false, mockJsonMapper());
245253

246254
HttpExchange ex = mock(HttpExchange.class);
247255
Headers headers = new Headers();
@@ -295,7 +303,8 @@ void orFallsBackToSecondGroupWhenFirstDenied() throws Exception {
295303
"bearerAuth", (req, cred) -> Optional.of("bearer-ok"));
296304

297305
SecurityFilter filter =
298-
new SecurityFilter(Map.of("getX", op), schemes, List.of(), validators, false);
306+
new SecurityFilter(
307+
Map.of("getX", op), schemes, List.of(), validators, false, mockJsonMapper());
299308

300309
HttpExchange ex = mock(HttpExchange.class);
301310
Headers headers = new Headers();
@@ -339,7 +348,8 @@ void externalAuthBypassesEverything() throws Exception {
339348
Map.of("bearerAuth", new HttpBearer(Optional.empty())),
340349
List.of(),
341350
Map.of(), // NO validators
342-
/* externalAuth= */ true);
351+
/* externalAuth= */ true,
352+
mockJsonMapper());
343353

344354
HttpExchange ex = mock(HttpExchange.class);
345355
Chain chain = mock(Chain.class);
@@ -351,4 +361,24 @@ void externalAuthBypassesEverything() throws Exception {
351361
private static Request newMinimalRequest(String operationId) {
352362
return new Request(new byte[0], null, null, operationId, Map.of(), null, h -> null);
353363
}
364+
365+
private static TypeMapper mockJsonMapper() {
366+
return new TypeMapper() {
367+
@Override
368+
public Object readFrom(byte[] body, String contentTypeHeader) {
369+
return null;
370+
}
371+
372+
@Override
373+
public byte[] writeTo(Object value) {
374+
if (value instanceof ProblemDetail pd) {
375+
return String.format(
376+
"{\"type\":\"%s\",\"title\":\"%s\",\"status\":%d,\"detail\":\"%s\"}",
377+
pd.type(), pd.title(), pd.status(), pd.detail())
378+
.getBytes(StandardCharsets.UTF_8);
379+
}
380+
return "{}".getBytes(StandardCharsets.UTF_8);
381+
}
382+
};
383+
}
354384
}

0 commit comments

Comments
 (0)