feat: settings modal with database export and import
Adds a gear button to the nav that opens a settings modal with: - Export: GET /api/export returns all instances as a JSON backup file with a Content-Disposition attachment header - Import: POST /api/import validates and bulk-replaces all instances; client uses FileReader to POST the parsed JSON, with a confirm dialog before destructive replace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,52 @@ describe('DELETE /api/instances/:vmid', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ── GET /api/export ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/export', () => {
|
||||
it('returns 200 with instances array and attachment header', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
const res = await request(app).get('/api/export')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers['content-disposition']).toMatch(/attachment/)
|
||||
expect(res.body.instances).toHaveLength(1)
|
||||
expect(res.body.instances[0].name).toBe('traefik')
|
||||
})
|
||||
|
||||
it('returns empty instances array when no data', async () => {
|
||||
const res = await request(app).get('/api/export')
|
||||
expect(res.body.instances).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// ── POST /api/import ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/import', () => {
|
||||
it('replaces all instances and returns imported count', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
const res = await request(app).post('/api/import')
|
||||
.send({ instances: [{ ...base, vmid: 999, name: 'imported' }] })
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.imported).toBe(1)
|
||||
expect((await request(app).get('/api/instances')).body[0].name).toBe('imported')
|
||||
})
|
||||
|
||||
it('returns 400 if instances is not an array', async () => {
|
||||
expect((await request(app).post('/api/import').send({ instances: 'bad' })).status).toBe(400)
|
||||
})
|
||||
|
||||
it('returns 400 with per-row errors for invalid rows', async () => {
|
||||
const res = await request(app).post('/api/import')
|
||||
.send({ instances: [{ ...base, name: '', vmid: 1 }] })
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body.errors[0].index).toBe(0)
|
||||
})
|
||||
|
||||
it('returns 400 if body has no instances key', async () => {
|
||||
expect((await request(app).post('/api/import').send({})).status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Static assets & SPA routing ───────────────────────────────────────────────
|
||||
|
||||
describe('static assets and SPA routing', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import {
|
||||
_resetForTest,
|
||||
getInstances, getInstance, getDistinctStacks,
|
||||
createInstance, updateInstance, deleteInstance,
|
||||
createInstance, updateInstance, deleteInstance, importInstances,
|
||||
} from '../server/db.js'
|
||||
|
||||
beforeEach(() => _resetForTest());
|
||||
@@ -166,6 +166,25 @@ describe('deleteInstance', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── importInstances ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('importInstances', () => {
|
||||
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
|
||||
|
||||
it('replaces all existing instances with the imported set', () => {
|
||||
createInstance({ ...base, name: 'old', vmid: 1 });
|
||||
importInstances([{ ...base, name: 'new', vmid: 2 }]);
|
||||
expect(getInstance(1)).toBeNull();
|
||||
expect(getInstance(2)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('clears all instances when passed an empty array', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
importInstances([]);
|
||||
expect(getInstances()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test environment boot isolation ───────────────────────────────────────────
|
||||
|
||||
describe('test environment boot isolation', () => {
|
||||
|
||||
Reference in New Issue
Block a user