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.

249 lines
8.7 KiB

import { acceptedCookie } from './terms-fixture.js';
import { it, expect } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { io, type Socket } from 'socket.io-client';
import { FileStore } from '../server/store.mjs';
import { createService } from '../server/service.mjs';
import type { Room } from '../src/lib/types.js';
it('dos clientes reales: registro, sala, chat, jugadas, reconexión y resultado', async () => {
const dir = await mkdtemp(join(tmpdir(), 'mesa-socket-'));
const store = new FileStore(join(dir, 'store.json'));
await store.init();
const origin = 'http://localhost:4173',
service = createService(store, { APP_ORIGIN: origin, NODE_ENV: 'test' });
await new Promise<void>((resolve) =>
service.server.listen(0, '127.0.0.1', resolve),
);
const address = service.server.address();
if (!address || typeof address === 'string') throw new Error('Sin dirección');
const base = `http://127.0.0.1:${address.port}`,
sockets: Socket[] = [];
async function request(
path: string,
data?: unknown,
cookie = '',
customOrigin = origin,
) {
return fetch(base + '/api' + path, {
method: data === undefined ? 'GET' : 'POST',
headers: {
origin: customOrigin,
'content-type': 'application/json',
cookie: acceptedCookie(cookie),
},
body: data === undefined ? undefined : JSON.stringify(data),
});
}
async function player(n: number) {
const response = await request('/auth/register', {
name: `Prueba ${n}`,
nick: `prueba_${n}`,
email: `prueba${n}@example.test`,
pin: '3581',
});
expect(response.status).toBe(201);
const cookie = response.headers.get('set-cookie')!.split(';')[0];
const socket = io(base, {
transports: ['websocket'],
extraHeaders: { cookie: acceptedCookie(cookie), origin },
forceNew: true,
});
sockets.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('connect_error', reject);
});
return { cookie, socket };
}
const emit = (socket: Socket, event: string, input: unknown) =>
new Promise<{ ok: boolean; room: Room; error?: string }>(
(resolve, reject) =>
socket
.timeout(4000)
.emit(
event,
input,
(
e: Error | null,
result: { ok: boolean; room: Room; error?: string },
) => (e ? reject(e) : resolve(result)),
),
);
try {
const a = await player(1),
b = await player(2);
expect(
(await request('/rooms', {}, a.cookie, 'https://other.example')).status,
).toBe(403);
const response = await request(
'/rooms',
{
gameId: 'ajedrez',
name: 'Partida de integración',
maxPlayers: 2,
visibility: 'private',
},
a.cookie,
);
let room = (await response.json()).room as Room;
expect((await request('/rooms')).status).toBe(200);
expect((await (await request('/rooms')).json()).rooms).toHaveLength(0);
await request('/rooms/join', { code: room.code }, b.cookie);
await emit(a.socket, 'room:subscribe', { roomId: room.id });
await emit(b.socket, 'room:subscribe', { roomId: room.id });
await emit(a.socket, 'room:ready', { roomId: room.id, ready: true });
room = (
await emit(b.socket, 'room:ready', { roomId: room.id, ready: true })
).room;
expect(room.status).toBe('playing');
const preset = await request(
'/profile/avatar',
{ preset: 'gato' },
a.cookie,
);
expect(preset.status).toBe(200);
expect((await preset.json()).user.avatar).toBe('/avatars/gato.svg');
expect(
(await (await request(`/rooms/${room.id}`, undefined, b.cookie)).json())
.room.players[0].avatar,
).toBe('/avatars/gato.svg');
expect((await request('/profile/avatar', { preset: 'torre' })).status).toBe(
401,
);
expect(
(
await request(
'/profile/avatar',
{ image: 'data:image/svg+xml;base64,AAAA' },
a.cookie,
)
).status,
).toBe(400);
const png =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScLbtAAAAABJRU5ErkJggg==';
const uploaded = await request('/profile/avatar', { image: png }, a.cookie);
expect(uploaded.status).toBe(200);
const avatarUrl = (await uploaded.json()).user.avatar;
const served = await request(avatarUrl.slice(4), undefined, b.cookie);
expect(served.headers.get('content-type')).toBe('image/png');
expect(Buffer.from(await served.arrayBuffer()).toString('base64')).toBe(
png.slice(22),
);
expect(
(await emit(b.socket, 'room:subscribe', { roomId: room.id })).room
.players[0].avatar,
).toBe(avatarUrl);
expect(
(await request('/profile/avatar', { preset: null }, a.cookie)).status,
).toBe(200);
const chat = await emit(a.socket, 'chat:send', {
roomId: room.id,
text: 'Buena partida <script>',
});
expect(chat.room.messages?.[0].text).toBe('Buena partida <script>');
const action = {
roomId: room.id,
actionId: randomUUID(),
expectedVersion: room.version,
action: { type: 'move', from: 'e2', to: 'e4' },
};
room = (await emit(a.socket, 'game:action', action)).room;
expect(room.state?.turn).toBe(1);
expect(room.events?.[0].sound).toBe('move');
expect(room.events?.[0]).not.toHaveProperty('action');
expect(room.definition?.sounds.move.src).toBe('/sounds/v2/chess-move.wav');
expect(
(await emit(a.socket, 'game:action', action)).room.events,
).toHaveLength(1);
b.socket.disconnect();
b.socket.connect();
await new Promise<void>((resolve, reject) => {
b.socket.once('connect', resolve);
b.socket.once('connect_error', reject);
});
const recovered = (
await emit(b.socket, 'room:subscribe', { roomId: room.id })
).room;
expect(recovered.version).toBe(room.version);
expect(recovered.messages).toHaveLength(1);
room = (
await emit(b.socket, 'game:action', {
roomId: room.id,
actionId: randomUUID(),
expectedVersion: room.version,
action: { type: 'resign' },
})
).room;
expect(room.status).toBe('finished');
const profile = await (
await request('/profile', undefined, a.cookie)
).json();
expect(profile.stats[0].wins).toBe(1);
const raceResponse = await request(
'/rooms',
{
gameId: 'parchis',
name: 'Turnos automáticos',
maxPlayers: 2,
visibility: 'private',
},
a.cookie,
);
let race = (await raceResponse.json()).room as Room;
await request('/rooms/join', { code: race.code }, b.cookie);
await emit(a.socket, 'room:subscribe', { roomId: race.id });
await emit(b.socket, 'room:subscribe', { roomId: race.id });
await emit(a.socket, 'room:ready', { roomId: race.id, ready: true });
await emit(b.socket, 'room:ready', { roomId: race.id, ready: true });
await store.updateRoom(
race.id,
(stored: { state: { pieces: number[][]; die: number | null } }) => {
stored.state.pieces[0][0] = 10;
stored.state.die = 3;
},
);
race = (await emit(a.socket, 'room:subscribe', { roomId: race.id })).room;
const notified = new Promise<Room>((resolve, reject) => {
const timer = setTimeout(() => {
b.socket.off('room:state', listener);
reject(new Error('El segundo jugador no recibió el cambio de turno'));
}, 4000);
function listener(next: Room) {
if (next.id === race.id && next.state?.turn === 1) {
clearTimeout(timer);
b.socket.off('room:state', listener);
resolve(next);
}
}
b.socket.on('room:state', listener);
});
const moved = (
await emit(a.socket, 'game:action', {
roomId: race.id,
actionId: randomUUID(),
expectedVersion: race.version,
action: { type: 'move', piece: 0 },
})
).room;
expect(moved.state?.turn).toBe(1);
expect(moved.legalActions).toEqual([]);
expect((await notified).legalActions).toEqual([{ type: 'roll' }]);
await request('/auth/logout', {}, a.cookie);
expect(
(await (await request('/session', undefined, a.cookie)).json()).user,
).toBeNull();
expect(
(await request('/rooms/' + room.id, undefined, a.cookie)).status,
).toBe(401);
} finally {
for (const socket of sockets) socket.disconnect();
await service.close();
await store.close();
await rm(dir, { recursive: true, force: true });
}
}, 20000);

Powered by TurnKey Linux.