import { suite } from '../lib/context.js' /** * Helper: make a raw WebDAV request with Basic Auth. * The API client doesn't support custom HTTP methods, so we use fetch directly. */ async function dav( baseUrl: string, method: string, path: string, opts: { auth: string; headers?: Record; body?: string | Buffer } = { auth: '' }, ) { const headers: Record = { Authorization: opts.auth, ...(opts.headers ?? {}), } const res = await fetch(`${baseUrl}${path}`, { method, headers, body: opts.body, }) const text = await res.text() return { status: res.status, body: text, headers: Object.fromEntries(res.headers.entries()) } } suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => { // Use the same test user created by the test runner const email = 'test@lunarfront.dev' const password = 'testpassword1234' const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64') const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64') // --- OPTIONS --- t.test('OPTIONS returns DAV headers on root', { tags: ['options'] }, async () => { const res = await dav(t.baseUrl, 'OPTIONS', '/webdav/') // CORS plugin may return 204 for preflight, our handler returns 200 — both are valid t.assert.ok(res.status === 200 || res.status === 204, `Expected 200 or 204, got ${res.status}`) }) t.test('OPTIONS returns DAV headers on subpath', { tags: ['options'] }, async () => { const res = await dav(t.baseUrl, 'OPTIONS', '/webdav/any/path') t.assert.ok(res.status === 200 || res.status === 204, `Expected 200 or 204, got ${res.status}`) }) // --- Authentication --- t.test('returns 401 without credentials', { tags: ['auth'] }, async () => { const res = await fetch(`${t.baseUrl}/webdav/`, { method: 'PROPFIND' }) t.assert.equal(res.status, 401) t.assert.contains(res.headers.get('www-authenticate') ?? '', 'Basic') await res.text() }) t.test('returns 401 with wrong password', { tags: ['auth'] }, async () => { const res = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: badAuth, headers: { Depth: '0' } }) t.assert.equal(res.status, 401) }) t.test('authenticates with correct credentials', { tags: ['auth'] }, async () => { const res = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: basicAuth, headers: { Depth: '0' } }) t.assert.equal(res.status, 207) }) // --- PROPFIND --- t.test('PROPFIND root with depth 0 returns collection', { tags: ['propfind'] }, async () => { const res = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: basicAuth, headers: { Depth: '0' } }) t.assert.equal(res.status, 207) t.assert.contains(res.headers['content-type'] ?? '', 'application/xml') t.assert.contains(res.body, ' { // Create a folder first await dav(t.baseUrl, 'MKCOL', '/webdav/PropfindTest', { auth: basicAuth }) const res = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: basicAuth, headers: { Depth: '1' } }) t.assert.equal(res.status, 207) t.assert.contains(res.body, 'PropfindTest') }) t.test('PROPFIND on folder lists files', { tags: ['propfind'] }, async () => { // Create folder and upload a file await dav(t.baseUrl, 'MKCOL', '/webdav/PropfindFiles', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/PropfindFiles/readme.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'hello', }) const res = await dav(t.baseUrl, 'PROPFIND', '/webdav/PropfindFiles', { auth: basicAuth, headers: { Depth: '1' }, }) t.assert.equal(res.status, 207) t.assert.contains(res.body, 'readme.txt') t.assert.contains(res.body, 'text/plain') }) // --- MKCOL --- t.test('MKCOL creates a folder', { tags: ['mkcol'] }, async () => { const res = await dav(t.baseUrl, 'MKCOL', '/webdav/NewFolder', { auth: basicAuth }) t.assert.equal(res.status, 201) // Verify it shows in listing const listing = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: basicAuth, headers: { Depth: '1' } }) t.assert.contains(listing.body, 'NewFolder') }) t.test('MKCOL creates nested folder', { tags: ['mkcol'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/ParentDir', { auth: basicAuth }) const res = await dav(t.baseUrl, 'MKCOL', '/webdav/ParentDir/ChildDir', { auth: basicAuth }) t.assert.equal(res.status, 201) const listing = await dav(t.baseUrl, 'PROPFIND', '/webdav/ParentDir', { auth: basicAuth, headers: { Depth: '1' }, }) t.assert.contains(listing.body, 'ChildDir') }) t.test('MKCOL returns 405 if folder already exists', { tags: ['mkcol'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/DuplicateDir', { auth: basicAuth }) const res = await dav(t.baseUrl, 'MKCOL', '/webdav/DuplicateDir', { auth: basicAuth }) t.assert.equal(res.status, 405) }) // --- PUT / GET / DELETE --- t.test('PUT uploads a file, GET retrieves it', { tags: ['put', 'get'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/Uploads', { auth: basicAuth }) const content = 'Hello, WebDAV!' const putRes = await dav(t.baseUrl, 'PUT', '/webdav/Uploads/test.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: content, }) t.assert.equal(putRes.status, 201) t.assert.ok(putRes.headers['etag']) const getRes = await dav(t.baseUrl, 'GET', '/webdav/Uploads/test.txt', { auth: basicAuth }) t.assert.equal(getRes.status, 200) t.assert.equal(getRes.body, content) t.assert.contains(getRes.headers['content-type'] ?? '', 'text/plain') }) t.test('PUT overwrites an existing file', { tags: ['put'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/Overwrite', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/Overwrite/doc.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'version 1', }) const putRes = await dav(t.baseUrl, 'PUT', '/webdav/Overwrite/doc.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'version 2', }) t.assert.equal(putRes.status, 204) const getRes = await dav(t.baseUrl, 'GET', '/webdav/Overwrite/doc.txt', { auth: basicAuth }) t.assert.equal(getRes.body, 'version 2') }) t.test('DELETE removes a file', { tags: ['delete'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/DeleteFile', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/DeleteFile/gone.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'delete me', }) const delRes = await dav(t.baseUrl, 'DELETE', '/webdav/DeleteFile/gone.txt', { auth: basicAuth }) t.assert.equal(delRes.status, 204) const getRes = await dav(t.baseUrl, 'GET', '/webdav/DeleteFile/gone.txt', { auth: basicAuth }) t.assert.equal(getRes.status, 404) }) t.test('DELETE removes a folder', { tags: ['delete'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/DeleteFolder', { auth: basicAuth }) const res = await dav(t.baseUrl, 'DELETE', '/webdav/DeleteFolder', { auth: basicAuth }) t.assert.equal(res.status, 204) const listing = await dav(t.baseUrl, 'PROPFIND', '/webdav/', { auth: basicAuth, headers: { Depth: '1' } }) // Should not contain the deleted folder // Note: other test folders may exist, just check this one is gone // We can't easily assert "not contains" without adding it to assert, so verify with GET const getRes = await dav(t.baseUrl, 'PROPFIND', '/webdav/DeleteFolder', { auth: basicAuth, headers: { Depth: '0' } }) t.assert.equal(getRes.status, 404) }) // --- LOCK / UNLOCK --- t.test('LOCK returns a lock token', { tags: ['lock'] }, async () => { const res = await dav(t.baseUrl, 'LOCK', '/webdav/locktest', { auth: basicAuth }) t.assert.equal(res.status, 200) t.assert.contains(res.headers['lock-token'] ?? '', 'opaquelocktoken:') t.assert.contains(res.body, ' { const lockRes = await dav(t.baseUrl, 'LOCK', '/webdav/unlocktest', { auth: basicAuth }) const lockToken = lockRes.headers['lock-token'] ?? '' const res = await dav(t.baseUrl, 'UNLOCK', '/webdav/unlocktest', { auth: basicAuth, headers: { 'Lock-Token': lockToken }, }) t.assert.equal(res.status, 204) }) // --- COPY --- t.test('COPY duplicates a file to another folder', { tags: ['copy'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/CopySrc', { auth: basicAuth }) await dav(t.baseUrl, 'MKCOL', '/webdav/CopyDst', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/CopySrc/original.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'copy me', }) const res = await dav(t.baseUrl, 'COPY', '/webdav/CopySrc/original.txt', { auth: basicAuth, headers: { Destination: `${t.baseUrl}/webdav/CopyDst/copied.txt` }, }) t.assert.equal(res.status, 201) // Verify copy exists const getRes = await dav(t.baseUrl, 'GET', '/webdav/CopyDst/copied.txt', { auth: basicAuth }) t.assert.equal(getRes.status, 200) t.assert.equal(getRes.body, 'copy me') // Verify original still exists const origRes = await dav(t.baseUrl, 'GET', '/webdav/CopySrc/original.txt', { auth: basicAuth }) t.assert.equal(origRes.status, 200) }) // --- MOVE --- t.test('MOVE moves a file to another folder', { tags: ['move'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/MoveSrc', { auth: basicAuth }) await dav(t.baseUrl, 'MKCOL', '/webdav/MoveDst', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/MoveSrc/moveme.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'move me', }) const res = await dav(t.baseUrl, 'MOVE', '/webdav/MoveSrc/moveme.txt', { auth: basicAuth, headers: { Destination: `${t.baseUrl}/webdav/MoveDst/moved.txt` }, }) t.assert.equal(res.status, 201) // Verify moved file exists at destination const getRes = await dav(t.baseUrl, 'GET', '/webdav/MoveDst/moved.txt', { auth: basicAuth }) t.assert.equal(getRes.status, 200) t.assert.equal(getRes.body, 'move me') // Verify original is gone const origRes = await dav(t.baseUrl, 'GET', '/webdav/MoveSrc/moveme.txt', { auth: basicAuth }) t.assert.equal(origRes.status, 404) }) // --- HEAD --- t.test('HEAD returns correct headers for a file', { tags: ['head'] }, async () => { await dav(t.baseUrl, 'MKCOL', '/webdav/HeadTest', { auth: basicAuth }) await dav(t.baseUrl, 'PUT', '/webdav/HeadTest/info.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'head check', }) const res = await dav(t.baseUrl, 'HEAD', '/webdav/HeadTest/info.txt', { auth: basicAuth }) t.assert.equal(res.status, 200) t.assert.contains(res.headers['content-type'] ?? '', 'text/plain') t.assert.ok(res.headers['etag']) }) // --- 404 cases --- t.test('GET returns 404 for nonexistent file', { tags: ['get'] }, async () => { const res = await dav(t.baseUrl, 'GET', '/webdav/NoSuchFolder/nofile.txt', { auth: basicAuth }) t.assert.equal(res.status, 404) }) t.test('PUT returns 409 when parent folder missing', { tags: ['put'] }, async () => { const res = await dav(t.baseUrl, 'PUT', '/webdav/NonExistent/file.txt', { auth: basicAuth, headers: { 'Content-Type': 'text/plain' }, body: 'orphan', }) t.assert.equal(res.status, 409) }) })