Files
Oxicloud/frontend/src/lib/api/endpoints/share.test.ts
T
Bradley Nelson e3823ce470 test(e2e): Playwright + Vitest coverage harness and test instrumentation
Add an end-to-end and unit test suite for the SvelteKit frontend:

- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
  codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
  ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
  public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
  middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
  v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.

Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
  setup so storage behaves identically across Node versions (Node 26 ships a
  native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
  so the dev shell and CI run the same toolchain versions.

Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
  (cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
  batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
  wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
  blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
  existing file id) instead of the stale 409 expectation.

Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
2026-06-22 00:05:06 -06:00

93 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/api/client', () => ({ apiFetch: vi.fn(), apiJson: vi.fn() }));
vi.mock('$lib/api/csrf', () => ({ getCsrfHeaders: () => ({}) }));
import { apiFetch, apiJson } from '$lib/api/client';
import {
shareDownloadUrl,
shareFileUrl,
shareZipUrl,
getShareMeta,
verifySharePassword,
getShareContents
} from './share';
const fetchMock = apiFetch as unknown as ReturnType<typeof vi.fn>;
const jsonMock = apiJson as unknown as ReturnType<typeof vi.fn>;
describe('share URL builders', () => {
it('build encoded share URLs', () => {
expect(shareDownloadUrl('tok en')).toBe('/api/s/tok%20en/download');
expect(shareFileUrl('t', 'f/1')).toBe('/api/s/t/file/f%2F1');
expect(shareZipUrl('t')).toBe('/api/s/t/zip');
expect(shareZipUrl('t', 'fid')).toBe('/api/s/t/zip/fid');
});
});
describe('share API calls', () => {
beforeEach(() => {
vi.clearAllMocks();
fetchMock.mockResolvedValue({ ok: true, status: 200, json: async () => ({}) });
jsonMock.mockResolvedValue({});
});
it('hit the API for meta / verify / contents', async () => {
await getShareMeta('t').catch(() => {});
await verifySharePassword('t', 'pw').catch(() => {});
await getShareContents('t').catch(() => {});
expect(fetchMock.mock.calls.length + jsonMock.mock.calls.length).toBeGreaterThan(0);
});
});
describe('share status branches', () => {
const resp = (over: Record<string, unknown>) => ({
ok: false,
status: 200,
json: async () => ({}),
...over
});
beforeEach(() => vi.clearAllMocks());
it('returns ok meta on 200', async () => {
fetchMock.mockResolvedValue(
resp({ ok: true, json: async () => ({ item_type: 'folder', item_name: 'Docs' }) })
);
expect(await getShareMeta('t')).toEqual({
status: 'ok',
data: { item_type: 'folder', item_name: 'Docs' }
});
});
it('maps 401+requiresPassword to a password prompt', async () => {
fetchMock.mockResolvedValue(
resp({ status: 401, json: async () => ({ requiresPassword: true }) })
);
expect(await getShareMeta('t')).toEqual({ status: 'password' });
});
it('maps meta 410 to expired and 404 to invalid', async () => {
fetchMock.mockResolvedValueOnce(resp({ status: 410 }));
expect(await getShareMeta('t')).toEqual({ status: 'expired' });
fetchMock.mockResolvedValueOnce(resp({ status: 404 }));
expect(await getShareMeta('t')).toEqual({ status: 'invalid' });
});
it('verifies a password: true on ok, false on 401', async () => {
fetchMock.mockResolvedValueOnce(resp({ ok: true }));
expect(await verifySharePassword('t', 'pw')).toBe(true);
fetchMock.mockResolvedValueOnce(resp({ status: 401 }));
expect(await verifySharePassword('t', 'bad')).toBe(false);
});
it('lists contents and maps 401→password, 410→expired', async () => {
fetchMock.mockResolvedValueOnce(
resp({ ok: true, json: async () => ({ folders: [], files: [] }) })
);
expect((await getShareContents('t')).status).toBe('ok');
fetchMock.mockResolvedValueOnce(resp({ status: 401 }));
expect((await getShareContents('t')).status).toBe('password');
fetchMock.mockResolvedValueOnce(resp({ status: 410 }));
expect((await getShareContents('t', 'fid')).status).toBe('expired');
});
});