diff --git a/server/src/services/players.ts b/server/src/services/players.ts index d6aa961..113560d 100644 --- a/server/src/services/players.ts +++ b/server/src/services/players.ts @@ -192,11 +192,49 @@ function canonicalSteamId(input: string | null | undefined): string | null { return normalized; } -async function assertSteamIdFree(steamId: string, exceptId?: number): Promise { - const existing = await prisma.player.findUnique({ where: { steamId }, select: { id: true } }); - if (existing && existing.id !== exceptId) { - throw conflict('STEAMID_TAKEN', 'Diese SteamID ist bereits einem Spieler zugeordnet'); +/** + * Wer diese SteamID schon führt - als Hauptangabe oder als früheres Konto. + * + * Beides muss geprüft werden: Eine Kennung, die bei einem Spieler als + * früheres Konto hängt, gehört ihm genauso. Stünde sie daneben bei einem + * anderen als Hauptangabe, wüsste weder der Abgleich noch der Import, wem + * ein Kauf gehört. + */ +async function findeSteamIdInhaber( + steamId: string, + exceptId?: number, +): Promise<{ id: number; displayName: string; alsAlias: boolean } | null> { + const haupt = await prisma.player.findUnique({ + where: { steamId }, + select: { id: true, displayName: true }, + }); + if (haupt && haupt.id !== exceptId) return { ...haupt, alsAlias: false }; + + const alias = await prisma.playerAlias.findUnique({ + where: { type_value: { type: AliasType.STEAM_ID, value: steamId } }, + select: { playerId: true, player: { select: { displayName: true } } }, + }); + if (alias && alias.playerId !== exceptId) { + return { id: alias.playerId, displayName: alias.player.displayName, alsAlias: true }; } + + return null; +} + +/** + * Stellt sicher, dass die Kennung frei ist - und sagt sonst, bei wem sie + * liegt. Ohne den Namen sucht man von Hand durch den ganzen Bestand. + */ +async function assertSteamIdFree(steamId: string, exceptId?: number): Promise { + const inhaber = await findeSteamIdInhaber(steamId, exceptId); + if (!inhaber) return; + + throw conflict( + 'STEAMID_TAKEN', + inhaber.alsAlias + ? `Diese SteamID ist bei "${inhaber.displayName}" als früheres Konto eingetragen` + : `Diese SteamID gehört bereits zu "${inhaber.displayName}"`, + ); } export async function createPlayer(input: PlayerInput, ctx: ActionContext) { @@ -419,6 +457,21 @@ export async function addAlias( ); } + // Auch die Hauptangaben der anderen: dieselbe Kennung kann nicht bei einem + // als früheres Konto und beim nächsten als aktuelles stehen. + if (type === AliasType.STEAM_ID) { + const fremd = await prisma.player.findUnique({ + where: { steamId: value }, + select: { id: true, displayName: true }, + }); + if (fremd && fremd.id !== playerId) { + throw conflict( + 'ALIAS_TAKEN', + `Diese SteamID ist die aktuelle Kennung von "${fremd.displayName}"`, + ); + } + } + const created = await prisma.playerAlias.create({ data: { playerId, type, value, note: note?.trim() || null }, }); diff --git a/server/test/aliases.test.ts b/server/test/aliases.test.ts index 5bc86e3..f6dc0cd 100644 --- a/server/test/aliases.test.ts +++ b/server/test/aliases.test.ts @@ -100,3 +100,95 @@ describe('Frühere Konten', () => { assert.equal(alias.value, 'AlterName2019'); }); }); + +describe('Eine SteamID gehört genau einem Spieler', () => { + it('nennt den Spieler, bei dem sie schon liegt', async () => { + await players.createPlayer( + { displayName: 'Hat sie schon', steamId: '76561198000060001' }, + ctx, + ); + + await assert.rejects( + () => players.createPlayer({ displayName: 'Will sie auch', steamId: '76561198000060001' }, ctx), + (error: Error & { code?: string }) => { + assert.equal(error.code, 'STEAMID_TAKEN'); + assert.match( + error.message, + /Hat sie schon/, + 'ohne den Namen sucht man von Hand durch den ganzen Bestand', + ); + return true; + }, + ); + }); + + it('findet sie auch, wenn sie als früheres Konto hängt', async () => { + const umzuegler = await players.createPlayer( + { displayName: 'Umgezogen', steamId: '76561198000060002' }, + ctx, + ); + await players.addAlias(umzuegler.id, 'STEAM_ID', '76561198000060003', 'Altes Konto', ctx); + + await assert.rejects( + () => players.createPlayer({ displayName: 'Neuling', steamId: '76561198000060003' }, ctx), + (error: Error & { code?: string }) => { + assert.equal(error.code, 'STEAMID_TAKEN'); + assert.match(error.message, /Umgezogen/); + assert.match(error.message, /früheres Konto/); + return true; + }, + ); + }); + + it('erkennt sie auch in einer anderen Schreibweise', async () => { + // STEAM_0:0:19897141 und 76561198000060010 sind dieselbe Kennung. + await players.createPlayer( + { displayName: 'Legacy-Schreiber', steamId: '76561198000060010' }, + ctx, + ); + + await assert.rejects( + () => players.createPlayer({ displayName: 'Doppelt', steamId: 'STEAM_0:0:19897141' }, ctx), + (error: Error & { code?: string }) => { + assert.equal(error.code, 'STEAMID_TAKEN'); + assert.match(error.message, /Legacy-Schreiber/); + return true; + }, + ); + }); + + it('lässt die aktuelle Kennung eines anderen nicht als früheres Konto eintragen', async () => { + const einer = await players.createPlayer( + { displayName: 'Aktueller Inhaber', steamId: '76561198000060004' }, + ctx, + ); + const anderer = await players.createPlayer( + { displayName: 'Möchtegern', steamId: '76561198000060005' }, + ctx, + ); + + await assert.rejects( + () => players.addAlias(anderer.id, 'STEAM_ID', '76561198000060004', undefined, ctx), + (error: Error & { code?: string }) => { + assert.equal(error.code, 'ALIAS_TAKEN'); + assert.match(error.message, /Aktueller Inhaber/); + return true; + }, + ); + assert.ok(einer.id); + }); + + it('stört sich nicht an der eigenen Kennung beim Bearbeiten', async () => { + const spieler = await players.createPlayer( + { displayName: 'Bleibt sich treu', steamId: '76561198000060006' }, + ctx, + ); + + const geaendert = await players.updatePlayer( + spieler.id, + { displayName: 'Neuer Name', steamId: '76561198000060006' }, + ctx, + ); + assert.equal(geaendert.displayName, 'Neuer Name'); + }); +});