watchparty/server/test/partyService.test.ts
Nicolas fc678b313c
Some checks failed
Publier l'image Docker / test (push) Failing after 11s
Publier l'image Docker / publish (push) Has been skipped
Add a real server test suite and gate CI's Docker publish on it
- server/test/*.test.ts: formalizes this project's many ad-hoc scratchpad
  test scripts into a committed suite, one file per service
  (musicQueueService, partyService, socketGateway, catalogService), using
  Node's built-in node:test + node:assert/strict - zero new dependencies.
  Runs straight against TypeScript source via `tsx --test` (npm test), no
  compile step.
- server/tsconfig.test.json: extends the base config, overrides rootDir to
  "." and adds test/**/*.ts to include, so `npm run typecheck` covers the
  test files too without pulling them into dist/ (npm run build still
  uses the unmodified base tsconfig, scoped to src/ only).
- server/package.json: typecheck now runs against tsconfig.test.json; new
  test script.
- .forgejo/workflows/publish-docker.yml: new `test` job (node:20-alpine
  container - the publish job's docker:27-cli has no Node) running server
  typecheck+test and client typecheck+build; publish now depends on it via
  needs: test. Removed the stale "no test suite yet" comment.
- README.md, docs/architecture.md: documented the test suite, how to run
  it locally, and the new CI job/gating.

Verified locally: server typecheck, server test (25/25 passing), server
build, client typecheck, and client build all pass individually - the
same commands the new CI job runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 09:57:47 +02:00

70 lines
3 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { PartyService } from '../src/services/PartyService';
describe('PartyService', () => {
it('create() sets mediaType and seeds the host as the first member', () => {
const party = PartyService.create('u-alice', 'Alice', 'ep1', 'S01E01 - Pilote', '/img/ep1', [
{ itemId: 'ep1', itemName: 'S01E01 - Pilote', itemImageUrl: '/img/ep1' },
{ itemId: 'ep2', itemName: 'S01E02 - Suite', itemImageUrl: '/img/ep2' }
], 'video');
assert.equal(party.mediaType, 'video');
assert.equal(party.members.size, 1);
assert.equal(party.hostUserId, 'u-alice');
});
it('advanceQueue moves through the fixed queue and stops at the end', () => {
const party = PartyService.create('u-alice', 'Alice', 'ep1', 'S01E01', '/img/ep1', [
{ itemId: 'ep1', itemName: 'S01E01', itemImageUrl: '/img/ep1' },
{ itemId: 'ep2', itemName: 'S01E02', itemImageUrl: '/img/ep2' }
], 'video');
assert.equal(PartyService.advanceQueue(party), true);
assert.equal(party.itemId, 'ep2');
assert.equal(PartyService.advanceQueue(party), false, 'nothing left after the last item');
});
it('leave(): a non-host leaving neither destroys the party nor hands off', () => {
const party = PartyService.create('u-alice', 'Alice', 'm1', 'Un Film', '/img/m1', null, 'video');
PartyService.join(party, 'u-bob', 'Bob');
const result = PartyService.leave(party, 'u-bob');
assert.equal(result.destroyed, false);
assert.equal(result.hostChanged, false);
});
it('leave(): the host leaving with someone left hands off, and party.hostUserId actually updates', () => {
const party = PartyService.create('u-alice', 'Alice', 'm1', 'Un Film', '/img/m1', null, 'video');
PartyService.join(party, 'u-bob', 'Bob');
const result = PartyService.leave(party, 'u-alice');
assert.equal(result.hostChanged, true);
assert.equal(result.newHostUserId, 'u-bob');
assert.equal(party.hostUserId, 'u-bob');
});
it('leave(): the last member leaving destroys the party', () => {
const party = PartyService.create('u-alice', 'Alice', 'm1', 'Un Film', '/img/m1', null, 'video');
const result = PartyService.leave(party, 'u-alice');
assert.equal(result.destroyed, true);
assert.equal(PartyService.getById(party.id), null);
});
it('removeIdleParties sweeps a party whose lastActivityUtc is past the cutoff', () => {
const party = PartyService.create('u-dave', 'Dave', 'm1', 'Un Film', '/img/m1', null, 'video');
party.lastActivityUtc = Date.now() - 5 * 60 * 60 * 1000;
PartyService.removeIdleParties(4 * 60 * 60 * 1000);
assert.equal(PartyService.getById(party.id), null);
});
it('toPublicState reports youAreHost for the creator, and music:null for a video party', () => {
const party = PartyService.create('u-eve', 'Eve', 'm2', 'Un Autre Film', '/img/m2', null, 'video');
const state = PartyService.toPublicState(party, 'u-eve');
assert.equal(state.youAreHost, true);
assert.equal(state.music, null);
});
});