@bytescale/sdk 3.57.0 → 3.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/cjs/main.js +724 -313
- package/dist/browser/esm/main.mjs +724 -313
- package/dist/node/cjs/main.js +123 -29
- package/dist/node/esm/main.mjs +123 -29
- package/dist/types/private/AuthSessionState.d.ts +7 -0
- package/dist/types/private/UploadManagerBase.d.ts +0 -1
- package/dist/types/private/dtos/AuthSwSetConfigDto.d.ts +2 -0
- package/dist/types/private/model/AuthManagerInterface.d.ts +1 -93
- package/dist/types/private/model/AuthSession.d.ts +12 -5
- package/dist/types/private/model/AuthSessionConfig.d.ts +3 -0
- package/dist/types/private/model/AuthSessionConfigAuto.d.ts +6 -0
- package/dist/types/private/model/AuthSessionConfigBase.d.ts +12 -0
- package/dist/types/private/model/AuthSessionConfigManual.d.ts +7 -0
- package/dist/types/private/model/BeginAuthSessionParams.d.ts +3 -0
- package/dist/types/private/model/BeginAuthSessionParamsOptions.d.ts +2 -0
- package/dist/types/private/model/BeginAuthSessionParamsV1.d.ts +11 -0
- package/dist/types/private/model/BeginAuthSessionParamsV2.d.ts +16 -0
- package/dist/types/private/model/NonEmptyArray.d.ts +1 -0
- package/dist/types/private/model/UrlRewriteRule.d.ts +6 -0
- package/dist/types/public/browser/AuthManagerBrowser.d.ts +27 -11
- package/dist/types/public/node/AuthManagerNode.d.ts +12 -2
- package/dist/types/public/shared/generated/runtime.d.ts +13 -6
- package/dist/worker/cjs/main.js +123 -29
- package/dist/worker/esm/main.mjs +123 -29
- package/package.json +1 -1
- package/tests/ApiClientAuth.test.ts +222 -0
- package/tests/AuthManagerBrowser.test.ts +312 -192
- package/tests/AuthServiceWorkerRewrite.test.ts +337 -0
- package/tests/UploadManagerAuth.test.ts +156 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { runInNewContext } from "node:vm";
|
|
5
|
+
import { Headers as NodeFetchHeaders, Request as NodeFetchRequest, Response as NodeFetchResponse } from "node-fetch";
|
|
6
|
+
import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
|
|
7
|
+
import type { AuthSwConfigEntryDto, UrlRewriteRule } from "../src/index.browser";
|
|
8
|
+
|
|
9
|
+
const workerSource = readFileSync(resolve(process.cwd(), "src/index.auth-sw.js"), "utf8");
|
|
10
|
+
|
|
11
|
+
describe("Auth service-worker URL rewriting", () => {
|
|
12
|
+
test("preserves the remaining path, query string, and fragment", async () => {
|
|
13
|
+
const harness = new AuthServiceWorkerHarness();
|
|
14
|
+
const rules = [rewriteRule("download/", "account-a/")];
|
|
15
|
+
await harness.setConfig([], rules);
|
|
16
|
+
|
|
17
|
+
const result = await harness.dispatchFetch(
|
|
18
|
+
"https://dashboard.example.com/download/path/to/file.pdf?download=true&version=2"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/path/to/file.pdf?download=true&version=2");
|
|
22
|
+
expect(harness.getRewrittenUrl("https://dashboard.example.com/download/file.pdf?download=true#page=2", rules)).toBe(
|
|
23
|
+
"https://upcdn.io/account-a/file.pdf?download=true#page=2"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("matches authentication against the rewritten URL and supports navigation requests", async () => {
|
|
28
|
+
const upstreamResponse = new NodeFetchResponse("streamed-body", {
|
|
29
|
+
headers: {
|
|
30
|
+
"Accept-Ranges": "bytes",
|
|
31
|
+
"Content-Disposition": 'attachment; filename="file.pdf"',
|
|
32
|
+
"Content-Length": "13",
|
|
33
|
+
"Content-Range": "bytes 0-12/13",
|
|
34
|
+
"Content-Type": "application/pdf"
|
|
35
|
+
},
|
|
36
|
+
status: 206
|
|
37
|
+
});
|
|
38
|
+
const blob = jest.spyOn(upstreamResponse, "blob");
|
|
39
|
+
const arrayBuffer = jest.spyOn(upstreamResponse, "arrayBuffer");
|
|
40
|
+
const harness = new AuthServiceWorkerHarness(upstreamResponse);
|
|
41
|
+
await harness.setConfig([authConfig("account-a/", "token-a")], [rewriteRule("download/", "account-a/")]);
|
|
42
|
+
|
|
43
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf", {
|
|
44
|
+
headers: {
|
|
45
|
+
"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00 GMT",
|
|
46
|
+
"If-None-Match": '"etag"',
|
|
47
|
+
"If-Range": '"range-etag"',
|
|
48
|
+
"Range": "bytes=0-12"
|
|
49
|
+
},
|
|
50
|
+
navigation: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
54
|
+
expect(result.outboundRequest?.mode).toBe("cors");
|
|
55
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
56
|
+
expect(result.outboundRequest?.headers.get("Range")).toBe("bytes=0-12");
|
|
57
|
+
expect(result.outboundRequest?.headers.get("If-Range")).toBe('"range-etag"');
|
|
58
|
+
expect(result.outboundRequest?.headers.get("If-None-Match")).toBe('"etag"');
|
|
59
|
+
expect(result.outboundRequest?.headers.get("If-Modified-Since")).toBe("Wed, 21 Oct 2015 07:28:00 GMT");
|
|
60
|
+
expect(result.response).toBe(upstreamResponse);
|
|
61
|
+
expect(result.response?.status).toBe(206);
|
|
62
|
+
expect(result.response?.headers.get("Content-Disposition")).toBe('attachment; filename="file.pdf"');
|
|
63
|
+
expect(result.response?.headers.get("Accept-Ranges")).toBe("bytes");
|
|
64
|
+
expect(blob).not.toHaveBeenCalled();
|
|
65
|
+
expect(arrayBuffer).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("selects different authentication configurations for different rewrite destinations", async () => {
|
|
69
|
+
const harness = new AuthServiceWorkerHarness();
|
|
70
|
+
await harness.setConfig(
|
|
71
|
+
[authConfig("account-a/", "token-a"), authConfig("account-b/", "token-b")],
|
|
72
|
+
[rewriteRule("download-a/", "account-a/"), rewriteRule("download-b/", "account-b/")]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const first = await harness.dispatchFetch("https://dashboard.example.com/download-a/file.pdf");
|
|
76
|
+
const second = await harness.dispatchFetch("https://dashboard.example.com/download-b/file.pdf");
|
|
77
|
+
|
|
78
|
+
expect(first.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
79
|
+
expect(second.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("never selects a token from the original URL", async () => {
|
|
83
|
+
const harness = new AuthServiceWorkerHarness();
|
|
84
|
+
await harness.setConfig(
|
|
85
|
+
[authConfigForUrl("https://dashboard.example.com/download/", "original-token")],
|
|
86
|
+
[rewriteRule("download/", "unconfigured-account/")]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
90
|
+
|
|
91
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/unconfigured-account/file.pdf");
|
|
92
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("fetches rewritten requests even when no authentication configuration matches", async () => {
|
|
96
|
+
const harness = new AuthServiceWorkerHarness();
|
|
97
|
+
await harness.setConfig([], [rewriteRule("download/", "public-account/")]);
|
|
98
|
+
|
|
99
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/public.pdf");
|
|
100
|
+
|
|
101
|
+
expect(result.responded).toBe(true);
|
|
102
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/public-account/public.pdf");
|
|
103
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("uses only the first matching rewrite rule", async () => {
|
|
107
|
+
const harness = new AuthServiceWorkerHarness();
|
|
108
|
+
await harness.setConfig(
|
|
109
|
+
[authConfig("account-a/", "token-a"), authConfig("account-b/", "token-b")],
|
|
110
|
+
[rewriteRule("download/", "account-a/"), rewriteRule("download/", "account-b/")]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
114
|
+
|
|
115
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
116
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("does not recursively rewrite the rewritten URL", async () => {
|
|
120
|
+
const harness = new AuthServiceWorkerHarness();
|
|
121
|
+
await harness.setConfig(
|
|
122
|
+
[authConfig("account-a/", "token-a")],
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
fromUrlPrefix: "https://dashboard.example.com/download/",
|
|
126
|
+
toUrlPrefix: "https://proxy.example.com/download/"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
fromUrlPrefix: "https://proxy.example.com/download/",
|
|
130
|
+
toUrlPrefix: "https://upcdn.io/account-a/"
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
136
|
+
|
|
137
|
+
expect(result.outboundRequest?.url).toBe("https://proxy.example.com/download/file.pdf");
|
|
138
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("retains existing behavior when no rewrite rule matches", async () => {
|
|
142
|
+
const harness = new AuthServiceWorkerHarness();
|
|
143
|
+
await harness.setConfig([authConfig("account-a/", "token-a")], [rewriteRule("download/", "account-a/")]);
|
|
144
|
+
|
|
145
|
+
const authenticated = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf");
|
|
146
|
+
const untouched = await harness.dispatchFetch("https://dashboard.example.com/ordinary-page");
|
|
147
|
+
|
|
148
|
+
expect(authenticated.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
149
|
+
expect(authenticated.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
150
|
+
expect(untouched.responded).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("keeps source-page authorization independent across multiple client IDs", async () => {
|
|
154
|
+
const harness = new AuthServiceWorkerHarness();
|
|
155
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
156
|
+
harness.setWindowClient("client-b", "https://app.example.com/b/");
|
|
157
|
+
await harness.setConfig([
|
|
158
|
+
{
|
|
159
|
+
...authConfig("account-a/", "token-a"),
|
|
160
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
161
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
...authConfig("account-b/", "token-b"),
|
|
165
|
+
sourceUrlPrefixes: ["https://app.example.com/b/"],
|
|
166
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-b/"
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const a = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", { clientId: "client-a" });
|
|
171
|
+
const wrongSource = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
172
|
+
clientId: "client-b"
|
|
173
|
+
});
|
|
174
|
+
const b = await harness.dispatchFetch("https://upcdn.io/account-b/file.pdf", { clientId: "client-b" });
|
|
175
|
+
|
|
176
|
+
expect(a.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
177
|
+
expect(wrongSource.responded).toBe(true);
|
|
178
|
+
expect(wrongSource.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
179
|
+
expect(b.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("source-scoped auth replaces both authentication headers and preserves unrelated headers", async () => {
|
|
183
|
+
const harness = new AuthServiceWorkerHarness();
|
|
184
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
185
|
+
await harness.setConfig([
|
|
186
|
+
{
|
|
187
|
+
...authConfig("account-a/", "token-a"),
|
|
188
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
189
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const result = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
194
|
+
clientId: "client-a",
|
|
195
|
+
headers: {
|
|
196
|
+
"Authorization": "Bearer stale",
|
|
197
|
+
"Authorization-Token": "stale-token",
|
|
198
|
+
"X-Trace-Id": "trace"
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
203
|
+
expect(result.outboundRequest?.headers.has("Authorization-Token")).toBe(false);
|
|
204
|
+
expect(result.outboundRequest?.headers.get("X-Trace-Id")).toBe("trace");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
interface FetchOptions {
|
|
209
|
+
clientId?: string;
|
|
210
|
+
headers?: HeadersInit;
|
|
211
|
+
navigation?: boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface FetchResult {
|
|
215
|
+
outboundRequest: TestRequest | undefined;
|
|
216
|
+
responded: boolean;
|
|
217
|
+
response: NodeFetchResponse | undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type WorkerEventListener = (event: unknown) => void;
|
|
221
|
+
|
|
222
|
+
class AuthServiceWorkerHarness {
|
|
223
|
+
private readonly clientsById = new Map<string, { type: "window"; url: string }>();
|
|
224
|
+
private readonly context: {
|
|
225
|
+
getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
|
|
226
|
+
setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
private readonly fetchListener: WorkerEventListener;
|
|
230
|
+
private readonly fetchMock: jest.MockedFunction<(request: TestRequest) => Promise<NodeFetchResponse>>;
|
|
231
|
+
|
|
232
|
+
constructor(private readonly upstreamResponse: NodeFetchResponse = new NodeFetchResponse("ok")) {
|
|
233
|
+
const listeners = new Map<string, WorkerEventListener>();
|
|
234
|
+
const cacheEntries = new Map<string, NodeFetchResponse>();
|
|
235
|
+
this.fetchMock = jest.fn(async (_request: TestRequest): Promise<NodeFetchResponse> => this.upstreamResponse);
|
|
236
|
+
|
|
237
|
+
const self = {
|
|
238
|
+
addEventListener: (type: string, listener: WorkerEventListener): void => {
|
|
239
|
+
listeners.set(type, listener);
|
|
240
|
+
},
|
|
241
|
+
clients: {
|
|
242
|
+
claim: async (): Promise<void> => {},
|
|
243
|
+
get: async (clientId: string): Promise<{ type: "window"; url: string } | undefined> =>
|
|
244
|
+
this.clientsById.get(clientId)
|
|
245
|
+
},
|
|
246
|
+
skipWaiting: async (): Promise<void> => {}
|
|
247
|
+
};
|
|
248
|
+
const cache = {
|
|
249
|
+
match: async (key: string): Promise<NodeFetchResponse | undefined> => cacheEntries.get(key)?.clone(),
|
|
250
|
+
put: async (key: string, value: NodeFetchResponse): Promise<void> => {
|
|
251
|
+
cacheEntries.set(key, value.clone());
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const sandbox = {
|
|
255
|
+
caches: { open: async (): Promise<typeof cache> => cache },
|
|
256
|
+
console: { error: jest.fn(), log: jest.fn() },
|
|
257
|
+
fetch: this.fetchMock,
|
|
258
|
+
Headers: NodeFetchHeaders,
|
|
259
|
+
Promise,
|
|
260
|
+
Request: TestRequest,
|
|
261
|
+
Response: NodeFetchResponse,
|
|
262
|
+
self,
|
|
263
|
+
setTimeout
|
|
264
|
+
};
|
|
265
|
+
runInNewContext(workerSource, sandbox);
|
|
266
|
+
|
|
267
|
+
this.context = sandbox as typeof sandbox & AuthServiceWorkerHarness["context"];
|
|
268
|
+
const fetchListener = listeners.get("fetch");
|
|
269
|
+
if (fetchListener === undefined) {
|
|
270
|
+
throw new Error("Auth service worker did not register a fetch listener.");
|
|
271
|
+
}
|
|
272
|
+
this.fetchListener = fetchListener;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async setConfig(config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]): Promise<void> {
|
|
276
|
+
await this.context.setConfig(config, rules);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
getRewrittenUrl(url: string, rules: UrlRewriteRule[]): string | undefined {
|
|
280
|
+
return this.context.getRewrittenUrl(url, rules);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
setWindowClient(clientId: string, url: string): void {
|
|
284
|
+
this.clientsById.set(clientId, { type: "window", url });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
|
|
288
|
+
const request = new TestRequest(url, { headers: options.headers as NodeFetchRequestInit["headers"] });
|
|
289
|
+
if (options.navigation === true) {
|
|
290
|
+
Object.defineProperty(request, "mode", { configurable: true, value: "navigate" });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let responsePromise: Promise<NodeFetchResponse> | undefined;
|
|
294
|
+
this.fetchListener({
|
|
295
|
+
clientId: options.clientId ?? "",
|
|
296
|
+
request,
|
|
297
|
+
respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
|
|
298
|
+
responsePromise = Promise.resolve(response);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const response = await responsePromise;
|
|
303
|
+
return {
|
|
304
|
+
outboundRequest: this.fetchMock.mock.calls.at(-1)?.[0],
|
|
305
|
+
responded: responsePromise !== undefined,
|
|
306
|
+
response
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class TestRequest extends NodeFetchRequest {
|
|
312
|
+
readonly mode: RequestMode;
|
|
313
|
+
|
|
314
|
+
constructor(input: string | NodeFetchRequest, init: NodeFetchRequestInit & { mode?: RequestMode } = {}) {
|
|
315
|
+
super(input, init);
|
|
316
|
+
this.mode = init.mode ?? (input instanceof TestRequest ? input.mode : "cors");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function authConfig(accountPath: string, token: string): AuthSwConfigEntryDto {
|
|
321
|
+
return authConfigForUrl(`https://upcdn.io/${accountPath}`, token);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function authConfigForUrl(urlPrefix: string, token: string): AuthSwConfigEntryDto {
|
|
325
|
+
return {
|
|
326
|
+
expires: undefined,
|
|
327
|
+
headers: [{ key: "Authorization", value: `Bearer ${token}` }],
|
|
328
|
+
urlPrefix
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function rewriteRule(fromPath: string, toPath: string): UrlRewriteRule {
|
|
333
|
+
return {
|
|
334
|
+
fromUrlPrefix: `https://dashboard.example.com/${fromPath}`,
|
|
335
|
+
toUrlPrefix: `https://upcdn.io/${toPath}`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { AuthSessionState } from "../src/private/AuthSessionState";
|
|
3
|
+
import { UploadManagerBase } from "../src/private/UploadManagerBase";
|
|
4
|
+
import { AddCancellationHandler } from "../src/private/model/AddCancellationHandler";
|
|
5
|
+
import { AuthSessionConfigState } from "../src/private/model/AuthSession";
|
|
6
|
+
import { OnPartProgress } from "../src/private/model/OnPartProgress";
|
|
7
|
+
import { PreUploadInfo } from "../src/private/model/PreUploadInfo";
|
|
8
|
+
import { PutUploadPartResult } from "../src/private/model/PutUploadPartResult";
|
|
9
|
+
import { UploadManagerParams, UploadSource } from "../src/public/shared/CommonTypes";
|
|
10
|
+
import {
|
|
11
|
+
BeginMultipartUploadResponse,
|
|
12
|
+
CompleteMultipartUploadResponse,
|
|
13
|
+
UploadPart
|
|
14
|
+
} from "../src/public/shared/generated";
|
|
15
|
+
|
|
16
|
+
const accountA = "A123abc";
|
|
17
|
+
|
|
18
|
+
class TestUploadManager extends UploadManagerBase<string, undefined> {
|
|
19
|
+
protected processUploadSource(data: UploadSource): string {
|
|
20
|
+
return String(data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected getPreUploadInfoPartial(
|
|
24
|
+
_request: UploadManagerParams,
|
|
25
|
+
_source: string
|
|
26
|
+
): Partial<PreUploadInfo> & { size: number } {
|
|
27
|
+
return { size: 1 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected preUpload(_source: string): undefined {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async postUpload(_init: undefined): Promise<void> {}
|
|
35
|
+
|
|
36
|
+
protected async doPutUploadPart(
|
|
37
|
+
_part: UploadPart,
|
|
38
|
+
_contentLength: number,
|
|
39
|
+
_source: string,
|
|
40
|
+
_onProgress: OnPartProgress,
|
|
41
|
+
_addCancellationHandler: AddCancellationHandler
|
|
42
|
+
): Promise<PutUploadPartResult> {
|
|
43
|
+
return { etag: "etag", status: 200 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("UploadManager AuthManager configuration", () => {
|
|
48
|
+
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => AuthSessionState.setSession(undefined));
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
if (originalWindow === undefined) {
|
|
58
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
59
|
+
} else {
|
|
60
|
+
Object.defineProperty(globalThis, "window", originalWindow);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("allows keyless construction and resolves the selected account once upload begins", async () => {
|
|
65
|
+
const manager = new TestUploadManager({ authConfigId: "customer" });
|
|
66
|
+
await expect(manager.upload({ data: "x" })).rejects.toThrow("No active AuthManager configuration");
|
|
67
|
+
|
|
68
|
+
setSession(state("customer", accountA));
|
|
69
|
+
const uploadApi = installUploadApiMock(manager);
|
|
70
|
+
await manager.upload({ data: "x" });
|
|
71
|
+
|
|
72
|
+
expect(uploadApi.beginMultipartUpload).toHaveBeenCalledWith(expect.objectContaining({ accountId: accountA }));
|
|
73
|
+
expect(uploadApi.completeUploadPart).toHaveBeenCalledWith(expect.objectContaining({ accountId: accountA }));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("fails before upload network work when keyless AuthManager auth is explicitly disabled", async () => {
|
|
77
|
+
const manager = new TestUploadManager({ authConfigId: false });
|
|
78
|
+
const uploadApi = installUploadApiMock(manager);
|
|
79
|
+
|
|
80
|
+
await expect(manager.upload({ data: "x" })).rejects.toThrow("provide an API key");
|
|
81
|
+
expect(uploadApi.beginMultipartUpload).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
interface UploadApiMock {
|
|
86
|
+
beginMultipartUpload: jest.MockedFunction<(request: { accountId: string }) => Promise<BeginMultipartUploadResponse>>;
|
|
87
|
+
completeUploadPart: jest.MockedFunction<(request: { accountId: string }) => Promise<CompleteMultipartUploadResponse>>;
|
|
88
|
+
getUploadPart: jest.MockedFunction<(request: { accountId: string }) => Promise<UploadPart>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function installUploadApiMock(manager: TestUploadManager): UploadApiMock {
|
|
92
|
+
const firstPart: UploadPart = {
|
|
93
|
+
range: { inclusiveEnd: 0, inclusiveStart: 0 },
|
|
94
|
+
uploadId: "upload-id",
|
|
95
|
+
uploadPartIndex: 0,
|
|
96
|
+
uploadUrl: "https://upload.example.com/part"
|
|
97
|
+
};
|
|
98
|
+
const beginResponse: BeginMultipartUploadResponse = {
|
|
99
|
+
file: {
|
|
100
|
+
accountId: accountA,
|
|
101
|
+
etag: null,
|
|
102
|
+
filePath: "/file.txt",
|
|
103
|
+
fileUrl: `https://upcdn.io/${accountA}/raw/file.txt`,
|
|
104
|
+
lastModified: { tYPE: "EpochMillis" },
|
|
105
|
+
metadata: {},
|
|
106
|
+
mime: "text/plain",
|
|
107
|
+
originalFileName: null,
|
|
108
|
+
size: 1,
|
|
109
|
+
tags: []
|
|
110
|
+
},
|
|
111
|
+
uploadId: "upload-id",
|
|
112
|
+
uploadParts: { count: 1, first: firstPart }
|
|
113
|
+
};
|
|
114
|
+
const uploadApi: UploadApiMock = {
|
|
115
|
+
beginMultipartUpload: jest.fn<(request: { accountId: string }) => Promise<BeginMultipartUploadResponse>>(
|
|
116
|
+
async _request => beginResponse
|
|
117
|
+
),
|
|
118
|
+
completeUploadPart: jest.fn<(request: { accountId: string }) => Promise<CompleteMultipartUploadResponse>>(
|
|
119
|
+
async (_request): Promise<CompleteMultipartUploadResponse> => ({ etag: "etag", status: "Completed" })
|
|
120
|
+
),
|
|
121
|
+
getUploadPart: jest.fn<(request: { accountId: string }) => Promise<UploadPart>>(async _request => firstPart)
|
|
122
|
+
};
|
|
123
|
+
(manager as unknown as { uploadApi: UploadApiMock }).uploadApi = uploadApi;
|
|
124
|
+
return uploadApi;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function state(authConfigId: string, accountId: string): AuthSessionConfigState {
|
|
128
|
+
return {
|
|
129
|
+
accessToken: "access-token",
|
|
130
|
+
config: {
|
|
131
|
+
accountId,
|
|
132
|
+
authConfigId,
|
|
133
|
+
enableServiceWorkerAuth: false,
|
|
134
|
+
getAuthorizationToken: async () => "jwt"
|
|
135
|
+
},
|
|
136
|
+
expiresAt: Date.now() + 60_000,
|
|
137
|
+
jwt: "jwt",
|
|
138
|
+
refreshHandle: undefined
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setSession(configState: AuthSessionConfigState): void {
|
|
143
|
+
AuthSessionState.setSession({
|
|
144
|
+
accessToken: undefined,
|
|
145
|
+
accessTokenRefreshHandle: undefined,
|
|
146
|
+
authConfigs: [configState],
|
|
147
|
+
authServiceWorker: undefined,
|
|
148
|
+
isActive: true,
|
|
149
|
+
isReady: true,
|
|
150
|
+
params: {
|
|
151
|
+
authConfigs: async () => [configState.config],
|
|
152
|
+
serviceWorkerScript: undefined
|
|
153
|
+
},
|
|
154
|
+
serviceWorkerConfigured: false
|
|
155
|
+
});
|
|
156
|
+
}
|