Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/map submission misc 2 #1073

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion apps/backend-e2e/src/maps-2.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ describe('Maps Part 2', () => {
query: { gamemode: Gamemode.AHOP, filter: 'around', take: 8 },
status: 200,
token: u7Token,
validatePaged: { type: MinimalLeaderboardRunDto, count: 9 }
validatePaged: { type: MinimalLeaderboardRunDto, returnCount: 9 }
});

// We're calling as user 7, taking 4 on each side, so we expect ranks
Expand Down
105 changes: 99 additions & 6 deletions apps/backend-e2e/src/maps.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
TrackType,
LeaderboardType,
FlatMapList,
GamemodeCategory
GamemodeCategory,
MAX_OPEN_MAP_SUBMISSIONS
} from '@momentum/constants';
import {
createSha1Hash,
Expand Down Expand Up @@ -1290,12 +1291,34 @@ describe('Maps', () => {
});
});

// This shouldn't ever really occur - on map approval the user is made a
// MAPPER or PORTER - but may as well have precise behaviour anyway.
it('should succeed if the user already has an APPROVED map and is not a mapper', async () => {
it('should 403 for if the user is not a mapper and has a map in submission', async () => {
await db.createMap({
submitter: { connect: { id: user.id } },
status: MapStatus.APPROVED
status: MapStatus.PRIVATE_TESTING
});

await uploadBspToPreSignedUrl(bspBuffer, token);

await req.postAttach({
url: 'maps',
status: 403,
data: createMapObject,
files: [
{ file: vmfBuffer, field: 'vmfs', fileName: 'surf_map.vmf' }
],
token
});
});

it('should not 403 for if the user is a mapper and has a map in submission', async () => {
await db.createMap({
submitter: { connect: { id: user.id } },
status: MapStatus.PRIVATE_TESTING
});

await prisma.user.update({
where: { id: user.id },
data: { roles: Role.MAPPER }
});

await uploadBspToPreSignedUrl(bspBuffer, token);
Expand All @@ -1309,8 +1332,31 @@ describe('Maps', () => {
],
token
});
});

await prisma.mMap.deleteMany();
it('should 403 if the user has MAX_OPEN_MAP_SUBMISSIONS maps in submission', async () => {
await db.createMaps(MAX_OPEN_MAP_SUBMISSIONS, {
submitter: { connect: { id: user.id } },
status: MapStatus.PRIVATE_TESTING
});

// Even mappers can't do this
await prisma.user.update({
where: { id: user.id },
data: { roles: Role.MAPPER }
});

await uploadBspToPreSignedUrl(bspBuffer, token);

await req.postAttach({
url: 'maps',
status: 403,
data: createMapObject,
files: [
{ file: vmfBuffer, field: 'vmfs', fileName: 'surf_map.vmf' }
],
token
});
});

it('should 403 if the user has a MAP_SUBMISSION ban', async () => {
Expand Down Expand Up @@ -4109,6 +4155,53 @@ describe('Maps', () => {
);
});

it('should limit -1 take to 100 for non-reviewers', async () => {
// Magic number sorry, if additional maps are added above in setup
// this'll break.
const newMaps = await db
.createMaps(98, {
status: MapStatus.PUBLIC_TESTING
})
.then((maps) => maps.map((m) => m.id));

await req.get({
url: 'maps/submissions',
status: 200,
query: { take: -1 },
validatePaged: { type: MapDto, returnCount: 100, totalCount: 101 },
token: u1Token
});

await prisma.mMap.deleteMany({ where: { id: { in: newMaps } } });
});

it('should not limit -1 take reviewers', async () => {
await prisma.user.update({
where: { id: u1.id },
data: { roles: Role.REVIEWER }
});

const newMaps = await db
.createMaps(97, {
status: MapStatus.PUBLIC_TESTING
})
.then((maps) => maps.map((m) => m.id));

await req.get({
url: 'maps/submissions',
status: 200,
query: { take: -1 },
validatePaged: { type: MapDto, returnCount: 101, totalCount: 101 },
token: u1Token
});

await prisma.mMap.deleteMany({ where: { id: { in: newMaps } } });
await prisma.user.update({
where: { id: u1.id },
data: { roles: 0 }
});
});

it('should 401 when no access token is provided', () =>
req.unauthorizedTest('maps/submissions', 'get'));
});
Expand Down
9 changes: 7 additions & 2 deletions apps/backend/src/app/dto/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,13 @@ export function SkipQueryProperty(def: number): PropertyDecorator {
* Decorator collection for take queries
* @param def - The default `take` value
* @param max - The maximum allowed `take` value
* @param min - The minimum allowed `take` value
*/
export function TakeQueryProperty(def: number, max = 100): PropertyDecorator {
export function TakeQueryProperty(
def: number,
max = 100,
min = 0
): PropertyDecorator {
return applyDecorators(
ApiPropertyOptional({
name: 'take',
Expand All @@ -241,7 +246,7 @@ export function TakeQueryProperty(def: number, max = 100): PropertyDecorator {
description: 'Take this many records'
}),
TypeDecorator(() => Number),
Min(0),
Min(min),
IsInt(),
Max(max),
IsOptional()
Expand Down
7 changes: 5 additions & 2 deletions apps/backend/src/app/dto/queries/map-queries.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class MapsGetAllBaseQueryDto extends QueryDto {
@SkipQueryProperty(0)
skip = 0;

@TakeQueryProperty(100)
take = 100;
@TakeQueryProperty(10, 100)
take = 10;

@StringQueryProperty({
description: 'Filter by partial map name match (contains)',
Expand Down Expand Up @@ -133,6 +133,9 @@ export class MapsGetAllSubmissionQueryDto
extends MapsGetAllBaseQueryDto
implements MapsGetAllSubmissionQuery
{
@TakeQueryProperty(10, 100, -1)
override readonly take = 10;

@ExpandQueryProperty([
'leaderboards',
'info',
Expand Down
34 changes: 26 additions & 8 deletions apps/backend/src/app/modules/maps/maps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import {
MapZones,
Role,
TrackType,
vmfsPath
vmfsPath,
MAX_OPEN_MAP_SUBMISSIONS
} from '@momentum/constants';
import * as Bitflags from '@momentum/bitflags';
import {
Expand Down Expand Up @@ -118,6 +119,7 @@ export class MapsService {
): Promise<PagedResponseDto<MapDto>> {
// Where
const where: Prisma.MMapWhereInput = {};
let take: number | undefined = query.take;
if (query.search) where.name = { contains: query.search };
if (query.searchStartsWith)
where.name = { startsWith: query.searchStartsWith };
Expand Down Expand Up @@ -186,6 +188,13 @@ export class MapsService {

if (roles == null) throw new BadRequestException();

// Allow unlimited take for reviewers and above
if (take === -1) {
take = Bitflags.has(roles, CombinedRoles.REVIEWER_AND_ABOVE)
? undefined
: 100;
}

// Logic here is a nightmare, for a breakdown of permissions see
// MapsService.getMapAndCheckReadAccess.
const filter = query.filter;
Expand Down Expand Up @@ -362,7 +371,7 @@ export class MapsService {
include,
orderBy: { createdAt: 'desc' },
skip: query.skip,
take: query.take
take
});

if (incPB || incWR) {
Expand Down Expand Up @@ -753,14 +762,23 @@ export class MapsService {
throw new ForbiddenException('User is banned from map submission');
}

// 403 if the user is not a mapper, porter, moderator or admin, and they
// already have a map in private testing, content approval, public testing,
// or final approval.
const submissionMaps = user.submittedMaps.filter(({ status }) =>
MapStatuses.IN_SUBMISSION.includes(status)
);

// Limit total maps a non-mod/admin can have in submission at once
if (
submissionMaps.length >= MAX_OPEN_MAP_SUBMISSIONS &&
!Bitflags.has(user.roles, CombinedRoles.MOD_OR_ADMIN)
) {
throw new ForbiddenException('Too many maps in submission');
}

// 403 if the user is not a mapper, porter, moderator or admin, they don't
// have any approved maps, and they have a map in submission.
if (
!Bitflags.has(user.roles, CombinedRoles.MAPPER_AND_ABOVE) &&
user.submittedMaps.some((map) =>
MapStatuses.IN_SUBMISSION.includes(map.status)
)
submissionMaps.length > 0
) {
throw new ForbiddenException(
'User is not an approved mapper and already has a map in review'
Expand Down
1 change: 1 addition & 0 deletions libs/constants/src/consts/limits.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export const MIN_MAP_DESCRIPTION_LENGTH = 10;
export const MAX_MAP_DESCRIPTION_LENGTH = 1500;
export const MIN_MAP_NAME_LENGTH = 3;
export const MAX_MAP_NAME_LENGTH = 32; // Seems high but this is actually a constant in engine.
export const MAX_OPEN_MAP_SUBMISSIONS = 20;
export const MAX_MAP_SUGGESTION_COMMENT_LENGTH = 500;
export const PRE_SIGNED_URL_EXPIRE_TIME = 5 * 60;
4 changes: 2 additions & 2 deletions libs/test-utils/src/matchers/to-be-valid-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export function toBeValidPagedDto(
if (
totalCount &&
typeof totalCount === 'number' &&
received.returnCount !== totalCount
received.totalCount !== totalCount
)
return {
message: () =>
`expected returnCount of ${totalCount}, got ${received.totalCount}`,
`expected totalCount of ${totalCount}, got ${received.totalCount}`,
pass: false
};

Expand Down