Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/api/artist/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createArtistPostHandler } from "@/lib/artists/createArtistPostHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/artist
*
* Creates a new artist account and associates it with an owner account.
*
* JSON body:
* - name (required): The name of the artist to create
* - account_id (required): The ID of the owner account (UUID)
*
* @param request - The request object containing JSON body
* @returns A NextResponse with the created artist data
*/
export async function POST(request: NextRequest) {
return createArtistPostHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
136 changes: 136 additions & 0 deletions lib/artists/__tests__/createArtistInDb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockInsertAccount = vi.fn();
const mockInsertAccountInfo = vi.fn();
const mockSelectAccountWithSocials = vi.fn();
const mockInsertAccountArtistId = vi.fn();
const mockAddArtistToOrganization = vi.fn();

vi.mock("@/lib/supabase/accounts/insertAccount", () => ({
insertAccount: (...args: unknown[]) => mockInsertAccount(...args),
}));

vi.mock("@/lib/supabase/account_info/insertAccountInfo", () => ({
insertAccountInfo: (...args: unknown[]) => mockInsertAccountInfo(...args),
}));

vi.mock("@/lib/supabase/accounts/selectAccountWithSocials", () => ({
selectAccountWithSocials: (...args: unknown[]) => mockSelectAccountWithSocials(...args),
}));

vi.mock("@/lib/supabase/account_artist_ids/insertAccountArtistId", () => ({
insertAccountArtistId: (...args: unknown[]) => mockInsertAccountArtistId(...args),
}));

vi.mock("@/lib/supabase/artist_organization_ids/addArtistToOrganization", () => ({
addArtistToOrganization: (...args: unknown[]) => mockAddArtistToOrganization(...args),
}));

import { createArtistInDb } from "../createArtistInDb";

describe("createArtistInDb", () => {
const mockAccount = {
id: "artist-123",
name: "Test Artist",
created_at: "2026-01-15T00:00:00Z",
updated_at: "2026-01-15T00:00:00Z",
};

const mockAccountInfo = {
id: "info-123",
account_id: "artist-123",
image: null,
instruction: null,
knowledges: null,
label: null,
organization: null,
company_name: null,
job_title: null,
role_type: null,
onboarding_status: null,
onboarding_data: null,
};

const mockFullAccount = {
...mockAccount,
account_socials: [],
account_info: [mockAccountInfo],
};

beforeEach(() => {
vi.clearAllMocks();
});

it("creates an artist account with all required steps", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });

const result = await createArtistInDb("Test Artist", "owner-456");

expect(mockInsertAccount).toHaveBeenCalledWith({ name: "Test Artist" });
expect(mockInsertAccountInfo).toHaveBeenCalledWith({ account_id: "artist-123" });
expect(mockSelectAccountWithSocials).toHaveBeenCalledWith("artist-123");
expect(mockInsertAccountArtistId).toHaveBeenCalledWith("owner-456", "artist-123");
expect(result).toMatchObject({
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
});
});

it("links artist to organization when organizationId is provided", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
mockAddArtistToOrganization.mockResolvedValue("org-rel-123");

const result = await createArtistInDb("Test Artist", "owner-456", "org-789");

expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789");
expect(result).not.toBeNull();
});

it("returns null when account creation fails", async () => {
mockInsertAccount.mockRejectedValue(new Error("Insert failed"));

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockInsertAccountInfo).not.toHaveBeenCalled();
});

it("returns null when account info creation fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(null);

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockSelectAccountWithSocials).not.toHaveBeenCalled();
});

it("returns null when fetching full account data fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(null);

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockInsertAccountArtistId).not.toHaveBeenCalled();
});

it("returns null when associating artist with owner fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockRejectedValue(new Error("Association failed"));

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
});
});
197 changes: 197 additions & 0 deletions lib/artists/__tests__/createArtistPostHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";

const mockCreateArtistInDb = vi.fn();
const mockValidateCreateArtistBody = vi.fn();

vi.mock("@/lib/artists/createArtistInDb", () => ({
createArtistInDb: (...args: unknown[]) => mockCreateArtistInDb(...args),
}));

vi.mock("@/lib/artists/validateCreateArtistBody", () => ({
validateCreateArtistBody: (...args: unknown[]) =>
mockValidateCreateArtistBody(...args),
}));

import { createArtistPostHandler } from "../createArtistPostHandler";

describe("createArtistPostHandler", () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
created_at: "2026-01-15T00:00:00Z",
updated_at: "2026-01-15T00:00:00Z",
image: null,
instruction: null,
knowledges: null,
label: null,
organization: null,
company_name: null,
job_title: null,
role_type: null,
onboarding_status: null,
onboarding_data: null,
account_info: [],
account_socials: [],
};

beforeEach(() => {
vi.clearAllMocks();
});

it("returns 201 with artist data on successful creation", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(201);
expect(data.artist).toEqual(mockArtist);
expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456");
});

it("parses JSON body from request", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

await createArtistPostHandler(request);

expect(mockValidateCreateArtistBody).toHaveBeenCalledWith(validatedBody);
});

it("returns validation error response when validation fails", async () => {
const { NextResponse } = await import("next/server");
const errorResponse = NextResponse.json(
{ status: "error", error: "name is required" },
{ status: 400 },
);
mockValidateCreateArtistBody.mockReturnValue(errorResponse);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify({ account_id: "owner-456" }),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);

expect(response.status).toBe(400);
expect(mockCreateArtistInDb).not.toHaveBeenCalled();
});

it("returns 500 when createArtistInDb returns null", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockResolvedValue(null);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(500);
expect(data.message).toBe("Failed to create artist");
});

it("returns 400 with error message when createArtistInDb throws", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockRejectedValue(new Error("Database error"));

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.message).toBe("Database error");
});

it("returns 400 when request body is not valid JSON", async () => {
const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: "not-json",
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.message).toBe("Invalid JSON body");
});

it("includes CORS headers in successful response", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);

expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
});

it("includes CORS headers in error response", async () => {
const validatedBody = {
name: "Test Artist",
account_id: "owner-456",
};
mockValidateCreateArtistBody.mockReturnValue(validatedBody);
mockCreateArtistInDb.mockResolvedValue(null);

const request = new NextRequest("http://localhost/api/artist", {
method: "POST",
body: JSON.stringify(validatedBody),
headers: { "Content-Type": "application/json" },
});

const response = await createArtistPostHandler(request);

expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
});
});
Loading