test: añadir tests E2E ICS para grupo, personal y agregado
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
parent
3a161e2821
commit
a8a0a3b5f6
@ -0,0 +1,179 @@
|
||||
import { describe, it, expect, afterAll } from 'bun:test';
|
||||
import Database from 'bun:sqlite';
|
||||
import { startWebServer } from './helpers/server';
|
||||
import { createTempDb } from './helpers/db';
|
||||
|
||||
async function sha256Hex(input: string): Promise<string> {
|
||||
const enc = new TextEncoder().encode(input);
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc);
|
||||
const bytes = new Uint8Array(buf);
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function toIsoSql(d = new Date()): string {
|
||||
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
function ymdUTC(date = new Date()): string {
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, '0');
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getUTCDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number): Date {
|
||||
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function pad4(n: number): string {
|
||||
const s = String(Math.floor(n));
|
||||
return s.length >= 4 ? s : '0'.repeat(4 - s.length) + s;
|
||||
}
|
||||
|
||||
describe('ICS - aggregate feed', () => {
|
||||
const PORT = 19133;
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
const USER = '34600123456';
|
||||
const G1 = 'g1@g.us';
|
||||
const G2 = 'g2@g.us';
|
||||
const G3 = 'g3@g.us'; // no permitido
|
||||
const SID = 'sid-ics-aggregate-1';
|
||||
|
||||
const tmp = createTempDb();
|
||||
const db: any = tmp.db as Database;
|
||||
|
||||
// Sembrar datos mínimos
|
||||
db.exec(`INSERT OR IGNORE INTO users (id) VALUES ('${USER}')`);
|
||||
for (const [gid, name] of [
|
||||
[G1, 'Group A'],
|
||||
[G2, 'Group B'],
|
||||
[G3, 'Group C'],
|
||||
] as const) {
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${gid}', 'comm', '${name}', 1)`
|
||||
);
|
||||
}
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G1}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G2}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G3}', 'blocked', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
for (const gid of [G1, G2]) {
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
||||
VALUES ('${gid}', '${USER}', 0, 1, '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const dueIn3 = ymdUTC(addDays(today, 3));
|
||||
const dueIn6 = ymdUTC(addDays(today, 6));
|
||||
const dueFar = ymdUTC(addDays(today, 400));
|
||||
|
||||
const insTask = db.prepare(
|
||||
`INSERT INTO tasks (description, due_date, group_id, created_by, completed, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
const createdBy = USER;
|
||||
|
||||
// G1 unassigned -> incluido
|
||||
const r1 = insTask.run('G1 unassigned', dueIn3, G1, createdBy, 0, toIsoSql());
|
||||
const t1 = Number(r1.lastInsertRowid);
|
||||
|
||||
// G1 assigned -> excluido (aggregate sólo "sin responsable")
|
||||
const r2 = insTask.run('G1 assigned', dueIn3, G1, createdBy, 0, toIsoSql());
|
||||
const t2 = Number(r2.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t2, USER, USER, toIsoSql());
|
||||
|
||||
// G2 unassigned -> incluido
|
||||
const r3 = insTask.run('G2 unassigned', dueIn6, G2, createdBy, 0, toIsoSql());
|
||||
const t3 = Number(r3.lastInsertRowid);
|
||||
|
||||
// G3 unassigned (no permitido) -> excluido
|
||||
const r4 = insTask.run('G3 unassigned', dueIn6, G3, createdBy, 0, toIsoSql());
|
||||
const t4 = Number(r4.lastInsertRowid);
|
||||
|
||||
// G1 far future -> excluido por horizonte
|
||||
const r5 = insTask.run('G1 far', dueFar, G1, createdBy, 0, toIsoSql());
|
||||
const t5 = Number(r5.lastInsertRowid);
|
||||
|
||||
const sidHashPromise = sha256Hex(SID);
|
||||
const serverPromise = startWebServer({
|
||||
port: PORT,
|
||||
env: {
|
||||
DB_PATH: tmp.path,
|
||||
WEB_BASE_URL: BASE,
|
||||
},
|
||||
});
|
||||
|
||||
let server: Awaited<typeof serverPromise> | null = null;
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await server?.stop();
|
||||
} catch {}
|
||||
try {
|
||||
tmp.cleanup();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('serves ICS for aggregate token with correct filtering, supports ETag, and returns 410 when revoked', async () => {
|
||||
server = await serverPromise;
|
||||
|
||||
const sidHash = await sidHashPromise;
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at)
|
||||
VALUES ('sess-ics-aggregate', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(
|
||||
addDays(new Date(), 1)
|
||||
)}')
|
||||
`);
|
||||
|
||||
// Obtener URLs de feeds
|
||||
const resFeeds = await fetch(`${BASE}/api/integrations/feeds`, {
|
||||
headers: { cookie: `sid=${SID}` },
|
||||
});
|
||||
expect(resFeeds.status).toBe(200);
|
||||
const feeds = await resFeeds.json();
|
||||
expect(feeds.aggregate && typeof feeds.aggregate.url === 'string').toBe(true);
|
||||
|
||||
const aggregateUrl: string = feeds.aggregate.url;
|
||||
const token = new URL(aggregateUrl).pathname.split('/').pop()!.replace(/\.ics$/i, '');
|
||||
|
||||
// Primera petición ICS
|
||||
const resIcs = await fetch(aggregateUrl);
|
||||
expect(resIcs.status).toBe(200);
|
||||
expect((resIcs.headers.get('content-type') || '').includes('text/calendar')).toBe(true);
|
||||
const body1 = await resIcs.text();
|
||||
|
||||
// Incluidos: t1, t3
|
||||
expect(body1.includes(`[T${pad4(t1)}]`)).toBe(true);
|
||||
expect(body1.includes(`[T${pad4(t3)}]`)).toBe(true);
|
||||
|
||||
// Excluidos: t2 (assigned), t4 (no permitido), t5 (far)
|
||||
expect(body1.includes(`[T${pad4(t2)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t4)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t5)}]`)).toBe(false);
|
||||
|
||||
const etag = resIcs.headers.get('etag') || '';
|
||||
const res304 = await fetch(aggregateUrl, { headers: { 'if-none-match': etag } });
|
||||
expect(res304.status).toBe(304);
|
||||
|
||||
const row = db
|
||||
.prepare(`SELECT last_used_at FROM calendar_tokens WHERE token_plain = ?`)
|
||||
.get(token) as any;
|
||||
expect(row && row.last_used_at).toBeTruthy();
|
||||
|
||||
db.prepare(`UPDATE calendar_tokens SET revoked_at = ? WHERE token_plain = ?`).run(toIsoSql(), token);
|
||||
const resGone = await fetch(aggregateUrl);
|
||||
expect(resGone.status).toBe(410);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,166 @@
|
||||
import { describe, it, expect, afterAll } from 'bun:test';
|
||||
import Database from 'bun:sqlite';
|
||||
import { startWebServer } from './helpers/server';
|
||||
import { createTempDb } from './helpers/db';
|
||||
|
||||
async function sha256Hex(input: string): Promise<string> {
|
||||
const enc = new TextEncoder().encode(input);
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc);
|
||||
const bytes = new Uint8Array(buf);
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function toIsoSql(d = new Date()): string {
|
||||
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
function ymdUTC(date = new Date()): string {
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, '0');
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getUTCDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number): Date {
|
||||
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function pad4(n: number): string {
|
||||
const s = String(Math.floor(n));
|
||||
return s.length >= 4 ? s : '0'.repeat(4 - s.length) + s;
|
||||
}
|
||||
|
||||
describe('ICS - group feed', () => {
|
||||
const PORT = 19131;
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
const USER = '34600123456';
|
||||
const GROUP = '123@g.us';
|
||||
const SID = 'sid-ics-group-1';
|
||||
|
||||
const tmp = createTempDb();
|
||||
const db: any = tmp.db as Database;
|
||||
|
||||
// Sembrar datos mínimos
|
||||
db.exec(`INSERT OR IGNORE INTO users (id) VALUES ('${USER}')`);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${GROUP}', 'comm1', 'Group One', 1)`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${GROUP}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
||||
VALUES ('${GROUP}', '${USER}', 0, 1, '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
|
||||
// Tareas del grupo (varias condiciones)
|
||||
const today = new Date();
|
||||
const dueIn2 = ymdUTC(addDays(today, 2));
|
||||
const dueIn400 = ymdUTC(addDays(today, 400));
|
||||
|
||||
const insTask = db.prepare(
|
||||
`INSERT INTO tasks (description, due_date, group_id, created_by, completed, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
const createdBy = USER;
|
||||
|
||||
// t1: sin responsable, dentro de horizonte -> debe aparecer
|
||||
const r1 = insTask.run('Task unassigned in range', dueIn2, GROUP, createdBy, 0, toIsoSql());
|
||||
const t1 = Number(r1.lastInsertRowid);
|
||||
|
||||
// t2: con responsable -> NO debe aparecer
|
||||
const r2 = insTask.run('Task assigned', dueIn2, GROUP, createdBy, 0, toIsoSql());
|
||||
const t2 = Number(r2.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t2, USER, USER, toIsoSql());
|
||||
|
||||
// t3: completada -> NO debe aparecer
|
||||
const r3 = insTask.run('Task completed', dueIn2, GROUP, createdBy, 1, toIsoSql());
|
||||
const t3 = Number(r3.lastInsertRowid);
|
||||
|
||||
// t4: muy lejos (fuera de horizonte) -> NO debe aparecer
|
||||
const r4 = insTask.run('Task far future', dueIn400, GROUP, createdBy, 0, toIsoSql());
|
||||
const t4 = Number(r4.lastInsertRowid);
|
||||
|
||||
// t5: sin due_date -> NO debe aparecer
|
||||
const r5 = insTask.run('Task without due', null, GROUP, createdBy, 0, toIsoSql());
|
||||
const t5 = Number(r5.lastInsertRowid);
|
||||
|
||||
const sidHashPromise = sha256Hex(SID);
|
||||
const serverPromise = startWebServer({
|
||||
port: PORT,
|
||||
env: {
|
||||
DB_PATH: tmp.path,
|
||||
WEB_BASE_URL: BASE,
|
||||
},
|
||||
});
|
||||
|
||||
let server: Awaited<typeof serverPromise> | null = null;
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await server?.stop();
|
||||
} catch {}
|
||||
try {
|
||||
tmp.cleanup();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('serves ICS for group token with correct filtering, supports ETag, and returns 410 when revoked', async () => {
|
||||
server = await serverPromise;
|
||||
|
||||
const sidHash = await sidHashPromise;
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at)
|
||||
VALUES ('sess-ics-group', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(
|
||||
addDays(new Date(), 1)
|
||||
)}')
|
||||
`);
|
||||
|
||||
// Obtener URLs de feeds (autogenera tokens)
|
||||
const resFeeds = await fetch(`${BASE}/api/integrations/feeds`, {
|
||||
headers: { cookie: `sid=${SID}` },
|
||||
});
|
||||
expect(resFeeds.status).toBe(200);
|
||||
const feeds = await resFeeds.json();
|
||||
const groupItem = (feeds.groups || []).find((g: any) => g.groupId === GROUP);
|
||||
expect(groupItem && typeof groupItem.url === 'string' && groupItem.url.endsWith('.ics')).toBe(true);
|
||||
|
||||
const groupUrl = groupItem.url as string;
|
||||
const token = new URL(groupUrl).pathname.split('/').pop()!.replace(/\.ics$/i, '');
|
||||
|
||||
// Primera petición ICS
|
||||
const resIcs = await fetch(groupUrl);
|
||||
expect(resIcs.status).toBe(200);
|
||||
expect((resIcs.headers.get('content-type') || '').includes('text/calendar')).toBe(true);
|
||||
const body1 = await resIcs.text();
|
||||
|
||||
// Debe contener solo t1
|
||||
expect(body1.includes(`[T${pad4(t1)}]`)).toBe(true);
|
||||
expect(body1.includes(`[T${pad4(t2)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t3)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t4)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t5)}]`)).toBe(false);
|
||||
|
||||
const etag = resIcs.headers.get('etag') || '';
|
||||
|
||||
// Segunda petición con If-None-Match -> 304
|
||||
const res304 = await fetch(groupUrl, { headers: { 'if-none-match': etag } });
|
||||
expect(res304.status).toBe(304);
|
||||
|
||||
// last_used_at actualizado
|
||||
const row = db
|
||||
.prepare(`SELECT last_used_at FROM calendar_tokens WHERE token_plain = ?`)
|
||||
.get(token) as any;
|
||||
expect(row && row.last_used_at).toBeTruthy();
|
||||
|
||||
// Revocar el token y comprobar 410
|
||||
db.prepare(`UPDATE calendar_tokens SET revoked_at = ? WHERE token_plain = ?`).run(toIsoSql(), token);
|
||||
const resGone = await fetch(groupUrl);
|
||||
expect(resGone.status).toBe(410);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,178 @@
|
||||
import { describe, it, expect, afterAll } from 'bun:test';
|
||||
import Database from 'bun:sqlite';
|
||||
import { startWebServer } from './helpers/server';
|
||||
import { createTempDb } from './helpers/db';
|
||||
|
||||
async function sha256Hex(input: string): Promise<string> {
|
||||
const enc = new TextEncoder().encode(input);
|
||||
const buf = await crypto.subtle.digest('SHA-256', enc);
|
||||
const bytes = new Uint8Array(buf);
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function toIsoSql(d = new Date()): string {
|
||||
return d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
}
|
||||
|
||||
function ymdUTC(date = new Date()): string {
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, '0');
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getUTCDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number): Date {
|
||||
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function pad4(n: number): string {
|
||||
const s = String(Math.floor(n));
|
||||
return s.length >= 4 ? s : '0'.repeat(4 - s.length) + s;
|
||||
}
|
||||
|
||||
describe('ICS - personal feed', () => {
|
||||
const PORT = 19132;
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
const USER = '34600123456';
|
||||
const GROUP_ALLOWED = '111@g.us';
|
||||
const GROUP_BLOCKED = '222@g.us';
|
||||
const SID = 'sid-ics-personal-1';
|
||||
|
||||
const tmp = createTempDb();
|
||||
const db: any = tmp.db as Database;
|
||||
|
||||
// Sembrar datos mínimos
|
||||
db.exec(`INSERT OR IGNORE INTO users (id) VALUES ('${USER}')`);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${GROUP_ALLOWED}', 'comm1', 'Allowed', 1)`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${GROUP_BLOCKED}', 'comm2', 'Blocked', 1)`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${GROUP_ALLOWED}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${GROUP_BLOCKED}', 'blocked', '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
db.exec(
|
||||
`INSERT OR IGNORE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
||||
VALUES ('${GROUP_ALLOWED}', '${USER}', 0, 1, '${toIsoSql()}', '${toIsoSql()}')`
|
||||
);
|
||||
|
||||
const today = new Date();
|
||||
const dueIn2 = ymdUTC(addDays(today, 2));
|
||||
const dueIn5 = ymdUTC(addDays(today, 5));
|
||||
const duePast = ymdUTC(addDays(today, -2));
|
||||
|
||||
const insTask = db.prepare(
|
||||
`INSERT INTO tasks (description, due_date, group_id, created_by, completed, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
const createdBy = USER;
|
||||
|
||||
// Privada asignada (incluida)
|
||||
const r1 = insTask.run('Private assigned', dueIn2, null, createdBy, 0, toIsoSql());
|
||||
const t1 = Number(r1.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t1, USER, USER, toIsoSql());
|
||||
|
||||
// Grupo allowed asignada (incluida)
|
||||
const r2 = insTask.run('Allowed group assigned', dueIn5, GROUP_ALLOWED, createdBy, 0, toIsoSql());
|
||||
const t2 = Number(r2.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t2, USER, USER, toIsoSql());
|
||||
|
||||
// Grupo blocked asignada (excluida)
|
||||
const r3 = insTask.run('Blocked group assigned', dueIn5, GROUP_BLOCKED, createdBy, 0, toIsoSql());
|
||||
const t3 = Number(r3.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t3, USER, USER, toIsoSql());
|
||||
|
||||
// Grupo allowed sin due_date (excluida)
|
||||
const r4 = insTask.run('No due date', null, GROUP_ALLOWED, createdBy, 0, toIsoSql());
|
||||
const t4 = Number(r4.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t4, USER, USER, toIsoSql());
|
||||
|
||||
// Grupo allowed completada (excluida)
|
||||
const r5 = insTask.run('Completed assigned', dueIn2, GROUP_ALLOWED, createdBy, 1, toIsoSql());
|
||||
const t5 = Number(r5.lastInsertRowid);
|
||||
db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
|
||||
VALUES (?, ?, ?, ?)`).run(t5, USER, USER, toIsoSql());
|
||||
|
||||
const sidHashPromise = sha256Hex(SID);
|
||||
const serverPromise = startWebServer({
|
||||
port: PORT,
|
||||
env: {
|
||||
DB_PATH: tmp.path,
|
||||
WEB_BASE_URL: BASE,
|
||||
},
|
||||
});
|
||||
|
||||
let server: Awaited<typeof serverPromise> | null = null;
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await server?.stop();
|
||||
} catch {}
|
||||
try {
|
||||
tmp.cleanup();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('serves ICS for personal token with correct filtering, supports ETag, and returns 410 when revoked', async () => {
|
||||
server = await serverPromise;
|
||||
|
||||
const sidHash = await sidHashPromise;
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at)
|
||||
VALUES ('sess-ics-personal', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(
|
||||
addDays(new Date(), 1)
|
||||
)}')
|
||||
`);
|
||||
|
||||
// Obtener URLs de feeds
|
||||
const resFeeds = await fetch(`${BASE}/api/integrations/feeds`, {
|
||||
headers: { cookie: `sid=${SID}` },
|
||||
});
|
||||
expect(resFeeds.status).toBe(200);
|
||||
const feeds = await resFeeds.json();
|
||||
expect(feeds.personal && typeof feeds.personal.url === 'string').toBe(true);
|
||||
|
||||
const personalUrl: string = feeds.personal.url;
|
||||
const token = new URL(personalUrl).pathname.split('/').pop()!.replace(/\.ics$/i, '');
|
||||
|
||||
// Primera petición ICS
|
||||
const resIcs = await fetch(personalUrl);
|
||||
expect(resIcs.status).toBe(200);
|
||||
expect((resIcs.headers.get('content-type') || '').includes('text/calendar')).toBe(true);
|
||||
const body1 = await resIcs.text();
|
||||
|
||||
// Debe contener solo t1 y t2
|
||||
expect(body1.includes(`[T${pad4(t1)}]`)).toBe(true);
|
||||
expect(body1.includes(`[T${pad4(t2)}]`)).toBe(true);
|
||||
|
||||
// Excluidos
|
||||
expect(body1.includes(`[T${pad4(t3)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t4)}]`)).toBe(false);
|
||||
expect(body1.includes(`[T${pad4(t5)}]`)).toBe(false);
|
||||
|
||||
const etag = resIcs.headers.get('etag') || '';
|
||||
const res304 = await fetch(personalUrl, { headers: { 'if-none-match': etag } });
|
||||
expect(res304.status).toBe(304);
|
||||
|
||||
const row = db
|
||||
.prepare(`SELECT last_used_at FROM calendar_tokens WHERE token_plain = ?`)
|
||||
.get(token) as any;
|
||||
expect(row && row.last_used_at).toBeTruthy();
|
||||
|
||||
db.prepare(`UPDATE calendar_tokens SET revoked_at = ? WHERE token_plain = ?`).run(toIsoSql(), token);
|
||||
const resGone = await fetch(personalUrl);
|
||||
expect(resGone.status).toBe(410);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue