mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-16 21:48:45 +00:00
feat(auto-update): show pending image updates fleet-wide on the Auto-Updates page (#770)
Group readiness cards by node so updates pending on every reachable node are visible without having to switch the active node. Apply now targets the owning node directly, and Recheck fans out to every reachable node in parallel; per-node cooldowns are surfaced in the toast. Adds POST /image-updates/fleet/refresh and invalidates the fleet aggregation cache after auto-update execute so the next read reflects the new state immediately. A small banner appears under the hero when some online nodes did not respond within the request timeout.
This commit is contained in:
@@ -94,6 +94,54 @@ describe('GET /api/image-updates/fleet', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/image-updates/fleet/refresh', () => {
|
||||
it('rejects unauthenticated requests with 401', async () => {
|
||||
const res = await request(app).post('/api/image-updates/fleet/refresh');
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('rejects non-admin users with 403', async () => {
|
||||
const res = await request(app).post('/api/image-updates/fleet/refresh').set('Cookie', viewerCookie);
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it('returns triggered/rateLimited/failed arrays for admin caller', async () => {
|
||||
const res = await request(app).post('/api/image-updates/fleet/refresh').set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body.triggered)).toBe(true);
|
||||
expect(Array.isArray(res.body.rateLimited)).toBe(true);
|
||||
expect(Array.isArray(res.body.failed)).toBe(true);
|
||||
// The single local node should land in either triggered (first hit) or
|
||||
// rateLimited (cooldown from a prior /refresh in this suite).
|
||||
const localNodeBuckets = res.body.triggered.length + res.body.rateLimited.length;
|
||||
expect(localNodeBuckets).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('invalidates the fleet aggregation cache', async () => {
|
||||
const { CacheService } = await import('../services/CacheService');
|
||||
// Prime the cache by hitting the GET endpoint, then refresh, then
|
||||
// confirm the cache key was wiped.
|
||||
await request(app).get('/api/image-updates/fleet').set('Cookie', adminCookie);
|
||||
expect(CacheService.getInstance().get('fleet-updates')).toBeDefined();
|
||||
await request(app).post('/api/image-updates/fleet/refresh').set('Cookie', adminCookie);
|
||||
expect(CacheService.getInstance().get('fleet-updates')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('downgrades to 402-style upgrade response when license is community', async () => {
|
||||
const { LicenseService } = await import('../services/LicenseService');
|
||||
const tierSpy = vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
|
||||
try {
|
||||
const res = await request(app).post('/api/image-updates/fleet/refresh').set('Cookie', adminCookie);
|
||||
// requirePaid responds with a non-2xx status carrying an upgrade payload.
|
||||
expect(res.status).not.toBe(200);
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
} finally {
|
||||
tierSpy.mockRestore();
|
||||
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/auto-update/execute', () => {
|
||||
it('rejects unauthenticated requests with 401', async () => {
|
||||
const res = await request(app).post('/api/auto-update/execute').send({ target: '*' });
|
||||
|
||||
Reference in New Issue
Block a user