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
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();
|
|
}
|