c86e97e816
Deploy to route.crispygoat.com / deploy (push) Failing after 7s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
/**
|
|
* Unit tests for `getAdminUser()`. The dev_session cookie bypass has been
|
|
* removed; the only path into the admin is through a real Neon Auth
|
|
* session. These tests mock the `getSession()` function and the DB layer
|
|
* to exercise the lookup.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// Stub the server-only guard so the module can be imported under vitest.
|
|
vi.mock("server-only", () => ({}));
|
|
|
|
// Mock the Drizzle client wrapper so we don't need a real DB.
|
|
type MockFn = (arg: unknown) => Promise<unknown>;
|
|
const mockSelect = vi.fn();
|
|
const mockWithPlatformAdmin = vi.fn(async (fn: MockFn) => fn({ select: mockSelect }));
|
|
vi.mock("@/db/client", () => ({
|
|
withPlatformAdmin: (fn: MockFn) => mockWithPlatformAdmin(fn),
|
|
}));
|
|
|
|
// Mock the getSession() function. The default mock returns null (no session).
|
|
const getSessionMock = vi.fn();
|
|
vi.mock("@/lib/auth", () => ({
|
|
getSession: getSessionMock,
|
|
}));
|
|
|
|
// Mock cookies() so we don't read a real cookie store.
|
|
const cookieStoreGet = vi.fn();
|
|
vi.mock("next/headers", () => ({
|
|
cookies: () =>
|
|
Promise.resolve({
|
|
get: (name: string) => cookieStoreGet(name),
|
|
}),
|
|
}));
|
|
|
|
import { getAdminUser, buildDevAdmin, permissionsForRole } from "@/lib/admin-permissions";
|
|
|
|
beforeEach(() => {
|
|
mockSelect.mockReset();
|
|
getSessionMock.mockReset();
|
|
cookieStoreGet.mockReset();
|
|
});
|
|
|
|
describe("getAdminUser()", () => {
|
|
it("returns null when there is no Neon Auth session", async () => {
|
|
getSessionMock.mockResolvedValue({ data: null });
|
|
cookieStoreGet.mockReturnValue(undefined);
|
|
const u = await getAdminUser();
|
|
expect(u).toBeNull();
|
|
});
|
|
|
|
it("returns null when the session has no email", async () => {
|
|
getSessionMock.mockResolvedValue({ data: { user: { name: "no-email" } } });
|
|
const u = await getAdminUser();
|
|
expect(u).toBeNull();
|
|
});
|
|
|
|
it("returns null when the email is not in the users table", async () => {
|
|
getSessionMock.mockResolvedValue({ data: { user: { email: "unknown@example.com" } } });
|
|
// First select: users. Returns empty.
|
|
mockSelect.mockReturnValueOnce({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: async () => [],
|
|
}),
|
|
}),
|
|
});
|
|
const u = await getAdminUser();
|
|
expect(u).toBeNull();
|
|
});
|
|
|
|
it("returns null when the user exists but has no admin_user_brands row", async () => {
|
|
getSessionMock.mockResolvedValue({ data: { user: { email: "no-brand@example.com" } } });
|
|
// First select: users — returns the user
|
|
mockSelect
|
|
.mockReturnValueOnce({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: async () => [
|
|
{
|
|
id: "user-1",
|
|
email: "no-brand@example.com",
|
|
name: "No Brand",
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
})
|
|
// Second select: adminUserBrands — returns empty
|
|
.mockReturnValueOnce({
|
|
from: () => ({
|
|
innerJoin: () => ({
|
|
where: () => ({
|
|
limit: async () => [],
|
|
}),
|
|
}),
|
|
}),
|
|
});
|
|
const u = await getAdminUser();
|
|
expect(u).toBeNull();
|
|
});
|
|
|
|
it("returns a fully-populated AdminUser for a provisioned brand_admin", async () => {
|
|
getSessionMock.mockResolvedValue({ data: { user: { email: "admin@tuxedo.example" } } });
|
|
mockSelect
|
|
.mockReturnValueOnce({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: async () => [
|
|
{
|
|
id: "user-tux",
|
|
email: "admin@tuxedo.example",
|
|
name: "Tux Admin",
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
})
|
|
.mockReturnValueOnce({
|
|
from: () => ({
|
|
innerJoin: () => ({
|
|
where: () => ({
|
|
limit: async () => [
|
|
{
|
|
brandId: "brand-tux",
|
|
brandName: "Tuxedo Citrus",
|
|
brandSlug: "tuxedo",
|
|
role: "brand_admin",
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
}),
|
|
});
|
|
|
|
const u = await getAdminUser();
|
|
expect(u).not.toBeNull();
|
|
expect(u?.email).toBe("admin@tuxedo.example");
|
|
expect(u?.brand_id).toBe("brand-tux");
|
|
expect(u?.brand_slug).toBe("tuxedo");
|
|
expect(u?.role).toBe("brand_admin");
|
|
expect(u?.can_manage_products).toBe(true);
|
|
expect(u?.can_manage_team).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildDevAdmin()", () => {
|
|
it("returns a platform_admin with no brand", () => {
|
|
const u = buildDevAdmin("platform_admin");
|
|
expect(u.role).toBe("platform_admin");
|
|
expect(u.brand_id).toBeNull();
|
|
expect(u.brand_slug).toBeNull();
|
|
expect(u.can_manage_billing).toBe(true);
|
|
});
|
|
|
|
it("returns a brand_admin tied to the tuxedo brand", () => {
|
|
const u = buildDevAdmin("brand_admin");
|
|
expect(u.role).toBe("brand_admin");
|
|
expect(u.brand_slug).toBe("tuxedo");
|
|
expect(u.can_manage_users).toBe(false);
|
|
});
|
|
|
|
it("returns a store_employee with limited permissions", () => {
|
|
const u = buildDevAdmin("store_employee");
|
|
expect(u.role).toBe("store_employee");
|
|
expect(u.can_manage_products).toBe(false);
|
|
expect(u.can_manage_orders).toBe(true);
|
|
expect(u.can_manage_billing).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("permissionsForRole()", () => {
|
|
it("platform_admin can do everything", () => {
|
|
const p = permissionsForRole("platform_admin");
|
|
expect(Object.values(p).every((v) => v === true)).toBe(true);
|
|
});
|
|
it("brand_admin can manage most things but not users", () => {
|
|
const p = permissionsForRole("brand_admin");
|
|
expect(p.can_manage_users).toBe(false);
|
|
expect(p.can_manage_billing).toBe(true);
|
|
});
|
|
it("store_employee is restricted to orders + pickup", () => {
|
|
const p = permissionsForRole("store_employee");
|
|
expect(p.can_manage_orders).toBe(true);
|
|
expect(p.can_manage_pickup).toBe(true);
|
|
expect(p.can_manage_products).toBe(false);
|
|
expect(p.can_manage_billing).toBe(false);
|
|
});
|
|
});
|