Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6651,6 +6651,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
'409':
description: Only pending requests can be modified
delete:
summary: Delete request
description: Removes a request. If the user has the `MANAGE_REQUESTS` permission, any request can be removed. Otherwise, only pending requests can be removed.
Expand Down Expand Up @@ -6691,6 +6693,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
'409':
description: Only failed requests can be retried
/request/{requestId}/{status}:
post:
summary: Update a request's status
Expand Down Expand Up @@ -6722,6 +6726,10 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
'400':
description: Status must be approve or decline
'409':
description: Only pending requests can be approved or declined
/movie/{movieId}:
get:
summary: Get movie details
Expand Down
81 changes: 81 additions & 0 deletions server/routes/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,27 @@ describe('PUT /request/:requestId (movie)', () => {
assert.strictEqual(saved.profileId, 7);
assert.strictEqual(saved.rootFolder, '/updated/movies');
});

it('refuses to modify a request that is no longer pending', async () => {
const requestRepo = getRepository(MediaRequest);
const mediaRequest = await seedRequest(MediaRequestStatus.APPROVED);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.MOVIE,
serverId: 3,
rootFolder: '/updated/movies',
});

assert.strictEqual(res.status, 409);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
fallenbagel marked this conversation as resolved.
assert.match(res.body.message, /pending/i);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.strictEqual(saved.serverId, null);
assert.strictEqual(saved.rootFolder, null);
});
});

describe('POST /request/:requestId/:status', () => {
Expand Down Expand Up @@ -241,6 +262,52 @@ describe('POST /request/:requestId/:status', () => {
assert.ok(persisted.updatedAt > pending.updatedAt);
});
}

it('rejects a status the route does not define', async () => {
const repo = getRepository(MediaRequest);
const pending = await seedRequest();
const admin = await loginAs('admin@seerr.dev', 'test1234');

const res = await admin.post(`/request/${pending.id}/frobnicate`);

assert.strictEqual(res.status, 400);
assert.match(res.body.message, /approve/i);

const persisted = await repo.findOneOrFail({
where: { id: pending.id },
relations: { modifiedBy: true },
});
assert.strictEqual(persisted.status, MediaRequestStatus.PENDING);
assert.ok(!persisted.modifiedBy);
});

Comment thread
fallenbagel marked this conversation as resolved.
it('refuses to act on a request that is no longer pending', async () => {
const repo = getRepository(MediaRequest);
const approved = await seedRequest(MediaRequestStatus.APPROVED);
const admin = await loginAs('admin@seerr.dev', 'test1234');

const res = await admin.post(`/request/${approved.id}/decline`);

assert.strictEqual(res.status, 409);
assert.match(res.body.message, /pending/i);

const persisted = await repo.findOneOrFail({ where: { id: approved.id } });
assert.strictEqual(persisted.status, MediaRequestStatus.APPROVED);
});

it('rejects the removed pending verb even on a non-pending request', async () => {
const repo = getRepository(MediaRequest);
const declined = await seedRequest(MediaRequestStatus.DECLINED);
const admin = await loginAs('admin@seerr.dev', 'test1234');

const res = await admin.post(`/request/${declined.id}/pending`);

assert.strictEqual(res.status, 400);
assert.match(res.body.message, /approve/i);

const persisted = await repo.findOneOrFail({ where: { id: declined.id } });
assert.strictEqual(persisted.status, MediaRequestStatus.DECLINED);
});
});

describe('POST /request/:requestId/retry', () => {
Expand All @@ -264,6 +331,20 @@ describe('POST /request/:requestId/retry', () => {
assert.strictEqual(persisted.modifiedBy?.email, 'admin@seerr.dev');
assert.ok(persisted.updatedAt > failed.updatedAt);
});

it('refuses to retry a request that has not failed', async () => {
const repo = getRepository(MediaRequest);
const pending = await seedRequest();
const admin = await loginAs('admin@seerr.dev', 'test1234');

const res = await admin.post(`/request/${pending.id}/retry`);

assert.strictEqual(res.status, 409);
assert.match(res.body.message, /failed/i);

const persisted = await repo.findOneOrFail({ where: { id: pending.id } });
assert.strictEqual(persisted.status, MediaRequestStatus.PENDING);
});
});

describe('DELETE /request/:requestId, deleted media status restoration', () => {
Expand Down
31 changes: 27 additions & 4 deletions server/routes/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,13 @@ requestRoutes.put<{ requestId: string }>(
});
}

if (request.status !== MediaRequestStatus.PENDING) {
return next({
status: 409,
message: 'Only pending requests can be modified.',
});
}

let requestUser = request.requestedBy;

if (
Expand Down Expand Up @@ -644,6 +651,13 @@ requestRoutes.post<{
relations: { requestedBy: true, modifiedBy: true },
});

if (request.status !== MediaRequestStatus.FAILED) {
return next({
status: 409,
message: 'Only failed requests can be retried.',
});
}

// this also triggers updating the parent media's status & sending to *arr
request.status = MediaRequestStatus.APPROVED;
request.modifiedBy = req.user;
Expand All @@ -662,7 +676,7 @@ requestRoutes.post<{

requestRoutes.post<{
requestId: string;
status: 'pending' | 'approve' | 'decline';
status: 'approve' | 'decline';
}>(
'/:requestId/:status',
isAuthenticated(Permission.MANAGE_REQUESTS),
Expand All @@ -678,15 +692,24 @@ requestRoutes.post<{
let newStatus: MediaRequestStatus;

switch (req.params.status) {
case 'pending':
newStatus = MediaRequestStatus.PENDING;
break;
case 'approve':
newStatus = MediaRequestStatus.APPROVED;
break;
case 'decline':
newStatus = MediaRequestStatus.DECLINED;
break;
default:
return next({
status: 400,
message: 'Status must be approve or decline.',
});
}

if (request.status !== MediaRequestStatus.PENDING) {
return next({
status: 409,
message: 'Only pending requests can be approved or declined.',
});
}

request.status = newStatus;
Expand Down
Loading