diff --git a/srv/channel-submission-service.js b/srv/channel-submission-service.js index 32adf80de..552e92d67 100644 --- a/srv/channel-submission-service.js +++ b/srv/channel-submission-service.js @@ -4,6 +4,11 @@ export default class ChannelSubmissionService extends cds.ApplicationService { async init() { // Stamp server-controlled fields; never trust client-sent status / reviewer / submitter. this.before('CREATE', 'Submissions', (req) => { + // EDIT / REMOVE act on an existing channel — reject at submit time (400) instead of + // letting the row sit PENDING only to fail at approve. ADD carries no target. + if ((req.data.kind === 'EDIT' || req.data.kind === 'REMOVE') && !req.data.targetChannel_ID) { + return req.reject(400, `A target channel is required for ${req.data.kind} submissions.`, 'targetChannel_ID'); + } req.data.submitterId = req.user.id; req.data.status = 'PENDING'; req.data.reviewerId = null; diff --git a/test/channel-submission-service.test.js b/test/channel-submission-service.test.js index 025c634b3..074c63230 100644 --- a/test/channel-submission-service.test.js +++ b/test/channel-submission-service.test.js @@ -37,9 +37,23 @@ describe('ChannelSubmissionService', () => { expect(row.reviewNote).toBeNull(); }); - test('service is insert-only — READ is not allowed', async () => { + test('service is insert-only — READ is rejected 405 Method Not Allowed', async () => { + // CAP 10 answers a READ on an @insertonly entity with 405 (not a generic 4xx/5xx). + // Asserting the exact code proves the entity is insert-only rather than merely erroring. await expect( project.get('/channel-submissions/Submissions', authUser), - ).rejects.toMatchObject({ response: { status: expect.any(Number) } }); + ).rejects.toMatchObject({ response: { status: 405 } }); + }); + + test('EDIT without a target channel is rejected at submit time (400)', async () => { + await expect( + project.post('/channel-submissions/Submissions', { kind: 'EDIT', proposed: '{}' }, authUser), + ).rejects.toMatchObject({ response: { status: 400 } }); + }); + + test('REMOVE without a target channel is rejected at submit time (400)', async () => { + await expect( + project.post('/channel-submissions/Submissions', { kind: 'REMOVE', proposed: '{}' }, authUser), + ).rejects.toMatchObject({ response: { status: 400 } }); }); });