Añadido Dockerfil docker-compose.yml
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import type {
|
||||
Patient,
|
||||
Slot,
|
||||
Message,
|
||||
Conversation,
|
||||
Procedure,
|
||||
Staff,
|
||||
AgentConfig,
|
||||
MessageTemplate,
|
||||
} from "@/lib/types";
|
||||
|
||||
// ─── Reusable test fixtures ────────────────────────────────────────────────
|
||||
|
||||
export const testPatient: Patient = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
|
||||
name: "María García",
|
||||
phone: "+57 300 123 4567",
|
||||
email: "maria@example.com",
|
||||
status: "activo",
|
||||
created_at: "2024-01-15T10:00:00Z",
|
||||
updated_at: "2024-01-15T10:00:00Z",
|
||||
};
|
||||
|
||||
export const testProspect: Patient = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
name: "Carlos López",
|
||||
phone: "+57 310 987 6543",
|
||||
email: null,
|
||||
status: "prospecto",
|
||||
created_at: "2024-02-20T14:30:00Z",
|
||||
updated_at: "2024-02-20T14:30:00Z",
|
||||
};
|
||||
|
||||
export const testProcedure: Procedure = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a13",
|
||||
name: "Rinoplastia",
|
||||
description: "Cirugía de nariz estética y funcional",
|
||||
duration_min: 120,
|
||||
preparation_notes: "Ayuno 8h previo",
|
||||
is_active: true,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
export const testSlot: Slot = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a14",
|
||||
start_at: "2024-03-15T09:00:00Z",
|
||||
end_at: "2024-03-15T11:00:00Z",
|
||||
block_type: "appointment",
|
||||
patient_id: testPatient.id,
|
||||
procedure_id: testProcedure.id,
|
||||
status: "confirmed",
|
||||
channel: "whatsapp",
|
||||
notes: null,
|
||||
created_at: "2024-03-01T10:00:00Z",
|
||||
updated_at: "2024-03-01T10:00:00Z",
|
||||
};
|
||||
|
||||
export const testBlockSlot: Slot = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a15",
|
||||
start_at: "2024-03-16T08:00:00Z",
|
||||
end_at: "2024-03-16T18:00:00Z",
|
||||
block_type: "surgery",
|
||||
patient_id: null,
|
||||
procedure_id: null,
|
||||
status: "blocked",
|
||||
channel: null,
|
||||
notes: "Bloqueo quirófano",
|
||||
created_at: "2024-03-01T10:00:00Z",
|
||||
updated_at: "2024-03-01T10:00:00Z",
|
||||
};
|
||||
|
||||
export const testConversation: Conversation = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a16",
|
||||
patient_id: testPatient.id,
|
||||
channel: "whatsapp",
|
||||
status: "pending_human",
|
||||
ai_context: null,
|
||||
last_message_at: "2024-03-15T10:00:00Z",
|
||||
created_at: "2024-03-10T08:00:00Z",
|
||||
updated_at: "2024-03-15T10:00:00Z",
|
||||
};
|
||||
|
||||
export const testMessage: Message = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17",
|
||||
patient_id: testPatient.id,
|
||||
conversation_id: testConversation.id,
|
||||
channel: "whatsapp",
|
||||
direction: "in",
|
||||
type: "text",
|
||||
content: "Hola, quiero agendar una cita",
|
||||
audio_url: null,
|
||||
transcript: "Hola, quiero agendar una cita",
|
||||
created_at: "2024-03-15T10:00:00Z",
|
||||
};
|
||||
|
||||
export const testStaff: Staff = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18",
|
||||
name: "Dr. Silvio Alzate",
|
||||
role: "admin",
|
||||
email: "silvioalzate@gmail.com",
|
||||
is_active: true,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
export const testAgentConfig: AgentConfig = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
|
||||
system_prompt: "Eres un asistente de una clínica de cirugía plástica.",
|
||||
working_hours: {
|
||||
mon: ["08:00", "18:00"],
|
||||
tue: ["08:00", "18:00"],
|
||||
wed: ["08:00", "18:00"],
|
||||
thu: ["08:00", "18:00"],
|
||||
fri: ["08:00", "18:00"],
|
||||
sat: ["08:00", "13:00"],
|
||||
sun: null,
|
||||
},
|
||||
faq: [
|
||||
{ q: "¿Cuánto dura la recuperación?", a: "Depende del procedimiento, generalmente 1-2 semanas." },
|
||||
],
|
||||
enabled_channels: ["whatsapp", "web"],
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
export const testTemplate: MessageTemplate = {
|
||||
id: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a1a",
|
||||
name: "Confirmación de cita",
|
||||
type: "confirmation",
|
||||
content: "Hola {nombre}, tu cita para {procedimiento} está confirmada para el {fecha} a las {hora}.",
|
||||
channel: "whatsapp",
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
// ─── Test data builders ────────────────────────────────────────────────────
|
||||
|
||||
export function buildPatient(overrides: Partial<Patient> = {}): Patient {
|
||||
return {
|
||||
...testPatient,
|
||||
id: crypto.randomUUID(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSlot(overrides: Partial<Slot> = {}): Slot {
|
||||
return {
|
||||
...testSlot,
|
||||
id: crypto.randomUUID(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
...testMessage,
|
||||
id: crypto.randomUUID(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
/**
|
||||
* Helper to test Next.js App Router API routes.
|
||||
* Creates a Request object and extracts the Response.
|
||||
*/
|
||||
|
||||
export function createTestRequest(
|
||||
options: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
} = {}
|
||||
): Request {
|
||||
const { method = "POST", url = "http://localhost/api/test", body, headers = {} } = options;
|
||||
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
};
|
||||
|
||||
if (body !== undefined) {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return new Request(url, init);
|
||||
}
|
||||
|
||||
export async function parseJsonResponse(response: Response): Promise<unknown> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function expectJsonResponse(response: Response, expectedStatus: number) {
|
||||
expect(response.status).toBe(expectedStatus);
|
||||
expect(response.headers.get("content-type")?.toLowerCase()).toContain("application/json");
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// @ts-nocheck
|
||||
import { vi } from "vitest";
|
||||
import type { SupabaseClient, User, Session } from "@supabase/supabase-js";
|
||||
|
||||
// ─── Mock Supabase client factory ──────────────────────────────────────────
|
||||
|
||||
type MockQueryBuilder = {
|
||||
select: ReturnType<typeof vi.fn>;
|
||||
insert: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
eq: ReturnType<typeof vi.fn>;
|
||||
neq: ReturnType<typeof vi.fn>;
|
||||
gte: ReturnType<typeof vi.fn>;
|
||||
lte: ReturnType<typeof vi.fn>;
|
||||
gt: ReturnType<typeof vi.fn>;
|
||||
lt: ReturnType<typeof vi.fn>;
|
||||
in: ReturnType<typeof vi.fn>;
|
||||
is: ReturnType<typeof vi.fn>;
|
||||
not: ReturnType<typeof vi.fn>;
|
||||
or: ReturnType<typeof vi.fn>;
|
||||
ilike: ReturnType<typeof vi.fn>;
|
||||
order: ReturnType<typeof vi.fn>;
|
||||
limit: ReturnType<typeof vi.fn>;
|
||||
maybeSingle: ReturnType<typeof vi.fn>;
|
||||
single: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
then: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
export function createMockQueryBuilder(): MockQueryBuilder {
|
||||
const builder: Partial<MockQueryBuilder> = {};
|
||||
|
||||
const chainable = (returnValue?: unknown) => {
|
||||
const fn = vi.fn(() => returnValue ?? builder);
|
||||
return fn;
|
||||
};
|
||||
|
||||
builder.select = vi.fn(() => builder);
|
||||
builder.insert = vi.fn(() => builder);
|
||||
builder.update = vi.fn(() => builder);
|
||||
builder.delete = vi.fn(() => builder);
|
||||
builder.eq = vi.fn(() => builder);
|
||||
builder.neq = vi.fn(() => builder);
|
||||
builder.gte = vi.fn(() => builder);
|
||||
builder.lte = vi.fn(() => builder);
|
||||
builder.gt = vi.fn(() => builder);
|
||||
builder.lt = vi.fn(() => builder);
|
||||
builder.in = vi.fn(() => builder);
|
||||
builder.is = vi.fn(() => builder);
|
||||
builder.not = vi.fn(() => builder);
|
||||
builder.or = vi.fn(() => builder);
|
||||
builder.ilike = vi.fn(() => builder);
|
||||
builder.order = vi.fn(() => builder);
|
||||
builder.limit = vi.fn(() => builder);
|
||||
builder.maybeSingle = vi.fn(() =>
|
||||
Promise.resolve({ data: null, error: null })
|
||||
);
|
||||
builder.single = vi.fn(() => Promise.resolve({ data: null, error: null }));
|
||||
builder.count = vi.fn(() => builder);
|
||||
builder.then = vi.fn((onfulfilled) => {
|
||||
return Promise.resolve({ data: [], error: null }).then(onfulfilled);
|
||||
});
|
||||
|
||||
return builder as MockQueryBuilder;
|
||||
}
|
||||
|
||||
export function createMockSupabaseClient(
|
||||
overrides: Partial<SupabaseClient<unknown, "public", unknown>> = {}
|
||||
): SupabaseClient<unknown, "public", unknown> {
|
||||
const mockQuery = createMockQueryBuilder();
|
||||
|
||||
const client = {
|
||||
from: vi.fn(() => mockQuery),
|
||||
auth: {
|
||||
getUser: vi.fn(() =>
|
||||
Promise.resolve({ data: { user: null }, error: null })
|
||||
),
|
||||
getSession: vi.fn(() =>
|
||||
Promise.resolve({ data: { session: null }, error: null })
|
||||
),
|
||||
signInWithPassword: vi.fn(() =>
|
||||
Promise.resolve({ data: { user: null, session: null }, error: null })
|
||||
),
|
||||
signOut: vi.fn(() => Promise.resolve({ error: null })),
|
||||
onAuthStateChange: vi.fn(() => ({
|
||||
data: { subscription: { unsubscribe: vi.fn() } },
|
||||
})),
|
||||
updateUser: vi.fn(() => Promise.resolve({ data: { user: null }, error: null })),
|
||||
},
|
||||
channel: vi.fn(() => ({
|
||||
on: vi.fn(function () { return this; }),
|
||||
subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
|
||||
})),
|
||||
removeChannel: vi.fn(),
|
||||
...overrides,
|
||||
} as unknown as SupabaseClient<unknown, "public", unknown>;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function createMockAuthUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
id: "test-user-id",
|
||||
email: "test@example.com",
|
||||
app_metadata: { role: "admin" },
|
||||
user_metadata: {},
|
||||
aud: "authenticated",
|
||||
confirmation_sent_at: null,
|
||||
recovery_sent_at: null,
|
||||
email_change_sent_at: null,
|
||||
new_email: null,
|
||||
invited_at: null,
|
||||
action_link: null,
|
||||
phone: "",
|
||||
created_at: new Date().toISOString(),
|
||||
confirmed_at: null,
|
||||
last_sign_in_at: null,
|
||||
role: "authenticated",
|
||||
updated_at: new Date().toISOString(),
|
||||
identities: [],
|
||||
is_anonymous: false,
|
||||
factors: [],
|
||||
...overrides,
|
||||
} as User;
|
||||
}
|
||||
|
||||
export function createMockSession(overrides: Partial<Session> = {}): Session {
|
||||
return {
|
||||
access_token: "mock-access-token",
|
||||
refresh_token: "mock-refresh-token",
|
||||
expires_in: 3600,
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||
token_type: "bearer",
|
||||
user: createMockAuthUser(),
|
||||
...overrides,
|
||||
} as Session;
|
||||
}
|
||||
|
||||
// Helper to set mock auth state
|
||||
export function mockAuthenticatedUser(
|
||||
client: ReturnType<typeof createMockSupabaseClient>,
|
||||
user?: User
|
||||
) {
|
||||
const mockUser = user ?? createMockAuthUser();
|
||||
client.auth.getUser = vi.fn(() =>
|
||||
Promise.resolve({ data: { user: mockUser }, error: null })
|
||||
);
|
||||
client.auth.getSession = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { session: createMockSession({ user: mockUser }) },
|
||||
error: null,
|
||||
})
|
||||
);
|
||||
return mockUser;
|
||||
}
|
||||
|
||||
export function mockUnauthenticatedUser(
|
||||
client: ReturnType<typeof createMockSupabaseClient>
|
||||
) {
|
||||
client.auth.getUser = vi.fn(() =>
|
||||
Promise.resolve({ data: { user: null }, error: null })
|
||||
);
|
||||
client.auth.getSession = vi.fn(() =>
|
||||
Promise.resolve({ data: { session: null }, error: null })
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// ─── Global test setup ─────────────────────────────────────────────────────
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
back: vi.fn(),
|
||||
forward: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
}),
|
||||
usePathname: () => "/dashboard/calendar",
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock next/headers (server-only)
|
||||
vi.mock("next/headers", () => ({
|
||||
cookies: () =>
|
||||
Promise.resolve({
|
||||
getAll: () => [],
|
||||
set: vi.fn(),
|
||||
get: () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Suppress console.error in tests unless explicitly testing error paths
|
||||
const originalConsoleError = console.error;
|
||||
console.error = (...args: unknown[]) => {
|
||||
// Filter out known React warnings in test environment
|
||||
const msg = String(args[0] ?? "");
|
||||
if (
|
||||
msg.includes("Warning: ReactDOMTestUtils.act") ||
|
||||
msg.includes("not wrapped in act")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleError(...args);
|
||||
};
|
||||
Reference in New Issue
Block a user