Skip to content
Merged
19 changes: 19 additions & 0 deletions app/api/artists/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getArtistsHandler } from "@/lib/artists/getArtistsHandler";
import { createArtistPostHandler } from "@/lib/artists/createArtistPostHandler";

/**
* OPTIONS handler for CORS preflight requests.
Expand Down Expand Up @@ -36,3 +37,21 @@ export async function GET(request: NextRequest) {
return getArtistsHandler(request);
}

/**
* POST /api/artists
*
* Creates a new artist account.
*
* Request body:
* - name (required): The name of the artist to create
* - account_id (optional): The ID of the account to create the artist for (UUID).
* Only required for organization API keys creating artists on behalf of other accounts.
* - organization_id (optional): The organization ID to link the new artist to (UUID)
*
* @param request - The request object containing JSON body
* @returns A NextResponse with the created artist data (201) or error
*/
export async function POST(request: NextRequest) {
return createArtistPostHandler(request);
}

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

const mockCreateArtistInDb = vi.fn();
const mockGetApiKeyDetails = vi.fn();
const mockCanAccessAccount = vi.fn();

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

vi.mock("@/lib/keys/getApiKeyDetails", () => ({
getApiKeyDetails: (...args: unknown[]) => mockGetApiKeyDetails(...args),
}));

vi.mock("@/lib/organizations/canAccessAccount", () => ({
canAccessAccount: (...args: unknown[]) => mockCanAccessAccount(...args),
}));

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

function createRequest(body: unknown, apiKey = "test-api-key"): NextRequest {
return new NextRequest("http://localhost/api/artists", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(body),
});
}

describe("createArtistPostHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetApiKeyDetails.mockResolvedValue({
accountId: "api-key-account-id",
orgId: null,
});
});

it("creates artist using account_id from API key", async () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = createRequest({ name: "Test Artist" });
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",
"api-key-account-id",
undefined,
);
});

it("uses account_id override for org API keys", async () => {
mockGetApiKeyDetails.mockResolvedValue({
accountId: "org-account-id",
orgId: "org-account-id",
});
mockCanAccessAccount.mockResolvedValue(true);

const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = createRequest({
name: "Test Artist",
account_id: "550e8400-e29b-41d4-a716-446655440000",
});
const response = await createArtistPostHandler(request);

expect(mockCanAccessAccount).toHaveBeenCalledWith({
orgId: "org-account-id",
targetAccountId: "550e8400-e29b-41d4-a716-446655440000",
});
expect(mockCreateArtistInDb).toHaveBeenCalledWith(
"Test Artist",
"550e8400-e29b-41d4-a716-446655440000",
undefined,
);
expect(response.status).toBe(201);
});

it("returns 403 when org API key lacks access to account_id", async () => {
mockGetApiKeyDetails.mockResolvedValue({
accountId: "org-account-id",
orgId: "org-account-id",
});
mockCanAccessAccount.mockResolvedValue(false);

const request = createRequest({
name: "Test Artist",
account_id: "550e8400-e29b-41d4-a716-446655440000",
});
const response = await createArtistPostHandler(request);

expect(response.status).toBe(403);
});

it("passes organization_id to createArtistInDb", async () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const request = createRequest({
name: "Test Artist",
organization_id: "660e8400-e29b-41d4-a716-446655440001",
});

await createArtistPostHandler(request);

expect(mockCreateArtistInDb).toHaveBeenCalledWith(
"Test Artist",
"api-key-account-id",
"660e8400-e29b-41d4-a716-446655440001",
);
});

it("returns 401 when API key is missing", async () => {
const request = new NextRequest("http://localhost/api/artists", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Test Artist" }),
});

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

expect(response.status).toBe(401);
expect(data.error).toBe("x-api-key header required");
});

it("returns 401 when API key is invalid", async () => {
mockGetApiKeyDetails.mockResolvedValue(null);

const request = createRequest({ name: "Test Artist" });
const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(401);
expect(data.error).toBe("Invalid API key");
});

it("returns 400 when name is missing", async () => {
const request = createRequest({});
const response = await createArtistPostHandler(request);

expect(response.status).toBe(400);
});

it("returns 400 for invalid JSON body", async () => {
const request = new NextRequest("http://localhost/api/artists", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "test-api-key",
},
body: "invalid json",
});

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

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

it("returns 500 when artist creation fails", async () => {
mockCreateArtistInDb.mockResolvedValue(null);

const request = createRequest({ name: "Test Artist" });
const response = await createArtistPostHandler(request);
const data = await response.json();

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

it("returns 500 with error message when exception thrown", async () => {
mockCreateArtistInDb.mockRejectedValue(new Error("Database error"));

const request = createRequest({ name: "Test Artist" });
const response = await createArtistPostHandler(request);
const data = await response.json();

expect(response.status).toBe(500);
expect(data.error).toBe("Database error");
});
});
Loading