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
16 changes: 16 additions & 0 deletions src/db/migrations/1785617601022-add_permissoes_provas_cursinho.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddPermissoesProvasCursinho1785617601022 implements MigrationInterface {
name = 'AddPermissoesProvasCursinho1785617601022'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`roles\` ADD \`visualizar_provas_cursinho\` tinyint NOT NULL DEFAULT 0`);
await queryRunner.query(`ALTER TABLE \`roles\` ADD \`cadastrar_provas_cursinho\` tinyint NOT NULL DEFAULT 0`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`roles\` DROP COLUMN \`cadastrar_provas_cursinho\``);
await queryRunner.query(`ALTER TABLE \`roles\` DROP COLUMN \`visualizar_provas_cursinho\``);
}

}
2 changes: 2 additions & 0 deletions src/db/seeds/2-role-update-admin.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export class RoleUpdateAdminSeedService {
gerenciarTemas: true,
revisarRedacoes: true,
revisarTodasRedacoes: true,
visualizarProvasCursinho: true,
cadastrarProvasCursinho: true,
});

this.logger.log('Role admin atualizada com sucesso');
Expand Down
12 changes: 12 additions & 0 deletions src/modules/prepCourse/collaborator/collaborator.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export class CollaboratorRepository extends BaseRepository<Collaborator> {
});
}

async findActiveByUserIdWithPrep(
userId: string,
): Promise<Collaborator | null> {
return this.repository
.createQueryBuilder('entity')
.innerJoin('entity.user', 'user')
.leftJoinAndSelect('entity.partnerPrepCourse', 'prep')
.where('user.id = :userId', { userId })
.andWhere('entity.actived = true')
.getOne();
}

async findActiveByUserIdWithDetails(
userId: string,
): Promise<Collaborator | null> {
Expand Down
8 changes: 8 additions & 0 deletions src/modules/role/dto/create-role.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,12 @@ export class CreateRoleDtoInput {

@IsBoolean()
editarMateriasFrentes: boolean;

@IsBoolean()
@IsOptional()
visualizarProvasCursinho?: boolean;

@IsBoolean()
@IsOptional()
cadastrarProvasCursinho?: boolean;
}
2 changes: 2 additions & 0 deletions src/modules/role/permissions/permission-field-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ export const PERMISSION_FIELD_MAP: Record<Permissions, string> = {
[Permissions.supportAgent]: 'supportAgent',
[Permissions.partnerPrepSupportAgent]: 'partnerPrepSupportAgent',
[Permissions.editarMateriasFrentes]: 'editarMateriasFrentes',
[Permissions.visualizarProvasCursinho]: 'visualizarProvasCursinho',
[Permissions.cadastrarProvasCursinho]: 'cadastrarProvasCursinho',
};
17 changes: 17 additions & 0 deletions src/modules/role/permissions/permission-hierarchy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ export const PERMISSION_HIERARCHY: PermissionGroup[] = [
},
],
},
{
key: 'provas_cursinho',
label: 'Provas (Cursinho)',
permissions: [
{
key: Permissions.cadastrarProvasCursinho,
label: 'Cadastrar provas (cursinho)',
type: PermissionType.prepCourse,
implies: [Permissions.visualizarProvasCursinho],
},
{
key: Permissions.visualizarProvasCursinho,
label: 'Visualizar provas (cursinho)',
type: PermissionType.prepCourse,
},
],
},
{
key: 'demanda',
label: 'Demanda',
Expand Down
27 changes: 27 additions & 0 deletions src/modules/role/permissions/permission-resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Permissions } from './permissions';
import { PERMISSION_FIELD_MAP } from './permission-field-map';
import { resolveImpliedPermissions } from './permission-resolver';

describe('resolveImpliedPermissions — provas cursinho', () => {
it('cadastrarProvasCursinho implica visualizarProvasCursinho', () => {
const resolved = resolveImpliedPermissions({
[Permissions.cadastrarProvasCursinho]: true,
});
expect(resolved[Permissions.visualizarProvasCursinho]).toBe(true);
});

it('visualizarProvasCursinho sozinho não liga cadastrar', () => {
const resolved = resolveImpliedPermissions({
[Permissions.visualizarProvasCursinho]: true,
});
expect(resolved[Permissions.cadastrarProvasCursinho]).toBeUndefined();
});
});

describe('PERMISSION_FIELD_MAP — completude', () => {
it('todo valor do enum Permissions tem entrada no field map', () => {
for (const key of Object.values(Permissions)) {
expect(PERMISSION_FIELD_MAP[key]).toBeDefined();
}
});
});
2 changes: 2 additions & 0 deletions src/modules/role/permissions/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ export enum Permissions {
supportAgent = 'support_agent',
partnerPrepSupportAgent = 'partner_prep_support_agent',
editarMateriasFrentes = 'editar_materias_frentes',
visualizarProvasCursinho = 'visualizar_provas_cursinho',
cadastrarProvasCursinho = 'cadastrar_provas_cursinho',
}
6 changes: 6 additions & 0 deletions src/modules/role/role.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export class Role extends BaseEntity {
@Column({ name: Permissions.editarMateriasFrentes, default: false })
editarMateriasFrentes: boolean;

@Column({ name: Permissions.visualizarProvasCursinho, default: false })
visualizarProvasCursinho: boolean;

@Column({ name: Permissions.cadastrarProvasCursinho, default: false })
cadastrarProvasCursinho: boolean;

@OneToMany(() => User, (user) => user.role)
users: User[];

Expand Down
49 changes: 49 additions & 0 deletions src/modules/role/role.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { RoleService } from './role.service';
import { CreateRoleDtoInput } from './dto/create-role.dto';

function makeService() {
const roleRepository = {
findOneBy: jest.fn().mockResolvedValue(null),
create: jest.fn().mockImplementation(async (r) => r),
update: jest.fn().mockImplementation(async (r) => r),
};
const service = new RoleService(roleRepository as any);
return { service, roleRepository };
}

function baseDto(
overrides: Partial<CreateRoleDtoInput> = {},
): CreateRoleDtoInput {
return {
name: 'Cursinho X',
base: false,
...overrides,
} as CreateRoleDtoInput;
}

describe('RoleService.create — permissões provas cursinho', () => {
it('persiste cadastrarProvasCursinho e liga visualizar (implies)', async () => {
const { service } = makeService();
const role = await service.create(
baseDto({ cadastrarProvasCursinho: true }),
);
expect(role.cadastrarProvasCursinho).toBe(true);
expect(role.visualizarProvasCursinho).toBe(true);
});

it('visualizarProvasCursinho sozinho não liga cadastrar', async () => {
const { service } = makeService();
const role = await service.create(
baseDto({ visualizarProvasCursinho: true }),
);
expect(role.visualizarProvasCursinho).toBe(true);
expect(role.cadastrarProvasCursinho).toBe(false);
});

it('sem os campos (undefined) → ambos false', async () => {
const { service } = makeService();
const role = await service.create(baseDto());
expect(role.cadastrarProvasCursinho).toBe(false);
expect(role.visualizarProvasCursinho).toBe(false);
});
});
4 changes: 4 additions & 0 deletions src/modules/role/role.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export class RoleService extends BaseService<Role> {
[Permissions.revisarRedacoes]: roleDto.revisarRedacoes,
[Permissions.partnerPrepSupportAgent]: roleDto.partnerPrepSupportAgent,
[Permissions.editarMateriasFrentes]: roleDto.editarMateriasFrentes,
[Permissions.cadastrarProvasCursinho]: roleDto.cadastrarProvasCursinho,
[Permissions.visualizarProvasCursinho]: roleDto.visualizarProvasCursinho,
});

const role = new Role();
Expand Down Expand Up @@ -175,6 +177,8 @@ export class RoleService extends BaseService<Role> {
[Permissions.supportAgent]: roleDto.supportAgent,
[Permissions.partnerPrepSupportAgent]: roleDto.partnerPrepSupportAgent,
[Permissions.editarMateriasFrentes]: roleDto.editarMateriasFrentes,
[Permissions.cadastrarProvasCursinho]: roleDto.cadastrarProvasCursinho,
[Permissions.visualizarProvasCursinho]: roleDto.visualizarProvasCursinho,
});

role.name = roleDto.name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CursinhoProvaController } from './cursinho-prova.controller';

describe('CursinhoProvaController', () => {
function make() {
const provaService = {
createProva: jest.fn().mockResolvedValue({ _id: 'p1' }),
getAllByCursinho: jest.fn().mockResolvedValue({ data: [] }),
};
const resolver = {
resolveCursinhoIdByUserId: jest.fn().mockResolvedValue('curs-A'),
};
const controller = new CursinhoProvaController(
provaService as any,
resolver as any,
);
return { controller, provaService, resolver };
}

it('POST injeta criadorId+cursinhoId resolvidos, ignorando o body', async () => {
const { controller, provaService, resolver } = make();
const req = { user: { id: 'user-1' } } as any;
const files = { file: [{ name: 'f' }], gabarito: undefined } as any;
const dto = { nome: 'X', criadorId: 'HACK', cursinhoId: 'HACK' } as any;

await controller.create(dto, files, req);

expect(resolver.resolveCursinhoIdByUserId).toHaveBeenCalledWith('user-1');
expect(provaService.createProva).toHaveBeenCalledWith(
dto,
{ name: 'f' },
undefined,
'user-1',
'curs-A',
);
});

it('GET resolve cursinhoId e encaminha page/limit', async () => {
const { controller, provaService, resolver } = make();
const req = { user: { id: 'user-1' } } as any;

await controller.getAll('2', '10', req);

expect(resolver.resolveCursinhoIdByUserId).toHaveBeenCalledWith('user-1');
expect(provaService.getAllByCursinho).toHaveBeenCalledWith(
'curs-A',
'2',
'10',
);
});
});
76 changes: 76 additions & 0 deletions src/modules/simulado/prova/cursinho/cursinho-prova.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Body,
Controller,
Get,
Post,
Query,
Req,
SetMetadata,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { Permissions } from 'src/modules/role/permissions/permissions';
import { User } from 'src/modules/user/user.entity';
import { JwtAuthGuard } from 'src/shared/guards/jwt-auth.guard';
import { PermissionsGuard } from 'src/shared/guards/permission.guard';
import { CreateProvaDTOInput } from '../dtos/prova-create.dto.input';
import { ProvaService } from '../prova.service';
import { CursinhoResolverService } from './cursinho-resolver.service';

@ApiTags('Simulado - Prova Cursinho')
@Controller('mssimulado/cursinho/prova')
export class CursinhoProvaController {
constructor(
private readonly provaService: ProvaService,
private readonly cursinhoResolver: CursinhoResolverService,
) {}

@Get()
@ApiBearerAuth()
@ApiResponse({ status: 200, description: 'lista provas do cursinho' })
@UseGuards(JwtAuthGuard, PermissionsGuard)
@SetMetadata(PermissionsGuard.name, Permissions.visualizarProvasCursinho)
public async getAll(
@Query('page') page: string,
@Query('limit') limit: string,
@Req() req: Request,
) {
const cursinhoId = await this.cursinhoResolver.resolveCursinhoIdByUserId(
(req.user as User).id,
);
return await this.provaService.getAllByCursinho(cursinhoId, page, limit);
}

@Post()
@ApiBearerAuth()
@ApiResponse({ status: 200, description: 'cria prova do cursinho' })
@UseGuards(JwtAuthGuard, PermissionsGuard)
@SetMetadata(PermissionsGuard.name, Permissions.cadastrarProvasCursinho)
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'file', maxCount: 1 },
{ name: 'gabarito', maxCount: 1 },
]),
)
public async create(
@Body() dto: CreateProvaDTOInput,
@UploadedFiles()
files: { file?: Express.Multer.File[]; gabarito?: Express.Multer.File[] },
@Req() req: Request,
) {
const userId = (req.user as User).id;
const cursinhoId =
await this.cursinhoResolver.resolveCursinhoIdByUserId(userId);
return await this.provaService.createProva(
dto,
files?.file?.[0],
files?.gabarito?.[0],
userId,
cursinhoId,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ForbiddenException } from '@nestjs/common';
import { CursinhoResolverService } from './cursinho-resolver.service';

describe('CursinhoResolverService', () => {
function make(collab: any) {
const collaboratorRepository = {
findActiveByUserIdWithPrep: jest.fn().mockResolvedValue(collab),
};
const service = new CursinhoResolverService(collaboratorRepository as any);
return { service, collaboratorRepository };
}

it('retorna o id do partnerPrepCourse do colaborador ativo', async () => {
const { service } = make({ partnerPrepCourse: { id: 'curs-1' } });
await expect(service.resolveCursinhoIdByUserId('u1')).resolves.toBe(
'curs-1',
);
});

it('lança Forbidden quando não há colaborador ativo', async () => {
const { service } = make(null);
await expect(
service.resolveCursinhoIdByUserId('u1'),
).rejects.toBeInstanceOf(ForbiddenException);
});

it('lança Forbidden quando colaborador sem cursinho vinculado', async () => {
const { service } = make({ partnerPrepCourse: null });
await expect(
service.resolveCursinhoIdByUserId('u1'),
).rejects.toBeInstanceOf(ForbiddenException);
});
});
18 changes: 18 additions & 0 deletions src/modules/simulado/prova/cursinho/cursinho-resolver.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { CollaboratorRepository } from 'src/modules/prepCourse/collaborator/collaborator.repository';

@Injectable()
export class CursinhoResolverService {
constructor(
private readonly collaboratorRepository: CollaboratorRepository,
) {}

async resolveCursinhoIdByUserId(userId: string): Promise<string> {
const collab =
await this.collaboratorRepository.findActiveByUserIdWithPrep(userId);
if (!collab || !collab.partnerPrepCourse) {
throw new ForbiddenException('Usuário não vinculado a nenhum cursinho');
}
return collab.partnerPrepCourse.id;
}
}
Loading
Loading