@channel.io/app-sdk-server 0.7.3 → 0.8.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/__tests__/native-client.test.js +55 -1
- package/dist/__tests__/native-client.test.js.map +1 -1
- package/dist/__tests__/proxy-api.test.js +39 -3
- package/dist/__tests__/proxy-api.test.js.map +1 -1
- package/dist/__tests__/token-manager.test.js +330 -552
- package/dist/__tests__/token-manager.test.js.map +1 -1
- package/dist/appstore/client.d.ts +3 -0
- package/dist/appstore/client.d.ts.map +1 -1
- package/dist/appstore/client.js +3 -0
- package/dist/appstore/client.js.map +1 -1
- package/dist/native/client.d.ts +3 -1
- package/dist/native/client.d.ts.map +1 -1
- package/dist/native/client.js +0 -19
- package/dist/native/client.js.map +1 -1
- package/dist/native/proxy-api.d.ts +13 -1
- package/dist/native/proxy-api.d.ts.map +1 -1
- package/dist/native/proxy-api.js +18 -0
- package/dist/native/proxy-api.js.map +1 -1
- package/dist/native/proxy-api.types.d.ts +1 -231
- package/dist/native/proxy-api.types.d.ts.map +1 -1
- package/dist/native/proxy-api.types.js +0 -8
- package/dist/native/proxy-api.types.js.map +1 -1
- package/dist/nestjs/channel-app.module.d.ts.map +1 -1
- package/dist/nestjs/channel-app.module.js +51 -6
- package/dist/nestjs/channel-app.module.js.map +1 -1
- package/dist/nestjs/channel-app.service.d.ts +6 -14
- package/dist/nestjs/channel-app.service.d.ts.map +1 -1
- package/dist/nestjs/channel-app.service.js +52 -50
- package/dist/nestjs/channel-app.service.js.map +1 -1
- package/dist/nestjs/channel-app.service.test.js +121 -23
- package/dist/nestjs/channel-app.service.test.js.map +1 -1
- package/dist/nestjs/types.d.ts +10 -0
- package/dist/nestjs/types.d.ts.map +1 -1
- package/dist/nestjs/types.js.map +1 -1
- package/dist/simple/channel-app.d.ts +3 -0
- package/dist/simple/channel-app.d.ts.map +1 -1
- package/dist/simple/channel-app.js +11 -4
- package/dist/simple/channel-app.js.map +1 -1
- package/dist/simple/index.d.ts.map +1 -1
- package/dist/simple/index.js +1 -0
- package/dist/simple/index.js.map +1 -1
- package/dist/token/cache.js +3 -3
- package/dist/token/cache.js.map +1 -1
- package/dist/token/manager.d.ts +20 -114
- package/dist/token/manager.d.ts.map +1 -1
- package/dist/token/manager.js +151 -202
- package/dist/token/manager.js.map +1 -1
- package/dist/token/types.d.ts +13 -25
- package/dist/token/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,51 +1,43 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { TokenManager, TokenManagerError } from "../token/manager.js";
|
|
3
3
|
import { InMemoryTokenCache } from "../token/cache.js";
|
|
4
|
-
// ============================================
|
|
5
|
-
// Helpers
|
|
6
|
-
// ============================================
|
|
7
4
|
function makeTokenResponse(overrides = {}) {
|
|
8
5
|
return {
|
|
9
|
-
accessToken: overrides.accessToken ?? "access-token
|
|
10
|
-
refreshToken: overrides.refreshToken ?? "refresh-token
|
|
6
|
+
accessToken: overrides.accessToken ?? "access-token",
|
|
7
|
+
refreshToken: overrides.refreshToken ?? "refresh-token",
|
|
11
8
|
expiresIn: overrides.expiresIn ?? 3600,
|
|
12
|
-
expiresAt: overrides.expiresAt ?? Date.now() + 3600 * 1000,
|
|
13
|
-
refreshTokenExpiresAt: overrides.refreshTokenExpiresAt ?? Date.now() + 604800 * 1000,
|
|
14
|
-
tokenType: overrides.tokenType ?? "Bearer",
|
|
15
|
-
scopes: overrides.scopes ?? [],
|
|
16
9
|
};
|
|
17
10
|
}
|
|
18
|
-
function
|
|
11
|
+
function makeNativeFunctionResponse(overrides = {}) {
|
|
19
12
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
result: {
|
|
14
|
+
accessToken: overrides.accessToken ?? "access-token",
|
|
15
|
+
refreshToken: overrides.refreshToken ?? "refresh-token",
|
|
16
|
+
expiresIn: overrides.expiresIn ?? 3600,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function createResponse(body, ok = true, status = 200) {
|
|
21
|
+
return {
|
|
22
|
+
ok,
|
|
23
|
+
status,
|
|
24
|
+
statusText: ok ? "OK" : "Error",
|
|
25
|
+
json: async () => body,
|
|
26
|
+
text: async () => JSON.stringify(body),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
function createMockFetch(responses) {
|
|
30
|
-
if (responses.length === 0) {
|
|
31
|
-
throw new Error("createMockFetch requires at least one response");
|
|
32
|
-
}
|
|
33
30
|
let callIndex = 0;
|
|
34
31
|
return vi.fn(async () => {
|
|
35
32
|
const resp = (responses[callIndex] ?? responses[responses.length - 1]);
|
|
36
33
|
callIndex++;
|
|
37
|
-
return
|
|
38
|
-
ok: resp.ok,
|
|
39
|
-
status: resp.status,
|
|
40
|
-
statusText: resp.ok ? "OK" : "Error",
|
|
41
|
-
json: async () => resp.body,
|
|
42
|
-
text: async () => JSON.stringify(resp.body),
|
|
43
|
-
};
|
|
34
|
+
return createResponse(resp.body, resp.ok, resp.status);
|
|
44
35
|
});
|
|
45
36
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
function getRequest(index) {
|
|
38
|
+
const call = globalThis.fetch.mock.calls[index];
|
|
39
|
+
return JSON.parse(call[1].body);
|
|
40
|
+
}
|
|
49
41
|
describe("InMemoryTokenCache", () => {
|
|
50
42
|
let cache;
|
|
51
43
|
beforeEach(() => {
|
|
@@ -56,63 +48,41 @@ describe("InMemoryTokenCache", () => {
|
|
|
56
48
|
cache.destroy();
|
|
57
49
|
vi.useRealTimers();
|
|
58
50
|
});
|
|
59
|
-
it("
|
|
60
|
-
const result = await cache.get("nonexistent");
|
|
61
|
-
expect(result).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
it("should store and retrieve a cached token", async () => {
|
|
64
|
-
const token = makeTokenResponse();
|
|
65
|
-
const cached = { token, cachedAt: Date.now(), key: "test-key" };
|
|
66
|
-
await cache.set("test-key", cached);
|
|
67
|
-
const result = await cache.get("test-key");
|
|
68
|
-
expect(result).not.toBeNull();
|
|
69
|
-
expect(result.token.accessToken).toBe("access-token-123");
|
|
70
|
-
});
|
|
71
|
-
it("should respect TTL and expire entries", async () => {
|
|
51
|
+
it("stores, retrieves, expires, deletes, and clears cached tokens", async () => {
|
|
72
52
|
const token = makeTokenResponse();
|
|
73
|
-
const cached = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(await cache.get("
|
|
84
|
-
await cache.
|
|
85
|
-
expect(await cache.get("
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
await cache.set("
|
|
90
|
-
|
|
53
|
+
const cached = {
|
|
54
|
+
token,
|
|
55
|
+
cachedAt: Date.now(),
|
|
56
|
+
expiresAt: Date.now() + token.expiresIn * 1000,
|
|
57
|
+
key: "key",
|
|
58
|
+
};
|
|
59
|
+
expect(await cache.get("key")).toBeNull();
|
|
60
|
+
await cache.set("key", cached, 5000);
|
|
61
|
+
expect((await cache.get("key"))?.token.accessToken).toBe("access-token");
|
|
62
|
+
vi.advanceTimersByTime(6000);
|
|
63
|
+
expect(await cache.get("key")).toBeNull();
|
|
64
|
+
await cache.set("key", cached, 0);
|
|
65
|
+
expect(await cache.get("key")).toBeNull();
|
|
66
|
+
await cache.set("key", cached);
|
|
67
|
+
await cache.delete("key");
|
|
68
|
+
expect(await cache.get("key")).toBeNull();
|
|
69
|
+
await cache.set("a", {
|
|
70
|
+
token,
|
|
71
|
+
cachedAt: Date.now(),
|
|
72
|
+
expiresAt: cached.expiresAt,
|
|
73
|
+
key: "a",
|
|
74
|
+
});
|
|
75
|
+
await cache.set("b", {
|
|
76
|
+
token,
|
|
77
|
+
cachedAt: Date.now(),
|
|
78
|
+
expiresAt: cached.expiresAt,
|
|
79
|
+
key: "b",
|
|
80
|
+
});
|
|
91
81
|
expect(cache.getStats().size).toBe(2);
|
|
92
82
|
await cache.clear();
|
|
93
83
|
expect(cache.getStats().size).toBe(0);
|
|
94
84
|
});
|
|
95
|
-
it("should periodically cleanup expired entries", async () => {
|
|
96
|
-
const token = makeTokenResponse();
|
|
97
|
-
const cached = { token, cachedAt: Date.now(), key: "cleanup-key" };
|
|
98
|
-
await cache.set("cleanup-key", cached, 5000);
|
|
99
|
-
expect(cache.getStats().size).toBe(1);
|
|
100
|
-
vi.advanceTimersByTime(65000); // Past TTL + cleanup interval
|
|
101
|
-
expect(cache.getStats().size).toBe(0);
|
|
102
|
-
});
|
|
103
|
-
it("should report stats correctly", async () => {
|
|
104
|
-
const token = makeTokenResponse();
|
|
105
|
-
await cache.set("a", { token, cachedAt: Date.now(), key: "a" });
|
|
106
|
-
await cache.set("b", { token, cachedAt: Date.now(), key: "b" });
|
|
107
|
-
const stats = cache.getStats();
|
|
108
|
-
expect(stats.size).toBe(2);
|
|
109
|
-
expect(stats.keys).toContain("a");
|
|
110
|
-
expect(stats.keys).toContain("b");
|
|
111
|
-
});
|
|
112
85
|
});
|
|
113
|
-
// ============================================
|
|
114
|
-
// TokenManager Tests
|
|
115
|
-
// ============================================
|
|
116
86
|
describe("TokenManager", () => {
|
|
117
87
|
let originalFetch;
|
|
118
88
|
beforeEach(() => {
|
|
@@ -127,498 +97,306 @@ describe("TokenManager", () => {
|
|
|
127
97
|
return new TokenManager({
|
|
128
98
|
appId: "test-app",
|
|
129
99
|
appSecret: "test-secret",
|
|
130
|
-
debug: true,
|
|
131
100
|
...overrides,
|
|
132
101
|
});
|
|
133
102
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
-
it("should issue a manager token", async () => {
|
|
148
|
-
const token = makeTokenResponse({ accessToken: "mgr-token" });
|
|
149
|
-
globalThis.fetch = createMockFetch([{ ok: true, status: 200, body: makeApiResponse(token) }]);
|
|
150
|
-
const manager = createManager();
|
|
151
|
-
const result = await manager.getManagerToken({ channelId: "ch-1", managerId: "mgr-1" });
|
|
152
|
-
expect(result.accessToken).toBe("mgr-token");
|
|
153
|
-
manager.destroy();
|
|
154
|
-
});
|
|
155
|
-
it("should issue a user token", async () => {
|
|
156
|
-
const token = makeTokenResponse({ accessToken: "user-token" });
|
|
157
|
-
globalThis.fetch = createMockFetch([{ ok: true, status: 200, body: makeApiResponse(token) }]);
|
|
158
|
-
const manager = createManager();
|
|
159
|
-
const result = await manager.getUserToken({ channelId: "ch-1", userId: "usr-1" });
|
|
160
|
-
expect(result.accessToken).toBe("user-token");
|
|
161
|
-
manager.destroy();
|
|
162
|
-
});
|
|
163
|
-
it("should issue an app token (no channelId)", async () => {
|
|
164
|
-
const token = makeTokenResponse({ accessToken: "app-token" });
|
|
165
|
-
globalThis.fetch = createMockFetch([{ ok: true, status: 200, body: makeApiResponse(token) }]);
|
|
166
|
-
const manager = createManager();
|
|
167
|
-
const result = await manager.getAppToken();
|
|
168
|
-
expect(result.accessToken).toBe("app-token");
|
|
169
|
-
// Verify the API was called with the correct URL (no channelId in path)
|
|
170
|
-
const fetchCall = globalThis.fetch.mock.calls[0];
|
|
171
|
-
const url = fetchCall[0];
|
|
172
|
-
expect(url).toBe("https://api.channel.io/app/v1/token");
|
|
173
|
-
manager.destroy();
|
|
174
|
-
});
|
|
175
|
-
it("should throw TokenManagerError on API failure", async () => {
|
|
176
|
-
globalThis.fetch = createMockFetch([
|
|
177
|
-
{ ok: false, status: 401, body: { error: "Unauthorized" } },
|
|
178
|
-
]);
|
|
179
|
-
const manager = createManager();
|
|
180
|
-
await expect(manager.getChannelToken({ channelId: "ch-1" })).rejects.toThrow(TokenManagerError);
|
|
181
|
-
manager.destroy();
|
|
103
|
+
it("issues app and channel tokens through AppStore native functions using PUT", async () => {
|
|
104
|
+
globalThis.fetch = createMockFetch([
|
|
105
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "app-token" }) },
|
|
106
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "channel-token" }) },
|
|
107
|
+
]);
|
|
108
|
+
const manager = createManager();
|
|
109
|
+
const appToken = await manager.getAppToken();
|
|
110
|
+
const channelToken = await manager.getChannelToken({ channelId: "ch-1" });
|
|
111
|
+
expect(appToken).toEqual({
|
|
112
|
+
accessToken: "app-token",
|
|
113
|
+
refreshToken: "refresh-token",
|
|
114
|
+
expiresIn: 3600,
|
|
182
115
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
describe("cache hit", () => {
|
|
188
|
-
it("should return cached token on second call without API request", async () => {
|
|
189
|
-
const token = makeTokenResponse();
|
|
190
|
-
globalThis.fetch = createMockFetch([{ ok: true, status: 200, body: makeApiResponse(token) }]);
|
|
191
|
-
const manager = createManager();
|
|
192
|
-
// First call - cache miss
|
|
193
|
-
await manager.getChannelToken({ channelId: "ch-1" });
|
|
194
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
195
|
-
// Second call - cache hit
|
|
196
|
-
const result = await manager.getChannelToken({ channelId: "ch-1" });
|
|
197
|
-
expect(result.accessToken).toBe("access-token-123");
|
|
198
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(1); // No additional API call
|
|
199
|
-
manager.destroy();
|
|
200
|
-
});
|
|
201
|
-
it("should use separate cache entries for different channelIds", async () => {
|
|
202
|
-
const token1 = makeTokenResponse({ accessToken: "token-ch1" });
|
|
203
|
-
const token2 = makeTokenResponse({ accessToken: "token-ch2" });
|
|
204
|
-
globalThis.fetch = createMockFetch([
|
|
205
|
-
{ ok: true, status: 200, body: makeApiResponse(token1) },
|
|
206
|
-
{ ok: true, status: 200, body: makeApiResponse(token2) },
|
|
207
|
-
]);
|
|
208
|
-
const manager = createManager();
|
|
209
|
-
const r1 = await manager.getChannelToken({ channelId: "ch-1" });
|
|
210
|
-
const r2 = await manager.getChannelToken({ channelId: "ch-2" });
|
|
211
|
-
expect(r1.accessToken).toBe("token-ch1");
|
|
212
|
-
expect(r2.accessToken).toBe("token-ch2");
|
|
213
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
214
|
-
manager.destroy();
|
|
116
|
+
expect(channelToken).toEqual({
|
|
117
|
+
accessToken: "channel-token",
|
|
118
|
+
refreshToken: "refresh-token",
|
|
119
|
+
expiresIn: 3600,
|
|
215
120
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
]);
|
|
223
|
-
const manager = createManager();
|
|
224
|
-
const r1 = await manager.getAppToken();
|
|
225
|
-
const r2 = await manager.getChannelToken({ channelId: "ch-1" });
|
|
226
|
-
expect(r1.accessToken).toBe("app-token");
|
|
227
|
-
expect(r2.accessToken).toBe("ch-token");
|
|
228
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
229
|
-
// Second call to getAppToken should be cached
|
|
230
|
-
const r3 = await manager.getAppToken();
|
|
231
|
-
expect(r3.accessToken).toBe("app-token");
|
|
232
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
233
|
-
manager.destroy();
|
|
121
|
+
const calls = globalThis.fetch.mock.calls;
|
|
122
|
+
expect(calls[0][0]).toBe("https://app-store.channel.io/general/v1/native/functions");
|
|
123
|
+
expect(calls[0][1].method).toBe("PUT");
|
|
124
|
+
expect(getRequest(0)).toEqual({
|
|
125
|
+
method: "issueToken",
|
|
126
|
+
params: { secret: "test-secret" },
|
|
234
127
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
globalThis.fetch = createMockFetch([
|
|
239
|
-
{ ok: true, status: 200, body: makeApiResponse(channelToken) },
|
|
240
|
-
{ ok: true, status: 200, body: makeApiResponse(managerToken) },
|
|
241
|
-
]);
|
|
242
|
-
const manager = createManager();
|
|
243
|
-
const r1 = await manager.getChannelToken({ channelId: "ch-1" });
|
|
244
|
-
const r2 = await manager.getManagerToken({ channelId: "ch-1", managerId: "mgr-1" });
|
|
245
|
-
expect(r1.accessToken).toBe("ch-token");
|
|
246
|
-
expect(r2.accessToken).toBe("mgr-token");
|
|
247
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
248
|
-
manager.destroy();
|
|
128
|
+
expect(getRequest(1)).toEqual({
|
|
129
|
+
method: "issueToken",
|
|
130
|
+
params: { secret: "test-secret", channelId: "ch-1" },
|
|
249
131
|
});
|
|
132
|
+
manager.destroy();
|
|
250
133
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
ok: true,
|
|
274
|
-
status: 200,
|
|
275
|
-
statusText: "OK",
|
|
276
|
-
json: async () => makeApiResponse(expiringToken),
|
|
277
|
-
text: async () => JSON.stringify(makeApiResponse(expiringToken)),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
if (urlStr.includes("/token/refresh")) {
|
|
281
|
-
// Refresh call
|
|
282
|
-
return {
|
|
283
|
-
ok: true,
|
|
284
|
-
status: 200,
|
|
285
|
-
statusText: "OK",
|
|
286
|
-
json: async () => makeApiResponse(refreshedToken),
|
|
287
|
-
text: async () => JSON.stringify(makeApiResponse(refreshedToken)),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
// Fallback re-issue
|
|
291
|
-
return {
|
|
292
|
-
ok: true,
|
|
293
|
-
status: 200,
|
|
294
|
-
statusText: "OK",
|
|
295
|
-
json: async () => makeApiResponse(refreshedToken),
|
|
296
|
-
text: async () => JSON.stringify(makeApiResponse(refreshedToken)),
|
|
297
|
-
};
|
|
298
|
-
});
|
|
299
|
-
const manager = createManager({ refreshBufferMs: 5 * 60 * 1000 });
|
|
300
|
-
// First call issues token
|
|
301
|
-
const first = await manager.getChannelToken({ channelId: "ch-1" });
|
|
302
|
-
expect(first.accessToken).toBe("old-token");
|
|
303
|
-
// Second call should trigger refresh because token expires within buffer
|
|
304
|
-
const second = await manager.getChannelToken({ channelId: "ch-1" });
|
|
305
|
-
expect(second.accessToken).toBe("refreshed-token");
|
|
306
|
-
manager.destroy();
|
|
307
|
-
});
|
|
308
|
-
it("should re-issue token when refresh fails", async () => {
|
|
309
|
-
const now = Date.now();
|
|
310
|
-
// Token that expires within buffer
|
|
311
|
-
const expiringToken = makeTokenResponse({
|
|
312
|
-
accessToken: "old-token",
|
|
313
|
-
expiresAt: now + 2 * 60 * 1000,
|
|
314
|
-
});
|
|
315
|
-
const newToken = makeTokenResponse({
|
|
316
|
-
accessToken: "new-token",
|
|
317
|
-
expiresAt: now + 3600 * 1000,
|
|
318
|
-
});
|
|
319
|
-
let callCount = 0;
|
|
320
|
-
globalThis.fetch = vi.fn(async (url) => {
|
|
321
|
-
callCount++;
|
|
322
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
323
|
-
if (callCount === 1) {
|
|
324
|
-
// Initial issue
|
|
325
|
-
return {
|
|
326
|
-
ok: true,
|
|
327
|
-
status: 200,
|
|
328
|
-
statusText: "OK",
|
|
329
|
-
json: async () => makeApiResponse(expiringToken),
|
|
330
|
-
text: async () => JSON.stringify(makeApiResponse(expiringToken)),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
if (urlStr.includes("/token/refresh")) {
|
|
334
|
-
// Refresh fails
|
|
335
|
-
return {
|
|
336
|
-
ok: false,
|
|
337
|
-
status: 401,
|
|
338
|
-
statusText: "Unauthorized",
|
|
339
|
-
json: async () => ({}),
|
|
340
|
-
text: async () => "Unauthorized",
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
// Re-issue succeeds
|
|
344
|
-
return {
|
|
345
|
-
ok: true,
|
|
346
|
-
status: 200,
|
|
347
|
-
statusText: "OK",
|
|
348
|
-
json: async () => makeApiResponse(newToken),
|
|
349
|
-
text: async () => JSON.stringify(makeApiResponse(newToken)),
|
|
350
|
-
};
|
|
351
|
-
});
|
|
352
|
-
const manager = createManager({ refreshBufferMs: 5 * 60 * 1000 });
|
|
353
|
-
await manager.getChannelToken({ channelId: "ch-1" });
|
|
354
|
-
const result = await manager.getChannelToken({ channelId: "ch-1" });
|
|
355
|
-
expect(result.accessToken).toBe("new-token");
|
|
356
|
-
manager.destroy();
|
|
134
|
+
it("uses cached tokens until they enter the refresh buffer", async () => {
|
|
135
|
+
globalThis.fetch = createMockFetch([
|
|
136
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse() },
|
|
137
|
+
{
|
|
138
|
+
ok: true,
|
|
139
|
+
status: 200,
|
|
140
|
+
body: makeNativeFunctionResponse({ accessToken: "refreshed-token" }),
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
const manager = createManager();
|
|
144
|
+
await manager.getChannelToken({ channelId: "ch-1" });
|
|
145
|
+
const cached = await manager.getChannelToken({ channelId: "ch-1" });
|
|
146
|
+
expect(cached.accessToken).toBe("access-token");
|
|
147
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
148
|
+
vi.advanceTimersByTime(56 * 60 * 1000);
|
|
149
|
+
const refreshed = await manager.getChannelToken({ channelId: "ch-1" });
|
|
150
|
+
expect(refreshed.accessToken).toBe("refreshed-token");
|
|
151
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(getRequest(1)).toEqual({
|
|
153
|
+
method: "refreshToken",
|
|
154
|
+
params: { refreshToken: "refresh-token" },
|
|
357
155
|
});
|
|
156
|
+
manager.destroy();
|
|
358
157
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
let resolvePromise;
|
|
365
|
-
const pendingPromise = new Promise((resolve) => {
|
|
366
|
-
resolvePromise = resolve;
|
|
367
|
-
});
|
|
368
|
-
const token = makeTokenResponse({ accessToken: "deduped-token" });
|
|
369
|
-
let callCount = 0;
|
|
370
|
-
globalThis.fetch = vi.fn(() => {
|
|
371
|
-
callCount++;
|
|
372
|
-
if (callCount === 1) {
|
|
373
|
-
return pendingPromise;
|
|
374
|
-
}
|
|
375
|
-
// Should never reach here for the same key
|
|
376
|
-
return Promise.resolve({
|
|
377
|
-
ok: true,
|
|
378
|
-
status: 200,
|
|
379
|
-
statusText: "OK",
|
|
380
|
-
json: async () => makeApiResponse(makeTokenResponse({ accessToken: "second-token" })),
|
|
381
|
-
text: async () => "",
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
const manager = createManager();
|
|
385
|
-
// Fire 3 concurrent requests for the same channel
|
|
386
|
-
const p1 = manager.getChannelToken({ channelId: "ch-1" });
|
|
387
|
-
const p2 = manager.getChannelToken({ channelId: "ch-1" });
|
|
388
|
-
const p3 = manager.getChannelToken({ channelId: "ch-1" });
|
|
389
|
-
// Resolve the single pending fetch
|
|
390
|
-
resolvePromise({
|
|
158
|
+
it("falls back to issueToken when refreshToken fails", async () => {
|
|
159
|
+
globalThis.fetch = createMockFetch([
|
|
160
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse() },
|
|
161
|
+
{ ok: true, status: 200, body: { error: { code: 401, message: "Unauthorized" } } },
|
|
162
|
+
{
|
|
391
163
|
ok: true,
|
|
392
164
|
status: 200,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
});
|
|
406
|
-
it("should not deduplicate requests for different keys", async () => {
|
|
407
|
-
const token1 = makeTokenResponse({ accessToken: "token-1" });
|
|
408
|
-
const token2 = makeTokenResponse({ accessToken: "token-2" });
|
|
409
|
-
globalThis.fetch = createMockFetch([
|
|
410
|
-
{ ok: true, status: 200, body: makeApiResponse(token1) },
|
|
411
|
-
{ ok: true, status: 200, body: makeApiResponse(token2) },
|
|
412
|
-
]);
|
|
413
|
-
const manager = createManager();
|
|
414
|
-
const [r1, r2] = await Promise.all([
|
|
415
|
-
manager.getChannelToken({ channelId: "ch-1" }),
|
|
416
|
-
manager.getChannelToken({ channelId: "ch-2" }),
|
|
417
|
-
]);
|
|
418
|
-
expect(r1.accessToken).toBe("token-1");
|
|
419
|
-
expect(r2.accessToken).toBe("token-2");
|
|
420
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
421
|
-
manager.destroy();
|
|
422
|
-
});
|
|
423
|
-
it("should allow new requests after in-flight promise resolves", async () => {
|
|
424
|
-
const token1 = makeTokenResponse({ accessToken: "first" });
|
|
425
|
-
const token2 = makeTokenResponse({ accessToken: "second" });
|
|
426
|
-
let callCount = 0;
|
|
427
|
-
globalThis.fetch = vi.fn(async () => {
|
|
428
|
-
callCount++;
|
|
429
|
-
const token = callCount === 1 ? token1 : token2;
|
|
430
|
-
return {
|
|
431
|
-
ok: true,
|
|
432
|
-
status: 200,
|
|
433
|
-
statusText: "OK",
|
|
434
|
-
json: async () => makeApiResponse(token),
|
|
435
|
-
text: async () => JSON.stringify(makeApiResponse(token)),
|
|
436
|
-
};
|
|
437
|
-
});
|
|
438
|
-
const manager = createManager();
|
|
439
|
-
// First request
|
|
440
|
-
const r1 = await manager.getChannelToken({ channelId: "ch-1" });
|
|
441
|
-
expect(r1.accessToken).toBe("first");
|
|
442
|
-
// Invalidate cache to force new request
|
|
443
|
-
await manager.invalidateToken("channel", "ch-1");
|
|
444
|
-
// Second request should make a new API call
|
|
445
|
-
const r2 = await manager.getChannelToken({ channelId: "ch-1" });
|
|
446
|
-
expect(r2.accessToken).toBe("second");
|
|
447
|
-
expect(callCount).toBe(2);
|
|
448
|
-
manager.destroy();
|
|
449
|
-
});
|
|
450
|
-
it("should clean up in-flight promise on error", async () => {
|
|
451
|
-
let callCount = 0;
|
|
452
|
-
const token = makeTokenResponse();
|
|
453
|
-
globalThis.fetch = vi.fn(async () => {
|
|
454
|
-
callCount++;
|
|
455
|
-
if (callCount === 1) {
|
|
456
|
-
return {
|
|
457
|
-
ok: false,
|
|
458
|
-
status: 500,
|
|
459
|
-
statusText: "Internal Server Error",
|
|
460
|
-
json: async () => ({}),
|
|
461
|
-
text: async () => "Server Error",
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
return {
|
|
465
|
-
ok: true,
|
|
466
|
-
status: 200,
|
|
467
|
-
statusText: "OK",
|
|
468
|
-
json: async () => makeApiResponse(token),
|
|
469
|
-
text: async () => JSON.stringify(makeApiResponse(token)),
|
|
470
|
-
};
|
|
471
|
-
});
|
|
472
|
-
const manager = createManager();
|
|
473
|
-
// First request fails
|
|
474
|
-
await expect(manager.getChannelToken({ channelId: "ch-1" })).rejects.toThrow(TokenManagerError);
|
|
475
|
-
// Second request should work (in-flight was cleaned up)
|
|
476
|
-
const result = await manager.getChannelToken({ channelId: "ch-1" });
|
|
477
|
-
expect(result.accessToken).toBe("access-token-123");
|
|
478
|
-
expect(callCount).toBe(2);
|
|
479
|
-
manager.destroy();
|
|
480
|
-
});
|
|
165
|
+
body: makeNativeFunctionResponse({ accessToken: "reissued-token" }),
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
const manager = createManager();
|
|
169
|
+
await manager.getAppToken();
|
|
170
|
+
vi.advanceTimersByTime(56 * 60 * 1000);
|
|
171
|
+
const token = await manager.getAppToken();
|
|
172
|
+
expect(token.accessToken).toBe("reissued-token");
|
|
173
|
+
expect(getRequest(0).method).toBe("issueToken");
|
|
174
|
+
expect(getRequest(1).method).toBe("refreshToken");
|
|
175
|
+
expect(getRequest(2).method).toBe("issueToken");
|
|
176
|
+
manager.destroy();
|
|
481
177
|
});
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
{ ok: true, status: 200, body: makeApiResponse(token) },
|
|
523
|
-
{ ok: true, status: 200, body: makeApiResponse(token) },
|
|
524
|
-
{ ok: true, status: 200, body: makeApiResponse(token) },
|
|
525
|
-
{ ok: true, status: 200, body: makeApiResponse(token) },
|
|
526
|
-
]);
|
|
527
|
-
const manager = createManager();
|
|
528
|
-
await manager.getChannelToken({ channelId: "ch-1" });
|
|
529
|
-
await manager.getChannelToken({ channelId: "ch-2" });
|
|
530
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
531
|
-
await manager.clearCache();
|
|
532
|
-
// Both should require new API calls
|
|
533
|
-
await manager.getChannelToken({ channelId: "ch-1" });
|
|
534
|
-
await manager.getChannelToken({ channelId: "ch-2" });
|
|
535
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(4);
|
|
536
|
-
manager.destroy();
|
|
537
|
-
});
|
|
178
|
+
it("deduplicates concurrent requests for the same cache key", async () => {
|
|
179
|
+
globalThis.fetch = vi.fn(() => new Promise((resolve) => {
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
resolve(createResponse(makeNativeFunctionResponse()));
|
|
182
|
+
}, 100);
|
|
183
|
+
}));
|
|
184
|
+
const manager = createManager();
|
|
185
|
+
const p1 = manager.getChannelToken({ channelId: "ch-1" });
|
|
186
|
+
const p2 = manager.getChannelToken({ channelId: "ch-1" });
|
|
187
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
188
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
189
|
+
expect(r1.accessToken).toBe("access-token");
|
|
190
|
+
expect(r2.accessToken).toBe("access-token");
|
|
191
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
192
|
+
manager.destroy();
|
|
193
|
+
});
|
|
194
|
+
it("retries instead of returning an issued token invalidated during the request", async () => {
|
|
195
|
+
let requestCount = 0;
|
|
196
|
+
globalThis.fetch = vi.fn(() => new Promise((resolve) => {
|
|
197
|
+
requestCount++;
|
|
198
|
+
const accessToken = requestCount === 1 ? "stale-token" : "fresh-token";
|
|
199
|
+
const response = createResponse(makeNativeFunctionResponse({ accessToken }));
|
|
200
|
+
if (requestCount === 1) {
|
|
201
|
+
setTimeout(() => resolve(response), 100);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
resolve(response);
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
const manager = createManager();
|
|
208
|
+
const tokenPromise = manager.getAppToken();
|
|
209
|
+
await Promise.resolve();
|
|
210
|
+
await Promise.resolve();
|
|
211
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
212
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
213
|
+
await manager.invalidateAppToken();
|
|
214
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
215
|
+
await expect(tokenPromise).resolves.toMatchObject({ accessToken: "fresh-token" });
|
|
216
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
217
|
+
manager.destroy();
|
|
538
218
|
});
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
219
|
+
it("fails when token issuance keeps getting invalidated past the retry limit", async () => {
|
|
220
|
+
globalThis.fetch = vi.fn(() => new Promise((resolve) => {
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
resolve(createResponse(makeNativeFunctionResponse({ accessToken: "stale-token" })));
|
|
223
|
+
}, 100);
|
|
224
|
+
}));
|
|
225
|
+
const manager = createManager();
|
|
226
|
+
async function invalidateActiveRequest() {
|
|
227
|
+
await Promise.resolve();
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
230
|
+
await manager.invalidateAppToken();
|
|
231
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
232
|
+
await Promise.resolve();
|
|
233
|
+
await Promise.resolve();
|
|
234
|
+
}
|
|
235
|
+
const tokenPromise = manager.getAppToken();
|
|
236
|
+
await invalidateActiveRequest();
|
|
237
|
+
await invalidateActiveRequest();
|
|
238
|
+
const rejection = expect(tokenPromise).rejects.toThrow("retry limit exceeded");
|
|
239
|
+
await invalidateActiveRequest();
|
|
240
|
+
await rejection;
|
|
241
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(3);
|
|
242
|
+
manager.destroy();
|
|
243
|
+
});
|
|
244
|
+
it("retries instead of returning a refreshed token invalidated during the request", async () => {
|
|
245
|
+
let issueCount = 0;
|
|
246
|
+
globalThis.fetch = vi.fn((_url, init) => {
|
|
247
|
+
const request = JSON.parse(init.body);
|
|
248
|
+
if (request.method === "refreshToken") {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
resolve(createResponse(makeNativeFunctionResponse({
|
|
252
|
+
accessToken: "stale-refreshed-token",
|
|
253
|
+
refreshToken: "stale-refresh-token",
|
|
254
|
+
})));
|
|
255
|
+
}, 100);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
issueCount++;
|
|
259
|
+
const response = issueCount === 1
|
|
260
|
+
? makeNativeFunctionResponse({
|
|
261
|
+
accessToken: "old-token",
|
|
262
|
+
refreshToken: "old-refresh-token",
|
|
263
|
+
})
|
|
264
|
+
: makeNativeFunctionResponse({
|
|
265
|
+
accessToken: "fresh-token",
|
|
266
|
+
refreshToken: "fresh-refresh-token",
|
|
267
|
+
});
|
|
268
|
+
return Promise.resolve(createResponse(response));
|
|
552
269
|
});
|
|
270
|
+
const manager = createManager();
|
|
271
|
+
await expect(manager.getAppToken()).resolves.toMatchObject({ accessToken: "old-token" });
|
|
272
|
+
vi.advanceTimersByTime(56 * 60 * 1000);
|
|
273
|
+
const tokenPromise = manager.getAppToken();
|
|
274
|
+
await Promise.resolve();
|
|
275
|
+
await Promise.resolve();
|
|
276
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
277
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
278
|
+
await manager.invalidateAppToken();
|
|
279
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
280
|
+
await expect(tokenPromise).resolves.toMatchObject({ accessToken: "fresh-token" });
|
|
281
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(3);
|
|
282
|
+
expect(getRequest(1).method).toBe("refreshToken");
|
|
283
|
+
expect(getRequest(2).method).toBe("issueToken");
|
|
284
|
+
manager.destroy();
|
|
285
|
+
});
|
|
286
|
+
it("invalidates and clears cached tokens", async () => {
|
|
287
|
+
globalThis.fetch = createMockFetch([
|
|
288
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "token-1" }) },
|
|
289
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "token-2" }) },
|
|
290
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "token-3" }) },
|
|
291
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "token-4" }) },
|
|
292
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse({ accessToken: "token-5" }) },
|
|
293
|
+
]);
|
|
294
|
+
const manager = createManager();
|
|
295
|
+
await manager.getChannelToken({ channelId: "ch-1" });
|
|
296
|
+
await manager.invalidateChannelToken("ch-1");
|
|
297
|
+
expect((await manager.getChannelToken({ channelId: "ch-1" })).accessToken).toBe("token-2");
|
|
298
|
+
await manager.getAppToken();
|
|
299
|
+
await manager.invalidateAppToken();
|
|
300
|
+
expect((await manager.getAppToken()).accessToken).toBe("token-4");
|
|
301
|
+
await manager.clearCache();
|
|
302
|
+
expect((await manager.getAppToken()).accessToken).toBe("token-5");
|
|
303
|
+
manager.destroy();
|
|
553
304
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
305
|
+
it("does not log token-bearing native error bodies", async () => {
|
|
306
|
+
const logger = { debug: vi.fn() };
|
|
307
|
+
globalThis.fetch = createMockFetch([
|
|
308
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse() },
|
|
309
|
+
{
|
|
310
|
+
ok: true,
|
|
311
|
+
status: 200,
|
|
312
|
+
body: {
|
|
313
|
+
error: {
|
|
314
|
+
code: 401,
|
|
315
|
+
message: "Unauthorized",
|
|
316
|
+
data: {
|
|
317
|
+
accessToken: "secret-access-token",
|
|
318
|
+
refreshToken: "secret-refresh-token",
|
|
319
|
+
},
|
|
569
320
|
},
|
|
570
321
|
},
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
ok: true,
|
|
325
|
+
status: 200,
|
|
326
|
+
body: makeNativeFunctionResponse({ accessToken: "reissued-token" }),
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
const manager = createManager({ debug: true, logger });
|
|
330
|
+
await manager.getAppToken();
|
|
331
|
+
vi.advanceTimersByTime(56 * 60 * 1000);
|
|
332
|
+
await manager.getAppToken();
|
|
333
|
+
const logPayload = JSON.stringify(logger.debug.mock.calls);
|
|
334
|
+
expect(logPayload).toContain("Unauthorized");
|
|
335
|
+
expect(logPayload).not.toContain("secret-access-token");
|
|
336
|
+
expect(logPayload).not.toContain("secret-refresh-token");
|
|
337
|
+
manager.destroy();
|
|
581
338
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
});
|
|
339
|
+
it("times out hung native function requests and clears in-flight state", async () => {
|
|
340
|
+
globalThis.fetch = vi
|
|
341
|
+
.fn()
|
|
342
|
+
.mockImplementationOnce((_url, init) => {
|
|
343
|
+
return new Promise((_resolve, reject) => {
|
|
344
|
+
const signal = init.signal;
|
|
345
|
+
signal?.addEventListener("abort", () => {
|
|
346
|
+
const error = new Error("The operation was aborted.");
|
|
347
|
+
error.name = "AbortError";
|
|
348
|
+
reject(error);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
})
|
|
352
|
+
.mockResolvedValueOnce(createResponse(makeNativeFunctionResponse({ accessToken: "after-timeout" })));
|
|
353
|
+
const manager = createManager();
|
|
354
|
+
const timedOut = manager.getAppToken();
|
|
355
|
+
await Promise.resolve();
|
|
356
|
+
await Promise.resolve();
|
|
357
|
+
const rejection = expect(timedOut).rejects.toMatchObject({ name: "AbortError" });
|
|
358
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
359
|
+
await rejection;
|
|
360
|
+
await expect(manager.getAppToken()).resolves.toMatchObject({ accessToken: "after-timeout" });
|
|
361
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
362
|
+
manager.destroy();
|
|
601
363
|
});
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
364
|
+
it("throws TokenManagerError for transport and native function errors", async () => {
|
|
365
|
+
globalThis.fetch = createMockFetch([{ ok: false, status: 500, body: "server error" }]);
|
|
366
|
+
const manager = createManager();
|
|
367
|
+
await expect(manager.getAppToken()).rejects.toThrow(TokenManagerError);
|
|
368
|
+
manager.destroy();
|
|
369
|
+
globalThis.fetch = createMockFetch([
|
|
370
|
+
{ ok: true, status: 200, body: { error: { code: 401, message: "Unauthorized" } } },
|
|
371
|
+
]);
|
|
372
|
+
const errorManager = createManager();
|
|
373
|
+
await expect(errorManager.getAppToken()).rejects.toThrow(TokenManagerError);
|
|
374
|
+
errorManager.destroy();
|
|
375
|
+
globalThis.fetch = createMockFetch([
|
|
376
|
+
{
|
|
377
|
+
ok: true,
|
|
378
|
+
status: 200,
|
|
379
|
+
body: { result: { refreshToken: "refresh-token", expiresIn: 3600 } },
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
const invalidResponseManager = createManager();
|
|
383
|
+
await expect(invalidResponseManager.getAppToken()).rejects.toThrow("Invalid native token response");
|
|
384
|
+
invalidResponseManager.destroy();
|
|
385
|
+
});
|
|
386
|
+
it("uses custom AppStore URL and cache storage", async () => {
|
|
387
|
+
const storage = new InMemoryTokenCache();
|
|
388
|
+
globalThis.fetch = createMockFetch([
|
|
389
|
+
{ ok: true, status: 200, body: makeNativeFunctionResponse() },
|
|
390
|
+
]);
|
|
391
|
+
const manager = createManager({
|
|
392
|
+
appStoreUrl: "https://app-store.example.com",
|
|
393
|
+
cacheStorage: storage,
|
|
621
394
|
});
|
|
395
|
+
await manager.getAppToken();
|
|
396
|
+
expect(globalThis.fetch.mock.calls[0][0]).toBe("https://app-store.example.com/general/v1/native/functions");
|
|
397
|
+
expect(storage.getStats().keys).toEqual(["test-app:app"]);
|
|
398
|
+
manager.destroy();
|
|
399
|
+
storage.destroy();
|
|
622
400
|
});
|
|
623
401
|
});
|
|
624
402
|
//# sourceMappingURL=token-manager.test.js.map
|