Skip to content
Open
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
135 changes: 135 additions & 0 deletions packages/common/base/src/apiClient/ApiClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, expect, test, vi, beforeEach } from "vitest";
import { ApiClient } from "./ApiClient";

// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;

class TestApiClient extends ApiClient {
get authHeaders(): HeadersInit {
return { "x-api-key": "test-key" };
}

get baseUrl(): string {
return "https://api.test.com";
}
}

describe("ApiClient", () => {
let client: TestApiClient;

beforeEach(() => {
client = new TestApiClient();
mockFetch.mockClear();
});

describe("buildUrl", () => {
test("should correctly build URL with normalized paths", () => {
const url = client.buildUrl("/test/path");
expect(url).toBe("https://api.test.com/test/path");
});

test("should handle paths with leading and trailing slashes", () => {
const url = client.buildUrl("/test/path/");
expect(url).toBe("https://api.test.com/test/path");
});

test("should handle empty path", () => {
const url = client.buildUrl("");
expect(url).toBe("https://api.test.com/");
});
});

describe("HTTP methods", () => {
const testPath = "/test";
const testParams = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ test: "data" }),
};

test("should make GET request with correct parameters", async () => {
mockFetch.mockResolvedValueOnce(new Response());
await client.get(testPath, testParams);

expect(mockFetch).toHaveBeenCalledWith(
"https://api.test.com/test",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
"x-api-key": "test-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ test: "data" }),
})
);
});

test("should make POST request with correct parameters", async () => {
mockFetch.mockResolvedValueOnce(new Response());
await client.post(testPath, testParams);

expect(mockFetch).toHaveBeenCalledWith(
"https://api.test.com/test",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"x-api-key": "test-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ test: "data" }),
})
);
});

test("should make PUT request with correct parameters", async () => {
mockFetch.mockResolvedValueOnce(new Response());
await client.put(testPath, testParams);

expect(mockFetch).toHaveBeenCalledWith(
"https://api.test.com/test",
expect.objectContaining({
method: "PUT",
headers: expect.objectContaining({
"x-api-key": "test-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ test: "data" }),
})
);
});

test("should make DELETE request with correct parameters", async () => {
mockFetch.mockResolvedValueOnce(new Response());
await client.delete(testPath, testParams);

expect(mockFetch).toHaveBeenCalledWith(
"https://api.test.com/test",
expect.objectContaining({
method: "DELETE",
headers: expect.objectContaining({
"x-api-key": "test-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ test: "data" }),
})
);
});

test("should make PATCH request with correct parameters", async () => {
mockFetch.mockResolvedValueOnce(new Response());
await client.patch(testPath, testParams);

expect(mockFetch).toHaveBeenCalledWith(
"https://api.test.com/test",
expect.objectContaining({
method: "PATCH",
headers: expect.objectContaining({
"x-api-key": "test-key",
"Content-Type": "application/json",
}),
body: JSON.stringify({ test: "data" }),
})
);
});
});
});
101 changes: 101 additions & 0 deletions packages/common/base/src/apiClient/CrossmintApiClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from "vitest";
import { CrossmintApiClient } from "./CrossmintApiClient";
import { environmentToCrossmintBaseURL } from "../apiKey/utils/environmentToCrossmintBaseURL";

const VALID_API_KEY =
"ck_development_A61UZQnvjSQcM5qVBaBactgqebxafWAVsNdD2xLkgBxoYuH5q2guM8r9DUmZQzE1WYyoByGVYpEG2o9gVSzAZFsrLbfKGERUJ6D5CW6S9AsJGAc3ctgrsD4n2ioekzGj7KPbLwT3SysDjMamYXLxEroUbQSdwf6aLF4zeEpECq2crkTUQeLFzxzmjWNxFDHFYefDrfrFPCURvBXJLf5pCxCQ";

describe("CrossmintApiClient", () => {
const internalConfig = {
sdkMetadata: {
name: "test-sdk",
version: "1.0.0",
},
};

test("should throw error when API key is invalid", () => {
expect(() => {
new CrossmintApiClient(
{ apiKey: "invalid-key" },
{ internalConfig }
);
}).toThrow("Malformed API key. Must start with 'ck' or 'sk'.");
});

test("should throw error when API key has wrong environment", () => {
expect(() => {
new CrossmintApiClient(
{ apiKey: VALID_API_KEY },
{
internalConfig: {
...internalConfig,
apiKeyExpectations: { environment: "production" },
},
}
);
}).toThrow("Disallowed API key. You passed a development API key, but a production API key is required.");
});

test("should throw error when API key has wrong usage origin", () => {
expect(() => {
new CrossmintApiClient(
{ apiKey: VALID_API_KEY },
{
internalConfig: {
...internalConfig,
apiKeyExpectations: { usageOrigin: "server" },
},
}
);
}).toThrow("Disallowed API key. You passed a client API key, but a server API key is required.");
});

test("should initialize with valid API key", () => {
const client = new CrossmintApiClient(
{ apiKey: VALID_API_KEY },
{ internalConfig }
);

expect(client.environment).toBe("development");
expect(client.baseUrl).toBe(environmentToCrossmintBaseURL("development"));
expect(client.authHeaders).toEqual({
"x-api-key": VALID_API_KEY,
});
});

test("should use override base URL when provided", () => {
const overrideUrl = "https://custom-api.example.com";
const client = new CrossmintApiClient(
{ apiKey: VALID_API_KEY, overrideBaseUrl: overrideUrl },
{ internalConfig }
);

expect(client.baseUrl).toBe(overrideUrl);
});

test("should include JWT in auth headers when provided", () => {
const jwt = "test.jwt.token";
const client = new CrossmintApiClient(
{ apiKey: VALID_API_KEY, jwt },
{ internalConfig }
);

expect(client.authHeaders).toEqual({
"x-api-key": VALID_API_KEY,
Authorization: `Bearer ${jwt}`,
});
});

test("should handle different environments correctly", () => {
const environments = ["development", "staging", "production"] as const;

for (const env of environments) {
expect(() => {
new CrossmintApiClient(
{ apiKey: `ck_${env}_5KtPn3` },
{ internalConfig }
);
}).toThrow("Invalid API key. Failed to validate signature");
}
});
});
54 changes: 54 additions & 0 deletions packages/common/base/src/apiKey/validateAPIKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe("validateAPIKey", () => {
}

expect(result.isValid).toBe(false);
expect(result.message).toBe("Malformed API key. Must start with 'ck' or 'sk'.");
});

test("Should disallow when signature is invalid", () => {
Expand Down Expand Up @@ -46,4 +47,57 @@ describe("validateAPIKey", () => {
expect(result.environment).toBe("development");
expect(result.prefix).toBe("ck_development");
});

// Additional edge cases
test("Should disallow empty API key", () => {
const result = validateAPIKey("");
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Malformed API key. Must start with 'ck' or 'sk'.");
}
});

test("Should disallow API key with invalid base58 encoding", () => {
const result = validateAPIKey("ck_development_5KtPn3");
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Invalid API key. Failed to validate signature");
}
});

test("Should disallow API key with malformed data format", () => {
const malformedData = base58.encode(new TextEncoder().encode("invalid_format"));
const result = validateAPIKey(`ck_development_${malformedData}`);
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Invalid API key. Failed to validate signature");
}
});

test("Should disallow API key with missing project ID", () => {
const malformedData = base58.encode(new TextEncoder().encode(":5gt3DJTWBAw1AjL5pHo6z6NunHZNJqj15iEAveVN5CBUSqBB94Hetn9paFpx9zLFreQGAgy1TkDQaWSUXFMXjgvU"));
const result = validateAPIKey(`ck_development_${malformedData}`);
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Invalid API key. Failed to validate signature");
}
});

test("Should disallow API key with invalid project ID format", () => {
const malformedData = base58.encode(new TextEncoder().encode("not-a-uuid:5gt3DJTWBAw1AjL5pHo6z6NunHZNJqj15iEAveVN5CBUSqBB94Hetn9paFpx9zLFreQGAgy1TkDQaWSUXFMXjgvU"));
const result = validateAPIKey(`ck_development_${malformedData}`);
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Invalid API key. Failed to validate signature");
}
});

test("Should handle API key with maximum length", () => {
const longData = "a".repeat(1000);
const result = validateAPIKey(`ck_development_${base58.encode(new TextEncoder().encode(longData))}`);
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.message).toBe("Invalid API key. Failed to validate signature");
}
});
});
Loading