fix(account): Login bei bereits aktiver Sitzung, showModal-Fehler abfangen

- login(): bei user_session_already_exists aktuelle Sitzung beenden und genau
  einmal neu versuchen; Meldung für den Restfall in errorMessage
- requireLogin(): schlägt showModal fehl, wird aufgeräumt und der Fehler geworfen,
  damit spätere Aufrufe nicht an einem toten Promise hängen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 09:02:42 +00:00
1 parent 10c55ca933
commit a147d3b496
6 files changed
+143 -9

No files matched your search

+76
View File
@@ -106,6 +106,82 @@ describe('login', () => {
});
});
describe('login bei bereits aktiver Sitzung (user_session_already_exists)', () => {
const existing = () => fail(401, 'user_session_already_exists', 'Creation of a session is prohibited when a session is active.');
it('löscht die aktuelle Sitzung, versucht es einmal neu und liefert den Nutzer', async () => {
const auth = await freshAuth();
mocks.createEmailPasswordSession.mockRejectedValueOnce(existing()).mockResolvedValueOnce({});
mocks.deleteSession.mockResolvedValue({});
mocks.get.mockResolvedValue({ $id: 'u1', email: 'a@b.de' });
await expect(auth.login('a@b.de', 'geheim123')).resolves.toEqual({ $id: 'u1', email: 'a@b.de' });
expect(mocks.deleteSession).toHaveBeenCalledTimes(1);
expect(mocks.deleteSession).toHaveBeenCalledWith({ sessionId: 'current' });
expect(mocks.createEmailPasswordSession).toHaveBeenCalledTimes(2);
expect(mocks.createEmailPasswordSession).toHaveBeenLastCalledWith({ email: 'a@b.de', password: 'geheim123' });
expect(auth.currentUserId()).toBe('u1');
});
it('ignoriert 401 beim Löschen (Sitzung war schon weg) und versucht es trotzdem neu', async () => {
const auth = await freshAuth();
mocks.createEmailPasswordSession.mockRejectedValueOnce(existing()).mockResolvedValueOnce({});
mocks.deleteSession.mockRejectedValue(fail(401, 'general_unauthorized_scope'));
mocks.get.mockResolvedValue({ $id: 'u1', email: 'a@b.de' });
await expect(auth.login('a@b.de', 'geheim123')).resolves.toEqual({ $id: 'u1', email: 'a@b.de' });
expect(mocks.createEmailPasswordSession).toHaveBeenCalledTimes(2);
});
it('scheitert der zweite Versuch auch, fliegt dieser Fehler weiter (keine Endlosschleife)', async () => {
const auth = await freshAuth();
mocks.createEmailPasswordSession.mockRejectedValue(existing());
mocks.deleteSession.mockResolvedValue({});
await expect(auth.login('a@b.de', 'geheim123')).rejects.toMatchObject({ type: 'user_session_already_exists' });
expect(mocks.deleteSession).toHaveBeenCalledTimes(1);
expect(mocks.createEmailPasswordSession).toHaveBeenCalledTimes(2);
expect(mocks.get).not.toHaveBeenCalled();
});
it('falsche Zugangsdaten im zweiten Versuch kommen als solche an, der alte Nutzer ist dann abgemeldet', async () => {
const auth = await freshAuth();
mocks.get.mockResolvedValueOnce({ $id: 'old', email: 'old@b.de' });
await auth.getCurrentUser();
const listener = vi.fn();
auth.onAuthChange(listener);
const wrong = fail(401, 'user_invalid_credentials');
mocks.createEmailPasswordSession.mockRejectedValueOnce(existing()).mockRejectedValueOnce(wrong);
mocks.deleteSession.mockResolvedValue({});
await expect(auth.login('a@b.de', 'falsch123')).rejects.toBe(wrong);
expect(auth.currentUserId()).toBeNull();
expect(listener).toHaveBeenCalledWith(null);
});
it('scheitert das Löschen anders (z. B. Netz), fliegt dieser Fehler weiter und es gibt keinen zweiten Versuch', async () => {
const auth = await freshAuth();
const error = new TypeError('Failed to fetch');
mocks.createEmailPasswordSession.mockRejectedValueOnce(existing());
mocks.deleteSession.mockRejectedValue(error);
await expect(auth.login('a@b.de', 'geheim123')).rejects.toBe(error);
expect(mocks.createEmailPasswordSession).toHaveBeenCalledTimes(1);
});
it('andere Fehler beim ersten Versuch löschen nichts', async () => {
const auth = await freshAuth();
mocks.createEmailPasswordSession.mockRejectedValue(fail(401, 'user_invalid_credentials'));
await expect(auth.login('a@b.de', 'falsch123')).rejects.toMatchObject({ type: 'user_invalid_credentials' });
expect(mocks.deleteSession).not.toHaveBeenCalled();
});
it('register: das anschließende Login räumt eine vorhandene Sitzung ebenfalls ab', async () => {
const auth = await freshAuth();
mocks.create.mockResolvedValue({});
mocks.createEmailPasswordSession.mockRejectedValueOnce(existing()).mockResolvedValueOnce({});
mocks.deleteSession.mockResolvedValue({});
mocks.get.mockResolvedValue({ $id: 'u2', email: 'neu@b.de' });
await expect(auth.register('neu@b.de', 'geheim123')).resolves.toEqual({ $id: 'u2', email: 'neu@b.de' });
expect(mocks.deleteSession).toHaveBeenCalledTimes(1);
});
});
describe('register', () => {
it('legt das Konto an (Name aus der E-Mail) und meldet danach an', async () => {
const auth = await freshAuth();