@codybrom/denim 1.3.4 → 1.3.6
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/deno.json +1 -1
- package/deno.lock +40 -1
- package/examples/edge-function.ts +56 -20
- package/mock_threads_api.ts +174 -0
- package/mod.ts +293 -141
- package/mod_test.ts +405 -471
- package/package.json +1 -1
- package/types.ts +235 -0
package/mod_test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// mod_test.ts
|
|
2
|
+
import type { ThreadsPostRequest } from "./types.ts";
|
|
2
3
|
import {
|
|
3
4
|
assertEquals,
|
|
4
5
|
assertRejects,
|
|
@@ -8,494 +9,427 @@ import {
|
|
|
8
9
|
publishThreadsContainer,
|
|
9
10
|
createCarouselItem,
|
|
10
11
|
getPublishingLimit,
|
|
11
|
-
|
|
12
|
+
getThreadsList,
|
|
13
|
+
getSingleThread,
|
|
12
14
|
} from "./mod.ts";
|
|
15
|
+
import { MockThreadsAPI } from "./mock_threads_api.ts";
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
input: string | URL | Request,
|
|
17
|
-
init?: RequestInit
|
|
18
|
-
): Promise<Response> => {
|
|
19
|
-
const url =
|
|
20
|
-
typeof input === "string"
|
|
21
|
-
? input
|
|
22
|
-
: input instanceof URL
|
|
23
|
-
? input.toString()
|
|
24
|
-
: input.url;
|
|
25
|
-
|
|
26
|
-
const body =
|
|
27
|
-
init?.body instanceof URLSearchParams ? init.body : new URLSearchParams();
|
|
28
|
-
|
|
29
|
-
if (url.includes("threads")) {
|
|
30
|
-
if (url.includes("threads_publish")) {
|
|
31
|
-
return Promise.resolve({
|
|
32
|
-
ok: true,
|
|
33
|
-
status: 200,
|
|
34
|
-
statusText: "OK",
|
|
35
|
-
text: () => Promise.resolve(JSON.stringify({ id: "published123" })),
|
|
36
|
-
} as Response);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (body.get("is_carousel_item") === "true") {
|
|
40
|
-
if (body.get("access_token") === "invalid_token") {
|
|
41
|
-
return Promise.resolve({
|
|
42
|
-
ok: false,
|
|
43
|
-
status: 400,
|
|
44
|
-
statusText: "Bad Request",
|
|
45
|
-
text: () =>
|
|
46
|
-
Promise.resolve(JSON.stringify({ error: "Invalid access token" })),
|
|
47
|
-
} as Response);
|
|
48
|
-
}
|
|
49
|
-
return Promise.resolve({
|
|
50
|
-
ok: true,
|
|
51
|
-
status: 200,
|
|
52
|
-
statusText: "OK",
|
|
53
|
-
text: () => Promise.resolve(JSON.stringify({ id: "item123" })),
|
|
54
|
-
} as Response);
|
|
55
|
-
}
|
|
17
|
+
Deno.test("Threads API", async (t) => {
|
|
18
|
+
let mockAPI: MockThreadsAPI;
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
statusText: "OK",
|
|
61
|
-
text: () => Promise.resolve(JSON.stringify({ id: "container123" })),
|
|
62
|
-
} as Response);
|
|
20
|
+
function setupMockAPI() {
|
|
21
|
+
mockAPI = new MockThreadsAPI();
|
|
22
|
+
(globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI = mockAPI;
|
|
63
23
|
}
|
|
64
24
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
status: 500,
|
|
68
|
-
statusText: "Internal Server Error",
|
|
69
|
-
text: () => Promise.resolve("Error"),
|
|
70
|
-
} as Response);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
Deno.test(
|
|
74
|
-
"createThreadsContainer should return container ID for basic text post",
|
|
75
|
-
async () => {
|
|
76
|
-
const requestData: ThreadsPostRequest = {
|
|
77
|
-
userId: "12345",
|
|
78
|
-
accessToken: "token",
|
|
79
|
-
mediaType: "TEXT",
|
|
80
|
-
text: "Hello, Threads!",
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const containerId = await createThreadsContainer(requestData);
|
|
84
|
-
assertEquals(containerId, "container123");
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
Deno.test(
|
|
89
|
-
"createThreadsContainer should return container ID with text post with link attachment, reply control, and allowlisted countries",
|
|
90
|
-
async () => {
|
|
91
|
-
const requestData: ThreadsPostRequest = {
|
|
92
|
-
userId: "12345",
|
|
93
|
-
accessToken: "token",
|
|
94
|
-
mediaType: "TEXT",
|
|
95
|
-
text: "Hello, Threads!",
|
|
96
|
-
linkAttachment: "https://example.com",
|
|
97
|
-
replyControl: "everyone",
|
|
98
|
-
allowlistedCountryCodes: ["US", "CA"],
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const containerId = await createThreadsContainer(requestData);
|
|
102
|
-
assertEquals(containerId, "container123");
|
|
25
|
+
function teardownMockAPI() {
|
|
26
|
+
delete (globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI;
|
|
103
27
|
}
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
Deno.test(
|
|
107
|
-
"createThreadsContainer should handle image post with alt text",
|
|
108
|
-
async () => {
|
|
109
|
-
const requestData: ThreadsPostRequest = {
|
|
110
|
-
userId: "12345",
|
|
111
|
-
accessToken: "token",
|
|
112
|
-
mediaType: "IMAGE",
|
|
113
|
-
text: "Check out this image!",
|
|
114
|
-
imageUrl: "https://example.com/image.jpg",
|
|
115
|
-
altText: "A beautiful sunset",
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const containerId = await createThreadsContainer(requestData);
|
|
119
|
-
assertEquals(containerId, "container123");
|
|
120
|
-
}
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
Deno.test(
|
|
124
|
-
"createThreadsContainer should handle video post with all features",
|
|
125
|
-
async () => {
|
|
126
|
-
const requestData: ThreadsPostRequest = {
|
|
127
|
-
userId: "12345",
|
|
128
|
-
accessToken: "token",
|
|
129
|
-
mediaType: "VIDEO",
|
|
130
|
-
text: "Watch this video!",
|
|
131
|
-
videoUrl: "https://example.com/video.mp4",
|
|
132
|
-
altText: "A tutorial video",
|
|
133
|
-
replyControl: "mentioned_only",
|
|
134
|
-
allowlistedCountryCodes: ["US", "GB"],
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const containerId = await createThreadsContainer(requestData);
|
|
138
|
-
assertEquals(containerId, "container123");
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
Deno.test("createThreadsContainer should throw error on failure", async () => {
|
|
143
|
-
const requestData: ThreadsPostRequest = {
|
|
144
|
-
userId: "12345",
|
|
145
|
-
accessToken: "token",
|
|
146
|
-
mediaType: "TEXT",
|
|
147
|
-
text: "Hello, Threads!",
|
|
148
|
-
linkAttachment: "https://example.com",
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
globalThis.fetch = (): Promise<Response> =>
|
|
152
|
-
Promise.resolve({
|
|
153
|
-
ok: false,
|
|
154
|
-
status: 500,
|
|
155
|
-
statusText: "Internal Server Error",
|
|
156
|
-
text: () => Promise.resolve("Error"),
|
|
157
|
-
} as Response);
|
|
158
|
-
|
|
159
|
-
await assertRejects(
|
|
160
|
-
async () => {
|
|
161
|
-
await createThreadsContainer(requestData);
|
|
162
|
-
},
|
|
163
|
-
Error,
|
|
164
|
-
"Failed to create Threads container"
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
Deno.test("createCarouselItem should return item ID", async () => {
|
|
168
|
-
const requestData = {
|
|
169
|
-
userId: "12345",
|
|
170
|
-
accessToken: "token",
|
|
171
|
-
mediaType: "IMAGE" as const,
|
|
172
|
-
imageUrl: "https://example.com/image.jpg",
|
|
173
|
-
altText: "Test image",
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
globalThis.fetch = (
|
|
177
|
-
_input: string | URL | Request,
|
|
178
|
-
init?: RequestInit
|
|
179
|
-
): Promise<Response> => {
|
|
180
|
-
const body =
|
|
181
|
-
init?.body instanceof URLSearchParams ? init.body : new URLSearchParams();
|
|
182
|
-
if (body.get("is_carousel_item") === "true") {
|
|
183
|
-
return Promise.resolve({
|
|
184
|
-
ok: true,
|
|
185
|
-
status: 200,
|
|
186
|
-
statusText: "OK",
|
|
187
|
-
text: () => Promise.resolve(JSON.stringify({ id: "item123" })),
|
|
188
|
-
} as Response);
|
|
189
|
-
}
|
|
190
|
-
return Promise.resolve({
|
|
191
|
-
ok: false,
|
|
192
|
-
status: 500,
|
|
193
|
-
statusText: "Internal Server Error",
|
|
194
|
-
text: () => Promise.resolve("Error"),
|
|
195
|
-
} as Response);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const itemId = await createCarouselItem(requestData);
|
|
199
|
-
assertEquals(itemId, "item123");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
Deno.test("createCarouselItem should handle video items", async () => {
|
|
203
|
-
const requestData = {
|
|
204
|
-
userId: "12345",
|
|
205
|
-
accessToken: "token",
|
|
206
|
-
mediaType: "VIDEO" as const,
|
|
207
|
-
videoUrl: "https://example.com/video.mp4",
|
|
208
|
-
altText: "Test video",
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
globalThis.fetch = (
|
|
212
|
-
_input: string | URL | Request,
|
|
213
|
-
init?: RequestInit
|
|
214
|
-
): Promise<Response> => {
|
|
215
|
-
const body =
|
|
216
|
-
init?.body instanceof URLSearchParams ? init.body : new URLSearchParams();
|
|
217
|
-
if (body.get("is_carousel_item") === "true") {
|
|
218
|
-
return Promise.resolve({
|
|
219
|
-
ok: true,
|
|
220
|
-
status: 200,
|
|
221
|
-
statusText: "OK",
|
|
222
|
-
text: () => Promise.resolve(JSON.stringify({ id: "item123" })),
|
|
223
|
-
} as Response);
|
|
224
|
-
}
|
|
225
|
-
return Promise.resolve({
|
|
226
|
-
ok: false,
|
|
227
|
-
status: 500,
|
|
228
|
-
statusText: "Internal Server Error",
|
|
229
|
-
text: () => Promise.resolve("Error"),
|
|
230
|
-
} as Response);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const itemId = await createCarouselItem(requestData);
|
|
234
|
-
assertEquals(itemId, "item123");
|
|
235
|
-
});
|
|
236
28
|
|
|
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
|
-
|
|
29
|
+
await t.step("createThreadsContainer", async (t) => {
|
|
30
|
+
await t.step("should return container ID for basic text post", async () => {
|
|
31
|
+
setupMockAPI();
|
|
32
|
+
const requestData: ThreadsPostRequest = {
|
|
33
|
+
userId: "12345",
|
|
34
|
+
accessToken: "token",
|
|
35
|
+
mediaType: "TEXT",
|
|
36
|
+
text: "Hello, Threads!",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await createThreadsContainer(requestData);
|
|
40
|
+
if (typeof result === "string") {
|
|
41
|
+
assertEquals(result.length > 0, true);
|
|
42
|
+
} else {
|
|
43
|
+
assertEquals(typeof result.id, "string");
|
|
44
|
+
assertEquals(result.id.length > 0, true);
|
|
45
|
+
}
|
|
46
|
+
teardownMockAPI();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await t.step("should handle image post with alt text", async () => {
|
|
50
|
+
setupMockAPI();
|
|
51
|
+
const requestData: ThreadsPostRequest = {
|
|
52
|
+
userId: "12345",
|
|
53
|
+
accessToken: "token",
|
|
54
|
+
mediaType: "IMAGE",
|
|
55
|
+
text: "Check out this image!",
|
|
56
|
+
imageUrl: "https://example.com/image.jpg",
|
|
57
|
+
altText: "A beautiful sunset",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const containerId = await createThreadsContainer(requestData);
|
|
61
|
+
if (typeof containerId === "string") {
|
|
62
|
+
assertEquals(containerId.length > 0, true);
|
|
63
|
+
} else {
|
|
64
|
+
assertEquals(typeof containerId.id, "string");
|
|
65
|
+
assertEquals(containerId.id.length > 0, true);
|
|
66
|
+
}
|
|
67
|
+
teardownMockAPI();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await t.step("should handle video post with all features", async () => {
|
|
71
|
+
setupMockAPI();
|
|
72
|
+
const requestData: ThreadsPostRequest = {
|
|
73
|
+
userId: "12345",
|
|
74
|
+
accessToken: "token",
|
|
75
|
+
mediaType: "VIDEO",
|
|
76
|
+
text: "Watch this video!",
|
|
77
|
+
videoUrl: "https://example.com/video.mp4",
|
|
78
|
+
altText: "A tutorial video",
|
|
79
|
+
replyControl: "mentioned_only",
|
|
80
|
+
allowlistedCountryCodes: ["US", "GB"],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const containerId = await createThreadsContainer(requestData);
|
|
84
|
+
if (typeof containerId === "string") {
|
|
85
|
+
assertEquals(containerId.length > 0, true);
|
|
86
|
+
} else {
|
|
87
|
+
assertEquals(typeof containerId.id, "string");
|
|
88
|
+
assertEquals(containerId.id.length > 0, true);
|
|
89
|
+
}
|
|
90
|
+
teardownMockAPI();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await t.step("should throw error on failure", async () => {
|
|
94
|
+
setupMockAPI();
|
|
95
|
+
const requestData: ThreadsPostRequest = {
|
|
96
|
+
userId: "12345",
|
|
97
|
+
accessToken: "invalid_token",
|
|
98
|
+
mediaType: "TEXT",
|
|
99
|
+
text: "Hello, Threads!",
|
|
100
|
+
linkAttachment: "https://example.com",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Mock the error in the MockThreadsAPI
|
|
104
|
+
mockAPI.setErrorMode(true);
|
|
105
|
+
|
|
106
|
+
await assertRejects(
|
|
107
|
+
() => createThreadsContainer(requestData),
|
|
108
|
+
Error,
|
|
109
|
+
"Failed to create Threads container"
|
|
110
|
+
);
|
|
111
|
+
teardownMockAPI();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await t.step(
|
|
115
|
+
"should throw error when CAROUSEL type is used without children",
|
|
116
|
+
async () => {
|
|
117
|
+
const requestData: ThreadsPostRequest = {
|
|
118
|
+
userId: "12345",
|
|
119
|
+
accessToken: "token",
|
|
120
|
+
mediaType: "CAROUSEL",
|
|
121
|
+
text: "This carousel has no items",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await assertRejects(
|
|
125
|
+
async () => await createThreadsContainer(requestData),
|
|
126
|
+
Error,
|
|
127
|
+
"CAROUSEL media type requires at least 2 children"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
320
130
|
);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
131
|
+
|
|
132
|
+
await t.step(
|
|
133
|
+
"should throw error when imageUrl is provided for non-IMAGE type",
|
|
134
|
+
async () => {
|
|
135
|
+
const requestData: ThreadsPostRequest = {
|
|
136
|
+
userId: "12345",
|
|
137
|
+
accessToken: "token",
|
|
138
|
+
mediaType: "TEXT",
|
|
139
|
+
text: "This shouldn't work",
|
|
140
|
+
imageUrl: "https://example.com/image.jpg",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
await assertRejects(
|
|
144
|
+
() => createThreadsContainer(requestData),
|
|
145
|
+
Error,
|
|
146
|
+
"imageUrl can only be used with IMAGE media type"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
339
149
|
);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
150
|
+
|
|
151
|
+
await t.step(
|
|
152
|
+
"should throw error when videoUrl is provided for non-VIDEO type",
|
|
153
|
+
async () => {
|
|
154
|
+
const requestData: ThreadsPostRequest = {
|
|
155
|
+
userId: "12345",
|
|
156
|
+
accessToken: "token",
|
|
157
|
+
mediaType: "IMAGE",
|
|
158
|
+
imageUrl: "https://example.com/image.jpg",
|
|
159
|
+
videoUrl: "https://example.com/video.mp4",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
await assertRejects(
|
|
163
|
+
() => createThreadsContainer(requestData),
|
|
164
|
+
Error,
|
|
165
|
+
"videoUrl can only be used with VIDEO media type"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
358
168
|
);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
Deno.test(
|
|
363
|
-
"createThreadsContainer should throw error when children is provided for non-CAROUSEL type",
|
|
364
|
-
async () => {
|
|
365
|
-
const requestData: ThreadsPostRequest = {
|
|
366
|
-
userId: "12345",
|
|
367
|
-
accessToken: "token",
|
|
368
|
-
mediaType: "IMAGE",
|
|
369
|
-
imageUrl: "https://example.com/image.jpg",
|
|
370
|
-
children: ["item1", "item2"],
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
await assertRejects(
|
|
169
|
+
|
|
170
|
+
await t.step(
|
|
171
|
+
"should throw error when linkAttachment is provided for non-TEXT type",
|
|
374
172
|
async () => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
173
|
+
const requestData: ThreadsPostRequest = {
|
|
174
|
+
userId: "12345",
|
|
175
|
+
accessToken: "token",
|
|
176
|
+
mediaType: "IMAGE",
|
|
177
|
+
imageUrl: "https://example.com/image.jpg",
|
|
178
|
+
linkAttachment: "https://example.com",
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
await assertRejects(
|
|
182
|
+
() => createThreadsContainer(requestData),
|
|
183
|
+
Error,
|
|
184
|
+
"linkAttachment can only be used with TEXT media type"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
379
187
|
);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
Deno.test(
|
|
384
|
-
"createThreadsContainer should throw error when CAROUSEL type is used without children",
|
|
385
|
-
async () => {
|
|
386
|
-
const requestData: ThreadsPostRequest = {
|
|
387
|
-
userId: "12345",
|
|
388
|
-
accessToken: "token",
|
|
389
|
-
mediaType: "CAROUSEL",
|
|
390
|
-
text: "This carousel has no items",
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
await assertRejects(
|
|
188
|
+
|
|
189
|
+
await t.step(
|
|
190
|
+
"should throw error when children is provided for non-CAROUSEL type",
|
|
394
191
|
async () => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
192
|
+
const requestData: ThreadsPostRequest = {
|
|
193
|
+
userId: "12345",
|
|
194
|
+
accessToken: "token",
|
|
195
|
+
mediaType: "IMAGE",
|
|
196
|
+
imageUrl: "https://example.com/image.jpg",
|
|
197
|
+
children: ["item1", "item2"],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await assertRejects(
|
|
201
|
+
async () => {
|
|
202
|
+
await createThreadsContainer(requestData);
|
|
203
|
+
},
|
|
204
|
+
Error,
|
|
205
|
+
"Failed to create Threads container"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
399
208
|
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await t.step("publishThreadsContainer", async (t) => {
|
|
212
|
+
await t.step("should publish container successfully", async () => {
|
|
213
|
+
setupMockAPI();
|
|
214
|
+
const userId = "12345";
|
|
215
|
+
const accessToken = "token";
|
|
216
|
+
const containerId = await createThreadsContainer({
|
|
217
|
+
userId,
|
|
218
|
+
accessToken,
|
|
219
|
+
mediaType: "TEXT",
|
|
220
|
+
text: "Test post",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await publishThreadsContainer(
|
|
224
|
+
userId,
|
|
225
|
+
accessToken,
|
|
226
|
+
typeof containerId === "string" ? containerId : containerId.id
|
|
227
|
+
);
|
|
228
|
+
if (typeof result === "string") {
|
|
229
|
+
assertEquals(result.length > 0, true);
|
|
230
|
+
} else {
|
|
231
|
+
assertEquals(typeof result.id, "string");
|
|
232
|
+
assertEquals(result.id.length > 0, true);
|
|
233
|
+
}
|
|
234
|
+
teardownMockAPI();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await t.step("should throw error on failure", async () => {
|
|
238
|
+
setupMockAPI();
|
|
239
|
+
const userId = "12345";
|
|
240
|
+
const accessToken = "invalid_token";
|
|
241
|
+
const containerId = "invalid_container";
|
|
242
|
+
|
|
243
|
+
// Mock the error in the MockThreadsAPI
|
|
244
|
+
mockAPI.setErrorMode(true);
|
|
245
|
+
|
|
246
|
+
await assertRejects(
|
|
247
|
+
() => publishThreadsContainer(userId, accessToken, containerId),
|
|
248
|
+
Error,
|
|
249
|
+
"Failed to publish Threads container"
|
|
250
|
+
);
|
|
251
|
+
teardownMockAPI();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await t.step("should return permalink when requested", async () => {
|
|
255
|
+
setupMockAPI();
|
|
256
|
+
const userId = "12345";
|
|
257
|
+
const accessToken = "token";
|
|
258
|
+
const containerId = await createThreadsContainer({
|
|
259
|
+
userId,
|
|
260
|
+
accessToken,
|
|
261
|
+
mediaType: "TEXT",
|
|
262
|
+
text: "Test post with permalink",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await publishThreadsContainer(
|
|
266
|
+
userId,
|
|
267
|
+
accessToken,
|
|
268
|
+
typeof containerId === "string" ? containerId : containerId.id,
|
|
269
|
+
true // Request permalink
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (typeof result === "string") {
|
|
273
|
+
throw new Error("Expected an object with permalink, but got a string");
|
|
274
|
+
} else {
|
|
275
|
+
assertEquals(typeof result, "object");
|
|
276
|
+
assertEquals(typeof result.id, "string");
|
|
277
|
+
assertEquals(typeof result.permalink, "string");
|
|
278
|
+
assertEquals(result.permalink.startsWith("https://"), true);
|
|
279
|
+
}
|
|
280
|
+
teardownMockAPI();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await t.step("should not return permalink when not requested", async () => {
|
|
284
|
+
setupMockAPI();
|
|
285
|
+
const userId = "12345";
|
|
286
|
+
const accessToken = "token";
|
|
287
|
+
const containerId = await createThreadsContainer({
|
|
288
|
+
userId,
|
|
289
|
+
accessToken,
|
|
290
|
+
mediaType: "TEXT",
|
|
291
|
+
text: "Test post without permalink",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const result = await publishThreadsContainer(
|
|
295
|
+
userId,
|
|
296
|
+
accessToken,
|
|
297
|
+
typeof containerId === "string" ? containerId : containerId.id,
|
|
298
|
+
false // Don't request permalink
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
assertEquals(typeof result, "string");
|
|
302
|
+
teardownMockAPI();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await t.step("createCarouselItem", async (t) => {
|
|
307
|
+
await t.step("should return item ID", async () => {
|
|
308
|
+
setupMockAPI();
|
|
309
|
+
const requestData = {
|
|
310
|
+
userId: "12345",
|
|
311
|
+
accessToken: "token",
|
|
312
|
+
mediaType: "IMAGE" as const,
|
|
313
|
+
imageUrl: "https://example.com/image.jpg",
|
|
314
|
+
altText: "Test image",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const itemId = await createCarouselItem(requestData);
|
|
318
|
+
if (typeof itemId === "string") {
|
|
319
|
+
assertEquals(itemId.length > 0, true);
|
|
320
|
+
} else {
|
|
321
|
+
assertEquals(typeof itemId.id, "string");
|
|
322
|
+
assertEquals(itemId.id.length > 0, true);
|
|
323
|
+
}
|
|
324
|
+
teardownMockAPI();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await t.step("should handle video items", async () => {
|
|
328
|
+
setupMockAPI();
|
|
329
|
+
const requestData = {
|
|
330
|
+
userId: "12345",
|
|
331
|
+
accessToken: "token",
|
|
332
|
+
mediaType: "VIDEO" as const,
|
|
333
|
+
videoUrl: "https://example.com/video.mp4",
|
|
334
|
+
altText: "Test video",
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const itemId = await createCarouselItem(requestData);
|
|
338
|
+
if (typeof itemId === "string") {
|
|
339
|
+
assertEquals(itemId.length > 0, true);
|
|
340
|
+
} else {
|
|
341
|
+
assertEquals(typeof itemId.id, "string");
|
|
342
|
+
assertEquals(itemId.id.length > 0, true);
|
|
343
|
+
}
|
|
344
|
+
teardownMockAPI();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await t.step("getThreadsList", async () => {
|
|
349
|
+
setupMockAPI();
|
|
350
|
+
const userId = "12345";
|
|
351
|
+
const accessToken = "valid_token";
|
|
352
|
+
|
|
353
|
+
// Create some test posts
|
|
354
|
+
await createThreadsContainer({
|
|
355
|
+
userId,
|
|
356
|
+
accessToken,
|
|
409
357
|
mediaType: "TEXT",
|
|
410
|
-
text: "
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const carouselRequest: ThreadsPostRequest = {
|
|
431
|
-
userId: "12345",
|
|
432
|
-
accessToken: "token",
|
|
433
|
-
mediaType: "CAROUSEL",
|
|
434
|
-
text: "A carousel post",
|
|
435
|
-
children: ["item1", "item2"],
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const textContainerId = await createThreadsContainer(textRequest);
|
|
439
|
-
const imageContainerId = await createThreadsContainer(imageRequest);
|
|
440
|
-
const videoContainerId = await createThreadsContainer(videoRequest);
|
|
441
|
-
const carouselContainerId = await createThreadsContainer(carouselRequest);
|
|
442
|
-
|
|
443
|
-
assertEquals(textContainerId, "container123");
|
|
444
|
-
assertEquals(imageContainerId, "container123");
|
|
445
|
-
assertEquals(videoContainerId, "container123");
|
|
446
|
-
assertEquals(carouselContainerId, "container123");
|
|
447
|
-
}
|
|
448
|
-
);
|
|
358
|
+
text: "Test post 1",
|
|
359
|
+
});
|
|
360
|
+
await createThreadsContainer({
|
|
361
|
+
userId,
|
|
362
|
+
accessToken,
|
|
363
|
+
mediaType: "TEXT",
|
|
364
|
+
text: "Test post 2",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const result = await getThreadsList(userId, accessToken);
|
|
368
|
+
assertEquals(Array.isArray(result.data), true);
|
|
369
|
+
assertEquals(result.data.length > 0, true);
|
|
370
|
+
assertEquals(result.data[0].text, "Test post 1");
|
|
371
|
+
assertEquals(result.data[1].text, "Test post 2");
|
|
372
|
+
if (result.paging) {
|
|
373
|
+
assertEquals(typeof result.paging.cursors.before, "string");
|
|
374
|
+
assertEquals(typeof result.paging.cursors.after, "string");
|
|
375
|
+
}
|
|
376
|
+
teardownMockAPI();
|
|
377
|
+
});
|
|
449
378
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
async () => {
|
|
379
|
+
await t.step("getSingleThread", async () => {
|
|
380
|
+
setupMockAPI();
|
|
453
381
|
const userId = "12345";
|
|
454
382
|
const accessToken = "valid_token";
|
|
383
|
+
const testText = "Test post for getSingleThread";
|
|
455
384
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
385
|
+
const containerId = await createThreadsContainer({
|
|
386
|
+
userId,
|
|
387
|
+
accessToken,
|
|
388
|
+
mediaType: "TEXT",
|
|
389
|
+
text: testText,
|
|
390
|
+
});
|
|
391
|
+
const mediaId = await publishThreadsContainer(
|
|
392
|
+
userId,
|
|
393
|
+
accessToken,
|
|
394
|
+
typeof containerId === "string" ? containerId : containerId.id
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const result = await getSingleThread(
|
|
398
|
+
typeof mediaId === "string" ? mediaId : mediaId.id,
|
|
399
|
+
accessToken
|
|
400
|
+
);
|
|
401
|
+
assertEquals(typeof result.id, "string");
|
|
402
|
+
assertEquals(result.text, testText);
|
|
403
|
+
teardownMockAPI();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
await t.step("getPublishingLimit", async (t) => {
|
|
407
|
+
await t.step("should return rate limit information", async () => {
|
|
408
|
+
setupMockAPI();
|
|
409
|
+
const userId = "12345";
|
|
410
|
+
const accessToken = "valid_token";
|
|
411
|
+
|
|
412
|
+
const result = await getPublishingLimit(userId, accessToken);
|
|
413
|
+
assertEquals(typeof result.quota_usage, "number");
|
|
414
|
+
assertEquals(typeof result.config.quota_total, "number");
|
|
415
|
+
assertEquals(typeof result.config.quota_duration, "number");
|
|
416
|
+
teardownMockAPI();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await t.step("should throw error on failure", async () => {
|
|
420
|
+
setupMockAPI();
|
|
421
|
+
const userId = "12345";
|
|
422
|
+
const accessToken = "invalid_token";
|
|
423
|
+
|
|
424
|
+
// Mock the error in the MockThreadsAPI
|
|
425
|
+
mockAPI.setErrorMode(true);
|
|
426
|
+
|
|
427
|
+
await assertRejects(
|
|
428
|
+
() => getPublishingLimit(userId, accessToken),
|
|
429
|
+
Error,
|
|
430
|
+
"Failed to get publishing limit"
|
|
431
|
+
);
|
|
432
|
+
teardownMockAPI();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
501
435
|
});
|