From 692600c7554eb8c618cd078dea581d5a3b0c1a1a Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 22 Jun 2026 06:44:33 +0200 Subject: [PATCH] test(12): close Nyquist validation gap --- .../12-VALIDATION.md | 46 +++++++--- .../__tests__/CloudCredentialModal.test.js | 92 +++++++++++++++++++ 2 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js diff --git a/.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md b/.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md index 73800ed..63fe7d2 100644 --- a/.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md +++ b/.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md @@ -5,7 +5,7 @@ status: complete nyquist_compliant: true wave_0_complete: true created: 2026-06-18 -last_audited: 2026-06-19 +last_audited: 2026-06-22 --- # Phase 12 — Validation Strategy @@ -20,10 +20,10 @@ last_audited: 2026-06-19 |----------|-------| | **Framework** | pytest 9.x, Vitest 4.x, Vue Test Utils 2.x | | **Config file** | `backend/pytest.ini`, `frontend/vitest.config.js` | -| **Quick backend command** | `docker compose run --rm backend pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_capabilities.py tests/test_cloud_items.py tests/test_cloud_security.py` | -| **Quick frontend command** | `cd frontend && npm test -- --run src/components/storage/__tests__/StorageBrowser.capabilities.test.js src/components/ui/__tests__/BreadcrumbBar.test.js src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudStorageView.test.js src/stores/__tests__/cloudConnections.test.js src/components/settings/__tests__/SettingsCloudTab.test.js` | +| **Quick backend command** | `docker compose run --rm backend pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_capabilities.py tests/test_cloud_items.py tests/test_cloud_security.py tests/test_compose_migrations.py tests/test_migration_0006.py` | +| **Quick frontend command** | `cd frontend && npm test -- --run src/components/storage/__tests__/StorageBrowser.capabilities.test.js src/components/ui/__tests__/BreadcrumbBar.test.js src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudStorageView.test.js src/stores/__tests__/cloudConnections.test.js src/components/settings/__tests__/SettingsCloudTab.test.js src/components/cloud/__tests__/CloudCredentialModal.test.js src/components/cloud/__tests__/CloudProviderTreeItem.test.js src/components/cloud/__tests__/CloudFolderTreeItem.test.js src/components/layout/__tests__/AppSidebar.test.js` | | **Full suite command** | `cd backend && pytest -v`; `cd frontend && npm test`; `cd frontend && npm run build` | -| **Fresh focused runtime** | Backend 155 tests in 4.14 s; frontend 62 tests in 703 ms (2026-06-19) | +| **Fresh focused runtime** | Backend cloud/migration slice 159 passed, 12 prerequisite skips in 3.48 s plus 7 Compose contract tests in 0.23 s; frontend focused slice 76 passed in 1.12 s (2026-06-22) | --- @@ -52,6 +52,12 @@ last_audited: 2026-06-19 | 12-04-01 | 04 | 4 | All Phase 12 | T-12-01..10 | Dedicated owner/admin/credential/SSRF/no-byte negatives close cross-layer threats | integration/regression | `tests/test_cloud_security.py` plus focused suites | ✅ | ✅ green | | 12-04-02 | 04 | 4 | CONN-04, CLOUD-08 | T-12-SC | Static/dependency security gates have no unresolved Phase 12 blocker | security gate | `SECURITY.md` Phase 12 evidence | ✅ | ✅ green | | 12-04-03 | 04 | 4 | All Phase 12 | — | Documentation/version claims match tracked shipped behavior | source + suites | `12-VERIFICATION.md`, summaries, version source assertions | ✅ | ✅ green | +| 12-05-01 | 05 | 5 | CONN-04, CLOUD-01 | T-12-05-01..04 | Exactly one hardened migrate service gates backend/worker/beat startup | compose contract | `tests/test_compose_migrations.py`; `docker compose config --quiet` | ✅ | ✅ green | +| 12-05-02 | 05 | 5 | CONN-04, CLOUD-01 | T-12-05-01, T-12-05-02 | A disposable PostgreSQL database upgrades 0005→0006 and preserves DDL separation | PostgreSQL integration | `tests/test_migration_0006.py` (12 tests; explicit prerequisite skip without disposable DSN) | ✅ | ✅ green | +| 12-05-03 | 05 | 5 | CONN-04, CLOUD-01 | T-12-05-03, T-12-05-04 | Cloud APIs run against migrated schema without credential disclosure | API regression + config | `tests/test_cloud.py`, `tests/test_cloud_security.py`, Compose config validation | ✅ | ✅ green | +| 12-06-01 | 06 | 6 | CONN-04, CLOUD-01 | T-12-06-01..05 | Same-provider accounts create independently; edits target one owned UUID; errors stay actionable and credential-free | API + component | `tests/test_cloud.py`, `tests/test_cloud_security.py`, `SettingsCloudTab.test.js`, `CloudCredentialModal.test.js` | ✅ | ✅ green | +| 12-06-02 | 06 | 6 | CLOUD-01, CLOUD-08 | T-12-06-06 | Sidebar browse/routes use connection UUIDs; breadcrumbs and deepest-node active state survive refresh failure | component/view | `CloudProviderTreeItem.test.js`, `CloudFolderTreeItem.test.js`, `AppSidebar.test.js`, `CloudFolderView.test.js`, `CloudStorageView.test.js` | ✅ | ✅ green | +| 12-06-03 | 06 | 6 | CONN-04, CLOUD-01, CLOUD-08 | T-12-06-SC | UAT regressions, version alignment, build, and security gates remain release-ready | cross-layer suite | Phase summaries/security evidence plus focused and full-suite commands | ✅ | ✅ green | *Status: ✅ green · ❌ red · ⚠️ partial/manual* @@ -61,8 +67,8 @@ last_audited: 2026-06-19 | Requirement | Status | Automated evidence | |-------------|--------|--------------------| -| CONN-04 | COVERED | Capability contract tests, connection store/settings tests, StorageBrowser action-state tests | -| CLOUD-01 | COVERED | Connection-ID API tests and shared CloudStorageView/CloudFolderView/StorageBrowser tests | +| CONN-04 | COVERED | Capability contract tests, independent same-provider lifecycle and owner-targeted edit tests, connection store/settings tests, StorageBrowser action-state tests | +| CLOUD-01 | COVERED | Migration-gated startup tests, connection-ID API tests, UUID-only sidebar navigation, and shared CloudStorageView/CloudFolderView/StorageBrowser tests | | CLOUD-08 | COVERED | Unsupported/temporarily-unavailable state, message, pointer, Enter, Space, and touch suppression tests | | CACHE-01 | COVERED | No-MinIO, no-byte-download, no-quota-mutation browse/refresh tests | | CACHE-02 | COVERED | Cloud metadata/extracted text/topic/semantic fields and cross-connection isolation tests | @@ -77,7 +83,10 @@ last_audited: 2026-06-19 - [x] Database constraint/isolation coverage for UUID ownership, unique connection/item identity, and cascade behavior. - [x] `CloudFolderView.test.js` and `CloudStorageView.test.js` — thin shared-browser view integration. - [x] Provider response fixtures for Google Drive, OneDrive, Nextcloud, and generic WebDAV normalization. -- [x] Focused runtime baseline recorded: backend 4.14 s, frontend 703 ms, excluding container startup. +- [x] `test_compose_migrations.py` and `test_migration_0006.py` — startup gate and disposable PostgreSQL 0005→0006 regression. +- [x] `CloudCredentialModal.test.js` — new-vs-edit endpoint selection, connection-ID targeting, omitted-password preservation, and actionable errors. +- [x] Connection tree/sidebar tests — UUID-only routes, nested folder selection, and exact overview active state. +- [x] Fresh focused runtime recorded for backend, frontend, and Compose contract slices. --- @@ -92,23 +101,32 @@ These checks supplement automated coverage; no Phase 12 requirement depends sole --- -## Validation Audit 2026-06-19 +## Validation Audit 2026-06-22 | Metric | Count | |--------|-------| | Phase requirements audited | 6 | | Automated requirements covered | 6 | -| Gaps found | 0 | -| Tests generated | 0 | +| Executed plans audited | 6 | +| Gaps found | 1 | +| Tests generated | 1 file / 3 tests | | Gaps escalated to manual-only | 0 | | Supplementary manual visual checks retained | 2 | +### Gap Closed + +- Plan 12-06 named `frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js` as required automated evidence, but the file was absent; only a mobile-layout test existed. +- Generated the missing functional suite with three regressions: fresh Nextcloud creation builds the WebDAV URL and calls create, editing targets the selected connection UUID while omitting a blank password, and API validation detail remains visible without closing the modal. + ### Fresh Execution Evidence -- Backend focused cloud suite: **155 passed**, 1 non-failing SlowAPI deprecation warning, 4.14 s. -- Frontend focused cloud/UI suite: **62 passed** across 6 files, 703 ms. +- Backend cloud/migration focused slice: **159 passed, 12 skipped**, 1 non-failing SlowAPI deprecation warning, 3.48 s. The 12 migration integration cases correctly require an explicitly disposable PostgreSQL DSN and did not touch the development database. +- Compose migration contract: **7 passed** in 0.23 s with the repository Compose file mounted read-only into the backend test container. +- Frontend focused suite after gap closure: **76 passed** across 10 files, 1.12 s. +- Compose environment contract: `docker compose config --quiet` passed with required variables resolved. - Execution closeout evidence: backend 509 passed with one documented environment-only `libmagic` failure; frontend 323 passed; production build succeeded. -- Phase verification: `.planning/phases/12-cloud-resource-foundation/12-VERIFICATION.md` status `passed`, 12/12 must-haves verified with one documented package-lock tracking override. +- Later Plan 12-06 closeout evidence: backend 516 passed (7 xfailed, 17 skipped), frontend 334 passed across 42 files, and production build succeeded. +- Phase verification: `.planning/phases/12-cloud-resource-foundation/12-VERIFICATION.md` status `passed`; versions align at 0.2.2. --- @@ -122,4 +140,4 @@ These checks supplement automated coverage; no Phase 12 requirement depends sole - [x] Focused feedback latency is below 60 seconds. - [x] `nyquist_compliant: true` is set in frontmatter. -**Approval:** approved 2026-06-19 +**Approval:** approved 2026-06-22 diff --git a/frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js b/frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js new file mode 100644 index 0000000..7dad4c7 --- /dev/null +++ b/frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' + +vi.mock('../../../api/client.js', () => ({ + getConnectionConfig: vi.fn(), + connectWebDav: vi.fn(), + updateWebDavCredentials: vi.fn(), +})) + +import * as api from '../../../api/client.js' +import CloudCredentialModal from '../CloudCredentialModal.vue' + +const nextcloudProvider = { key: 'nextcloud', label: 'Nextcloud' } +const global = { stubs: { AppIcon: true } } + +async function openModal(props = {}) { + const wrapper = mount(CloudCredentialModal, { + props: { + show: false, + provider: nextcloudProvider, + existing: null, + ...props, + }, + global, + }) + await wrapper.setProps({ show: true }) + await flushPromises() + return wrapper +} + +beforeEach(() => { + vi.clearAllMocks() + api.getConnectionConfig.mockResolvedValue({ + connection_username: 'alice', + server_url: 'https://cloud.example.com/remote.php/dav/files/alice/', + }) + api.connectWebDav.mockResolvedValue({ id: 'new-connection-id' }) + api.updateWebDavCredentials.mockResolvedValue({ id: 'connection-id' }) +}) + +describe('CloudCredentialModal connection lifecycle', () => { + it('creates a new Nextcloud connection and constructs its WebDAV URL', async () => { + const wrapper = await openModal() + + await wrapper.find('input[type="url"]').setValue('https://cloud.example.com') + await wrapper.find('input[type="text"]').setValue('alice@example.com') + await wrapper.find('input[type="password"]').setValue('app-password') + await wrapper.find('form').trigger('submit') + await flushPromises() + + expect(api.connectWebDav).toHaveBeenCalledWith( + 'nextcloud', + 'https://cloud.example.com/remote.php/dav/files/alice%40example.com/', + 'alice@example.com', + 'app-password', + ) + expect(api.updateWebDavCredentials).not.toHaveBeenCalled() + expect(wrapper.emitted('connected')).toHaveLength(1) + expect(wrapper.emitted('close')).toHaveLength(1) + }) + + it('updates the selected connection ID and omits a blank replacement password', async () => { + const wrapper = await openModal({ existing: { id: 'connection-id' } }) + + expect(api.getConnectionConfig).toHaveBeenCalledWith('connection-id') + expect(wrapper.find('input[type="text"]').element.value).toBe('alice') + await wrapper.find('form').trigger('submit') + await flushPromises() + + expect(api.updateWebDavCredentials).toHaveBeenCalledWith('connection-id', { + serverUrl: 'https://cloud.example.com/remote.php/dav/files/alice/', + username: 'alice', + password: undefined, + }) + expect(api.connectWebDav).not.toHaveBeenCalled() + }) + + it('shows an actionable API validation message without closing', async () => { + api.connectWebDav.mockRejectedValueOnce(new Error('server_url: URL must use HTTPS')) + const wrapper = await openModal() + + await wrapper.find('input[type="url"]').setValue('http://cloud.example.com') + await wrapper.find('input[type="text"]').setValue('alice') + await wrapper.find('input[type="password"]').setValue('secret') + await wrapper.find('form').trigger('submit') + await flushPromises() + + expect(wrapper.text()).toContain('server_url: URL must use HTTPS') + expect(wrapper.emitted('connected')).toBeUndefined() + expect(wrapper.emitted('close')).toBeUndefined() + }) +})