claude went crazy
This commit is contained in:
239
tests/api.test.js
Normal file
239
tests/api.test.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import request from 'supertest'
|
||||
import { app } from '../server/server.js'
|
||||
import { _resetForTest } from '../server/db.js'
|
||||
|
||||
beforeEach(() => _resetForTest())
|
||||
|
||||
const base = {
|
||||
name: 'traefik',
|
||||
vmid: 100,
|
||||
state: 'deployed',
|
||||
stack: 'production',
|
||||
atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0,
|
||||
tailscale_ip: '',
|
||||
hardware_acceleration: 0,
|
||||
}
|
||||
|
||||
// ── GET /api/instances ────────────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/instances', () => {
|
||||
it('returns empty array when no instances exist', async () => {
|
||||
const res = await request(app).get('/api/instances')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual([])
|
||||
})
|
||||
|
||||
it('returns all instances sorted by name', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'zebra' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'alpha' })
|
||||
const res = await request(app).get('/api/instances')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toHaveLength(2)
|
||||
expect(res.body[0].name).toBe('alpha')
|
||||
expect(res.body[1].name).toBe('zebra')
|
||||
})
|
||||
|
||||
it('filters by state', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', state: 'deployed' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', state: 'degraded' })
|
||||
const res = await request(app).get('/api/instances?state=deployed')
|
||||
expect(res.body).toHaveLength(1)
|
||||
expect(res.body[0].name).toBe('a')
|
||||
})
|
||||
|
||||
it('filters by stack', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', stack: 'production' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', stack: 'development', state: 'testing' })
|
||||
const res = await request(app).get('/api/instances?stack=development')
|
||||
expect(res.body).toHaveLength(1)
|
||||
expect(res.body[0].name).toBe('b')
|
||||
})
|
||||
|
||||
it('searches by name substring', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'plex' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'gitea' })
|
||||
const res = await request(app).get('/api/instances?search=ple')
|
||||
expect(res.body).toHaveLength(1)
|
||||
expect(res.body[0].name).toBe('plex')
|
||||
})
|
||||
|
||||
it('searches by vmid', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 137, name: 'a' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 200, name: 'b' })
|
||||
const res = await request(app).get('/api/instances?search=137')
|
||||
expect(res.body).toHaveLength(1)
|
||||
expect(res.body[0].vmid).toBe(137)
|
||||
})
|
||||
|
||||
it('combines search and state filters', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'plex', state: 'deployed' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'plex2', state: 'degraded' })
|
||||
const res = await request(app).get('/api/instances?search=plex&state=deployed')
|
||||
expect(res.body).toHaveLength(1)
|
||||
expect(res.body[0].name).toBe('plex')
|
||||
})
|
||||
})
|
||||
|
||||
// ── GET /api/instances/stacks ─────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/instances/stacks', () => {
|
||||
it('returns empty array when no instances exist', async () => {
|
||||
const res = await request(app).get('/api/instances/stacks')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body).toEqual([])
|
||||
})
|
||||
|
||||
it('returns unique stacks sorted alphabetically', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', stack: 'production' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', stack: 'development', state: 'testing' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 3, name: 'c', stack: 'production' })
|
||||
const res = await request(app).get('/api/instances/stacks')
|
||||
expect(res.body).toEqual(['development', 'production'])
|
||||
})
|
||||
})
|
||||
|
||||
// ── GET /api/instances/:vmid ──────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/instances/:vmid', () => {
|
||||
it('returns the instance for a known vmid', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 117, name: 'plex' })
|
||||
const res = await request(app).get('/api/instances/117')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.name).toBe('plex')
|
||||
expect(res.body.vmid).toBe(117)
|
||||
})
|
||||
|
||||
it('returns 404 for unknown vmid', async () => {
|
||||
const res = await request(app).get('/api/instances/999')
|
||||
expect(res.status).toBe(404)
|
||||
expect(res.body.error).toBeDefined()
|
||||
})
|
||||
|
||||
it('returns 400 for non-numeric vmid', async () => {
|
||||
const res = await request(app).get('/api/instances/abc')
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
// ── POST /api/instances ───────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/instances', () => {
|
||||
it('creates an instance and returns 201 with the created record', async () => {
|
||||
const res = await request(app).post('/api/instances').send(base)
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.name).toBe('traefik')
|
||||
expect(res.body.vmid).toBe(100)
|
||||
expect(res.body.created_at).not.toBeNull()
|
||||
expect(res.body.updated_at).not.toBeNull()
|
||||
})
|
||||
|
||||
it('stores service flags correctly', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, atlas: 1, tailscale: 1, hardware_acceleration: 1 })
|
||||
expect(res.body.atlas).toBe(1)
|
||||
expect(res.body.tailscale).toBe(1)
|
||||
expect(res.body.hardware_acceleration).toBe(1)
|
||||
expect(res.body.argus).toBe(0)
|
||||
})
|
||||
|
||||
it('returns 409 for duplicate vmid', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
const res = await request(app).post('/api/instances').send({ ...base, name: 'other' })
|
||||
expect(res.status).toBe(409)
|
||||
expect(res.body.error).toMatch(/vmid/)
|
||||
})
|
||||
|
||||
it('returns 400 when name is missing', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, name: '' })
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body.errors).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it('returns 400 for vmid less than 1', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, vmid: 0 })
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body.errors).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it('returns 400 for invalid state', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, state: 'invalid' })
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('returns 400 for invalid stack', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, stack: 'invalid' })
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('trims whitespace from name', async () => {
|
||||
const res = await request(app).post('/api/instances').send({ ...base, name: ' plex ' })
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.name).toBe('plex')
|
||||
})
|
||||
})
|
||||
|
||||
// ── PUT /api/instances/:vmid ──────────────────────────────────────────────────
|
||||
|
||||
describe('PUT /api/instances/:vmid', () => {
|
||||
it('updates fields and returns the updated record', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
const res = await request(app).put('/api/instances/100').send({ ...base, name: 'updated', state: 'degraded' })
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.name).toBe('updated')
|
||||
expect(res.body.state).toBe('degraded')
|
||||
})
|
||||
|
||||
it('can change the vmid', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
|
||||
expect((await request(app).get('/api/instances/100')).status).toBe(404)
|
||||
expect((await request(app).get('/api/instances/200')).status).toBe(200)
|
||||
})
|
||||
|
||||
it('returns 404 for unknown vmid', async () => {
|
||||
const res = await request(app).put('/api/instances/999').send(base)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
it('returns 400 for validation errors', async () => {
|
||||
await request(app).post('/api/instances').send(base)
|
||||
const res = await request(app).put('/api/instances/100').send({ ...base, name: '' })
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body.errors).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it('returns 409 when new vmid conflicts with an existing instance', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 100, name: 'a' })
|
||||
await request(app).post('/api/instances').send({ ...base, vmid: 200, name: 'b' })
|
||||
const res = await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
|
||||
expect(res.status).toBe(409)
|
||||
})
|
||||
})
|
||||
|
||||
// ── DELETE /api/instances/:vmid ───────────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/instances/:vmid', () => {
|
||||
it('deletes a development instance and returns 204', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, stack: 'development', state: 'testing' })
|
||||
const res = await request(app).delete('/api/instances/100')
|
||||
expect(res.status).toBe(204)
|
||||
expect((await request(app).get('/api/instances/100')).status).toBe(404)
|
||||
})
|
||||
|
||||
it('returns 422 when attempting to delete a production instance', async () => {
|
||||
await request(app).post('/api/instances').send({ ...base, stack: 'production' })
|
||||
const res = await request(app).delete('/api/instances/100')
|
||||
expect(res.status).toBe(422)
|
||||
expect(res.body.error).toMatch(/development/)
|
||||
})
|
||||
|
||||
it('returns 404 for unknown vmid', async () => {
|
||||
const res = await request(app).delete('/api/instances/999')
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
it('returns 400 for non-numeric vmid', async () => {
|
||||
const res = await request(app).delete('/api/instances/abc')
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
335
tests/db.test.js
335
tests/db.test.js
@@ -1,250 +1,167 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import initSqlJs from 'sql.js'
|
||||
import {
|
||||
_resetForTest,
|
||||
getInstances, getInstance, getDistinctStacks,
|
||||
createInstance, updateInstance, deleteInstance,
|
||||
} from '../server/db.js'
|
||||
|
||||
// ── Schema (mirrors db.js) ────────────────────────────────────────────────────
|
||||
beforeEach(() => _resetForTest());
|
||||
|
||||
const SCHEMA = `
|
||||
CREATE TABLE instances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
state TEXT DEFAULT 'deployed',
|
||||
stack TEXT DEFAULT '',
|
||||
vmid INTEGER UNIQUE NOT NULL,
|
||||
atlas INTEGER DEFAULT 0,
|
||||
argus INTEGER DEFAULT 0,
|
||||
semaphore INTEGER DEFAULT 0,
|
||||
patchmon INTEGER DEFAULT 0,
|
||||
tailscale INTEGER DEFAULT 0,
|
||||
andromeda INTEGER DEFAULT 0,
|
||||
tailscale_ip TEXT DEFAULT '',
|
||||
hardware_acceleration INTEGER DEFAULT 0,
|
||||
createdAt TEXT DEFAULT (datetime('now')),
|
||||
updatedAt TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
`
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
let db
|
||||
|
||||
beforeEach(async () => {
|
||||
const SQL = await initSqlJs()
|
||||
db = new SQL.Database()
|
||||
db.run(SCHEMA)
|
||||
})
|
||||
|
||||
function rows(res) {
|
||||
if (!res.length) return []
|
||||
const cols = res[0].columns
|
||||
return res[0].values.map(row => Object.fromEntries(cols.map((c, i) => [c, row[i]])))
|
||||
}
|
||||
|
||||
function insert(overrides = {}) {
|
||||
const defaults = {
|
||||
name: 'test-instance', state: 'deployed', stack: 'production', vmid: 100,
|
||||
atlas: 0, argus: 0, semaphore: 0, patchmon: 0,
|
||||
tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0,
|
||||
}
|
||||
const d = { ...defaults, ...overrides }
|
||||
db.run(
|
||||
`INSERT INTO instances
|
||||
(name, state, stack, vmid, atlas, argus, semaphore, patchmon, tailscale, andromeda, tailscale_ip, hardware_acceleration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[d.name, d.state, d.stack, d.vmid, d.atlas, d.argus, d.semaphore,
|
||||
d.patchmon, d.tailscale, d.andromeda, d.tailscale_ip, d.hardware_acceleration]
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
||||
function getInstances(filters = {}) {
|
||||
let sql = 'SELECT * FROM instances WHERE 1=1'
|
||||
const params = []
|
||||
if (filters.search) {
|
||||
sql += ' AND (name LIKE ? OR CAST(vmid AS TEXT) LIKE ? OR stack LIKE ?)'
|
||||
const s = `%${filters.search}%`
|
||||
params.push(s, s, s)
|
||||
}
|
||||
if (filters.state) { sql += ' AND state = ?'; params.push(filters.state) }
|
||||
if (filters.stack) { sql += ' AND stack = ?'; params.push(filters.stack) }
|
||||
sql += ' ORDER BY name ASC'
|
||||
return rows(db.exec(sql, params))
|
||||
}
|
||||
|
||||
function getInstance(vmid) {
|
||||
const res = rows(db.exec('SELECT * FROM instances WHERE vmid = ?', [vmid]))
|
||||
return res[0] ?? null
|
||||
}
|
||||
|
||||
function getDistinctStacks() {
|
||||
const res = db.exec(`SELECT DISTINCT stack FROM instances WHERE stack != '' ORDER BY stack`)
|
||||
if (!res.length) return []
|
||||
return res[0].values.map(r => r[0])
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
// ── getInstances ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getInstances', () => {
|
||||
it('returns empty array when no instances exist', () => {
|
||||
expect(getInstances()).toEqual([])
|
||||
})
|
||||
it('returns empty array when table is empty', () => {
|
||||
expect(getInstances()).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns all instances sorted by name', () => {
|
||||
insert({ name: 'zebra', vmid: 1 })
|
||||
insert({ name: 'alpha', vmid: 2 })
|
||||
const result = getInstances()
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].name).toBe('alpha')
|
||||
expect(result[1].name).toBe('zebra')
|
||||
})
|
||||
createInstance({ name: 'zebra', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'alpha', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
const result = getInstances();
|
||||
expect(result[0].name).toBe('alpha');
|
||||
expect(result[1].name).toBe('zebra');
|
||||
});
|
||||
|
||||
it('filters by state', () => {
|
||||
insert({ name: 'a', vmid: 1, state: 'deployed' })
|
||||
insert({ name: 'b', vmid: 2, state: 'degraded' })
|
||||
insert({ name: 'c', vmid: 3, state: 'testing' })
|
||||
expect(getInstances({ state: 'deployed' })).toHaveLength(1)
|
||||
expect(getInstances({ state: 'degraded' })).toHaveLength(1)
|
||||
expect(getInstances({ state: 'testing' })).toHaveLength(1)
|
||||
})
|
||||
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'b', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'c', state: 'testing', stack: 'development', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getInstances({ state: 'deployed' })).toHaveLength(1);
|
||||
expect(getInstances({ state: 'degraded' })).toHaveLength(1);
|
||||
expect(getInstances({ state: 'testing' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('filters by stack', () => {
|
||||
insert({ name: 'a', vmid: 1, stack: 'production' })
|
||||
insert({ name: 'b', vmid: 2, stack: 'development' })
|
||||
expect(getInstances({ stack: 'production' })).toHaveLength(1)
|
||||
expect(getInstances({ stack: 'development' })).toHaveLength(1)
|
||||
})
|
||||
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getInstances({ stack: 'production' })).toHaveLength(1);
|
||||
expect(getInstances({ stack: 'development' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('searches by name', () => {
|
||||
insert({ name: 'plex', vmid: 1 })
|
||||
insert({ name: 'gitea', vmid: 2 })
|
||||
expect(getInstances({ search: 'ple' })).toHaveLength(1)
|
||||
expect(getInstances({ search: 'ple' })[0].name).toBe('plex')
|
||||
})
|
||||
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'gitea', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getInstances({ search: 'ple' })).toHaveLength(1);
|
||||
expect(getInstances({ search: 'ple' })[0].name).toBe('plex');
|
||||
});
|
||||
|
||||
it('searches by vmid', () => {
|
||||
insert({ name: 'a', vmid: 137 })
|
||||
insert({ name: 'b', vmid: 200 })
|
||||
expect(getInstances({ search: '137' })).toHaveLength(1)
|
||||
})
|
||||
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 137, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'b', state: 'deployed', stack: 'production', vmid: 200, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getInstances({ search: '137' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('searches by stack', () => {
|
||||
insert({ name: 'a', vmid: 1, stack: 'production' })
|
||||
insert({ name: 'b', vmid: 2, stack: 'development' })
|
||||
expect(getInstances({ search: 'prod' })).toHaveLength(1)
|
||||
})
|
||||
it('combines filters', () => {
|
||||
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'plex2', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getInstances({ search: 'plex', state: 'deployed' })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('combines search and state filters', () => {
|
||||
insert({ name: 'plex', vmid: 1, state: 'deployed' })
|
||||
insert({ name: 'plex2', vmid: 2, state: 'degraded' })
|
||||
expect(getInstances({ search: 'plex', state: 'deployed' })).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('returns empty array when no results match', () => {
|
||||
insert({ name: 'plex', vmid: 1 })
|
||||
expect(getInstances({ search: 'zzz' })).toEqual([])
|
||||
})
|
||||
})
|
||||
// ── getInstance ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('returns the instance with the given vmid', () => {
|
||||
insert({ name: 'plex', vmid: 117 })
|
||||
const inst = getInstance(117)
|
||||
expect(inst).not.toBeNull()
|
||||
expect(inst.name).toBe('plex')
|
||||
expect(inst.vmid).toBe(117)
|
||||
})
|
||||
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 117, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
const inst = getInstance(117);
|
||||
expect(inst).not.toBeNull();
|
||||
expect(inst.name).toBe('plex');
|
||||
expect(inst.vmid).toBe(117);
|
||||
});
|
||||
|
||||
it('returns null for an unknown vmid', () => {
|
||||
expect(getInstance(999)).toBeNull()
|
||||
})
|
||||
})
|
||||
it('returns null for unknown vmid', () => {
|
||||
expect(getInstance(999)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── getDistinctStacks ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('getDistinctStacks', () => {
|
||||
it('returns empty array when no instances exist', () => {
|
||||
expect(getDistinctStacks()).toEqual([])
|
||||
})
|
||||
it('returns empty array when table is empty', () => {
|
||||
expect(getDistinctStacks()).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns unique stacks sorted alphabetically', () => {
|
||||
insert({ vmid: 1, stack: 'production' })
|
||||
insert({ vmid: 2, stack: 'development' })
|
||||
insert({ vmid: 3, stack: 'production' })
|
||||
expect(getDistinctStacks()).toEqual(['development', 'production'])
|
||||
})
|
||||
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
createInstance({ name: 'c', state: 'deployed', stack: 'production', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
|
||||
expect(getDistinctStacks()).toEqual(['development', 'production']);
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes blank stack values', () => {
|
||||
insert({ vmid: 1, stack: '' })
|
||||
insert({ vmid: 2, stack: 'production' })
|
||||
expect(getDistinctStacks()).toEqual(['production'])
|
||||
})
|
||||
})
|
||||
// ── createInstance ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('createInstance', () => {
|
||||
it('inserts a new instance', () => {
|
||||
insert({ name: 'traefik', vmid: 100, stack: 'production', state: 'deployed' })
|
||||
const inst = getInstance(100)
|
||||
expect(inst.name).toBe('traefik')
|
||||
expect(inst.stack).toBe('production')
|
||||
expect(inst.state).toBe('deployed')
|
||||
})
|
||||
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
|
||||
|
||||
it('inserts a new instance and sets timestamps', () => {
|
||||
createInstance({ ...base, name: 'traefik', vmid: 100 });
|
||||
const inst = getInstance(100);
|
||||
expect(inst.name).toBe('traefik');
|
||||
expect(inst.created_at).not.toBeNull();
|
||||
expect(inst.updated_at).not.toBeNull();
|
||||
});
|
||||
|
||||
it('stores service flags correctly', () => {
|
||||
insert({ vmid: 1, atlas: 1, argus: 0, tailscale: 1, hardware_acceleration: 1 })
|
||||
const inst = getInstance(1)
|
||||
expect(inst.atlas).toBe(1)
|
||||
expect(inst.argus).toBe(0)
|
||||
expect(inst.tailscale).toBe(1)
|
||||
expect(inst.hardware_acceleration).toBe(1)
|
||||
})
|
||||
createInstance({ ...base, name: 'plex', vmid: 1, atlas: 1, tailscale: 1, hardware_acceleration: 1 });
|
||||
const inst = getInstance(1);
|
||||
expect(inst.atlas).toBe(1);
|
||||
expect(inst.argus).toBe(0);
|
||||
expect(inst.tailscale).toBe(1);
|
||||
expect(inst.hardware_acceleration).toBe(1);
|
||||
});
|
||||
|
||||
it('rejects duplicate vmid', () => {
|
||||
insert({ vmid: 100 })
|
||||
expect(() => insert({ name: 'other', vmid: 100 })).toThrow()
|
||||
})
|
||||
createInstance({ ...base, name: 'a', vmid: 100 });
|
||||
expect(() => createInstance({ ...base, name: 'b', vmid: 100 })).toThrow();
|
||||
});
|
||||
|
||||
it('sets createdAt and updatedAt on insert', () => {
|
||||
insert({ vmid: 1 })
|
||||
const inst = getInstance(1)
|
||||
expect(inst.createdAt).not.toBeNull()
|
||||
expect(inst.updatedAt).not.toBeNull()
|
||||
})
|
||||
})
|
||||
it('rejects invalid state', () => {
|
||||
expect(() => createInstance({ ...base, name: 'a', vmid: 1, state: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid stack', () => {
|
||||
expect(() => createInstance({ ...base, name: 'a', vmid: 1, stack: 'invalid' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateInstance ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('updateInstance', () => {
|
||||
it('updates fields on an existing instance', () => {
|
||||
insert({ name: 'old-name', vmid: 100, state: 'testing', stack: 'development' })
|
||||
const before = getInstance(100)
|
||||
db.run(
|
||||
`UPDATE instances SET name=?, state=?, stack=?, updatedAt=datetime('now') WHERE id=?`,
|
||||
['new-name', 'deployed', 'production', before.id]
|
||||
)
|
||||
const after = getInstance(100)
|
||||
expect(after.name).toBe('new-name')
|
||||
expect(after.state).toBe('deployed')
|
||||
expect(after.stack).toBe('production')
|
||||
})
|
||||
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
|
||||
|
||||
it('updates updatedAt on write', () => {
|
||||
insert({ vmid: 1 })
|
||||
const before = getInstance(1)
|
||||
db.run(`UPDATE instances SET name=?, updatedAt=datetime('now') WHERE id=?`, ['updated', before.id])
|
||||
const after = getInstance(1)
|
||||
expect(after.updatedAt).not.toBeNull()
|
||||
})
|
||||
})
|
||||
it('updates fields and refreshes updated_at', () => {
|
||||
createInstance({ ...base, name: 'old', vmid: 100 });
|
||||
updateInstance(100, { ...base, name: 'new', vmid: 100, state: 'degraded' });
|
||||
const inst = getInstance(100);
|
||||
expect(inst.name).toBe('new');
|
||||
expect(inst.state).toBe('degraded');
|
||||
});
|
||||
|
||||
it('can change vmid', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 100 });
|
||||
updateInstance(100, { ...base, name: 'a', vmid: 200 });
|
||||
expect(getInstance(100)).toBeNull();
|
||||
expect(getInstance(200)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── deleteInstance ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('deleteInstance', () => {
|
||||
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
|
||||
|
||||
it('removes the instance', () => {
|
||||
insert({ vmid: 1 })
|
||||
const inst = getInstance(1)
|
||||
db.run('DELETE FROM instances WHERE id = ?', [inst.id])
|
||||
expect(getInstance(1)).toBeNull()
|
||||
})
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
deleteInstance(1);
|
||||
expect(getInstance(1)).toBeNull();
|
||||
});
|
||||
|
||||
it('only removes the targeted instance', () => {
|
||||
insert({ name: 'a', vmid: 1 })
|
||||
insert({ name: 'b', vmid: 2 })
|
||||
const inst = getInstance(1)
|
||||
db.run('DELETE FROM instances WHERE id = ?', [inst.id])
|
||||
expect(getInstance(1)).toBeNull()
|
||||
expect(getInstance(2)).not.toBeNull()
|
||||
})
|
||||
})
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
createInstance({ ...base, name: 'b', vmid: 2 });
|
||||
deleteInstance(1);
|
||||
expect(getInstance(1)).toBeNull();
|
||||
expect(getInstance(2)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
// ── esc() ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user