Show provisioning requesters and reserve Maxis for system actions
This commit is contained in:
@@ -54,6 +54,16 @@ function gate<T>() {
|
||||
}
|
||||
|
||||
describe('initial administrator setup', () => {
|
||||
it('reserves Maxis for system actions regardless of username case', async () => {
|
||||
for (const username of ['Maxis', 'maxis', 'MAXIS', 'mAxIs']) {
|
||||
await expect(auth.bootstrapAdmin({ username, password, token: setupToken }, '192.0.2.1'))
|
||||
.rejects.toMatchObject({ status: 400, message: 'Maxis is reserved for system actions. Choose another username.' });
|
||||
}
|
||||
expect(auth.setupRequired()).toBe(true);
|
||||
expect(auth.listUsers()).toEqual([]);
|
||||
expect(database.db().prepare('SELECT COUNT(*) AS count FROM auth_sessions').get()).toEqual({ count: 0 });
|
||||
});
|
||||
|
||||
it('requires an owner token and never creates an account from missing or wrong credentials', async () => {
|
||||
const input = { username: 'Admin', password, token: setupToken };
|
||||
vi.stubEnv('EC_SETUP_TOKEN', undefined);
|
||||
@@ -136,6 +146,20 @@ describe('password storage and verification', () => {
|
||||
});
|
||||
|
||||
describe('administrator user management', () => {
|
||||
it('rejects the reserved system username for every role while allowing distinct usernames', async () => {
|
||||
const admin = seedUser();
|
||||
for (const role of ['admin', 'operator', 'viewer'] as const) {
|
||||
for (const username of ['Maxis', 'maxis', 'MAXIS', 'mAxIs']) {
|
||||
await expect(auth.createUser(admin.id, { username, role, password }))
|
||||
.rejects.toMatchObject({ status: 400, message: 'Maxis is reserved for system actions. Choose another username.' });
|
||||
}
|
||||
}
|
||||
expect(auth.listUsers()).toHaveLength(1);
|
||||
expect(auth.listAuthAudit()).toEqual([]);
|
||||
await expect(auth.createUser(admin.id, { username: 'maxis-operator', role: 'operator', password }))
|
||||
.resolves.toMatchObject({ username: 'maxis-operator' });
|
||||
});
|
||||
|
||||
it('creates each supported role with a forced initial password change and rejects case-insensitive duplicate names', async () => {
|
||||
const admin = seedUser();
|
||||
for (const role of ['admin', 'operator', 'viewer'] as const) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ProvisioningDetail, ProvisioningProgress } from '../../src/lib/provisioning-detail.js';
|
||||
import { provisioningActions } from '../../src/lib/provisioning.js';
|
||||
import { isProvisioningDetail, isProvisioningEventPage, provisioningGroups, provisioningOutcome, provisioningReturn } from '../../src/lib/ui/provisioning-presentation.js';
|
||||
import { isProvisioningDetail, isProvisioningEventPage, provisioningGroups, provisioningOutcome, provisioningRequester, provisioningReturn } from '../../src/lib/ui/provisioning-presentation.js';
|
||||
|
||||
const at = '2026-09-28T12:00:00Z';
|
||||
function progress(patch: Partial<ProvisioningProgress> = {}): ProvisioningProgress {
|
||||
@@ -19,6 +19,19 @@ function fixture(): ProvisioningDetail {
|
||||
}
|
||||
|
||||
describe('provisioning outcome and evidence presentation', () => {
|
||||
it('shows recorded usernames and preserves unknown manual requesters', () => {
|
||||
expect(provisioningRequester({ triggered_by: 'ui', requested_by: 'alice' })).toBe('alice');
|
||||
expect(provisioningRequester({ triggered_by: 'rebalance:run', requested_by: 'bob' })).toBe('bob');
|
||||
expect(provisioningRequester({ triggered_by: 'ui', requested_by: null })).toBe('Not recorded');
|
||||
expect(provisioningRequester({ triggered_by: 'ui' })).toBe('Not recorded');
|
||||
});
|
||||
|
||||
it.each(['watchdog:incident', 'maintenance:job', 'autoscaling:policy', 'campaign:run', 'beta:75', 'rebalance:run', 'host-drain'])(
|
||||
'shows Maxis for system work from %s', triggered_by => {
|
||||
expect(provisioningRequester({ triggered_by, requested_by: null })).toBe('Maxis');
|
||||
expect(provisioningRequester({ triggered_by })).toBe('Maxis');
|
||||
});
|
||||
|
||||
it('distinguishes a failed creation retry from readiness verification and its linked recovery', () => {
|
||||
const detail = fixture();
|
||||
detail.deployment.state = 'failed';
|
||||
|
||||
@@ -9,17 +9,30 @@ afterEach(async () => { await fixture.cleanup(); });
|
||||
it('persists the manual requester while preserving source filtering and historical records', async () => {
|
||||
const { withRequestUsername } = await import('../../src/lib/server/auth/request-context.js');
|
||||
const { beginDeployment, getDeployment, listDeployments } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const legacy = beginDeployment('legacy', 'restart');
|
||||
const [manual, automatic] = await withRequestUsername('alice', async () => [
|
||||
beginDeployment('manual', 'restart'), beginDeployment('automatic', 'restart', 'watchdog:incident')
|
||||
const [manual, automatic, rebalance] = await withRequestUsername('alice', async () => [
|
||||
beginDeployment('manual', 'restart'), beginDeployment('automatic', 'restart', 'watchdog:incident'),
|
||||
beginDeployment('rebalance', 'restart', 'rebalance:run')
|
||||
]);
|
||||
closeDb();
|
||||
expect(getDeployment(manual)?.deployment).toMatchObject({ triggered_by: 'ui', requested_by: 'alice' });
|
||||
expect(getDeployment(automatic)?.deployment).toMatchObject({ triggered_by: 'watchdog:incident', requested_by: null });
|
||||
expect(getDeployment(rebalance)?.deployment).toMatchObject({ triggered_by: 'rebalance:run', requested_by: 'alice' });
|
||||
expect(getDeployment(legacy)?.deployment).toMatchObject({ triggered_by: 'ui', requested_by: null });
|
||||
expect(listDeployments({ source: 'manual' }).map(row => row.id)).toEqual(expect.arrayContaining([manual, legacy]));
|
||||
expect(listDeployments({ source: 'manual' }).map(row => row.id)).not.toContain(automatic);
|
||||
const { GET } = await import('../../src/routes/api/provisioning/+server.js');
|
||||
for (const view of ['active', 'history']) {
|
||||
if (view === 'history') db().prepare("UPDATE server_deployments SET state='done'").run();
|
||||
const result = await GET({ url: new URL(`http://localhost/api/provisioning?view=${view}`) }).json();
|
||||
expect(result.deployments).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: manual, requested_by: 'alice' }),
|
||||
expect.objectContaining({ id: automatic, requested_by: null }),
|
||||
expect.objectContaining({ id: rebalance, requested_by: 'alice' }),
|
||||
expect.objectContaining({ id: legacy, requested_by: null })
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
it('isolates overlapping users and stops attributing deferred work after the request returns', async () => {
|
||||
|
||||
Reference in New Issue
Block a user