From 066b69f2be5738d2e43fb1f8cb59a9103205f9f6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Mon, 15 Jun 2026 16:14:18 -0400 Subject: [PATCH] fix(12): CR-01 remove extraneous providerType from postSetupCredential wire body The server's credentialSchema does not declare providerType; it was being silently stripped by Zod. Remove it from the request body and add a contract test suite asserting the exact wire keys sent, mirroring the existing BUG-1 tests for postSetupConfig. Co-Authored-By: Claude Sonnet 4.6 --- apps/pwa/src/api/client.ts | 2 +- apps/pwa/src/api/setupClient.contract.test.ts | 56 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts index d02de1c..e758b0d 100644 --- a/apps/pwa/src/api/client.ts +++ b/apps/pwa/src/api/client.ts @@ -671,9 +671,9 @@ export async function postSetupCredential(payload: SetupCredentialPayload): Prom method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - providerType: 'caldav', fastmailEmail: payload.fastmailEmail, appPassword: payload.appPassword, + // providerType omitted — not in credentialSchema; server hard-codes 'caldav' }), }); diff --git a/apps/pwa/src/api/setupClient.contract.test.ts b/apps/pwa/src/api/setupClient.contract.test.ts index b43b2fb..e00942e 100644 --- a/apps/pwa/src/api/setupClient.contract.test.ts +++ b/apps/pwa/src/api/setupClient.contract.test.ts @@ -20,7 +20,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { postSetupConfig, type SetupConfigPayload } from './client.js'; +import { postSetupConfig, postSetupCredential, type SetupConfigPayload, type SetupCredentialPayload } from './client.js'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -176,3 +176,57 @@ describe('postSetupConfig — error rendering (BUG 2 regression)', () => { ).resolves.toBeUndefined(); }); }); + +// ── CR-01: postSetupCredential wire body contract ───────────────────────────── +// Guards that postSetupCredential sends exactly { fastmailEmail, appPassword } +// matching the server's credentialSchema — no extra fields (e.g. providerType). + +describe('postSetupCredential — payload contract (CR-01 regression)', () => { + let fetchSpy: ReturnType; + + const VALID_CREDENTIAL_PAYLOAD: SetupCredentialPayload = { + fastmailEmail: 'user@fastmail.com', + appPassword: 'secret-app-password', + }; + + beforeEach(() => { + fetchSpy = vi.fn(); + vi.stubGlobal('fetch', fetchSpy); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('sends exactly the 2 canonical fields matching credentialSchema', async () => { + fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200)); + await postSetupCredential(VALID_CREDENTIAL_PAYLOAD); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string); + expect(Object.keys(body).sort()).toEqual(['appPassword', 'fastmailEmail'].sort()); + }); + + it('does NOT send providerType (not in credentialSchema; server hard-codes caldav)', async () => { + fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200)); + await postSetupCredential(VALID_CREDENTIAL_PAYLOAD); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string); + expect(body).not.toHaveProperty('providerType'); + }); + + it('sends fastmailEmail matching the payload value', async () => { + fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200)); + await postSetupCredential(VALID_CREDENTIAL_PAYLOAD); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string); + expect(body).toHaveProperty('fastmailEmail', 'user@fastmail.com'); + }); + + it('sends appPassword matching the payload value', async () => { + fetchSpy.mockResolvedValueOnce(mockFetchResponse(null, 200)); + await postSetupCredential(VALID_CREDENTIAL_PAYLOAD); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body as string); + expect(body).toHaveProperty('appPassword', 'secret-app-password'); + }); +});