Files
lunarfront-app/packages/backend/api-tests/suites/webdav.ts
Ryan Moon 9400828f62 Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
2026-03-30 08:51:54 -05:00

307 lines
12 KiB
TypeScript

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<string, string>; body?: string | Buffer } = { auth: '' },
) {
const headers: Record<string, string> = {
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, '<D:multistatus')
t.assert.contains(res.body, '<D:collection/')
})
t.test('PROPFIND root with depth 1 lists folders', { tags: ['propfind'] }, async () => {
// 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, '<D:lockdiscovery')
})
t.test('UNLOCK returns 204', { tags: ['lock'] }, async () => {
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)
})
})