Skip to content

Commit f23b8cf

Browse files
committed
Add relative Location conformance proof
1 parent bdee358 commit f23b8cf

4 files changed

Lines changed: 412 additions & 0 deletions

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,9 @@ tasks.register('api2DevdockTusUploadCallbacks', JavaExec) {
7171
mainClass = 'io.tus.java.example.Api2DevdockTusUploadCallbacks'
7272
workingDir = rootProject.projectDir
7373
}
74+
75+
tasks.register('api2DevdockTusRelativeLocationResolution', JavaExec) {
76+
classpath = sourceSets.main.runtimeClasspath
77+
mainClass = 'io.tus.java.example.Api2DevdockTusRelativeLocationResolution'
78+
workingDir = rootProject.projectDir
79+
}

example/src/main/java/io/tus/java/example/Api2DevdockScenario.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,37 @@ static JSONObject createResponse(JSONObject scenario) {
9393
return scenario.getJSONObject("prepared").getJSONObject("createResponse");
9494
}
9595

96+
static JSONObject conformanceScenario(JSONObject scenario) {
97+
return scenario.getJSONObject("conformanceScenario");
98+
}
99+
100+
static byte[] conformanceInputSourceBytes(JSONObject conformanceScenario) {
101+
final JSONObject inputSource = conformanceScenario.getJSONObject("inputSource");
102+
final String kind = inputSource.getString("kind");
103+
if (!"blob".equals(kind)) {
104+
throw new IllegalArgumentException("unsupported conformance input source kind " + kind);
105+
}
106+
107+
return inputSource.getString("content").getBytes(StandardCharsets.UTF_8);
108+
}
109+
110+
static Map<String, String> conformanceInputStringMapOption(
111+
JSONObject conformanceScenario,
112+
String key
113+
) {
114+
final JSONObject values = conformanceInputJSONObjectOption(conformanceScenario, key);
115+
final Map<String, String> result = new LinkedHashMap<String, String>();
116+
for (String name : values.keySet()) {
117+
result.put(name, scalarString(values.get(name)));
118+
}
119+
120+
return result;
121+
}
122+
123+
static String conformanceInputStringOption(JSONObject conformanceScenario, String key) {
124+
return scalarString(conformanceInputOption(conformanceScenario, key));
125+
}
126+
96127
static byte[] scenarioBytes(JSONObject uploadConfig) {
97128
final JSONObject source = uploadConfig.getJSONObject("source");
98129
final String kind = source.getString("kind");
@@ -108,6 +139,32 @@ static byte[] scenarioBytes(JSONObject uploadConfig) {
108139
return source.getString("value").getBytes(StandardCharsets.UTF_8);
109140
}
110141

142+
private static Object conformanceInputOption(JSONObject conformanceScenario, String key) {
143+
final JSONArray entries = conformanceScenario.getJSONArray("inputOptionEntries");
144+
for (int index = 0; index < entries.length(); index++) {
145+
final JSONObject entry = entries.getJSONObject(index);
146+
if (key.equals(entry.getString("key"))) {
147+
return entry.get("value");
148+
}
149+
}
150+
151+
throw new IllegalArgumentException("missing conformance input option " + key);
152+
}
153+
154+
private static JSONObject conformanceInputJSONObjectOption(
155+
JSONObject conformanceScenario,
156+
String key
157+
) {
158+
final Object value = conformanceInputOption(conformanceScenario, key);
159+
if (!(value instanceof JSONObject)) {
160+
throw new IllegalArgumentException(
161+
"conformance input option " + key + " is not an object"
162+
);
163+
}
164+
165+
return (JSONObject) value;
166+
}
167+
111168
static int fixedChunkSizeBytes(JSONObject uploadConfig) {
112169
final JSONObject chunkSize = uploadConfig.getJSONObject("chunkSize");
113170
final String kind = chunkSize.getString("kind");
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package io.tus.java.example;
2+
3+
import com.sun.net.httpserver.Headers;
4+
import com.sun.net.httpserver.HttpExchange;
5+
import com.sun.net.httpserver.HttpHandler;
6+
import com.sun.net.httpserver.HttpServer;
7+
import org.json.JSONArray;
8+
import org.json.JSONObject;
9+
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.IOException;
12+
import java.io.OutputStream;
13+
import java.net.InetSocketAddress;
14+
import java.net.URI;
15+
import java.net.URL;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
final class Api2DevdockTusConformanceServer implements AutoCloseable {
21+
private final URL endpointOrigin;
22+
private final List<JSONObject> requests;
23+
private final HttpServer server;
24+
private final List<String> errors;
25+
private final List<String> requestMethods;
26+
private final List<String> requestUrls;
27+
private int nextRequestIndex;
28+
29+
Api2DevdockTusConformanceServer(JSONObject conformanceScenario, URL endpointOrigin)
30+
throws IOException {
31+
this.endpointOrigin = endpointOrigin;
32+
this.requests = new ArrayList<JSONObject>();
33+
final JSONArray requestArray = conformanceScenario.getJSONArray("requests");
34+
for (int index = 0; index < requestArray.length(); index++) {
35+
requests.add(requestArray.getJSONObject(index));
36+
}
37+
this.errors = new ArrayList<String>();
38+
this.requestMethods = new ArrayList<String>();
39+
this.requestUrls = new ArrayList<String>();
40+
this.server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
41+
server.createContext("/", new HttpHandler() {
42+
@Override
43+
public void handle(HttpExchange exchange) throws IOException {
44+
handleRequest(exchange);
45+
}
46+
});
47+
server.start();
48+
}
49+
50+
URL endpointUrl() throws IOException {
51+
return localUrl(endpointOrigin.toString());
52+
}
53+
54+
void assertExhausted() {
55+
assertNoErrors();
56+
if (nextRequestIndex == requests.size()) {
57+
return;
58+
}
59+
60+
throw new IllegalStateException(
61+
"expected "
62+
+ requests.size()
63+
+ " conformance request(s), got "
64+
+ nextRequestIndex
65+
);
66+
}
67+
68+
void assertNoErrors() {
69+
if (errors.isEmpty()) {
70+
return;
71+
}
72+
73+
throw new IllegalStateException(errorSummary());
74+
}
75+
76+
String canonicalUrl(String actualUrl) {
77+
return actualUrl.replace(localOrigin(), canonicalOrigin());
78+
}
79+
80+
String errorSummary() {
81+
if (errors.isEmpty()) {
82+
return "no conformance server errors";
83+
}
84+
85+
return String.join("; ", errors);
86+
}
87+
88+
JSONObject result() {
89+
return new JSONObject()
90+
.put("requestMethods", new JSONArray(requestMethods))
91+
.put("requestUrls", new JSONArray(requestUrls));
92+
}
93+
94+
@Override
95+
public void close() {
96+
server.stop(0);
97+
}
98+
99+
private void handleRequest(HttpExchange exchange) throws IOException {
100+
try {
101+
final byte[] body = readRequestBody(exchange);
102+
final int requestIndex = observeRequest(exchange, body);
103+
writeResponse(exchange, requests.get(requestIndex));
104+
} catch (Exception error) {
105+
errors.add(error.getMessage());
106+
final byte[] body = errorSummary().getBytes(StandardCharsets.UTF_8);
107+
exchange.sendResponseHeaders(500, body.length);
108+
try (OutputStream responseBody = exchange.getResponseBody()) {
109+
responseBody.write(body);
110+
}
111+
}
112+
}
113+
114+
private int observeRequest(HttpExchange exchange, byte[] body) throws IOException {
115+
if (nextRequestIndex >= requests.size()) {
116+
throw new IllegalStateException(
117+
"unexpected request "
118+
+ exchange.getRequestMethod()
119+
+ " "
120+
+ exchange.getRequestURI()
121+
);
122+
}
123+
124+
final int requestIndex = nextRequestIndex;
125+
final JSONObject requestPlan = requests.get(requestIndex);
126+
final String actualUrl = canonicalRequestUrl(exchange.getRequestURI());
127+
final String expectedUrl = requestPlan.getString("expectedUrl");
128+
final String expectedMethod = requestPlan.getString("effectiveMethod");
129+
if (!expectedMethod.equals(exchange.getRequestMethod())) {
130+
throw new IllegalStateException(
131+
"request "
132+
+ requestIndex
133+
+ " expected method "
134+
+ expectedMethod
135+
+ ", got "
136+
+ exchange.getRequestMethod()
137+
);
138+
}
139+
if (!expectedUrl.equals(actualUrl)) {
140+
throw new IllegalStateException(
141+
"request "
142+
+ requestIndex
143+
+ " expected URL "
144+
+ expectedUrl
145+
+ ", got "
146+
+ actualUrl
147+
);
148+
}
149+
if (!requestPlan.isNull("bodySize") && body.length != requestPlan.getInt("bodySize")) {
150+
throw new IllegalStateException(
151+
"request "
152+
+ requestIndex
153+
+ " expected body size "
154+
+ requestPlan.getInt("bodySize")
155+
+ ", got "
156+
+ body.length
157+
);
158+
}
159+
assertHeaders(
160+
requestIndex,
161+
requestPlan.getJSONObject("effectiveHeaders"),
162+
exchange.getRequestHeaders()
163+
);
164+
165+
requestMethods.add(exchange.getRequestMethod());
166+
requestUrls.add(actualUrl);
167+
nextRequestIndex += 1;
168+
169+
return requestIndex;
170+
}
171+
172+
private void assertHeaders(int requestIndex, JSONObject expectedHeaders, Headers actualHeaders) {
173+
for (String name : expectedHeaders.keySet()) {
174+
final String expectedValue = expectedHeaders.getString(name);
175+
final String actualValue = actualHeaders.getFirst(name);
176+
if (expectedValue.equals(actualValue)) {
177+
continue;
178+
}
179+
180+
throw new IllegalStateException(
181+
"request "
182+
+ requestIndex
183+
+ " expected header "
184+
+ name
185+
+ "="
186+
+ expectedValue
187+
+ ", got "
188+
+ actualValue
189+
);
190+
}
191+
}
192+
193+
private void writeResponse(HttpExchange exchange, JSONObject requestPlan) throws IOException {
194+
final JSONObject responsePlan = requestPlan.getJSONObject("response");
195+
final Headers headers = exchange.getResponseHeaders();
196+
final JSONObject responseHeaders = responsePlan.getJSONObject("effectiveHeaders");
197+
for (String name : responseHeaders.keySet()) {
198+
headers.set(name, localValue(responseHeaders.getString(name)));
199+
}
200+
201+
final byte[] body;
202+
if (responsePlan.isNull("body")) {
203+
body = new byte[0];
204+
} else {
205+
body = responsePlan.getString("body").getBytes(StandardCharsets.UTF_8);
206+
}
207+
final long responseBodyLength = body.length == 0 ? -1 : body.length;
208+
exchange.sendResponseHeaders(responsePlan.getInt("statusCode"), responseBodyLength);
209+
try (OutputStream responseBody = exchange.getResponseBody()) {
210+
responseBody.write(body);
211+
}
212+
}
213+
214+
private URL localUrl(String canonicalUrl) throws IOException {
215+
return new URL(localValue(canonicalUrl));
216+
}
217+
218+
private String localValue(String value) {
219+
return value.replace(canonicalOrigin(), localOrigin());
220+
}
221+
222+
private String canonicalRequestUrl(URI requestUri) throws IOException {
223+
return new URL(endpointOrigin, requestUri.toString()).toString();
224+
}
225+
226+
private static byte[] readRequestBody(HttpExchange exchange) throws IOException {
227+
final ByteArrayOutputStream body = new ByteArrayOutputStream();
228+
final byte[] buffer = new byte[8192];
229+
int read;
230+
while ((read = exchange.getRequestBody().read(buffer)) != -1) {
231+
body.write(buffer, 0, read);
232+
}
233+
234+
return body.toByteArray();
235+
}
236+
237+
private String canonicalOrigin() {
238+
return endpointOrigin.getProtocol() + "://" + endpointOrigin.getHost();
239+
}
240+
241+
private String localOrigin() {
242+
final InetSocketAddress address = server.getAddress();
243+
return "http://" + address.getHostString() + ":" + address.getPort();
244+
}
245+
246+
private Api2DevdockTusConformanceServer() {
247+
throw new IllegalStateException("Utility class");
248+
}
249+
}

0 commit comments

Comments
 (0)