From 9e28bf9eceb72c6bc30c9d14a52a853c3b7d3692 Mon Sep 17 00:00:00 2001 From: Kawa Date: Thu, 26 Mar 2026 10:17:35 +0100 Subject: [PATCH] test(01-02): add Wave 0 TDD stubs for SC-3 (Zod schemas) and SC-4 (wizard reducer) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/schemas/index.test.ts: 8 tests defining acceptance criteria for BACKEND_SCHEMAS (Plan 03) - src/store/reducer.test.ts: 8 tests defining acceptance criteria for wizardReducer (Plan 04) - Both files fail with "Cannot find module" — correct RED phase, implementation deferred to Plans 03-04 --- src/schemas/index.test.ts | 72 +++++++++++++++++++++++++++++++++++++++ src/store/reducer.test.ts | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/schemas/index.test.ts create mode 100644 src/store/reducer.test.ts diff --git a/src/schemas/index.test.ts b/src/schemas/index.test.ts new file mode 100644 index 0000000..da8fa5d --- /dev/null +++ b/src/schemas/index.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from 'vitest'; +import { BACKEND_SCHEMAS } from './index'; + +describe('Azure Blob Zod schema', () => { + it('accepts valid account + access key', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ + account: 'mystorageaccount', + key: 'dGVzdGtleQ==', + }); + expect(result.success).toBe(true); + }); + + it('accepts valid account + sas_url', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ + account: 'mystorageaccount', + sas_url: 'https://mystorageaccount.blob.core.windows.net/?sv=2021-01-01', + }); + expect(result.success).toBe(true); + }); + + it('rejects empty account (required field)', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: '' }); + expect(result.success).toBe(false); + }); + + it('rejects missing account (required field)', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ key: 'dGVzdGtleQ==' }); + expect(result.success).toBe(false); + }); +}); + +describe('S3 Zod schema', () => { + it('accepts valid S3 credentials', () => { + const result = BACKEND_SCHEMAS.s3.safeParse({ + provider: 'AWS', + access_key_id: 'AKIAIOSFODNN7EXAMPLE', + secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + region: 'us-east-1', + }); + expect(result.success).toBe(true); + }); + + it('rejects missing access_key_id (required field)', () => { + const result = BACKEND_SCHEMAS.s3.safeParse({ + provider: 'AWS', + secret_access_key: 'secret', + region: 'us-east-1', + }); + expect(result.success).toBe(false); + }); +}); + +describe('S3-compatible Zod schema', () => { + it('accepts valid S3-compatible credentials with endpoint', () => { + const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ + provider: 'Other', + access_key_id: 'mykey', + secret_access_key: 'mysecret', + endpoint: 'https://s3.wasabisys.com', + }); + expect(result.success).toBe(true); + }); + + it('rejects missing endpoint (required for s3-compatible)', () => { + const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ + provider: 'Other', + access_key_id: 'mykey', + secret_access_key: 'mysecret', + }); + expect(result.success).toBe(false); + }); +}); diff --git a/src/store/reducer.test.ts b/src/store/reducer.test.ts new file mode 100644 index 0000000..e13cda9 --- /dev/null +++ b/src/store/reducer.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { wizardReducer } from './reducer'; +import { INITIAL_STATE, WizardState } from './types'; + +describe('wizardReducer', () => { + it('returns INITIAL_STATE on first call', () => { + // @ts-expect-error intentional undefined action for initialization test + const state = wizardReducer(undefined, { type: '@@INIT' }); + expect(state.currentStep).toBe(0); + expect(state.remote.backendType).toBeNull(); + expect(state.remote.name).toBe(''); + expect(state.remote.params).toEqual({}); + expect(state.deployment.includeInstall).toBe(false); + expect(state.deployment.configPath).toBe('machine-wide'); + }); + + it('SET_STEP updates currentStep', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 2 }); + expect(state.currentStep).toBe(2); + }); + + it('SET_BACKEND_TYPE updates remote.backendType', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_BACKEND_TYPE', payload: 'azureblob' }); + expect(state.remote.backendType).toBe('azureblob'); + }); + + it('SET_REMOTE_NAME updates remote.name', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_NAME', payload: 'my-blob' }); + expect(state.remote.name).toBe('my-blob'); + }); + + it('SET_REMOTE_PARAMS updates remote.params', () => { + const state = wizardReducer(INITIAL_STATE, { + type: 'SET_REMOTE_PARAMS', + payload: { account: 'myaccount', key: 'mykey' }, + }); + expect(state.remote.params).toEqual({ account: 'myaccount', key: 'mykey' }); + }); + + it('SET_DEPLOYMENT partially updates deployment', () => { + const state = wizardReducer(INITIAL_STATE, { + type: 'SET_DEPLOYMENT', + payload: { includeInstall: true }, + }); + expect(state.deployment.includeInstall).toBe(true); + expect(state.deployment.configPath).toBe('machine-wide'); // unchanged + }); + + it('RESET returns to INITIAL_STATE', () => { + const modified: WizardState = { + ...INITIAL_STATE, + currentStep: 3, + remote: { name: 'test', backendType: 's3', params: { region: 'us-east-1' } }, + }; + const state = wizardReducer(modified, { type: 'RESET' }); + expect(state).toEqual(INITIAL_STATE); + }); + + it('is a pure function — does not mutate input state', () => { + const before = { ...INITIAL_STATE }; + wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 5 }); + expect(INITIAL_STATE.currentStep).toBe(before.currentStep); + }); +});