From 644a4ce6d6c185e7bf2f0cfba987b85b1659fd6f Mon Sep 17 00:00:00 2001 From: pradipthaadhi Date: Wed, 8 Apr 2026 14:31:36 +0700 Subject: [PATCH] feat: implement createTask function and associated tests - Added createTask function to handle task creation via the Recoup API, including error handling for various response scenarios. - Created unit tests for createTask to verify correct API interaction, including optional parameters and error cases. --- lib/tasks/__tests__/createTask.test.ts | 136 +++++++++++++++++++++++++ lib/tasks/createTask.ts | 55 ++++++++++ 2 files changed, 191 insertions(+) create mode 100644 lib/tasks/__tests__/createTask.test.ts create mode 100644 lib/tasks/createTask.ts diff --git a/lib/tasks/__tests__/createTask.test.ts b/lib/tasks/__tests__/createTask.test.ts new file mode 100644 index 000000000..672746e3c --- /dev/null +++ b/lib/tasks/__tests__/createTask.test.ts @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTask } from "@/lib/tasks/createTask"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +vi.mock("@/lib/api/getClientApiBaseUrl", () => ({ + getClientApiBaseUrl: vi.fn(), +})); + +describe("createTask", () => { + const accessToken = "test-token"; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getClientApiBaseUrl).mockReturnValue("https://api.recoupable.com"); + }); + + it("calls POST /api/tasks with bearer auth and required payload", async () => { + const createdTask = { id: "task-1" }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + status: "success", + tasks: [createdTask], + }), + }) as unknown as typeof fetch; + + const result = await createTask(accessToken, { + title: "Daily summary", + prompt: "Summarize fan growth", + schedule: "0 9 * * *", + artist_account_id: "artist-1", + }); + + expect(fetch).toHaveBeenCalledWith("https://api.recoupable.com/api/tasks", { + method: "POST", + headers: { + Authorization: "Bearer test-token", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: "Daily summary", + prompt: "Summarize fan growth", + schedule: "0 9 * * *", + artist_account_id: "artist-1", + }), + }); + expect(result).toEqual(createdTask); + }); + + it("includes optional account_id and model when provided", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + status: "success", + tasks: [{ id: "task-2" }], + }), + }) as unknown as typeof fetch; + + await createTask(accessToken, { + title: "Weekly sync", + prompt: "Generate weekly report", + schedule: "0 9 * * 1", + artist_account_id: "artist-2", + account_id: "account-2", + model: "anthropic/claude-sonnet-4.5", + }); + + const call = vi.mocked(fetch).mock.calls[0]; + const init = call[1] as RequestInit; + expect(init.body).toBe( + JSON.stringify({ + title: "Weekly sync", + prompt: "Generate weekly report", + schedule: "0 9 * * 1", + artist_account_id: "artist-2", + account_id: "account-2", + model: "anthropic/claude-sonnet-4.5", + }), + ); + }); + + it("throws on non-ok HTTP response", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 403, + text: vi.fn().mockResolvedValue('{"status":"error","error":"Access denied"}'), + }) as unknown as typeof fetch; + + await expect( + createTask(accessToken, { + title: "Denied", + prompt: "Denied", + schedule: "0 9 * * *", + artist_account_id: "artist-3", + }), + ).rejects.toThrow('HTTP 403: {"status":"error","error":"Access denied"}'); + }); + + it("throws when API returns status:error", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + status: "error", + error: "Validation failed", + }), + }) as unknown as typeof fetch; + + await expect( + createTask(accessToken, { + title: "Invalid", + prompt: "Invalid", + schedule: "0 9 * * *", + artist_account_id: "artist-4", + }), + ).rejects.toThrow("Validation failed"); + }); + + it("throws when success response has no created task", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + status: "success", + tasks: [], + }), + }) as unknown as typeof fetch; + + await expect( + createTask(accessToken, { + title: "No task", + prompt: "No task", + schedule: "0 9 * * *", + artist_account_id: "artist-5", + }), + ).rejects.toThrow("API returned success but no task was created"); + }); +}); diff --git a/lib/tasks/createTask.ts b/lib/tasks/createTask.ts new file mode 100644 index 000000000..a8b979688 --- /dev/null +++ b/lib/tasks/createTask.ts @@ -0,0 +1,55 @@ +import { Tables } from "@/types/database.types"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +import { GetTasksResponse } from "./getTasks"; + +type ScheduledAction = Tables<"scheduled_actions">; + +export interface CreateTaskParams { + title: string; + prompt: string; + schedule: string; + artist_account_id: string; + account_id?: string; + model?: string | null; +} + +/** + * Creates a new scheduled task via the Recoup API. + * Requires a valid bearer token because POST /api/tasks is auth-enforced. + */ +export async function createTask( + accessToken: string, + params: CreateTaskParams, +): Promise { + const response = await fetch(`${getClientApiBaseUrl()}/api/tasks`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: params.title, + prompt: params.prompt, + schedule: params.schedule, + artist_account_id: params.artist_account_id, + ...(params.account_id ? { account_id: params.account_id } : {}), + ...(params.model !== undefined ? { model: params.model } : {}), + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data: GetTasksResponse = await response.json(); + if (data.status === "error") { + throw new Error(data.error || "Unknown error occurred"); + } + + if (!data.tasks || data.tasks.length === 0) { + throw new Error("API returned success but no task was created"); + } + + return data.tasks[0]; +}