569 lines
19 KiB
TypeScript
569 lines
19 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
import { Repository } from '../db/repository.js';
|
|
import { BrowserSessionRepo } from '../db/browser-session-repo.js';
|
|
import { Scheduler } from '../scheduler.js';
|
|
import { mountScheduledTasksApi } from './scheduled-tasks-api.js';
|
|
import { mkdtempSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
let app: express.Application;
|
|
let repo: Repository;
|
|
let scheduler: Scheduler;
|
|
let tempDir: string;
|
|
|
|
beforeAll(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'agent-sched-api-'));
|
|
repo = new Repository(join(tempDir, 'test.db'));
|
|
scheduler = new Scheduler(repo, join(tempDir, 'workspaces'));
|
|
app = express();
|
|
app.use(express.json());
|
|
mountScheduledTasksApi(app, repo, scheduler);
|
|
});
|
|
|
|
afterAll(() => {
|
|
repo.close();
|
|
try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
|
|
});
|
|
|
|
describe('POST /api/scheduled-tasks with visibility', () => {
|
|
let vTempDir = '';
|
|
let vRepo: Repository;
|
|
let vApp: express.Application;
|
|
let aliceUser: Express.User;
|
|
|
|
beforeEach(() => {
|
|
vTempDir = mkdtempSync(join(tmpdir(), 'sched-vis-api-'));
|
|
vRepo = new Repository(join(vTempDir, 'db.sqlite'));
|
|
const real = vRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
aliceUser = {
|
|
...real,
|
|
orgIds: ['10'],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const vScheduler = new Scheduler(vRepo, join(vTempDir, 'workspaces'));
|
|
vApp = express();
|
|
vApp.use(express.json());
|
|
vApp.use((req, _res, next) => {
|
|
(req as unknown as { user: Express.User }).user = aliceUser;
|
|
next();
|
|
});
|
|
mountScheduledTasksApi(vApp, vRepo, vScheduler);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vRepo.close();
|
|
rmSync(vTempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('creates scheduled task with owner_id set from req.user and visibility=org', async () => {
|
|
const res = await request(vApp).post('/api/scheduled-tasks').send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
minute: 0,
|
|
visibility: 'org',
|
|
visibilityScopeOrgId: '10',
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.task.visibility).toBe('org');
|
|
expect(res.body.task.visibilityScopeOrgId).toBe('10');
|
|
expect(res.body.task.ownerId).toBe(aliceUser.id);
|
|
});
|
|
|
|
it('defaults visibility to private and owner from req.user when not provided', async () => {
|
|
const res = await request(vApp).post('/api/scheduled-tasks').send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 10,
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.task.visibility).toBe('private');
|
|
expect(res.body.task.visibilityScopeOrgId).toBeNull();
|
|
expect(res.body.task.ownerId).toBe(aliceUser.id);
|
|
});
|
|
|
|
it('rejects visibility=org with org not in user orgs', async () => {
|
|
const res = await request(vApp).post('/api/scheduled-tasks').send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
visibility: 'org',
|
|
visibilityScopeOrgId: '99',
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('rejects invalid visibility enum values', async () => {
|
|
const res = await request(vApp).post('/api/scheduled-tasks').send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
visibility: 'bogus',
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/scheduled-tasks in no-auth mode (synthetic local owner)', () => {
|
|
let nTempDir = '';
|
|
let nRepo: Repository;
|
|
let nApp: express.Application;
|
|
|
|
beforeEach(() => {
|
|
nTempDir = mkdtempSync(join(tmpdir(), 'sched-noauth-api-'));
|
|
nRepo = new Repository(join(nTempDir, 'db.sqlite'));
|
|
const nScheduler = new Scheduler(nRepo, join(nTempDir, 'workspaces'));
|
|
nApp = express();
|
|
nApp.use(express.json());
|
|
// No user middleware: no-auth deployment (req.user undefined). authActive:false
|
|
// tells the router to register the scheduled task under the 'local' owner so
|
|
// the jobs the scheduler later spawns inherit it (not NULL).
|
|
mountScheduledTasksApi(nApp, nRepo, nScheduler, { authActive: false });
|
|
});
|
|
|
|
afterEach(() => {
|
|
nRepo.close();
|
|
rmSync(nTempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('registers the scheduled task under owner "local" instead of NULL', async () => {
|
|
const res = await request(nApp).post('/api/scheduled-tasks').send({
|
|
body: 'nightly thing',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
minute: 0,
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.task.ownerId).toBe('local');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/scheduled-tasks', () => {
|
|
it('should create a daily schedule', async () => {
|
|
const res = await request(app)
|
|
.post('/api/scheduled-tasks')
|
|
.send({
|
|
title: 'テスト日次',
|
|
body: 'テストプロンプト',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
minute: 0,
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.task.cronExpression).toBe('0 9 * * *');
|
|
expect(res.body.task.isActive).toBe(true);
|
|
});
|
|
|
|
it('should require body', async () => {
|
|
const res = await request(app).post('/api/scheduled-tasks').send({ scheduleType: 'daily' });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/scheduled-tasks', () => {
|
|
it('should list all scheduled tasks', async () => {
|
|
const res = await request(app).get('/api/scheduled-tasks');
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body.tasks)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /api/scheduled-tasks/:id', () => {
|
|
it('should pause and resume', async () => {
|
|
const createRes = await request(app)
|
|
.post('/api/scheduled-tasks')
|
|
.send({ title: 'pause-test', body: 'test', scheduleType: 'daily', hour: 10 });
|
|
const id = createRes.body.task.id;
|
|
|
|
const pauseRes = await request(app).patch(`/api/scheduled-tasks/${id}`).send({ isActive: false });
|
|
expect(pauseRes.body.task.isActive).toBe(false);
|
|
|
|
const resumeRes = await request(app).patch(`/api/scheduled-tasks/${id}`).send({ isActive: true });
|
|
expect(resumeRes.body.task.isActive).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/scheduled-tasks/:id', () => {
|
|
it('should delete a scheduled task', async () => {
|
|
const createRes = await request(app)
|
|
.post('/api/scheduled-tasks')
|
|
.send({ title: 'delete-test', body: 'test', scheduleType: 'daily', hour: 10 });
|
|
const id = createRes.body.task.id;
|
|
|
|
const delRes = await request(app).delete(`/api/scheduled-tasks/${id}`);
|
|
expect(delRes.status).toBe(200);
|
|
|
|
const getRes = await request(app).get(`/api/scheduled-tasks/${id}`);
|
|
expect(getRes.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('PATCH/DELETE /api/scheduled-tasks/:id owner-or-admin', () => {
|
|
let pTempDir = '';
|
|
let pRepo: Repository;
|
|
|
|
afterEach(() => {
|
|
pRepo.close();
|
|
rmSync(pTempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function buildAppForUser(user: Express.User): express.Application {
|
|
const pScheduler = new Scheduler(pRepo, join(pTempDir, 'workspaces'));
|
|
const pApp = express();
|
|
pApp.use(express.json());
|
|
pApp.use((req, _res, next) => {
|
|
(req as unknown as { user: Express.User }).user = user;
|
|
next();
|
|
});
|
|
mountScheduledTasksApi(pApp, pRepo, pScheduler);
|
|
return pApp;
|
|
}
|
|
|
|
function seedTask(ownerId: string, visibility: 'private' | 'org' | 'public' = 'public') {
|
|
return pRepo.createScheduledTask({
|
|
title: 't',
|
|
body: 'b',
|
|
cronExpression: '0 9 * * *',
|
|
nextRunAt: '2099-01-01 09:00:00',
|
|
ownerId,
|
|
visibility,
|
|
});
|
|
}
|
|
|
|
it('non-owner non-admin gets 404 on PATCH (even when visibility=public)', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const task = await seedTask(alice.id, 'public');
|
|
|
|
const bobUser: Express.User = {
|
|
id: 'bob-id',
|
|
email: 'b@x.com',
|
|
name: 'b',
|
|
avatarUrl: null,
|
|
role: 'user',
|
|
status: 'active',
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const pApp = buildAppForUser(bobUser);
|
|
|
|
const res = await request(pApp)
|
|
.patch(`/api/scheduled-tasks/${task.id}`)
|
|
.send({ title: 'edited' });
|
|
expect(res.status).toBe(404);
|
|
|
|
// Task title not changed
|
|
const after = await pRepo.getScheduledTask(task.id);
|
|
expect(after?.title).toBe('t');
|
|
});
|
|
|
|
it('non-owner non-admin gets 404 on DELETE', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const task = await seedTask(alice.id, 'public');
|
|
|
|
const bobUser: Express.User = {
|
|
id: 'bob-id',
|
|
email: 'b@x.com',
|
|
name: 'b',
|
|
avatarUrl: null,
|
|
role: 'user',
|
|
status: 'active',
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const pApp = buildAppForUser(bobUser);
|
|
|
|
const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
|
|
expect(res.status).toBe(404);
|
|
|
|
const after = await pRepo.getScheduledTask(task.id);
|
|
expect(after).not.toBeNull();
|
|
});
|
|
|
|
it('admin can PATCH any scheduled task', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const task = await seedTask(alice.id, 'private');
|
|
|
|
const adminUser: Express.User = {
|
|
id: 'admin-id',
|
|
email: 'admin@x.com',
|
|
name: 'admin',
|
|
avatarUrl: null,
|
|
role: 'admin',
|
|
status: 'active',
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const pApp = buildAppForUser(adminUser);
|
|
|
|
const res = await request(pApp)
|
|
.patch(`/api/scheduled-tasks/${task.id}`)
|
|
.send({ title: 'edited-by-admin' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.task.title).toBe('edited-by-admin');
|
|
});
|
|
|
|
it('admin can DELETE any scheduled task', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const task = await seedTask(alice.id, 'private');
|
|
|
|
const adminUser: Express.User = {
|
|
id: 'admin-id',
|
|
email: 'admin@x.com',
|
|
name: 'admin',
|
|
avatarUrl: null,
|
|
role: 'admin',
|
|
status: 'active',
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const pApp = buildAppForUser(adminUser);
|
|
|
|
const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.ok).toBe(true);
|
|
|
|
const after = await pRepo.getScheduledTask(task.id);
|
|
expect(after).toBeNull();
|
|
});
|
|
|
|
it('owner can PATCH own scheduled task', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const aliceUser: Express.User = {
|
|
...alice,
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const task = await seedTask(alice.id, 'private');
|
|
|
|
const pApp = buildAppForUser(aliceUser);
|
|
const res = await request(pApp)
|
|
.patch(`/api/scheduled-tasks/${task.id}`)
|
|
.send({ title: 'edited-by-owner' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.task.title).toBe('edited-by-owner');
|
|
});
|
|
|
|
it('owner can DELETE own scheduled task', async () => {
|
|
pTempDir = mkdtempSync(join(tmpdir(), 'sched-perm-'));
|
|
pRepo = new Repository(join(pTempDir, 'db.sqlite'));
|
|
|
|
const alice = pRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const aliceUser: Express.User = {
|
|
...alice,
|
|
orgIds: [],
|
|
defaultVisibility: 'private',
|
|
defaultVisibilityOrgId: null,
|
|
};
|
|
const task = await seedTask(alice.id, 'private');
|
|
|
|
const pApp = buildAppForUser(aliceUser);
|
|
const res = await request(pApp).delete(`/api/scheduled-tasks/${task.id}`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/scheduled-tasks visibility filter', () => {
|
|
let lTempDir = '';
|
|
let lRepo: Repository;
|
|
|
|
afterEach(() => {
|
|
lRepo.close();
|
|
rmSync(lTempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function buildAppForUser(user: Express.User): express.Application {
|
|
const lScheduler = new Scheduler(lRepo, join(lTempDir, 'workspaces'));
|
|
const lApp = express();
|
|
lApp.use(express.json());
|
|
lApp.use((req, _res, next) => {
|
|
(req as unknown as { user: Express.User }).user = user;
|
|
next();
|
|
});
|
|
mountScheduledTasksApi(lApp, lRepo, lScheduler);
|
|
return lApp;
|
|
}
|
|
|
|
it('non-owner does not see private scheduled tasks in list', async () => {
|
|
lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
|
|
lRepo = new Repository(join(lTempDir, 'db.sqlite'));
|
|
const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
await lRepo.createScheduledTask({
|
|
title: 'alice-private', body: 'b',
|
|
cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
|
|
ownerId: alice.id, visibility: 'private',
|
|
});
|
|
|
|
const bobUser: Express.User = {
|
|
id: 'bob-id', email: 'b@x.com', name: 'b', avatarUrl: null,
|
|
role: 'user', status: 'active', orgIds: [],
|
|
defaultVisibility: 'private', defaultVisibilityOrgId: null,
|
|
};
|
|
const res = await request(buildAppForUser(bobUser)).get('/api/scheduled-tasks');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.tasks.map((t: { title: string }) => t.title)).not.toContain('alice-private');
|
|
});
|
|
|
|
it('owner sees own private scheduled tasks', async () => {
|
|
lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
|
|
lRepo = new Repository(join(lTempDir, 'db.sqlite'));
|
|
const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
const aliceUser: Express.User = {
|
|
...alice, orgIds: [],
|
|
defaultVisibility: 'private', defaultVisibilityOrgId: null,
|
|
};
|
|
await lRepo.createScheduledTask({
|
|
title: 'alice-private', body: 'b',
|
|
cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
|
|
ownerId: alice.id, visibility: 'private',
|
|
});
|
|
|
|
const res = await request(buildAppForUser(aliceUser)).get('/api/scheduled-tasks');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.tasks.map((t: { title: string }) => t.title)).toContain('alice-private');
|
|
});
|
|
|
|
it('admin sees all scheduled tasks regardless of visibility', async () => {
|
|
lTempDir = mkdtempSync(join(tmpdir(), 'sched-list-'));
|
|
lRepo = new Repository(join(lTempDir, 'db.sqlite'));
|
|
const alice = lRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
await lRepo.createScheduledTask({
|
|
title: 'alice-private', body: 'b',
|
|
cronExpression: '0 9 * * *', nextRunAt: '2099-01-01 09:00:00',
|
|
ownerId: alice.id, visibility: 'private',
|
|
});
|
|
|
|
const adminUser: Express.User = {
|
|
id: 'admin-id', email: 'admin@x.com', name: 'admin', avatarUrl: null,
|
|
role: 'admin', status: 'active', orgIds: [],
|
|
defaultVisibility: 'private', defaultVisibilityOrgId: null,
|
|
};
|
|
const res = await request(buildAppForUser(adminUser)).get('/api/scheduled-tasks');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.tasks.map((t: { title: string }) => t.title)).toContain('alice-private');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/scheduled-tasks browserSessionProfileId owner check', () => {
|
|
let bTempDir = '';
|
|
let bRepo: Repository;
|
|
let bSessRepo: BrowserSessionRepo;
|
|
let alice: { id: string };
|
|
let bob: { id: string };
|
|
let aliceProfileId: number;
|
|
let bobProfileId: number;
|
|
|
|
beforeEach(() => {
|
|
bTempDir = mkdtempSync(join(tmpdir(), 'sched-bsp-'));
|
|
bRepo = new Repository(join(bTempDir, 'db.sqlite'));
|
|
bSessRepo = new BrowserSessionRepo(bRepo.getDb());
|
|
alice = bRepo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
|
|
bob = bRepo.createUser({ email: 'b@x.com', name: 'b', role: 'user', status: 'active' });
|
|
aliceProfileId = bSessRepo.createProfile({
|
|
ownerId: alice.id,
|
|
label: 'alice-twitter',
|
|
startUrl: 'https://twitter.com/home',
|
|
matchPatterns: ['https://twitter.com/**'],
|
|
storageOrigins: ['https://twitter.com'],
|
|
loggedInSelector: null,
|
|
loginUrlPatterns: [],
|
|
});
|
|
bobProfileId = bSessRepo.createProfile({
|
|
ownerId: bob.id,
|
|
label: 'bob-twitter',
|
|
startUrl: 'https://twitter.com/home',
|
|
matchPatterns: ['https://twitter.com/**'],
|
|
storageOrigins: ['https://twitter.com'],
|
|
loggedInSelector: null,
|
|
loginUrlPatterns: [],
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
bRepo.close();
|
|
rmSync(bTempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function buildAppForUser(user: Express.User): express.Application {
|
|
const bScheduler = new Scheduler(bRepo, join(bTempDir, 'workspaces'));
|
|
const bApp = express();
|
|
bApp.use(express.json());
|
|
bApp.use((req, _res, next) => {
|
|
(req as unknown as { user: Express.User }).user = user;
|
|
next();
|
|
});
|
|
mountScheduledTasksApi(bApp, bRepo, bScheduler, { sessRepo: bSessRepo });
|
|
return bApp;
|
|
}
|
|
|
|
function asUser(u: { id: string }, email: string): Express.User {
|
|
return {
|
|
id: u.id, email, name: 'x', avatarUrl: null,
|
|
role: 'user', status: 'active', orgIds: [],
|
|
defaultVisibility: 'private', defaultVisibilityOrgId: null,
|
|
};
|
|
}
|
|
|
|
it('accepts a valid profile owned by the requesting user (201)', async () => {
|
|
const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
|
|
.post('/api/scheduled-tasks')
|
|
.send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
browserSessionProfileId: aliceProfileId,
|
|
});
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.task.browserSessionProfileId).toBe(aliceProfileId);
|
|
});
|
|
|
|
it('rejects a profile owned by a different user (400)', async () => {
|
|
const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
|
|
.post('/api/scheduled-tasks')
|
|
.send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
browserSessionProfileId: bobProfileId,
|
|
});
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/not owned by you|not found/i);
|
|
});
|
|
|
|
it('rejects a positive integer that does not match any profile (400)', async () => {
|
|
const res = await request(buildAppForUser(asUser(alice, 'a@x.com')))
|
|
.post('/api/scheduled-tasks')
|
|
.send({
|
|
body: 'hello',
|
|
scheduleType: 'daily',
|
|
hour: 9,
|
|
browserSessionProfileId: 999999,
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|