import { describe, expect, it } from 'vitest' import { parseDomains } from '~/domain/email-domains' import { attemptsWithinWindow, decideMagicLinkRequest, magicLinkMaxAttempts, magicLinkResponse, type MagicLinkRequest, } from '~/domain/magic-link' const domains = parseDomains('nyo.de,pocketrocket.de,pocket-rocket.io') function request(overrides: Partial = {}): MagicLinkRequest { return { email: 'paul@nyo.de', allowedDomains: domains, hasAccount: false, attempts: 1, ...overrides, } } describe('decideMagicLinkRequest', () => { it('schickt einen Link an jede freigegebene Domain, auch ohne Konto', () => { for (const email of ['paul@nyo.de', 'paul@pocketrocket.de', 'paul@pocket-rocket.io']) { expect(decideMagicLinkRequest(request({ email }))).toEqual({ email, outcome: 'send', reason: 'allowed-domain', }) } }) it('schickt einen Link an ein bestehendes Konto mit fremder Domain', () => { expect(decideMagicLinkRequest(request({ email: 'thevisualdesigners@gmail.com', hasAccount: true }))) .toEqual({ email: 'thevisualdesigners@gmail.com', outcome: 'send', reason: 'known-account', }) }) it('schickt keinen Link an eine fremde Domain ohne Konto', () => { expect(decideMagicLinkRequest(request({ email: 'fremd@example.com' }))).toEqual({ email: 'fremd@example.com', outcome: 'ignore', reason: 'domain-not-allowed', }) }) it('lässt sich nicht durch eine angehängte Domain täuschen', () => { expect(decideMagicLinkRequest(request({ email: 'paul@nyo.de.angreifer.example' })).outcome).toBe('ignore') }) it('behandelt Groß- und Kleinschreibung und Leerraum gleich', () => { expect(decideMagicLinkRequest(request({ email: ' Paul@NYO.de ' }))).toEqual({ email: 'paul@nyo.de', outcome: 'send', reason: 'allowed-domain', }) }) it('erlaubt genau fünf Anforderungen im Zeitfenster', () => { expect(decideMagicLinkRequest(request({ attempts: magicLinkMaxAttempts })).outcome).toBe('send') expect(decideMagicLinkRequest(request({ attempts: magicLinkMaxAttempts + 1 }))).toEqual({ email: 'paul@nyo.de', outcome: 'throttle', reason: 'too-many-requests', }) }) it('bremst jede Adresse, auch die abgewiesene', () => { const decision = decideMagicLinkRequest(request({ email: 'fremd@example.com', attempts: 6 })) expect(decision.outcome).toBe('throttle') }) it('bremst auch ein bestehendes Konto', () => { const decision = decideMagicLinkRequest(request({ email: 'thevisualdesigners@gmail.com', hasAccount: true, attempts: 6, })) expect(decision.outcome).toBe('throttle') }) }) describe('magicLinkResponse', () => { it('antwortet gleich, egal ob ein Link rausgeht oder nicht', () => { const sent = magicLinkResponse(decideMagicLinkRequest(request({ email: 'paul@nyo.de' }))) const known = magicLinkResponse(decideMagicLinkRequest(request({ email: 'paul@nyo.de', allowedDomains: parseDomains('example.org'), hasAccount: true, }))) const ignored = magicLinkResponse(decideMagicLinkRequest(request({ email: 'paul@nyo.de', allowedDomains: parseDomains('example.org'), }))) expect(sent).toEqual({ status: 'sent', email: 'paul@nyo.de' }) expect(known).toEqual(sent) expect(ignored).toEqual(sent) }) it('verrät auch bei fremder Adresse nichts über das Konto', () => { const withAccount = magicLinkResponse(decideMagicLinkRequest(request({ email: 'fremd@example.com', hasAccount: true, }))) const withoutAccount = magicLinkResponse(decideMagicLinkRequest(request({ email: 'fremd@example.com', hasAccount: false, }))) expect(withAccount).toEqual(withoutAccount) }) it('meldet nur die Bremse offen', () => { expect(magicLinkResponse(decideMagicLinkRequest(request({ attempts: 6 })))).toEqual({ status: 'error', email: 'paul@nyo.de', error: 'tooMany', }) }) }) describe('attemptsWithinWindow', () => { it('behält nur die Zeitpunkte im Fenster', () => { const now = 1_000_000 expect(attemptsWithinWindow([now - 1000, now - 900_000, now - 100], now, 900_000)).toEqual([ now - 1000, now - 100, ]) }) it('liefert für eine leere Liste nichts', () => { expect(attemptsWithinWindow([], 10, 900_000)).toEqual([]) }) })