mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
feat(schedules): make every scheduled action available on Skipper (#1141)
Collapse the Admiral carveout that restricted restart, prune, auto_backup, auto_stop, auto_down, and auto_start schedules to the Admiral variant. Scheduled Operations stays at Skipper+ (paid). The action picker now lists every supported operation for any paid admin, and the scheduler runner executes every action on either variant.
This commit is contained in:
@@ -14,13 +14,14 @@ let DatabaseService: typeof import('../services/DatabaseService').DatabaseServic
|
||||
let adminCookie: string;
|
||||
let viewerCookie: string;
|
||||
let variantSpy: ReturnType<typeof vi.spyOn>;
|
||||
let tierSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = await setupTestDb();
|
||||
({ DatabaseService } = await import('../services/DatabaseService'));
|
||||
|
||||
const { LicenseService } = await import('../services/LicenseService');
|
||||
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
||||
tierSpy = vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
||||
variantSpy = vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('admiral');
|
||||
vi.spyOn(LicenseService.getInstance(), 'getSeatLimits').mockReturnValue({ maxAdmins: null, maxViewers: null });
|
||||
|
||||
@@ -90,7 +91,7 @@ describe('GET /api/scheduled-tasks', () => {
|
||||
expect(Array.isArray(res.body[0].next_runs)).toBe(true);
|
||||
});
|
||||
|
||||
it('shows scan and snapshot tasks to Skipper users', async () => {
|
||||
it('shows every action to Skipper users', async () => {
|
||||
const db = DatabaseService.getInstance();
|
||||
const now = Date.now();
|
||||
db.createScheduledTask({
|
||||
@@ -154,7 +155,7 @@ describe('GET /api/scheduled-tasks', () => {
|
||||
|
||||
const res = await request(app).get('/api/scheduled-tasks').set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.map((t: { action: string }) => t.action).sort()).toEqual(['scan', 'snapshot']);
|
||||
expect(res.body.map((t: { action: string }) => t.action).sort()).toEqual(['prune', 'scan', 'snapshot']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -441,16 +442,35 @@ describe('POST /api/scheduled-tasks - Skipper tier gating', () => {
|
||||
expect(res.body.action).toBe('snapshot');
|
||||
});
|
||||
|
||||
for (const action of ['restart', 'prune', 'auto_backup', 'auto_stop', 'auto_down', 'auto_start']) {
|
||||
it(`rejects Skipper admins from creating ${action} tasks with 403`, async () => {
|
||||
for (const action of ['restart', 'auto_backup', 'auto_stop', 'auto_down', 'auto_start']) {
|
||||
it(`allows Skipper admins to create ${action} tasks`, async () => {
|
||||
const res = await request(app).post('/api/scheduled-tasks').set('Cookie', adminCookie).send({
|
||||
name: `skipper-${action}`, target_type: 'stack', target_id: 'my-stack', node_id: 1,
|
||||
action, cron_expression: '0 3 * * *', enabled: true,
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('ADMIRAL_REQUIRED');
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.action).toBe(action);
|
||||
});
|
||||
}
|
||||
|
||||
it('allows Skipper admins to create prune tasks', async () => {
|
||||
const res = await request(app).post('/api/scheduled-tasks').set('Cookie', adminCookie).send({
|
||||
name: 'skipper-prune', target_type: 'system', node_id: 1,
|
||||
action: 'prune', cron_expression: '0 4 * * *', enabled: true,
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.action).toBe('prune');
|
||||
});
|
||||
|
||||
it('rejects Community admins from creating any scheduled task with 403', async () => {
|
||||
tierSpy.mockReturnValueOnce('community');
|
||||
const res = await request(app).post('/api/scheduled-tasks').set('Cookie', adminCookie).send({
|
||||
name: 'community-update', target_type: 'stack', target_id: 'my-stack', node_id: 1,
|
||||
action: 'update', cron_expression: '0 3 * * *', enabled: true,
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.code).toBe('PAID_REQUIRED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/scheduled-tasks/:id - delete_after_run', () => {
|
||||
|
||||
@@ -292,15 +292,17 @@ describe('SchedulerService - license gating', () => {
|
||||
expect(mockCreateScheduledTaskRun).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips non-update/scan/snapshot tasks for non-admiral pro', async () => {
|
||||
it('executes restart tasks for non-admiral pro (Skipper)', async () => {
|
||||
mockGetTier.mockReturnValue('paid');
|
||||
mockGetVariant.mockReturnValue('individual');
|
||||
mockGetDueScheduledTasks.mockReturnValue([makeTask({ action: 'restart' })]);
|
||||
mockGetContainersByStack.mockResolvedValue([{ Id: 'c1', Service: 'web' }]);
|
||||
|
||||
const svc = SchedulerService.getInstance();
|
||||
await (svc as any).tick();
|
||||
|
||||
expect(mockCreateScheduledTaskRun).not.toHaveBeenCalled();
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(mockCreateScheduledTaskRun).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows snapshot tasks for non-admiral pro (Skipper)', async () => {
|
||||
@@ -1525,7 +1527,7 @@ describe('SchedulerService - lifecycle actions', () => {
|
||||
expect(mockUpdateScheduledTaskRun).toHaveBeenCalledWith(1, expect.objectContaining({ status: 'failure' }));
|
||||
});
|
||||
|
||||
it('non-admiral paid tier skips lifecycle actions', async () => {
|
||||
it('non-admiral paid tier executes lifecycle actions', async () => {
|
||||
mockGetTier.mockReturnValue('paid');
|
||||
mockGetVariant.mockReturnValue('standard');
|
||||
mockGetScheduledTask.mockReturnValue(makeLifecycleTask('auto_stop'));
|
||||
@@ -1534,7 +1536,8 @@ describe('SchedulerService - lifecycle actions', () => {
|
||||
const svc = SchedulerService.getInstance();
|
||||
await (svc as any).tick();
|
||||
|
||||
expect(mockRunCommand).not.toHaveBeenCalled();
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(mockRunCommand).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user