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
233 lines
8.4 KiB
TypeScript
233 lines
8.4 KiB
TypeScript
import { suite } from '../lib/context.js'
|
|
|
|
const MASTER_PASSWORD = 'test-vault-master-2024!'
|
|
|
|
suite('Vault', { tags: ['vault'] }, (t) => {
|
|
|
|
// --- Initialization & Unlock ---
|
|
|
|
t.test('initializes vault with master password', { tags: ['init'] }, async () => {
|
|
const res = await t.api.post('/v1/vault/initialize', { masterPassword: MASTER_PASSWORD })
|
|
t.assert.status(res, 201)
|
|
t.assert.contains(res.data.message, 'initialized')
|
|
})
|
|
|
|
t.test('rejects re-initialization', { tags: ['init'] }, async () => {
|
|
const res = await t.api.post('/v1/vault/initialize', { masterPassword: 'another-password' })
|
|
t.assert.status(res, 400)
|
|
})
|
|
|
|
t.test('reports vault status', { tags: ['status'] }, async () => {
|
|
const res = await t.api.get('/v1/vault/status')
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.initialized, true)
|
|
t.assert.equal(res.data.unlocked, true)
|
|
})
|
|
|
|
t.test('locks vault', { tags: ['lock'] }, async () => {
|
|
const res = await t.api.post('/v1/vault/lock')
|
|
t.assert.status(res, 200)
|
|
|
|
const status = await t.api.get('/v1/vault/status')
|
|
t.assert.equal(status.data.unlocked, false)
|
|
})
|
|
|
|
t.test('rejects operations when locked', { tags: ['lock'] }, async () => {
|
|
const res = await t.api.get('/v1/vault/categories')
|
|
t.assert.status(res, 403)
|
|
t.assert.contains(res.data.error.message, 'locked')
|
|
})
|
|
|
|
t.test('unlocks vault with correct password', { tags: ['lock'] }, async () => {
|
|
const res = await t.api.post('/v1/vault/unlock', { masterPassword: MASTER_PASSWORD })
|
|
t.assert.status(res, 200)
|
|
|
|
const status = await t.api.get('/v1/vault/status')
|
|
t.assert.equal(status.data.unlocked, true)
|
|
})
|
|
|
|
t.test('rejects unlock with wrong password', { tags: ['lock'] }, async () => {
|
|
await t.api.post('/v1/vault/lock')
|
|
const res = await t.api.post('/v1/vault/unlock', { masterPassword: 'wrong-password' })
|
|
t.assert.status(res, 401)
|
|
|
|
// Re-unlock for remaining tests
|
|
await t.api.post('/v1/vault/unlock', { masterPassword: MASTER_PASSWORD })
|
|
})
|
|
|
|
// --- Categories ---
|
|
|
|
t.test('creates a category', { tags: ['category'] }, async () => {
|
|
const res = await t.api.post('/v1/vault/categories', { name: 'WiFi Passwords', description: 'Store WiFi credentials' })
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.name, 'WiFi Passwords')
|
|
t.assert.ok(res.data.id)
|
|
})
|
|
|
|
t.test('lists accessible categories', { tags: ['category'] }, async () => {
|
|
const res = await t.api.get('/v1/vault/categories')
|
|
t.assert.status(res, 200)
|
|
t.assert.greaterThan(res.data.data.length, 0)
|
|
})
|
|
|
|
t.test('gets category detail with accessLevel', { tags: ['category'] }, async () => {
|
|
const list = await t.api.get('/v1/vault/categories')
|
|
const catId = list.data.data[0].id
|
|
|
|
const res = await t.api.get(`/v1/vault/categories/${catId}`)
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.accessLevel, 'admin')
|
|
})
|
|
|
|
t.test('updates a category', { tags: ['category'] }, async () => {
|
|
const list = await t.api.get('/v1/vault/categories')
|
|
const catId = list.data.data[0].id
|
|
|
|
const res = await t.api.patch(`/v1/vault/categories/${catId}`, { name: 'WiFi & Network' })
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.name, 'WiFi & Network')
|
|
})
|
|
|
|
// --- Entries ---
|
|
|
|
t.test('creates an entry with a secret', { tags: ['entry'] }, async () => {
|
|
const list = await t.api.get('/v1/vault/categories')
|
|
const catId = list.data.data[0].id
|
|
|
|
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
|
|
name: 'Store WiFi',
|
|
username: 'DemoUser',
|
|
url: 'http://192.168.1.1',
|
|
notes: 'Router admin panel',
|
|
secret: 'supersecretpassword123',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.name, 'Store WiFi')
|
|
t.assert.equal(res.data.username, 'DemoUser')
|
|
t.assert.equal(res.data.hasSecret, true)
|
|
// Secret value should NOT be in the response
|
|
t.assert.falsy(res.data.encryptedValue)
|
|
t.assert.falsy(res.data.iv)
|
|
t.assert.falsy(res.data.secret)
|
|
})
|
|
|
|
t.test('lists entries without revealing secrets', { tags: ['entry'] }, async () => {
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
|
|
const res = await t.api.get(`/v1/vault/categories/${catId}/entries`)
|
|
t.assert.status(res, 200)
|
|
t.assert.greaterThan(res.data.data.length, 0)
|
|
t.assert.equal(res.data.data[0].hasSecret, true)
|
|
t.assert.falsy(res.data.data[0].encryptedValue)
|
|
})
|
|
|
|
t.test('reveals a secret', { tags: ['entry', 'reveal'] }, async () => {
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
const entries = await t.api.get(`/v1/vault/categories/${catId}/entries`)
|
|
const entryId = entries.data.data[0].id
|
|
|
|
const res = await t.api.post(`/v1/vault/entries/${entryId}/reveal`)
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.value, 'supersecretpassword123')
|
|
})
|
|
|
|
t.test('updates an entry with new secret', { tags: ['entry'] }, async () => {
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
const entries = await t.api.get(`/v1/vault/categories/${catId}/entries`)
|
|
const entryId = entries.data.data[0].id
|
|
|
|
const res = await t.api.patch(`/v1/vault/entries/${entryId}`, {
|
|
name: 'Store WiFi (updated)',
|
|
secret: 'newsecret456',
|
|
})
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.name, 'Store WiFi (updated)')
|
|
|
|
// Verify new secret
|
|
const reveal = await t.api.post(`/v1/vault/entries/${entryId}/reveal`)
|
|
t.assert.equal(reveal.data.value, 'newsecret456')
|
|
})
|
|
|
|
t.test('creates entry without secret', { tags: ['entry'] }, async () => {
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
|
|
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
|
|
name: 'Vendor Contact',
|
|
notes: 'Call 555-1234',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.hasSecret, false)
|
|
})
|
|
|
|
t.test('deletes an entry', { tags: ['entry'] }, async () => {
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
|
|
// Create one to delete
|
|
const created = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
|
|
name: 'To Delete',
|
|
secret: 'deleteme',
|
|
})
|
|
|
|
const res = await t.api.del(`/v1/vault/entries/${created.data.id}`)
|
|
t.assert.status(res, 200)
|
|
|
|
const check = await t.api.get(`/v1/vault/entries/${created.data.id}`)
|
|
t.assert.status(check, 404)
|
|
})
|
|
|
|
// --- Master Password Change ---
|
|
|
|
t.test('changes master password and secrets still work', { tags: ['master'] }, async () => {
|
|
const newMaster = 'new-master-password-2024!'
|
|
|
|
const res = await t.api.post('/v1/vault/change-master-password', {
|
|
currentPassword: MASTER_PASSWORD,
|
|
newPassword: newMaster,
|
|
})
|
|
t.assert.status(res, 200)
|
|
|
|
// Verify existing secrets still decrypt
|
|
const cats = await t.api.get('/v1/vault/categories')
|
|
const catId = cats.data.data[0].id
|
|
const entries = await t.api.get(`/v1/vault/categories/${catId}/entries`)
|
|
const secretEntry = entries.data.data.find((e: any) => e.hasSecret)
|
|
if (secretEntry) {
|
|
const reveal = await t.api.post(`/v1/vault/entries/${secretEntry.id}/reveal`)
|
|
t.assert.status(reveal, 200)
|
|
t.assert.ok(reveal.data.value)
|
|
}
|
|
|
|
// Lock and re-unlock with new password
|
|
await t.api.post('/v1/vault/lock')
|
|
const unlock = await t.api.post('/v1/vault/unlock', { masterPassword: newMaster })
|
|
t.assert.status(unlock, 200)
|
|
|
|
// Old password should fail
|
|
await t.api.post('/v1/vault/lock')
|
|
const oldUnlock = await t.api.post('/v1/vault/unlock', { masterPassword: MASTER_PASSWORD })
|
|
t.assert.status(oldUnlock, 401)
|
|
|
|
// Re-unlock with new password for cleanup
|
|
await t.api.post('/v1/vault/unlock', { masterPassword: newMaster })
|
|
})
|
|
|
|
// --- Delete category ---
|
|
|
|
t.test('deletes a category and cascades entries', { tags: ['category'] }, async () => {
|
|
// Create a fresh category with an entry
|
|
const cat = await t.api.post('/v1/vault/categories', { name: 'To Delete Cat' })
|
|
await t.api.post(`/v1/vault/categories/${cat.data.id}/entries`, { name: 'Temp Entry', secret: 'temp' })
|
|
|
|
const res = await t.api.del(`/v1/vault/categories/${cat.data.id}`)
|
|
t.assert.status(res, 200)
|
|
|
|
const check = await t.api.get(`/v1/vault/categories/${cat.data.id}`)
|
|
t.assert.status(check, 404)
|
|
})
|
|
})
|