Skip to content
Open
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
270 changes: 239 additions & 31 deletions server/routes/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,45 +214,70 @@ describe('PUT /request/:requestId (movie)', () => {
});
});

describe('PUT /request/:requestId (tv)', () => {
it('does not add a season held by another request', async () => {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);
async function seedUser(
email: string,
quotas: {
movieQuotaLimit?: number;
tvQuotaLimit?: number;
tvQuotaDays?: number;
} = {}
) {
const userRepo = getRepository(User);
const user = await userRepo.findOneOrFail({ where: { email } });
Object.assign(user, quotas);

const owner = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});
const otherUser = await userRepo.findOneOrFail({
where: { email: 'friend@seerr.dev' },
});
return userRepo.save(user);
}

const media = await mediaRepo.save(
async function seedTvMedia(tmdbId: number) {
const mediaRepo = getRepository(Media);

return (
(await mediaRepo.findOne({
where: { tmdbId, mediaType: MediaType.TV },
})) ??
(await mediaRepo.save(
new Media({
mediaType: MediaType.TV,
tmdbId: 67890,
tmdbId,
status: MediaStatus.PENDING,
status4k: MediaStatus.UNKNOWN,
})
);
))
);
}

async function seedTvRequest(
requestedBy: User,
seasons: number[],
{ tmdbId = 67890, ignoreQuota = false, createdAt = new Date() } = {}
) {
return getRepository(MediaRequest).save(
new MediaRequest({
type: MediaType.TV,
status: MediaRequestStatus.PENDING,
media: await seedTvMedia(tmdbId),
requestedBy,
is4k: false,
ignoreQuota,
createdAt,
seasons: seasons.map(
(seasonNumber) =>
new SeasonRequest({
seasonNumber,
status: MediaRequestStatus.PENDING,
})
),
})
);
}

const seedTvRequest = (requestedBy: User, seasons: number[]) =>
requestRepo.save(
new MediaRequest({
type: MediaType.TV,
status: MediaRequestStatus.PENDING,
media,
requestedBy,
is4k: false,
seasons: seasons.map(
(seasonNumber) =>
new SeasonRequest({
seasonNumber,
status: MediaRequestStatus.PENDING,
})
),
})
);
describe('PUT /request/:requestId (tv)', () => {
it('does not add a season held by another request', async () => {
const requestRepo = getRepository(MediaRequest);

const owner = await seedUser('admin@seerr.dev');
const otherUser = await seedUser('friend@seerr.dev');

const mediaRequest = await seedTvRequest(owner, [1, 2]);
const otherRequest = await seedTvRequest(otherUser, [3]);
Expand Down Expand Up @@ -283,6 +308,189 @@ describe('PUT /request/:requestId (tv)', () => {
});
});

describe('PUT /request/:requestId (quota)', () => {
it('rejects adding seasons beyond the season limit', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('friend@seerr.dev', { tvQuotaLimit: 2 });
const mediaRequest = await seedTvRequest(owner, [1, 2]);

const agent = await loginAs('friend@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [1, 2, 3],
});

assert.strictEqual(res.status, 403);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.deepStrictEqual(
saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b),
[1, 2]
);
});

it('rejects adding seasons to a request older than the quota window', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('friend@seerr.dev', {
tvQuotaLimit: 2,
tvQuotaDays: 7,
});
const mediaRequest = await seedTvRequest(owner, [1, 2], {
createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
});

const agent = await loginAs('friend@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [1, 2, 3],
});

assert.strictEqual(res.status, 403);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.deepStrictEqual(
saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b),
[1, 2]
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it('allows swapping seasons at the season limit', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('friend@seerr.dev', { tvQuotaLimit: 2 });
const mediaRequest = await seedTvRequest(owner, [1, 2]);

const agent = await loginAs('friend@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [3, 4],
});

assert.strictEqual(res.status, 200);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.deepStrictEqual(
saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b),
[3, 4]
);
});

it('rejects reassignment to a user without room for the existing seasons', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('admin@seerr.dev');
const target = await seedUser('friend@seerr.dev', { tvQuotaLimit: 1 });
const mediaRequest = await seedTvRequest(owner, [1, 2]);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [1, 2],
userId: target.id,
});

assert.strictEqual(res.status, 403);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.strictEqual(saved.requestedBy.id, owner.id);
});

it('rejects reassignment of a movie request to a user at their limit', async () => {
const requestRepo = getRepository(MediaRequest);
const mediaRepo = getRepository(Media);

const owner = await seedUser('admin@seerr.dev');
const target = await seedUser('friend@seerr.dev', { movieQuotaLimit: 1 });

// Uses up the target's single movie request
await seedRequest();

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 55555,
status: MediaStatus.PENDING,
status4k: MediaStatus.UNKNOWN,
})
);
const mediaRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.PENDING,
media,
requestedBy: owner,
is4k: false,
})
);

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

assert.strictEqual(res.status, 403);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.strictEqual(saved.requestedBy.id, owner.id);
});

it('allows reassignment to a user who bypasses quotas', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('friend@seerr.dev', { tvQuotaLimit: 1 });
const target = await seedUser('admin@seerr.dev');
const mediaRequest = await seedTvRequest(owner, [1, 2]);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [1, 2],
userId: target.id,
});

assert.strictEqual(res.status, 200);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.strictEqual(saved.requestedBy.id, target.id);
});

it('allows an edit that exceeds the limit when the request ignores quota', async () => {
const requestRepo = getRepository(MediaRequest);
const owner = await seedUser('friend@seerr.dev', { tvQuotaLimit: 2 });

await seedTvRequest(owner, [1, 2], { tmdbId: 77777 });
const mediaRequest = await seedTvRequest(owner, [1, 2], {
ignoreQuota: true,
});

const agent = await loginAs('friend@seerr.dev', 'test1234');
const res = await agent.put(`/request/${mediaRequest.id}`).send({
mediaType: MediaType.TV,
seasons: [1, 2, 3],
});

assert.strictEqual(res.status, 200);

const saved = await requestRepo.findOneOrFail({
where: { id: mediaRequest.id },
});
assert.deepStrictEqual(
saved.seasons.map((s) => s.seasonNumber).sort((a, b) => a - b),
[1, 2, 3]
);
});
});

describe('POST /request/:requestId/:status', () => {
const cases = [
{ action: 'approve', expected: MediaRequestStatus.APPROVED },
Expand Down
Loading
Loading