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
307 lines
12 KiB
TypeScript
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)
|
|
})
|
|
})
|