159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
AUTUMN_DEFAULT_API_URL,
|
|
buildAutumnCustomerPayload,
|
|
syncAutumnCustomerOnRegistration,
|
|
} from "../src/autumn";
|
|
import type { Env } from "../src/env";
|
|
|
|
const baseEnv: Env = {
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
AUTUMN_SECRET_KEY: "autumn-secret",
|
|
AUTUMN_FREE_PLAN_ID: "free",
|
|
AUTUMN_API_URL: "https://autumn.example.com",
|
|
};
|
|
|
|
const user = {
|
|
id: "user_123",
|
|
email: "user@example.com",
|
|
name: "Example User",
|
|
};
|
|
|
|
let fetchMock: ReturnType<typeof vi.fn>;
|
|
let errorLog: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ id: "user_123" }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("Autumn registration sync", () => {
|
|
it("accepts the Autumn registration sync env shape", () => {
|
|
const env: Env = {
|
|
BETTER_AUTH_URL: "http://localhost:8788",
|
|
TRUSTED_ORIGINS: "http://localhost:8787",
|
|
AUTUMN_SECRET_KEY: "secret",
|
|
AUTUMN_FREE_PLAN_ID: "free",
|
|
AUTUMN_API_URL: "https://autumn.example.com",
|
|
AUTUMN_REGISTER_SYNC_ENABLED: "true",
|
|
};
|
|
|
|
expect(env.AUTUMN_FREE_PLAN_ID).toBe("free");
|
|
});
|
|
|
|
it("builds a stable user-scoped customer payload", () => {
|
|
expect(buildAutumnCustomerPayload(baseEnv, user)).toEqual({
|
|
customer_id: "user_123",
|
|
email: "user@example.com",
|
|
name: "Example User",
|
|
auto_enable_plan_id: "free",
|
|
metadata: {
|
|
source: "cfw-auth",
|
|
auth_provider: "better-auth",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("uses the Autumn default API URL when AUTUMN_API_URL is not configured", async () => {
|
|
await syncAutumnCustomerOnRegistration(
|
|
{
|
|
...baseEnv,
|
|
AUTUMN_API_URL: undefined,
|
|
},
|
|
user,
|
|
);
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
`${AUTUMN_DEFAULT_API_URL}/v1/customers.get_or_create`,
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("posts get_or_create with the configured secret and free plan", async () => {
|
|
await syncAutumnCustomerOnRegistration(baseEnv, user);
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
expect(url).toBe("https://autumn.example.com/v1/customers.get_or_create");
|
|
expect(init.method).toBe("POST");
|
|
expect(init.headers).toMatchObject({
|
|
Authorization: "Bearer autumn-secret",
|
|
"Content-Type": "application/json",
|
|
});
|
|
expect(JSON.parse(String(init.body))).toEqual({
|
|
customer_id: "user_123",
|
|
email: "user@example.com",
|
|
name: "Example User",
|
|
auto_enable_plan_id: "free",
|
|
metadata: {
|
|
source: "cfw-auth",
|
|
auth_provider: "better-auth",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not call Autumn when registration sync is disabled", async () => {
|
|
await syncAutumnCustomerOnRegistration(
|
|
{
|
|
...baseEnv,
|
|
AUTUMN_REGISTER_SYNC_ENABLED: "false",
|
|
},
|
|
user,
|
|
);
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
expect(errorLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails open and logs a redacted config error when required env is missing", async () => {
|
|
await expect(
|
|
syncAutumnCustomerOnRegistration(
|
|
{
|
|
...baseEnv,
|
|
AUTUMN_SECRET_KEY: undefined,
|
|
},
|
|
user,
|
|
),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
expect(errorLog).toHaveBeenCalledTimes(1);
|
|
const message = String(errorLog.mock.calls[0]?.[0]);
|
|
expect(message).toContain("autumn_registration_sync_failed");
|
|
expect(message).toContain("AUTUMN_SECRET_KEY");
|
|
expect(message).not.toContain("autumn-secret");
|
|
expect(message).not.toContain("Authorization");
|
|
});
|
|
|
|
it("fails open and logs a redacted Autumn API error", async () => {
|
|
fetchMock.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ message: "upstream exploded" }), {
|
|
status: 500,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
await expect(syncAutumnCustomerOnRegistration(baseEnv, user)).resolves.toBeUndefined();
|
|
|
|
expect(errorLog).toHaveBeenCalledTimes(1);
|
|
const message = String(errorLog.mock.calls[0]?.[0]);
|
|
expect(message).toContain("autumn_registration_sync_failed");
|
|
expect(message).toContain("user_123");
|
|
expect(message).toContain("500");
|
|
expect(message).not.toContain("autumn-secret");
|
|
expect(message).not.toContain("Bearer");
|
|
});
|
|
});
|