Añadido Dockerfil docker-compose.yml

This commit is contained in:
2026-05-30 21:47:44 -05:00
parent 40986b0359
commit 5a563b214f
101 changed files with 10207 additions and 950 deletions
+159
View File
@@ -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,
};
}
+40
View File
@@ -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");
}
+167
View File
@@ -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 })
);
}
+42
View File
@@ -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);
};