@codybrom/denim 1.3.6 → 2.0.1
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/.github/workflows/publish.yml +17 -7
- package/.vscode/settings.json +34 -9
- package/CHANGELOG.md +137 -0
- package/deno.json +22 -8
- package/deno.lock +17 -59
- package/examples/edge-function.ts +171 -177
- package/mod.ts +138 -635
- package/mod_test.ts +1287 -431
- package/package.json +22 -22
- package/readme.md +155 -191
- package/src/api/createCarouselItem.ts +86 -0
- package/src/api/createThreadsContainer.ts +122 -0
- package/src/api/debugToken.ts +35 -0
- package/src/api/deleteThread.ts +36 -0
- package/src/api/exchangeCodeForToken.ts +50 -0
- package/src/api/exchangeToken.ts +36 -0
- package/src/api/getAppAccessToken.ts +35 -0
- package/src/api/getConversation.ts +51 -0
- package/src/api/getGhostPosts.ts +50 -0
- package/src/api/getLocation.ts +38 -0
- package/src/api/getMediaInsights.ts +39 -0
- package/src/api/getMentions.ts +57 -0
- package/src/api/getOEmbed.ts +41 -0
- package/src/api/getProfile.ts +46 -0
- package/src/api/getProfilePosts.ts +53 -0
- package/src/api/getPublishingLimit.ts +59 -0
- package/src/api/getReplies.ts +51 -0
- package/src/api/getSingleThread.ts +37 -0
- package/src/api/getThreadsList.ts +49 -0
- package/src/api/getUserInsights.ts +54 -0
- package/src/api/getUserReplies.ts +54 -0
- package/src/api/lookupProfile.ts +53 -0
- package/src/api/manageReply.ts +41 -0
- package/src/api/publishThreadsContainer.ts +107 -0
- package/src/api/refreshToken.ts +33 -0
- package/src/api/repost.ts +38 -0
- package/src/api/searchKeyword.ts +86 -0
- package/src/api/searchLocations.ts +46 -0
- package/src/constants.ts +80 -0
- package/src/types.ts +925 -0
- package/src/utils/checkContainerStatus.ts +39 -0
- package/src/utils/getAPI.ts +13 -0
- package/src/utils/mock_threads_api.ts +582 -0
- package/src/utils/validateRequest.ts +166 -0
- package/mock_threads_api.ts +0 -174
- package/types.ts +0 -235
package/mod_test.ts
CHANGED
|
@@ -1,435 +1,1291 @@
|
|
|
1
1
|
// mod_test.ts
|
|
2
|
-
import
|
|
2
|
+
import { assertEquals, assertRejects } from "@std/assert";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
createCarouselItem,
|
|
5
|
+
createThreadsContainer,
|
|
6
|
+
debugToken,
|
|
7
|
+
deleteThread,
|
|
8
|
+
exchangeCodeForToken,
|
|
9
|
+
exchangeToken,
|
|
10
|
+
getAppAccessToken,
|
|
11
|
+
getConversation,
|
|
12
|
+
getGhostPosts,
|
|
13
|
+
getLocation,
|
|
14
|
+
getMediaInsights,
|
|
15
|
+
getMentions,
|
|
16
|
+
getOEmbed,
|
|
17
|
+
getProfile,
|
|
18
|
+
getProfilePosts,
|
|
19
|
+
getPublishingLimit,
|
|
20
|
+
getReplies,
|
|
21
|
+
getSingleThread,
|
|
22
|
+
getThreadsList,
|
|
23
|
+
getUserInsights,
|
|
24
|
+
getUserReplies,
|
|
25
|
+
lookupProfile,
|
|
26
|
+
manageReply,
|
|
27
|
+
type MockThreadsAPI,
|
|
28
|
+
MockThreadsAPIImpl,
|
|
29
|
+
publishThreadsContainer,
|
|
30
|
+
refreshToken,
|
|
31
|
+
repost,
|
|
32
|
+
searchKeyword,
|
|
33
|
+
searchLocations,
|
|
34
|
+
type ThreadsPostRequest,
|
|
14
35
|
} from "./mod.ts";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
36
|
+
|
|
37
|
+
let mockAPI: MockThreadsAPIImpl;
|
|
38
|
+
|
|
39
|
+
const validUrl = {
|
|
40
|
+
image:
|
|
41
|
+
"https://file-examples.com/wp-content/storage/2017/10/file_example_PNG_3MB.png",
|
|
42
|
+
video:
|
|
43
|
+
"https://file-examples.com/wp-content/storage/2018/04/file_example_MOV_1280_1_4MB.mov",
|
|
44
|
+
link: "https://example.com",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const invalidUrl = {
|
|
48
|
+
link: "invalid_url",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function setupMockAPI() {
|
|
52
|
+
mockAPI = new MockThreadsAPIImpl();
|
|
53
|
+
(globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI = mockAPI;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function teardownMockAPI() {
|
|
57
|
+
delete (globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Deno.test("Denim API Tests", async (t) => {
|
|
61
|
+
// ─── createThreadsContainer ──────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
await t.step("createThreadsContainer", async (t) => {
|
|
64
|
+
await t.step("should return container ID for basic text post", async () => {
|
|
65
|
+
setupMockAPI();
|
|
66
|
+
const requestData: ThreadsPostRequest = {
|
|
67
|
+
userId: "12345",
|
|
68
|
+
accessToken: "token",
|
|
69
|
+
mediaType: "TEXT",
|
|
70
|
+
text: "Hello, Threads!",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await createThreadsContainer(requestData);
|
|
74
|
+
assertEquals(typeof result, "string");
|
|
75
|
+
assertEquals(result.length > 0, true);
|
|
76
|
+
teardownMockAPI();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await t.step("should handle image post with alt text", async () => {
|
|
80
|
+
setupMockAPI();
|
|
81
|
+
const requestData: ThreadsPostRequest = {
|
|
82
|
+
userId: "12345",
|
|
83
|
+
accessToken: "token",
|
|
84
|
+
mediaType: "IMAGE",
|
|
85
|
+
text: "Check out this image!",
|
|
86
|
+
imageUrl: validUrl.image,
|
|
87
|
+
altText: "A beautiful sunset",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const containerId = await createThreadsContainer(requestData);
|
|
91
|
+
assertEquals(typeof containerId, "string");
|
|
92
|
+
assertEquals(containerId.length > 0, true);
|
|
93
|
+
teardownMockAPI();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await t.step("should handle video post with all features", async () => {
|
|
97
|
+
setupMockAPI();
|
|
98
|
+
const requestData: ThreadsPostRequest = {
|
|
99
|
+
userId: "12345",
|
|
100
|
+
accessToken: "token",
|
|
101
|
+
mediaType: "VIDEO",
|
|
102
|
+
text: "Watch this video!",
|
|
103
|
+
videoUrl: validUrl.video,
|
|
104
|
+
altText: "A tutorial video",
|
|
105
|
+
replyControl: "mentioned_only",
|
|
106
|
+
allowlistedCountryCodes: ["US", "GB"],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const containerId = await createThreadsContainer(requestData);
|
|
110
|
+
assertEquals(typeof containerId, "string");
|
|
111
|
+
assertEquals(containerId.length > 0, true);
|
|
112
|
+
teardownMockAPI();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await t.step("should handle text post with poll attachment", async () => {
|
|
116
|
+
setupMockAPI();
|
|
117
|
+
const requestData: ThreadsPostRequest = {
|
|
118
|
+
userId: "12345",
|
|
119
|
+
accessToken: "token",
|
|
120
|
+
mediaType: "TEXT",
|
|
121
|
+
text: "What do you prefer?",
|
|
122
|
+
pollAttachment: {
|
|
123
|
+
option_a: "Option A",
|
|
124
|
+
option_b: "Option B",
|
|
125
|
+
option_c: "Option C",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await createThreadsContainer(requestData);
|
|
130
|
+
assertEquals(typeof result, "string");
|
|
131
|
+
assertEquals(result.length > 0, true);
|
|
132
|
+
teardownMockAPI();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await t.step("should handle text post with GIF attachment", async () => {
|
|
136
|
+
setupMockAPI();
|
|
137
|
+
const requestData: ThreadsPostRequest = {
|
|
138
|
+
userId: "12345",
|
|
139
|
+
accessToken: "token",
|
|
140
|
+
mediaType: "TEXT",
|
|
141
|
+
text: "Check this GIF!",
|
|
142
|
+
gifAttachment: {
|
|
143
|
+
gif_id: "abc123",
|
|
144
|
+
provider: "TENOR",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const result = await createThreadsContainer(requestData);
|
|
149
|
+
assertEquals(typeof result, "string");
|
|
150
|
+
assertEquals(result.length > 0, true);
|
|
151
|
+
teardownMockAPI();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await t.step("should handle ghost post", async () => {
|
|
155
|
+
setupMockAPI();
|
|
156
|
+
const requestData: ThreadsPostRequest = {
|
|
157
|
+
userId: "12345",
|
|
158
|
+
accessToken: "token",
|
|
159
|
+
mediaType: "TEXT",
|
|
160
|
+
text: "A ghost post",
|
|
161
|
+
isGhostPost: true,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await createThreadsContainer(requestData);
|
|
165
|
+
assertEquals(typeof result, "string");
|
|
166
|
+
assertEquals(result.length > 0, true);
|
|
167
|
+
teardownMockAPI();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await t.step("should handle post with topic tag", async () => {
|
|
171
|
+
setupMockAPI();
|
|
172
|
+
const requestData: ThreadsPostRequest = {
|
|
173
|
+
userId: "12345",
|
|
174
|
+
accessToken: "token",
|
|
175
|
+
mediaType: "TEXT",
|
|
176
|
+
text: "Tagged post",
|
|
177
|
+
topicTag: "technology",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = await createThreadsContainer(requestData);
|
|
181
|
+
assertEquals(typeof result, "string");
|
|
182
|
+
assertEquals(result.length > 0, true);
|
|
183
|
+
teardownMockAPI();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await t.step("should handle reply to another post", async () => {
|
|
187
|
+
setupMockAPI();
|
|
188
|
+
const requestData: ThreadsPostRequest = {
|
|
189
|
+
userId: "12345",
|
|
190
|
+
accessToken: "token",
|
|
191
|
+
mediaType: "TEXT",
|
|
192
|
+
text: "This is a reply",
|
|
193
|
+
replyToId: "original_post_123",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = await createThreadsContainer(requestData);
|
|
197
|
+
assertEquals(typeof result, "string");
|
|
198
|
+
assertEquals(result.length > 0, true);
|
|
199
|
+
teardownMockAPI();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await t.step("should handle spoiler media", async () => {
|
|
203
|
+
setupMockAPI();
|
|
204
|
+
const requestData: ThreadsPostRequest = {
|
|
205
|
+
userId: "12345",
|
|
206
|
+
accessToken: "token",
|
|
207
|
+
mediaType: "TEXT",
|
|
208
|
+
text: "Spoiler content",
|
|
209
|
+
isSpoilerMedia: true,
|
|
210
|
+
textEntities: [{ entity_type: "spoiler", offset: 0, length: 7 }],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await createThreadsContainer(requestData);
|
|
214
|
+
assertEquals(typeof result, "string");
|
|
215
|
+
assertEquals(result.length > 0, true);
|
|
216
|
+
teardownMockAPI();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await t.step("should throw error on failure", async () => {
|
|
220
|
+
setupMockAPI();
|
|
221
|
+
const requestData: ThreadsPostRequest = {
|
|
222
|
+
userId: "12345",
|
|
223
|
+
accessToken: "invalid_token",
|
|
224
|
+
mediaType: "TEXT",
|
|
225
|
+
text: "Hello, Threads!",
|
|
226
|
+
linkAttachment: validUrl.link,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
mockAPI.setErrorMode(true);
|
|
230
|
+
|
|
231
|
+
await assertRejects(
|
|
232
|
+
() => createThreadsContainer(requestData),
|
|
233
|
+
Error,
|
|
234
|
+
"Failed to create Threads container",
|
|
235
|
+
);
|
|
236
|
+
teardownMockAPI();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await t.step(
|
|
240
|
+
"should throw error when CAROUSEL type is used without children",
|
|
241
|
+
async () => {
|
|
242
|
+
const requestData: ThreadsPostRequest = {
|
|
243
|
+
userId: "12345",
|
|
244
|
+
accessToken: "token",
|
|
245
|
+
mediaType: "CAROUSEL",
|
|
246
|
+
text: "This carousel has no items",
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await assertRejects(
|
|
250
|
+
async () => await createThreadsContainer(requestData),
|
|
251
|
+
Error,
|
|
252
|
+
"CAROUSEL media type requires at least 2 children",
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await t.step(
|
|
258
|
+
"should throw error when imageUrl is provided for non-IMAGE type",
|
|
259
|
+
async () => {
|
|
260
|
+
const requestData: ThreadsPostRequest = {
|
|
261
|
+
userId: "12345",
|
|
262
|
+
accessToken: "token",
|
|
263
|
+
mediaType: "TEXT",
|
|
264
|
+
text: "This shouldn't work",
|
|
265
|
+
imageUrl: validUrl.image,
|
|
266
|
+
};
|
|
267
|
+
await assertRejects(
|
|
268
|
+
() => createThreadsContainer(requestData),
|
|
269
|
+
Error,
|
|
270
|
+
"imageUrl can only be used with IMAGE media type",
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await t.step(
|
|
276
|
+
"should throw error when videoUrl is provided for non-VIDEO type",
|
|
277
|
+
async () => {
|
|
278
|
+
const requestData: ThreadsPostRequest = {
|
|
279
|
+
userId: "12345",
|
|
280
|
+
accessToken: "token",
|
|
281
|
+
mediaType: "IMAGE",
|
|
282
|
+
imageUrl: validUrl.image,
|
|
283
|
+
videoUrl: validUrl.video,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
await assertRejects(
|
|
287
|
+
() => createThreadsContainer(requestData),
|
|
288
|
+
Error,
|
|
289
|
+
"videoUrl can only be used with VIDEO media type",
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await t.step(
|
|
295
|
+
"should throw error when linkAttachment is provided for non-TEXT type",
|
|
296
|
+
async () => {
|
|
297
|
+
const requestData: ThreadsPostRequest = {
|
|
298
|
+
userId: "12345",
|
|
299
|
+
accessToken: "token",
|
|
300
|
+
mediaType: "IMAGE",
|
|
301
|
+
imageUrl: validUrl.image,
|
|
302
|
+
linkAttachment: validUrl.link,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
await assertRejects(
|
|
306
|
+
() => createThreadsContainer(requestData),
|
|
307
|
+
Error,
|
|
308
|
+
"linkAttachment can only be used with TEXT media type",
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
await t.step(
|
|
314
|
+
"should throw error when pollAttachment used with non-TEXT type",
|
|
315
|
+
async () => {
|
|
316
|
+
const requestData: ThreadsPostRequest = {
|
|
317
|
+
userId: "12345",
|
|
318
|
+
accessToken: "token",
|
|
319
|
+
mediaType: "IMAGE",
|
|
320
|
+
imageUrl: validUrl.image,
|
|
321
|
+
pollAttachment: { option_a: "A", option_b: "B" },
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
await assertRejects(
|
|
325
|
+
() => createThreadsContainer(requestData),
|
|
326
|
+
Error,
|
|
327
|
+
"pollAttachment can only be used with TEXT media type",
|
|
328
|
+
);
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
await t.step(
|
|
333
|
+
"should throw error when ghost post is used as reply",
|
|
334
|
+
async () => {
|
|
335
|
+
const requestData: ThreadsPostRequest = {
|
|
336
|
+
userId: "12345",
|
|
337
|
+
accessToken: "token",
|
|
338
|
+
mediaType: "TEXT",
|
|
339
|
+
text: "Ghost reply",
|
|
340
|
+
isGhostPost: true,
|
|
341
|
+
replyToId: "some_post",
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
await assertRejects(
|
|
345
|
+
() => createThreadsContainer(requestData),
|
|
346
|
+
Error,
|
|
347
|
+
"isGhostPost cannot be used together with replyToId",
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
await t.step(
|
|
353
|
+
"should throw error when textEntities exceeds 10",
|
|
354
|
+
async () => {
|
|
355
|
+
const requestData: ThreadsPostRequest = {
|
|
356
|
+
userId: "12345",
|
|
357
|
+
accessToken: "token",
|
|
358
|
+
mediaType: "TEXT",
|
|
359
|
+
text: "Too many entities",
|
|
360
|
+
textEntities: Array.from({ length: 11 }, (_, i) => ({
|
|
361
|
+
entity_type: "spoiler",
|
|
362
|
+
offset: i * 5,
|
|
363
|
+
length: 3,
|
|
364
|
+
})),
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
await assertRejects(
|
|
368
|
+
() => createThreadsContainer(requestData),
|
|
369
|
+
Error,
|
|
370
|
+
"textEntities cannot have more than 10 entries",
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
await t.step("should throw error for invalid link URL", async () => {
|
|
376
|
+
const requestData: ThreadsPostRequest = {
|
|
377
|
+
userId: "12345",
|
|
378
|
+
accessToken: "token",
|
|
379
|
+
mediaType: "TEXT",
|
|
380
|
+
text: "This shouldn't work",
|
|
381
|
+
linkAttachment: invalidUrl.link,
|
|
382
|
+
};
|
|
383
|
+
await assertRejects(
|
|
384
|
+
() => createThreadsContainer(requestData),
|
|
385
|
+
Error,
|
|
386
|
+
"Invalid URL format for linkAttachment",
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await t.step(
|
|
391
|
+
"should throw error when children is provided for non-CAROUSEL type",
|
|
392
|
+
async () => {
|
|
393
|
+
const requestData: ThreadsPostRequest = {
|
|
394
|
+
userId: "12345",
|
|
395
|
+
accessToken: "token",
|
|
396
|
+
mediaType: "IMAGE",
|
|
397
|
+
imageUrl: validUrl.image,
|
|
398
|
+
children: ["item1", "item2"],
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
await assertRejects(
|
|
402
|
+
async () => {
|
|
403
|
+
await createThreadsContainer(requestData);
|
|
404
|
+
},
|
|
405
|
+
Error,
|
|
406
|
+
"children can only be used with CAROUSEL media type",
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ─── publishThreadsContainer ─────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
await t.step("publishThreadsContainer", async (t) => {
|
|
415
|
+
await t.step("should publish container successfully", async () => {
|
|
416
|
+
setupMockAPI();
|
|
417
|
+
const userId = "12345";
|
|
418
|
+
const accessToken = "token";
|
|
419
|
+
const containerId = await createThreadsContainer({
|
|
420
|
+
userId,
|
|
421
|
+
accessToken,
|
|
422
|
+
mediaType: "TEXT",
|
|
423
|
+
text: "Test post",
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const result = await publishThreadsContainer(
|
|
427
|
+
userId,
|
|
428
|
+
accessToken,
|
|
429
|
+
containerId,
|
|
430
|
+
);
|
|
431
|
+
if (typeof result === "string") {
|
|
432
|
+
assertEquals(result.length > 0, true);
|
|
433
|
+
} else {
|
|
434
|
+
assertEquals(typeof result.id, "string");
|
|
435
|
+
assertEquals(result.id.length > 0, true);
|
|
436
|
+
}
|
|
437
|
+
teardownMockAPI();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await t.step("should throw error on failure", async () => {
|
|
441
|
+
setupMockAPI();
|
|
442
|
+
mockAPI.setErrorMode(true);
|
|
443
|
+
|
|
444
|
+
await assertRejects(
|
|
445
|
+
() => publishThreadsContainer("12345", "invalid_token", "invalid_id"),
|
|
446
|
+
Error,
|
|
447
|
+
"Failed to publish Threads container",
|
|
448
|
+
);
|
|
449
|
+
teardownMockAPI();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await t.step("should return permalink when requested", async () => {
|
|
453
|
+
setupMockAPI();
|
|
454
|
+
const userId = "12345";
|
|
455
|
+
const accessToken = "token";
|
|
456
|
+
const containerId = await createThreadsContainer({
|
|
457
|
+
userId,
|
|
458
|
+
accessToken,
|
|
459
|
+
mediaType: "TEXT",
|
|
460
|
+
text: "Test post with permalink",
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const result = await publishThreadsContainer(
|
|
464
|
+
userId,
|
|
465
|
+
accessToken,
|
|
466
|
+
containerId,
|
|
467
|
+
true,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (typeof result === "string") {
|
|
471
|
+
throw new Error("Expected an object with permalink, but got a string");
|
|
472
|
+
} else {
|
|
473
|
+
assertEquals(typeof result, "object");
|
|
474
|
+
assertEquals(typeof result.id, "string");
|
|
475
|
+
assertEquals(typeof result.permalink, "string");
|
|
476
|
+
assertEquals(result.permalink.startsWith("https://"), true);
|
|
477
|
+
}
|
|
478
|
+
teardownMockAPI();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
await t.step("should not return permalink when not requested", async () => {
|
|
482
|
+
setupMockAPI();
|
|
483
|
+
const userId = "12345";
|
|
484
|
+
const accessToken = "token";
|
|
485
|
+
const containerId = await createThreadsContainer({
|
|
486
|
+
userId,
|
|
487
|
+
accessToken,
|
|
488
|
+
mediaType: "TEXT",
|
|
489
|
+
text: "Test post without permalink",
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = await publishThreadsContainer(
|
|
493
|
+
userId,
|
|
494
|
+
accessToken,
|
|
495
|
+
containerId,
|
|
496
|
+
false,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
assertEquals(typeof result, "string");
|
|
500
|
+
teardownMockAPI();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ─── createCarouselItem ──────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
await t.step("createCarouselItem", async (t) => {
|
|
507
|
+
await t.step("should return item ID", async () => {
|
|
508
|
+
setupMockAPI();
|
|
509
|
+
const requestData = {
|
|
510
|
+
userId: "12345",
|
|
511
|
+
accessToken: "token",
|
|
512
|
+
mediaType: "IMAGE" as const,
|
|
513
|
+
imageUrl: validUrl.image,
|
|
514
|
+
altText: "Test image",
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const itemId = await createCarouselItem(requestData);
|
|
518
|
+
assertEquals(typeof itemId, "string");
|
|
519
|
+
assertEquals(itemId.length > 0, true);
|
|
520
|
+
teardownMockAPI();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await t.step("should handle video items", async () => {
|
|
524
|
+
setupMockAPI();
|
|
525
|
+
const requestData = {
|
|
526
|
+
userId: "12345",
|
|
527
|
+
accessToken: "token",
|
|
528
|
+
mediaType: "VIDEO" as const,
|
|
529
|
+
videoUrl: validUrl.video,
|
|
530
|
+
altText: "Test video",
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const itemId = await createCarouselItem(requestData);
|
|
534
|
+
assertEquals(typeof itemId, "string");
|
|
535
|
+
assertEquals(itemId.length > 0, true);
|
|
536
|
+
teardownMockAPI();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await t.step("should throw error on failure", async () => {
|
|
540
|
+
setupMockAPI();
|
|
541
|
+
mockAPI.setErrorMode(true);
|
|
542
|
+
await assertRejects(
|
|
543
|
+
() =>
|
|
544
|
+
createCarouselItem({
|
|
545
|
+
userId: "12345",
|
|
546
|
+
accessToken: "token",
|
|
547
|
+
mediaType: "IMAGE" as const,
|
|
548
|
+
imageUrl: validUrl.image,
|
|
549
|
+
}),
|
|
550
|
+
Error,
|
|
551
|
+
"Failed to create carousel item",
|
|
552
|
+
);
|
|
553
|
+
teardownMockAPI();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ─── getThreadsList ──────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
await t.step("getThreadsList", async (t) => {
|
|
560
|
+
await t.step("should return threads with data", async () => {
|
|
561
|
+
setupMockAPI();
|
|
562
|
+
const userId = "12345";
|
|
563
|
+
const accessToken = "valid_token";
|
|
564
|
+
|
|
565
|
+
await createThreadsContainer({
|
|
566
|
+
userId,
|
|
567
|
+
accessToken,
|
|
568
|
+
mediaType: "TEXT",
|
|
569
|
+
text: "Test post 1",
|
|
570
|
+
});
|
|
571
|
+
await createThreadsContainer({
|
|
572
|
+
userId,
|
|
573
|
+
accessToken,
|
|
574
|
+
mediaType: "TEXT",
|
|
575
|
+
text: "Test post 2",
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const result = await getThreadsList(userId, accessToken);
|
|
579
|
+
assertEquals(Array.isArray(result.data), true);
|
|
580
|
+
assertEquals(result.data.length > 0, true);
|
|
581
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
582
|
+
assertEquals(result.data[0].text, "Test post 1");
|
|
583
|
+
assertEquals(result.data[1].text, "Test post 2");
|
|
584
|
+
if (result.paging) {
|
|
585
|
+
assertEquals(typeof result.paging.cursors.before, "string");
|
|
586
|
+
assertEquals(typeof result.paging.cursors.after, "string");
|
|
587
|
+
}
|
|
588
|
+
teardownMockAPI();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await t.step("should throw error on failure", async () => {
|
|
592
|
+
setupMockAPI();
|
|
593
|
+
mockAPI.setErrorMode(true);
|
|
594
|
+
await assertRejects(
|
|
595
|
+
() => getThreadsList("12345", "token"),
|
|
596
|
+
Error,
|
|
597
|
+
"Failed to retrieve threads list",
|
|
598
|
+
);
|
|
599
|
+
teardownMockAPI();
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ─── getSingleThread ─────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
await t.step("getSingleThread", async (t) => {
|
|
606
|
+
await t.step("should return thread data", async () => {
|
|
607
|
+
setupMockAPI();
|
|
608
|
+
const userId = "12345";
|
|
609
|
+
const accessToken = "valid_token";
|
|
610
|
+
const testText = "Test post for getSingleThread";
|
|
611
|
+
|
|
612
|
+
const containerId = await createThreadsContainer({
|
|
613
|
+
userId,
|
|
614
|
+
accessToken,
|
|
615
|
+
mediaType: "TEXT",
|
|
616
|
+
text: testText,
|
|
617
|
+
});
|
|
618
|
+
const mediaId = await publishThreadsContainer(
|
|
619
|
+
userId,
|
|
620
|
+
accessToken,
|
|
621
|
+
containerId,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const result = await getSingleThread(
|
|
625
|
+
typeof mediaId === "string" ? mediaId : mediaId.id,
|
|
626
|
+
accessToken,
|
|
627
|
+
);
|
|
628
|
+
assertEquals(typeof result.id, "string");
|
|
629
|
+
assertEquals(result.text, testText);
|
|
630
|
+
teardownMockAPI();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
await t.step("should throw error on failure", async () => {
|
|
634
|
+
setupMockAPI();
|
|
635
|
+
mockAPI.setErrorMode(true);
|
|
636
|
+
await assertRejects(
|
|
637
|
+
() => getSingleThread("media_123", "token"),
|
|
638
|
+
Error,
|
|
639
|
+
"Failed to retrieve thread",
|
|
640
|
+
);
|
|
641
|
+
teardownMockAPI();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ─── getPublishingLimit ──────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
await t.step("getPublishingLimit", async (t) => {
|
|
648
|
+
await t.step("should return rate limit information", async () => {
|
|
649
|
+
setupMockAPI();
|
|
650
|
+
const result = await getPublishingLimit("12345", "valid_token");
|
|
651
|
+
assertEquals(typeof result.quota_usage, "number");
|
|
652
|
+
assertEquals(typeof result.config?.quota_total, "number");
|
|
653
|
+
assertEquals(typeof result.config?.quota_duration, "number");
|
|
654
|
+
teardownMockAPI();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await t.step("should return reply quota when available", async () => {
|
|
658
|
+
setupMockAPI();
|
|
659
|
+
const result = await getPublishingLimit("12345", "valid_token");
|
|
660
|
+
assertEquals(typeof result.reply_quota_usage, "number");
|
|
661
|
+
assertEquals(typeof result.reply_config?.quota_total, "number");
|
|
662
|
+
teardownMockAPI();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
await t.step("should throw error on failure", async () => {
|
|
666
|
+
setupMockAPI();
|
|
667
|
+
mockAPI.setErrorMode(true);
|
|
668
|
+
|
|
669
|
+
await assertRejects(
|
|
670
|
+
() => getPublishingLimit("12345", "invalid_token"),
|
|
671
|
+
Error,
|
|
672
|
+
"Failed to get publishing limit",
|
|
673
|
+
);
|
|
674
|
+
teardownMockAPI();
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// ─── repost ──────────────────────────────────────────────────────────────
|
|
679
|
+
|
|
680
|
+
await t.step("repost", async (t) => {
|
|
681
|
+
await t.step("should repost successfully", async () => {
|
|
682
|
+
setupMockAPI();
|
|
683
|
+
const containerId = await createThreadsContainer({
|
|
684
|
+
userId: "12345",
|
|
685
|
+
accessToken: "token",
|
|
686
|
+
mediaType: "TEXT",
|
|
687
|
+
text: "Post to repost",
|
|
688
|
+
});
|
|
689
|
+
const mediaId = await publishThreadsContainer(
|
|
690
|
+
"12345",
|
|
691
|
+
"token",
|
|
692
|
+
containerId,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const result = await repost(
|
|
696
|
+
typeof mediaId === "string" ? mediaId : mediaId.id,
|
|
697
|
+
"token",
|
|
698
|
+
);
|
|
699
|
+
assertEquals(typeof result.id, "string");
|
|
700
|
+
teardownMockAPI();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
await t.step("should throw error on failure", async () => {
|
|
704
|
+
setupMockAPI();
|
|
705
|
+
mockAPI.setErrorMode(true);
|
|
706
|
+
await assertRejects(
|
|
707
|
+
() => repost("media_123", "token"),
|
|
708
|
+
Error,
|
|
709
|
+
"Failed to repost",
|
|
710
|
+
);
|
|
711
|
+
teardownMockAPI();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// ─── deleteThread ────────────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
await t.step("deleteThread", async (t) => {
|
|
718
|
+
await t.step("should delete successfully", async () => {
|
|
719
|
+
setupMockAPI();
|
|
720
|
+
const containerId = await createThreadsContainer({
|
|
721
|
+
userId: "12345",
|
|
722
|
+
accessToken: "token",
|
|
723
|
+
mediaType: "TEXT",
|
|
724
|
+
text: "Post to delete",
|
|
725
|
+
});
|
|
726
|
+
const mediaId = await publishThreadsContainer(
|
|
727
|
+
"12345",
|
|
728
|
+
"token",
|
|
729
|
+
containerId,
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const result = await deleteThread(
|
|
733
|
+
typeof mediaId === "string" ? mediaId : mediaId.id,
|
|
734
|
+
"token",
|
|
735
|
+
);
|
|
736
|
+
assertEquals(result.success, true);
|
|
737
|
+
teardownMockAPI();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
await t.step("should throw error on failure", async () => {
|
|
741
|
+
setupMockAPI();
|
|
742
|
+
mockAPI.setErrorMode(true);
|
|
743
|
+
await assertRejects(
|
|
744
|
+
() => deleteThread("media_123", "token"),
|
|
745
|
+
Error,
|
|
746
|
+
"Failed to delete thread",
|
|
747
|
+
);
|
|
748
|
+
teardownMockAPI();
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// ─── getProfile ──────────────────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
await t.step("getProfile", async (t) => {
|
|
755
|
+
await t.step("should return profile data", async () => {
|
|
756
|
+
setupMockAPI();
|
|
757
|
+
const result = await getProfile("12345", "token");
|
|
758
|
+
assertEquals(result.id, "12345");
|
|
759
|
+
assertEquals(result.username, "testuser");
|
|
760
|
+
assertEquals(result.name, "Test User");
|
|
761
|
+
teardownMockAPI();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
await t.step("should throw error on failure", async () => {
|
|
765
|
+
setupMockAPI();
|
|
766
|
+
mockAPI.setErrorMode(true);
|
|
767
|
+
await assertRejects(
|
|
768
|
+
() => getProfile("12345", "token"),
|
|
769
|
+
Error,
|
|
770
|
+
"Failed to get profile",
|
|
771
|
+
);
|
|
772
|
+
teardownMockAPI();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// ─── lookupProfile ───────────────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
await t.step("lookupProfile", async (t) => {
|
|
779
|
+
await t.step("should look up profile by username", async () => {
|
|
780
|
+
setupMockAPI();
|
|
781
|
+
const result = await lookupProfile("token", "testuser");
|
|
782
|
+
assertEquals(result.id, "12345");
|
|
783
|
+
assertEquals(result.username, "testuser");
|
|
784
|
+
teardownMockAPI();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
await t.step("should throw error for unknown user", async () => {
|
|
788
|
+
setupMockAPI();
|
|
789
|
+
await assertRejects(
|
|
790
|
+
() => lookupProfile("token", "nonexistent_user"),
|
|
791
|
+
Error,
|
|
792
|
+
"Profile not found",
|
|
793
|
+
);
|
|
794
|
+
teardownMockAPI();
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// ─── getProfilePosts ─────────────────────────────────────────────────────
|
|
799
|
+
|
|
800
|
+
await t.step("getProfilePosts", async (t) => {
|
|
801
|
+
await t.step("should return profile posts", async () => {
|
|
802
|
+
setupMockAPI();
|
|
803
|
+
await createThreadsContainer({
|
|
804
|
+
userId: "12345",
|
|
805
|
+
accessToken: "token",
|
|
806
|
+
mediaType: "TEXT",
|
|
807
|
+
text: "Profile post",
|
|
808
|
+
});
|
|
809
|
+
const result = await getProfilePosts("token", "testuser");
|
|
810
|
+
assertEquals(Array.isArray(result.data), true);
|
|
811
|
+
assertEquals(result.data.length > 0, true);
|
|
812
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
813
|
+
teardownMockAPI();
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
await t.step("should throw error on failure", async () => {
|
|
817
|
+
setupMockAPI();
|
|
818
|
+
mockAPI.setErrorMode(true);
|
|
819
|
+
await assertRejects(
|
|
820
|
+
() => getProfilePosts("token", "testuser"),
|
|
821
|
+
Error,
|
|
822
|
+
"Failed to get profile posts",
|
|
823
|
+
);
|
|
824
|
+
teardownMockAPI();
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// ─── getGhostPosts ───────────────────────────────────────────────────────
|
|
829
|
+
|
|
830
|
+
await t.step("getGhostPosts", async (t) => {
|
|
831
|
+
await t.step("should return ghost posts", async () => {
|
|
832
|
+
setupMockAPI();
|
|
833
|
+
await createThreadsContainer({
|
|
834
|
+
userId: "12345",
|
|
835
|
+
accessToken: "token",
|
|
836
|
+
mediaType: "TEXT",
|
|
837
|
+
text: "Ghost post",
|
|
838
|
+
});
|
|
839
|
+
const result = await getGhostPosts("12345", "token");
|
|
840
|
+
assertEquals(Array.isArray(result.data), true);
|
|
841
|
+
assertEquals(result.data.length > 0, true);
|
|
842
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
843
|
+
teardownMockAPI();
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
await t.step("should throw error on failure", async () => {
|
|
847
|
+
setupMockAPI();
|
|
848
|
+
mockAPI.setErrorMode(true);
|
|
849
|
+
await assertRejects(
|
|
850
|
+
() => getGhostPosts("12345", "token"),
|
|
851
|
+
Error,
|
|
852
|
+
"Failed to get ghost posts",
|
|
853
|
+
);
|
|
854
|
+
teardownMockAPI();
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// ─── getUserReplies ──────────────────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
await t.step("getUserReplies", async (t) => {
|
|
861
|
+
await t.step("should return user replies with paging", async () => {
|
|
862
|
+
setupMockAPI();
|
|
863
|
+
const result = await getUserReplies("12345", "token");
|
|
864
|
+
assertEquals(Array.isArray(result.data), true);
|
|
865
|
+
if (result.paging) {
|
|
866
|
+
assertEquals(typeof result.paging.cursors.before, "string");
|
|
867
|
+
assertEquals(typeof result.paging.cursors.after, "string");
|
|
868
|
+
}
|
|
869
|
+
teardownMockAPI();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
await t.step("should throw error on failure", async () => {
|
|
873
|
+
setupMockAPI();
|
|
874
|
+
mockAPI.setErrorMode(true);
|
|
875
|
+
await assertRejects(
|
|
876
|
+
() => getUserReplies("12345", "token"),
|
|
877
|
+
Error,
|
|
878
|
+
"Failed to get user replies",
|
|
879
|
+
);
|
|
880
|
+
teardownMockAPI();
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// ─── getReplies ──────────────────────────────────────────────────────────
|
|
885
|
+
|
|
886
|
+
await t.step("getReplies", async (t) => {
|
|
887
|
+
await t.step("should return replies", async () => {
|
|
888
|
+
setupMockAPI();
|
|
889
|
+
await createThreadsContainer({
|
|
890
|
+
userId: "12345",
|
|
891
|
+
accessToken: "token",
|
|
892
|
+
mediaType: "TEXT",
|
|
893
|
+
text: "Post with replies",
|
|
894
|
+
});
|
|
895
|
+
const result = await getReplies("media_123", "token");
|
|
896
|
+
assertEquals(Array.isArray(result.data), true);
|
|
897
|
+
assertEquals(result.data.length > 0, true);
|
|
898
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
899
|
+
teardownMockAPI();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
await t.step("should throw error on failure", async () => {
|
|
903
|
+
setupMockAPI();
|
|
904
|
+
mockAPI.setErrorMode(true);
|
|
905
|
+
await assertRejects(
|
|
906
|
+
() => getReplies("media_123", "token"),
|
|
907
|
+
Error,
|
|
908
|
+
"Failed to get replies",
|
|
909
|
+
);
|
|
910
|
+
teardownMockAPI();
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// ─── getConversation ─────────────────────────────────────────────────────
|
|
915
|
+
|
|
916
|
+
await t.step("getConversation", async (t) => {
|
|
917
|
+
await t.step("should return conversation", async () => {
|
|
918
|
+
setupMockAPI();
|
|
919
|
+
await createThreadsContainer({
|
|
920
|
+
userId: "12345",
|
|
921
|
+
accessToken: "token",
|
|
922
|
+
mediaType: "TEXT",
|
|
923
|
+
text: "Conversation root",
|
|
924
|
+
});
|
|
925
|
+
const result = await getConversation("media_123", "token");
|
|
926
|
+
assertEquals(Array.isArray(result.data), true);
|
|
927
|
+
assertEquals(result.data.length > 0, true);
|
|
928
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
929
|
+
teardownMockAPI();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
await t.step("should throw error on failure", async () => {
|
|
933
|
+
setupMockAPI();
|
|
934
|
+
mockAPI.setErrorMode(true);
|
|
935
|
+
await assertRejects(
|
|
936
|
+
() => getConversation("media_123", "token"),
|
|
937
|
+
Error,
|
|
938
|
+
"Failed to get conversation",
|
|
939
|
+
);
|
|
940
|
+
teardownMockAPI();
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// ─── manageReply ─────────────────────────────────────────────────────────
|
|
945
|
+
|
|
946
|
+
await t.step("manageReply", async (t) => {
|
|
947
|
+
await t.step("should hide a reply", async () => {
|
|
948
|
+
setupMockAPI();
|
|
949
|
+
const result = await manageReply("reply_123", "token", true);
|
|
950
|
+
assertEquals(result.success, true);
|
|
951
|
+
teardownMockAPI();
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
await t.step("should unhide a reply", async () => {
|
|
955
|
+
setupMockAPI();
|
|
956
|
+
const result = await manageReply("reply_123", "token", false);
|
|
957
|
+
assertEquals(result.success, true);
|
|
958
|
+
teardownMockAPI();
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await t.step("should throw error on failure", async () => {
|
|
962
|
+
setupMockAPI();
|
|
963
|
+
mockAPI.setErrorMode(true);
|
|
964
|
+
await assertRejects(
|
|
965
|
+
() => manageReply("reply_123", "token", true),
|
|
966
|
+
Error,
|
|
967
|
+
"Failed to manage reply",
|
|
968
|
+
);
|
|
969
|
+
teardownMockAPI();
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// ─── getMentions ─────────────────────────────────────────────────────────
|
|
974
|
+
|
|
975
|
+
await t.step("getMentions", async (t) => {
|
|
976
|
+
await t.step("should return mentions", async () => {
|
|
977
|
+
setupMockAPI();
|
|
978
|
+
await createThreadsContainer({
|
|
979
|
+
userId: "12345",
|
|
980
|
+
accessToken: "token",
|
|
981
|
+
mediaType: "TEXT",
|
|
982
|
+
text: "Mentioned post",
|
|
983
|
+
});
|
|
984
|
+
const result = await getMentions("12345", "token");
|
|
985
|
+
assertEquals(Array.isArray(result.data), true);
|
|
986
|
+
assertEquals(result.data.length > 0, true);
|
|
987
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
988
|
+
teardownMockAPI();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
await t.step("should throw error on failure", async () => {
|
|
992
|
+
setupMockAPI();
|
|
993
|
+
mockAPI.setErrorMode(true);
|
|
994
|
+
await assertRejects(
|
|
995
|
+
() => getMentions("12345", "token"),
|
|
996
|
+
Error,
|
|
997
|
+
"Failed to get mentions",
|
|
998
|
+
);
|
|
999
|
+
teardownMockAPI();
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// ─── getMediaInsights ────────────────────────────────────────────────────
|
|
1004
|
+
|
|
1005
|
+
await t.step("getMediaInsights", async (t) => {
|
|
1006
|
+
await t.step("should return insight metrics", async () => {
|
|
1007
|
+
setupMockAPI();
|
|
1008
|
+
const result = await getMediaInsights("media_123", "token", [
|
|
1009
|
+
"views",
|
|
1010
|
+
"likes",
|
|
1011
|
+
]);
|
|
1012
|
+
assertEquals(result.data.length, 2);
|
|
1013
|
+
assertEquals(result.data[0].name, "views");
|
|
1014
|
+
assertEquals(result.data[1].name, "likes");
|
|
1015
|
+
teardownMockAPI();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
await t.step("should throw error on failure", async () => {
|
|
1019
|
+
setupMockAPI();
|
|
1020
|
+
mockAPI.setErrorMode(true);
|
|
1021
|
+
await assertRejects(
|
|
1022
|
+
() => getMediaInsights("media_123", "token", ["views"]),
|
|
1023
|
+
Error,
|
|
1024
|
+
"Failed to get media insights",
|
|
1025
|
+
);
|
|
1026
|
+
teardownMockAPI();
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// ─── getUserInsights ─────────────────────────────────────────────────────
|
|
1031
|
+
|
|
1032
|
+
await t.step("getUserInsights", async (t) => {
|
|
1033
|
+
await t.step("should return user metrics", async () => {
|
|
1034
|
+
setupMockAPI();
|
|
1035
|
+
const result = await getUserInsights("12345", "token", [
|
|
1036
|
+
"views",
|
|
1037
|
+
"followers_count",
|
|
1038
|
+
]);
|
|
1039
|
+
assertEquals(result.data.length, 2);
|
|
1040
|
+
assertEquals(result.data[0].name, "views");
|
|
1041
|
+
teardownMockAPI();
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
await t.step("should throw error on failure", async () => {
|
|
1045
|
+
setupMockAPI();
|
|
1046
|
+
mockAPI.setErrorMode(true);
|
|
1047
|
+
await assertRejects(
|
|
1048
|
+
() => getUserInsights("12345", "token", ["views"]),
|
|
1049
|
+
Error,
|
|
1050
|
+
"Failed to get user insights",
|
|
1051
|
+
);
|
|
1052
|
+
teardownMockAPI();
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// ─── searchKeyword ───────────────────────────────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
await t.step("searchKeyword", async (t) => {
|
|
1059
|
+
await t.step("should return search results", async () => {
|
|
1060
|
+
setupMockAPI();
|
|
1061
|
+
await createThreadsContainer({
|
|
1062
|
+
userId: "12345",
|
|
1063
|
+
accessToken: "token",
|
|
1064
|
+
mediaType: "TEXT",
|
|
1065
|
+
text: "Searchable post",
|
|
1066
|
+
});
|
|
1067
|
+
const result = await searchKeyword("token", {
|
|
1068
|
+
q: "test query",
|
|
1069
|
+
search_type: "TOP",
|
|
1070
|
+
});
|
|
1071
|
+
assertEquals(Array.isArray(result.data), true);
|
|
1072
|
+
assertEquals(result.data.length > 0, true);
|
|
1073
|
+
assertEquals(typeof result.data[0].id, "string");
|
|
1074
|
+
teardownMockAPI();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await t.step("should throw error on failure", async () => {
|
|
1078
|
+
setupMockAPI();
|
|
1079
|
+
mockAPI.setErrorMode(true);
|
|
1080
|
+
await assertRejects(
|
|
1081
|
+
() => searchKeyword("token", { q: "test" }),
|
|
1082
|
+
Error,
|
|
1083
|
+
"Failed to search keywords",
|
|
1084
|
+
);
|
|
1085
|
+
teardownMockAPI();
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// ─── searchLocations ─────────────────────────────────────────────────────
|
|
1090
|
+
|
|
1091
|
+
await t.step("searchLocations", async (t) => {
|
|
1092
|
+
await t.step("should return location results", async () => {
|
|
1093
|
+
setupMockAPI();
|
|
1094
|
+
const result = await searchLocations("token", {
|
|
1095
|
+
query: "San Francisco",
|
|
1096
|
+
});
|
|
1097
|
+
assertEquals(Array.isArray(result.data), true);
|
|
1098
|
+
assertEquals(result.data.length > 0, true);
|
|
1099
|
+
assertEquals(result.data[0].name, "Test Location");
|
|
1100
|
+
teardownMockAPI();
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
await t.step("should throw error on failure", async () => {
|
|
1104
|
+
setupMockAPI();
|
|
1105
|
+
mockAPI.setErrorMode(true);
|
|
1106
|
+
await assertRejects(
|
|
1107
|
+
() => searchLocations("token", { query: "test" }),
|
|
1108
|
+
Error,
|
|
1109
|
+
"Failed to search locations",
|
|
1110
|
+
);
|
|
1111
|
+
teardownMockAPI();
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// ─── getLocation ─────────────────────────────────────────────────────────
|
|
1116
|
+
|
|
1117
|
+
await t.step("getLocation", async (t) => {
|
|
1118
|
+
await t.step("should return location data", async () => {
|
|
1119
|
+
setupMockAPI();
|
|
1120
|
+
const result = await getLocation("loc_123", "token");
|
|
1121
|
+
assertEquals(result.id, "loc_123");
|
|
1122
|
+
assertEquals(result.name, "Test Location");
|
|
1123
|
+
assertEquals(typeof result.latitude, "number");
|
|
1124
|
+
teardownMockAPI();
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
await t.step("should throw error on failure", async () => {
|
|
1128
|
+
setupMockAPI();
|
|
1129
|
+
mockAPI.setErrorMode(true);
|
|
1130
|
+
await assertRejects(
|
|
1131
|
+
() => getLocation("loc_123", "token"),
|
|
1132
|
+
Error,
|
|
1133
|
+
"Failed to get location",
|
|
1134
|
+
);
|
|
1135
|
+
teardownMockAPI();
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// ─── exchangeCodeForToken ────────────────────────────────────────────────
|
|
1140
|
+
|
|
1141
|
+
await t.step("exchangeCodeForToken", async (t) => {
|
|
1142
|
+
await t.step("should return short-lived token and user ID", async () => {
|
|
1143
|
+
setupMockAPI();
|
|
1144
|
+
const result = await exchangeCodeForToken(
|
|
1145
|
+
"app_id",
|
|
1146
|
+
"app_secret",
|
|
1147
|
+
"auth_code",
|
|
1148
|
+
"https://example.com/callback",
|
|
1149
|
+
);
|
|
1150
|
+
assertEquals(typeof result.access_token, "string");
|
|
1151
|
+
assertEquals(typeof result.user_id, "string");
|
|
1152
|
+
teardownMockAPI();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
await t.step("should throw error on failure", async () => {
|
|
1156
|
+
setupMockAPI();
|
|
1157
|
+
mockAPI.setErrorMode(true);
|
|
1158
|
+
await assertRejects(
|
|
1159
|
+
() =>
|
|
1160
|
+
exchangeCodeForToken(
|
|
1161
|
+
"id",
|
|
1162
|
+
"secret",
|
|
1163
|
+
"code",
|
|
1164
|
+
"https://example.com",
|
|
1165
|
+
),
|
|
1166
|
+
Error,
|
|
1167
|
+
"Failed to exchange authorization code",
|
|
1168
|
+
);
|
|
1169
|
+
teardownMockAPI();
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// ─── getAppAccessToken ──────────────────────────────────────────────────
|
|
1174
|
+
|
|
1175
|
+
await t.step("getAppAccessToken", async (t) => {
|
|
1176
|
+
await t.step("should return app access token", async () => {
|
|
1177
|
+
setupMockAPI();
|
|
1178
|
+
const result = await getAppAccessToken("app_id", "app_secret");
|
|
1179
|
+
assertEquals(typeof result.access_token, "string");
|
|
1180
|
+
assertEquals(result.token_type, "bearer");
|
|
1181
|
+
teardownMockAPI();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
await t.step("should throw error on failure", async () => {
|
|
1185
|
+
setupMockAPI();
|
|
1186
|
+
mockAPI.setErrorMode(true);
|
|
1187
|
+
await assertRejects(
|
|
1188
|
+
() => getAppAccessToken("id", "secret"),
|
|
1189
|
+
Error,
|
|
1190
|
+
"Failed to get app access token",
|
|
1191
|
+
);
|
|
1192
|
+
teardownMockAPI();
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// ─── exchangeToken ───────────────────────────────────────────────────────
|
|
1197
|
+
|
|
1198
|
+
await t.step("exchangeToken", async (t) => {
|
|
1199
|
+
await t.step("should return long-lived token", async () => {
|
|
1200
|
+
setupMockAPI();
|
|
1201
|
+
const result = await exchangeToken("client_secret", "short_token");
|
|
1202
|
+
assertEquals(typeof result.access_token, "string");
|
|
1203
|
+
assertEquals(result.token_type, "bearer");
|
|
1204
|
+
assertEquals(typeof result.expires_in, "number");
|
|
1205
|
+
teardownMockAPI();
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
await t.step("should throw error on failure", async () => {
|
|
1209
|
+
setupMockAPI();
|
|
1210
|
+
mockAPI.setErrorMode(true);
|
|
1211
|
+
await assertRejects(
|
|
1212
|
+
() => exchangeToken("secret", "token"),
|
|
1213
|
+
Error,
|
|
1214
|
+
"Failed to exchange token",
|
|
1215
|
+
);
|
|
1216
|
+
teardownMockAPI();
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// ─── refreshToken ────────────────────────────────────────────────────────
|
|
1221
|
+
|
|
1222
|
+
await t.step("refreshToken", async (t) => {
|
|
1223
|
+
await t.step("should return refreshed token", async () => {
|
|
1224
|
+
setupMockAPI();
|
|
1225
|
+
const result = await refreshToken("old_token");
|
|
1226
|
+
assertEquals(typeof result.access_token, "string");
|
|
1227
|
+
assertEquals(result.token_type, "bearer");
|
|
1228
|
+
teardownMockAPI();
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
await t.step("should throw error on failure", async () => {
|
|
1232
|
+
setupMockAPI();
|
|
1233
|
+
mockAPI.setErrorMode(true);
|
|
1234
|
+
await assertRejects(
|
|
1235
|
+
() => refreshToken("token"),
|
|
1236
|
+
Error,
|
|
1237
|
+
"Failed to refresh token",
|
|
1238
|
+
);
|
|
1239
|
+
teardownMockAPI();
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// ─── debugToken ──────────────────────────────────────────────────────────
|
|
1244
|
+
|
|
1245
|
+
await t.step("debugToken", async (t) => {
|
|
1246
|
+
await t.step("should return token debug info", async () => {
|
|
1247
|
+
setupMockAPI();
|
|
1248
|
+
const result = await debugToken("access_token", "input_token");
|
|
1249
|
+
assertEquals(result.data.is_valid, true);
|
|
1250
|
+
assertEquals(Array.isArray(result.data.scopes), true);
|
|
1251
|
+
teardownMockAPI();
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
await t.step("should throw error on failure", async () => {
|
|
1255
|
+
setupMockAPI();
|
|
1256
|
+
mockAPI.setErrorMode(true);
|
|
1257
|
+
await assertRejects(
|
|
1258
|
+
() => debugToken("token", "input"),
|
|
1259
|
+
Error,
|
|
1260
|
+
"Failed to debug token",
|
|
1261
|
+
);
|
|
1262
|
+
teardownMockAPI();
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// ─── getOEmbed ───────────────────────────────────────────────────────────
|
|
1267
|
+
|
|
1268
|
+
await t.step("getOEmbed", async (t) => {
|
|
1269
|
+
await t.step("should return embed HTML", async () => {
|
|
1270
|
+
setupMockAPI();
|
|
1271
|
+
const result = await getOEmbed(
|
|
1272
|
+
"token",
|
|
1273
|
+
"https://www.threads.net/@user/post/abc",
|
|
1274
|
+
);
|
|
1275
|
+
assertEquals(typeof result.html, "string");
|
|
1276
|
+
assertEquals(result.provider_name, "Threads");
|
|
1277
|
+
teardownMockAPI();
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
await t.step("should throw error on failure", async () => {
|
|
1281
|
+
setupMockAPI();
|
|
1282
|
+
mockAPI.setErrorMode(true);
|
|
1283
|
+
await assertRejects(
|
|
1284
|
+
() => getOEmbed("token", "https://example.com"),
|
|
1285
|
+
Error,
|
|
1286
|
+
"Failed to get oEmbed",
|
|
1287
|
+
);
|
|
1288
|
+
teardownMockAPI();
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
435
1291
|
});
|