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