mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 23:32:19 +00:00
fix(fleet): harden remote node updates with admin enforcement, expiry fix, and diagnostics (#542)
- Fix completed-entry auto-expiry using resolvedAt instead of startedAt - Add admin role enforcement to update trigger and update-all endpoints - Add missing error field in rejected-promise fallback for update-status - Fix rejected promises in update-all losing node names - Harden frontend recheck button with try/catch/finally error handling - Align frontend isValidVersion with stricter regex validation - Add diagnostic logging gated behind developer_mode - Extract resolveTracker helper to centralize terminal state transitions - Add 15 new fleet test cases covering auth, tier gating, input validation, and admin roles - Document admin requirement and troubleshooting in fleet-view docs
This commit is contained in:
@@ -60,6 +60,21 @@ describe('Fleet endpoints require authentication', () => {
|
||||
const res = await request(app).post('/api/fleet/nodes/1/update');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('POST /api/fleet/update-all returns 401 without auth', async () => {
|
||||
const res = await request(app).post('/api/fleet/update-all');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/nodes/1/update-status returns 401 without auth', async () => {
|
||||
const res = await request(app).delete('/api/fleet/nodes/1/update-status');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/update-status returns 401 without auth', async () => {
|
||||
const res = await request(app).delete('/api/fleet/update-status');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Input Validation ───
|
||||
@@ -159,6 +174,125 @@ describe('Fleet tier gating', () => {
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
|
||||
it('POST /api/fleet/nodes/1/update returns 403 on free tier', async () => {
|
||||
mockTier('community');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/nodes/1/update')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
|
||||
it('POST /api/fleet/update-all returns 403 on free tier', async () => {
|
||||
mockTier('community');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/update-all')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/nodes/1/update-status returns 403 on free tier', async () => {
|
||||
mockTier('community');
|
||||
const res = await request(app)
|
||||
.delete('/api/fleet/nodes/1/update-status')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/update-status returns 403 on free tier', async () => {
|
||||
mockTier('community');
|
||||
const res = await request(app)
|
||||
.delete('/api/fleet/update-status')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update Endpoint Input Validation ───
|
||||
|
||||
describe('Fleet update input validation', () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('POST /api/fleet/nodes/abc/update returns 400 for NaN nodeId', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/nodes/abc/update')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toMatch(/invalid node id/i);
|
||||
});
|
||||
|
||||
it('POST /api/fleet/nodes/99999/update returns 404 for missing node', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/nodes/99999/update')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toMatch(/not found/i);
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/nodes/abc/update-status returns 400 for NaN nodeId', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.delete('/api/fleet/nodes/abc/update-status')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toMatch(/invalid node id/i);
|
||||
});
|
||||
|
||||
it('DELETE /api/fleet/nodes/99999/update-status returns 404 for missing node', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.delete('/api/fleet/nodes/99999/update-status')
|
||||
.set('Authorization', authHeader);
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toMatch(/not found/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Admin Role Enforcement ───
|
||||
|
||||
describe('Fleet update admin enforcement', () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
let viewerHeader: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a viewer user and generate a token for them
|
||||
const { DatabaseService } = await import('../services/DatabaseService');
|
||||
const db = DatabaseService.getInstance();
|
||||
const bcrypt = await import('bcrypt');
|
||||
const viewerHash = await bcrypt.hash('viewerpass', 1);
|
||||
try {
|
||||
db.addUser({ username: 'testviewer', password_hash: viewerHash, role: 'viewer' });
|
||||
} catch {
|
||||
// User may already exist from a prior run
|
||||
}
|
||||
const viewerToken = jwt.sign({ username: 'testviewer', role: 'viewer' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
||||
viewerHeader = `Bearer ${viewerToken}`;
|
||||
});
|
||||
|
||||
it('POST /api/fleet/nodes/1/update returns 403 for viewer', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/nodes/1/update')
|
||||
.set('Authorization', viewerHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('ADMIN_REQUIRED');
|
||||
});
|
||||
|
||||
it('POST /api/fleet/update-all returns 403 for viewer', async () => {
|
||||
mockTier('paid');
|
||||
const res = await request(app)
|
||||
.post('/api/fleet/update-all')
|
||||
.set('Authorization', viewerHeader);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('ADMIN_REQUIRED');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Snapshot CRUD ───
|
||||
|
||||
Reference in New Issue
Block a user