You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
9.2 KiB

import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { readFile, writeFile, unlink } from 'node:fs/promises';
import { Pool } from 'pg';
import { io } from 'socket.io-client';
import { terms } from '../shared/terms.mjs';
// Isolated synthetic QA identities. Never reads/imports a real browser session.
const base = process.env.SMOKE_BASE || 'http://192.168.18.155:3000';
const origin = process.env.SMOKE_ORIGIN || 'https://juegoland.imaginacion.com';
const statePath = '/var/lib/mesa-abierta/production-smoke.json';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const sockets = [];
async function request(path, cookie = '', data) {
const response = await fetch(`${base}/api${path}`, {
method: data === undefined ? 'GET' : 'POST',
headers: {
host: new URL(origin).host,
origin,
cookie,
'content-type': 'application/json',
},
body: data === undefined ? undefined : JSON.stringify(data),
});
return { response, body: await response.json() };
}
const cookieOf = (response) => response.headers.get('set-cookie').split(';')[0];
async function client(cookie) {
const socket = io(base, {
forceNew: true,
reconnection: false,
transports: ['websocket'],
extraHeaders: { host: new URL(origin).host, origin, cookie },
});
sockets.push(socket);
await new Promise((resolve, reject) => {
socket.once('connect', resolve);
socket.once('connect_error', reject);
});
return socket;
}
async function emit(socket, name, value) {
const result = await socket.timeout(5000).emitWithAck(name, value);
assert.equal(result.ok, true, result.error);
return result.room;
}
let fixture;
async function cleanup() {
if (!fixture) return;
const connection = await pool.connect();
try {
await connection.query('BEGIN');
for (const table of ['match_events', 'chat_messages', 'match_results'])
await connection.query(
`DELETE FROM ${table} WHERE room_id = ANY($1::uuid[])`,
[fixture.rooms],
);
await connection.query('DELETE FROM rooms WHERE id = ANY($1::uuid[])', [
fixture.rooms,
]);
await connection.query(
'DELETE FROM sessions WHERE user_id = ANY($1::uuid[])',
[fixture.users.map((p) => p.id)],
);
await connection.query('DELETE FROM users WHERE id = ANY($1::uuid[])', [
fixture.users.map((p) => p.id),
]);
await connection.query('COMMIT');
await unlink(statePath).catch((e) => {
if (e.code !== 'ENOENT') throw e;
});
} catch (e) {
await connection.query('ROLLBACK');
throw e;
} finally {
connection.release();
}
}
try {
if (process.argv[2] === 'verify-restart') {
fixture = JSON.parse(await readFile(statePath, 'utf8'));
const result = await request(
`/rooms/${fixture.chess}`,
fixture.users[0].cookie,
);
assert.equal(result.response.status, 200);
assert.equal(result.body.room.status, 'finished');
assert.equal(result.body.room.players[0].outcome, 'win');
assert.equal(
result.body.room.messages[0].text,
'Mensaje de la prueba de despliegue',
);
const profile = (await request('/profile', fixture.users[0].cookie)).body;
assert.ok(profile.history.some((room) => room.id === fixture.chess));
assert.ok(
profile.history.some(
(room) => room.id === fixture.inactive && room.result.cancelled,
),
);
assert.equal(profile.pending.length, 0);
assert.ok(
profile.history.some(
(room) => room.id === fixture.expired && room.result.cancelled,
),
);
assert.equal(
(
await pool.query(
'SELECT count(*)::int AS n FROM match_events WHERE room_id=$1 AND actor_id IS NULL',
[fixture.expired],
)
).rows[0].n,
1,
);
const socket = await client(fixture.users[0].cookie);
await emit(socket, 'room:subscribe', { roomId: fixture.chess });
console.log(
'OK: sesiones, aceptación, historial, mensajes y reconexión persistidos tras reiniciar servicios.',
);
await cleanup();
console.log(
'OK: registros de prueba eliminados; no se han tocado datos ajenos a la prueba.',
);
} else {
const page = await fetch(base + '/');
assert.equal(page.status, 200);
const html = await page.text();
assert.match(html, /Tu gente\. Tus juegos\./);
assert.match(html, /Antes de jugar/);
assert.doesNotMatch(html, /terms-gate|Leer las condiciones completas/);
assert.equal((await request('/health')).body.storage, 'postgresql');
assert.equal(
(await request('/auth/guest', '', { name: 'Blocked test' })).response
.status,
428,
);
const acceptance = await request('/terms/accept', '', {
accepted: true,
version: terms.version,
});
assert.equal(acceptance.response.status, 200);
assert.match(
acceptance.response.headers.get('set-cookie'),
/HttpOnly; SameSite=Lax/,
);
assert.equal(
acceptance.response.headers.get('set-cookie').includes('; Secure'),
origin.startsWith('https://'),
);
const consentCookie = cookieOf(acceptance.response);
fixture = { users: [], rooms: [] };
for (const name of ['QAprodUno', 'QAprodDos']) {
const created = await request('/auth/guest', consentCookie, { name });
assert.equal(created.response.status, 201);
fixture.users.push({
id: created.body.user.id,
cookie: `${cookieOf(created.response)}; ${consentCookie}`,
});
}
const [one, two] = fixture.users;
const a = await client(one.cookie),
b = await client(two.cookie);
let room = (
await request('/rooms', one.cookie, {
gameId: 'ajedrez',
name: 'QA deployment chess',
maxPlayers: 2,
visibility: 'private',
})
).body.room;
fixture.chess = room.id;
fixture.rooms.push(room.id);
assert.equal(
(await request('/rooms/join', two.cookie, { code: room.code })).response
.status,
200,
);
await emit(a, 'room:subscribe', { roomId: room.id });
await emit(b, 'room:subscribe', { roomId: room.id });
await emit(a, 'room:ready', { roomId: room.id, ready: true });
room = await emit(b, 'room:ready', { roomId: room.id, ready: true });
room = await emit(a, 'game:action', {
roomId: room.id,
actionId: randomUUID(),
expectedVersion: room.version,
action: { type: 'move', from: 'e2', to: 'e4' },
});
assert.equal(room.state.turn, 1);
await emit(a, 'chat:send', {
roomId: room.id,
text: 'Mensaje de la prueba de despliegue',
});
room = await emit(b, 'game:action', {
roomId: room.id,
actionId: randomUUID(),
expectedVersion: room.version,
action: { type: 'resign' },
});
assert.equal(room.status, 'finished');
assert.equal(room.players[0].outcome, 'win');
assert.equal(
(
await pool.query(
'SELECT count(*)::int AS n FROM match_results WHERE room_id=$1',
[room.id],
)
).rows[0].n,
2,
);
let inactive = (
await request('/rooms', one.cookie, {
gameId: 'oca',
name: 'QA inactivity closure',
maxPlayers: 2,
visibility: 'private',
})
).body.room;
fixture.inactive = inactive.id;
fixture.rooms.push(inactive.id);
await request('/rooms/join', two.cookie, { code: inactive.code });
await emit(a, 'room:subscribe', { roomId: inactive.id });
await emit(b, 'room:subscribe', { roomId: inactive.id });
await emit(a, 'room:ready', { roomId: inactive.id, ready: true });
inactive = await emit(b, 'room:ready', {
roomId: inactive.id,
ready: true,
});
await pool.query(
"UPDATE rooms SET data=jsonb_set(data,'{lastGameActivityAt}',to_jsonb($2::text)) WHERE id=$1",
[inactive.id, new Date(Date.now() - 660000).toISOString()],
);
assert.equal(
(
await request(`/rooms/${inactive.id}/close-inactive`, two.cookie, {
expectedVersion: inactive.version,
})
).response.status,
200,
);
assert.equal(
(
await pool.query(
'SELECT count(*)::int AS n FROM match_results WHERE room_id=$1',
[inactive.id],
)
).rows[0].n,
0,
);
const expired = (
await request('/rooms', one.cookie, {
gameId: 'oca',
name: 'QA automatic expiry',
maxPlayers: 2,
visibility: 'private',
})
).body.room;
fixture.expired = expired.id;
fixture.rooms.push(expired.id);
await pool.query(
"UPDATE rooms SET data=jsonb_set(data,'{lastGameActivityAt}',to_jsonb($2::text)) WHERE id=$1",
[expired.id, new Date(Date.now() - 25 * 3600000).toISOString()],
);
await writeFile(statePath, JSON.stringify(fixture), {
mode: 0o600,
flag: 'wx',
});
console.log(
`OK: web, API y WebSocket en puerto 3000, aceptación desde ${origin}, dos jugadores, chat, turno, victoria, PostgreSQL y cierre sin resultado.`,
);
console.log(
'Reinicia los servicios y ejecuta verify-restart para comprobar persistencia y limpiar estas identidades sintéticas.',
);
}
} catch (error) {
await cleanup();
throw error;
} finally {
sockets.forEach((socket) => socket.disconnect());
await pool.end();
}

Powered by TurnKey Linux.