UNPKG

@codybrom/denim

Version:

A Deno/TypeScript library for the Threads API

1,292 lines (1,147 loc) 37.6 kB
// mod_test.ts import { assertEquals, assertRejects } from "@std/assert"; import { createCarouselItem, createThreadsContainer, debugToken, deleteThread, exchangeCodeForToken, exchangeToken, getAppAccessToken, getConversation, getGhostPosts, getLocation, getMediaInsights, getMentions, getOEmbed, getProfile, getProfilePosts, getPublishingLimit, getReplies, getSingleThread, getThreadsList, getUserInsights, getUserReplies, lookupProfile, manageReply, type MockThreadsAPI, MockThreadsAPIImpl, publishThreadsContainer, refreshToken, repost, searchKeyword, searchLocations, type ThreadsPostRequest, } from "./mod.ts"; let mockAPI: MockThreadsAPIImpl; const validUrl = { image: "https://file-examples.com/wp-content/storage/2017/10/file_example_PNG_3MB.png", video: "https://file-examples.com/wp-content/storage/2018/04/file_example_MOV_1280_1_4MB.mov", link: "https://example.com", }; const invalidUrl = { link: "invalid_url", }; function setupMockAPI() { mockAPI = new MockThreadsAPIImpl(); (globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI = mockAPI; } function teardownMockAPI() { delete (globalThis as { threadsAPI?: MockThreadsAPI }).threadsAPI; } Deno.test("Denim API Tests", async (t) => { // ─── createThreadsContainer ────────────────────────────────────────────── await t.step("createThreadsContainer", async (t) => { await t.step("should return container ID for basic text post", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Hello, Threads!", }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle image post with alt text", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "IMAGE", text: "Check out this image!", imageUrl: validUrl.image, altText: "A beautiful sunset", }; const containerId = await createThreadsContainer(requestData); assertEquals(typeof containerId, "string"); assertEquals(containerId.length > 0, true); teardownMockAPI(); }); await t.step("should handle video post with all features", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "VIDEO", text: "Watch this video!", videoUrl: validUrl.video, altText: "A tutorial video", replyControl: "mentioned_only", allowlistedCountryCodes: ["US", "GB"], }; const containerId = await createThreadsContainer(requestData); assertEquals(typeof containerId, "string"); assertEquals(containerId.length > 0, true); teardownMockAPI(); }); await t.step("should handle text post with poll attachment", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "What do you prefer?", pollAttachment: { option_a: "Option A", option_b: "Option B", option_c: "Option C", }, }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle text post with GIF attachment", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Check this GIF!", gifAttachment: { gif_id: "abc123", provider: "TENOR", }, }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle ghost post", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "A ghost post", isGhostPost: true, }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle post with topic tag", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Tagged post", topicTag: "technology", }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle reply to another post", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "This is a reply", replyToId: "original_post_123", }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should handle spoiler media", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Spoiler content", isSpoilerMedia: true, textEntities: [{ entity_type: "spoiler", offset: 0, length: 7 }], }; const result = await createThreadsContainer(requestData); assertEquals(typeof result, "string"); assertEquals(result.length > 0, true); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "invalid_token", mediaType: "TEXT", text: "Hello, Threads!", linkAttachment: validUrl.link, }; mockAPI.setErrorMode(true); await assertRejects( () => createThreadsContainer(requestData), Error, "Failed to create Threads container", ); teardownMockAPI(); }); await t.step( "should throw error when CAROUSEL type is used without children", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "CAROUSEL", text: "This carousel has no items", }; await assertRejects( async () => await createThreadsContainer(requestData), Error, "CAROUSEL media type requires at least 2 children", ); }, ); await t.step( "should throw error when imageUrl is provided for non-IMAGE type", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "This shouldn't work", imageUrl: validUrl.image, }; await assertRejects( () => createThreadsContainer(requestData), Error, "imageUrl can only be used with IMAGE media type", ); }, ); await t.step( "should throw error when videoUrl is provided for non-VIDEO type", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "IMAGE", imageUrl: validUrl.image, videoUrl: validUrl.video, }; await assertRejects( () => createThreadsContainer(requestData), Error, "videoUrl can only be used with VIDEO media type", ); }, ); await t.step( "should throw error when linkAttachment is provided for non-TEXT type", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "IMAGE", imageUrl: validUrl.image, linkAttachment: validUrl.link, }; await assertRejects( () => createThreadsContainer(requestData), Error, "linkAttachment can only be used with TEXT media type", ); }, ); await t.step( "should throw error when pollAttachment used with non-TEXT type", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "IMAGE", imageUrl: validUrl.image, pollAttachment: { option_a: "A", option_b: "B" }, }; await assertRejects( () => createThreadsContainer(requestData), Error, "pollAttachment can only be used with TEXT media type", ); }, ); await t.step( "should throw error when ghost post is used as reply", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Ghost reply", isGhostPost: true, replyToId: "some_post", }; await assertRejects( () => createThreadsContainer(requestData), Error, "isGhostPost cannot be used together with replyToId", ); }, ); await t.step( "should throw error when textEntities exceeds 10", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Too many entities", textEntities: Array.from({ length: 11 }, (_, i) => ({ entity_type: "spoiler", offset: i * 5, length: 3, })), }; await assertRejects( () => createThreadsContainer(requestData), Error, "textEntities cannot have more than 10 entries", ); }, ); await t.step("should throw error for invalid link URL", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "TEXT", text: "This shouldn't work", linkAttachment: invalidUrl.link, }; await assertRejects( () => createThreadsContainer(requestData), Error, "Invalid URL format for linkAttachment", ); }); await t.step( "should throw error when children is provided for non-CAROUSEL type", async () => { const requestData: ThreadsPostRequest = { userId: "12345", accessToken: "token", mediaType: "IMAGE", imageUrl: validUrl.image, children: ["item1", "item2"], }; await assertRejects( async () => { await createThreadsContainer(requestData); }, Error, "children can only be used with CAROUSEL media type", ); }, ); }); // ─── publishThreadsContainer ───────────────────────────────────────────── await t.step("publishThreadsContainer", async (t) => { await t.step("should publish container successfully", async () => { setupMockAPI(); const userId = "12345"; const accessToken = "token"; const containerId = await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: "Test post", }); const result = await publishThreadsContainer( userId, accessToken, containerId, ); if (typeof result === "string") { assertEquals(result.length > 0, true); } else { assertEquals(typeof result.id, "string"); assertEquals(result.id.length > 0, true); } teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => publishThreadsContainer("12345", "invalid_token", "invalid_id"), Error, "Failed to publish Threads container", ); teardownMockAPI(); }); await t.step("should return permalink when requested", async () => { setupMockAPI(); const userId = "12345"; const accessToken = "token"; const containerId = await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: "Test post with permalink", }); const result = await publishThreadsContainer( userId, accessToken, containerId, true, ); if (typeof result === "string") { throw new Error("Expected an object with permalink, but got a string"); } else { assertEquals(typeof result, "object"); assertEquals(typeof result.id, "string"); assertEquals(typeof result.permalink, "string"); assertEquals(result.permalink.startsWith("https://"), true); } teardownMockAPI(); }); await t.step("should not return permalink when not requested", async () => { setupMockAPI(); const userId = "12345"; const accessToken = "token"; const containerId = await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: "Test post without permalink", }); const result = await publishThreadsContainer( userId, accessToken, containerId, false, ); assertEquals(typeof result, "string"); teardownMockAPI(); }); }); // ─── createCarouselItem ────────────────────────────────────────────────── await t.step("createCarouselItem", async (t) => { await t.step("should return item ID", async () => { setupMockAPI(); const requestData = { userId: "12345", accessToken: "token", mediaType: "IMAGE" as const, imageUrl: validUrl.image, altText: "Test image", }; const itemId = await createCarouselItem(requestData); assertEquals(typeof itemId, "string"); assertEquals(itemId.length > 0, true); teardownMockAPI(); }); await t.step("should handle video items", async () => { setupMockAPI(); const requestData = { userId: "12345", accessToken: "token", mediaType: "VIDEO" as const, videoUrl: validUrl.video, altText: "Test video", }; const itemId = await createCarouselItem(requestData); assertEquals(typeof itemId, "string"); assertEquals(itemId.length > 0, true); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => createCarouselItem({ userId: "12345", accessToken: "token", mediaType: "IMAGE" as const, imageUrl: validUrl.image, }), Error, "Failed to create carousel item", ); teardownMockAPI(); }); }); // ─── getThreadsList ────────────────────────────────────────────────────── await t.step("getThreadsList", async (t) => { await t.step("should return threads with data", async () => { setupMockAPI(); const userId = "12345"; const accessToken = "valid_token"; await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: "Test post 1", }); await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: "Test post 2", }); const result = await getThreadsList(userId, accessToken); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); assertEquals(result.data[0].text, "Test post 1"); assertEquals(result.data[1].text, "Test post 2"); if (result.paging) { assertEquals(typeof result.paging.cursors.before, "string"); assertEquals(typeof result.paging.cursors.after, "string"); } teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getThreadsList("12345", "token"), Error, "Failed to retrieve threads list", ); teardownMockAPI(); }); }); // ─── getSingleThread ───────────────────────────────────────────────────── await t.step("getSingleThread", async (t) => { await t.step("should return thread data", async () => { setupMockAPI(); const userId = "12345"; const accessToken = "valid_token"; const testText = "Test post for getSingleThread"; const containerId = await createThreadsContainer({ userId, accessToken, mediaType: "TEXT", text: testText, }); const mediaId = await publishThreadsContainer( userId, accessToken, containerId, ); const result = await getSingleThread( typeof mediaId === "string" ? mediaId : mediaId.id, accessToken, ); assertEquals(typeof result.id, "string"); assertEquals(result.text, testText); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getSingleThread("media_123", "token"), Error, "Failed to retrieve thread", ); teardownMockAPI(); }); }); // ─── getPublishingLimit ────────────────────────────────────────────────── await t.step("getPublishingLimit", async (t) => { await t.step("should return rate limit information", async () => { setupMockAPI(); const result = await getPublishingLimit("12345", "valid_token"); assertEquals(typeof result.quota_usage, "number"); assertEquals(typeof result.config?.quota_total, "number"); assertEquals(typeof result.config?.quota_duration, "number"); teardownMockAPI(); }); await t.step("should return reply quota when available", async () => { setupMockAPI(); const result = await getPublishingLimit("12345", "valid_token"); assertEquals(typeof result.reply_quota_usage, "number"); assertEquals(typeof result.reply_config?.quota_total, "number"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getPublishingLimit("12345", "invalid_token"), Error, "Failed to get publishing limit", ); teardownMockAPI(); }); }); // ─── repost ────────────────────────────────────────────────────────────── await t.step("repost", async (t) => { await t.step("should repost successfully", async () => { setupMockAPI(); const containerId = await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Post to repost", }); const mediaId = await publishThreadsContainer( "12345", "token", containerId, ); const result = await repost( typeof mediaId === "string" ? mediaId : mediaId.id, "token", ); assertEquals(typeof result.id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => repost("media_123", "token"), Error, "Failed to repost", ); teardownMockAPI(); }); }); // ─── deleteThread ──────────────────────────────────────────────────────── await t.step("deleteThread", async (t) => { await t.step("should delete successfully", async () => { setupMockAPI(); const containerId = await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Post to delete", }); const mediaId = await publishThreadsContainer( "12345", "token", containerId, ); const result = await deleteThread( typeof mediaId === "string" ? mediaId : mediaId.id, "token", ); assertEquals(result.success, true); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => deleteThread("media_123", "token"), Error, "Failed to delete thread", ); teardownMockAPI(); }); }); // ─── getProfile ────────────────────────────────────────────────────────── await t.step("getProfile", async (t) => { await t.step("should return profile data", async () => { setupMockAPI(); const result = await getProfile("12345", "token"); assertEquals(result.id, "12345"); assertEquals(result.username, "testuser"); assertEquals(result.name, "Test User"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getProfile("12345", "token"), Error, "Failed to get profile", ); teardownMockAPI(); }); }); // ─── lookupProfile ─────────────────────────────────────────────────────── await t.step("lookupProfile", async (t) => { await t.step("should look up profile by username", async () => { setupMockAPI(); const result = await lookupProfile("token", "testuser"); assertEquals(result.id, "12345"); assertEquals(result.username, "testuser"); teardownMockAPI(); }); await t.step("should throw error for unknown user", async () => { setupMockAPI(); await assertRejects( () => lookupProfile("token", "nonexistent_user"), Error, "Profile not found", ); teardownMockAPI(); }); }); // ─── getProfilePosts ───────────────────────────────────────────────────── await t.step("getProfilePosts", async (t) => { await t.step("should return profile posts", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Profile post", }); const result = await getProfilePosts("token", "testuser"); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getProfilePosts("token", "testuser"), Error, "Failed to get profile posts", ); teardownMockAPI(); }); }); // ─── getGhostPosts ─────────────────────────────────────────────────────── await t.step("getGhostPosts", async (t) => { await t.step("should return ghost posts", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Ghost post", }); const result = await getGhostPosts("12345", "token"); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getGhostPosts("12345", "token"), Error, "Failed to get ghost posts", ); teardownMockAPI(); }); }); // ─── getUserReplies ────────────────────────────────────────────────────── await t.step("getUserReplies", async (t) => { await t.step("should return user replies with paging", async () => { setupMockAPI(); const result = await getUserReplies("12345", "token"); assertEquals(Array.isArray(result.data), true); if (result.paging) { assertEquals(typeof result.paging.cursors.before, "string"); assertEquals(typeof result.paging.cursors.after, "string"); } teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getUserReplies("12345", "token"), Error, "Failed to get user replies", ); teardownMockAPI(); }); }); // ─── getReplies ────────────────────────────────────────────────────────── await t.step("getReplies", async (t) => { await t.step("should return replies", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Post with replies", }); const result = await getReplies("media_123", "token"); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getReplies("media_123", "token"), Error, "Failed to get replies", ); teardownMockAPI(); }); }); // ─── getConversation ───────────────────────────────────────────────────── await t.step("getConversation", async (t) => { await t.step("should return conversation", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Conversation root", }); const result = await getConversation("media_123", "token"); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getConversation("media_123", "token"), Error, "Failed to get conversation", ); teardownMockAPI(); }); }); // ─── manageReply ───────────────────────────────────────────────────────── await t.step("manageReply", async (t) => { await t.step("should hide a reply", async () => { setupMockAPI(); const result = await manageReply("reply_123", "token", true); assertEquals(result.success, true); teardownMockAPI(); }); await t.step("should unhide a reply", async () => { setupMockAPI(); const result = await manageReply("reply_123", "token", false); assertEquals(result.success, true); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => manageReply("reply_123", "token", true), Error, "Failed to manage reply", ); teardownMockAPI(); }); }); // ─── getMentions ───────────────────────────────────────────────────────── await t.step("getMentions", async (t) => { await t.step("should return mentions", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Mentioned post", }); const result = await getMentions("12345", "token"); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getMentions("12345", "token"), Error, "Failed to get mentions", ); teardownMockAPI(); }); }); // ─── getMediaInsights ──────────────────────────────────────────────────── await t.step("getMediaInsights", async (t) => { await t.step("should return insight metrics", async () => { setupMockAPI(); const result = await getMediaInsights("media_123", "token", [ "views", "likes", ]); assertEquals(result.data.length, 2); assertEquals(result.data[0].name, "views"); assertEquals(result.data[1].name, "likes"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getMediaInsights("media_123", "token", ["views"]), Error, "Failed to get media insights", ); teardownMockAPI(); }); }); // ─── getUserInsights ───────────────────────────────────────────────────── await t.step("getUserInsights", async (t) => { await t.step("should return user metrics", async () => { setupMockAPI(); const result = await getUserInsights("12345", "token", [ "views", "followers_count", ]); assertEquals(result.data.length, 2); assertEquals(result.data[0].name, "views"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getUserInsights("12345", "token", ["views"]), Error, "Failed to get user insights", ); teardownMockAPI(); }); }); // ─── searchKeyword ─────────────────────────────────────────────────────── await t.step("searchKeyword", async (t) => { await t.step("should return search results", async () => { setupMockAPI(); await createThreadsContainer({ userId: "12345", accessToken: "token", mediaType: "TEXT", text: "Searchable post", }); const result = await searchKeyword("token", { q: "test query", search_type: "TOP", }); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(typeof result.data[0].id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => searchKeyword("token", { q: "test" }), Error, "Failed to search keywords", ); teardownMockAPI(); }); }); // ─── searchLocations ───────────────────────────────────────────────────── await t.step("searchLocations", async (t) => { await t.step("should return location results", async () => { setupMockAPI(); const result = await searchLocations("token", { query: "San Francisco", }); assertEquals(Array.isArray(result.data), true); assertEquals(result.data.length > 0, true); assertEquals(result.data[0].name, "Test Location"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => searchLocations("token", { query: "test" }), Error, "Failed to search locations", ); teardownMockAPI(); }); }); // ─── getLocation ───────────────────────────────────────────────────────── await t.step("getLocation", async (t) => { await t.step("should return location data", async () => { setupMockAPI(); const result = await getLocation("loc_123", "token"); assertEquals(result.id, "loc_123"); assertEquals(result.name, "Test Location"); assertEquals(typeof result.latitude, "number"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getLocation("loc_123", "token"), Error, "Failed to get location", ); teardownMockAPI(); }); }); // ─── exchangeCodeForToken ──────────────────────────────────────────────── await t.step("exchangeCodeForToken", async (t) => { await t.step("should return short-lived token and user ID", async () => { setupMockAPI(); const result = await exchangeCodeForToken( "app_id", "app_secret", "auth_code", "https://example.com/callback", ); assertEquals(typeof result.access_token, "string"); assertEquals(typeof result.user_id, "string"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => exchangeCodeForToken( "id", "secret", "code", "https://example.com", ), Error, "Failed to exchange authorization code", ); teardownMockAPI(); }); }); // ─── getAppAccessToken ────────────────────────────────────────────────── await t.step("getAppAccessToken", async (t) => { await t.step("should return app access token", async () => { setupMockAPI(); const result = await getAppAccessToken("app_id", "app_secret"); assertEquals(typeof result.access_token, "string"); assertEquals(result.token_type, "bearer"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getAppAccessToken("id", "secret"), Error, "Failed to get app access token", ); teardownMockAPI(); }); }); // ─── exchangeToken ─────────────────────────────────────────────────────── await t.step("exchangeToken", async (t) => { await t.step("should return long-lived token", async () => { setupMockAPI(); const result = await exchangeToken("client_secret", "short_token"); assertEquals(typeof result.access_token, "string"); assertEquals(result.token_type, "bearer"); assertEquals(typeof result.expires_in, "number"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => exchangeToken("secret", "token"), Error, "Failed to exchange token", ); teardownMockAPI(); }); }); // ─── refreshToken ──────────────────────────────────────────────────────── await t.step("refreshToken", async (t) => { await t.step("should return refreshed token", async () => { setupMockAPI(); const result = await refreshToken("old_token"); assertEquals(typeof result.access_token, "string"); assertEquals(result.token_type, "bearer"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => refreshToken("token"), Error, "Failed to refresh token", ); teardownMockAPI(); }); }); // ─── debugToken ────────────────────────────────────────────────────────── await t.step("debugToken", async (t) => { await t.step("should return token debug info", async () => { setupMockAPI(); const result = await debugToken("access_token", "input_token"); assertEquals(result.data.is_valid, true); assertEquals(Array.isArray(result.data.scopes), true); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => debugToken("token", "input"), Error, "Failed to debug token", ); teardownMockAPI(); }); }); // ─── getOEmbed ─────────────────────────────────────────────────────────── await t.step("getOEmbed", async (t) => { await t.step("should return embed HTML", async () => { setupMockAPI(); const result = await getOEmbed( "token", "https://www.threads.net/@user/post/abc", ); assertEquals(typeof result.html, "string"); assertEquals(result.provider_name, "Threads"); teardownMockAPI(); }); await t.step("should throw error on failure", async () => { setupMockAPI(); mockAPI.setErrorMode(true); await assertRejects( () => getOEmbed("token", "https://example.com"), Error, "Failed to get oEmbed", ); teardownMockAPI(); }); }); });