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 src/modules/prova/dtos/create.dto.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,12 @@ export class CreateProvaDTOInput {
@ApiProperty()
@IsString()
criadorId: string;

// Campo INTERNO: injetado pelo api-vcnafacul (Card 03) a partir do Collaborator
// do usuário. Não vem do cliente final. @IsOptional + @IsString para sobreviver
// ao whitelist do ValidationPipe e aceitar ausência/null.
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
cursinhoId?: string | null;
}
12 changes: 5 additions & 7 deletions src/modules/prova/factory/enem_2010_2016_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,31 @@ export class Enem2010_2017Factory implements IProvaFactory {

public async createSimuladoDia1(prova: Prova) {
const mainName = `${prova.categoria.nome} ${prova.ano}`;
const exameId = prova.categoria.exame._id.toString();
prova.simulados.push(
await this.enemService.createSimuladoArea(
mainName,
prova,
EnemArea.CienciasHumanas,
exameId,
),
);
prova.simulados.push(
await this.enemService.createSimuladoArea(mainName, EnemArea.BioExatas, exameId),
await this.enemService.createSimuladoArea(prova, EnemArea.BioExatas),
);
prova.simulados.push(
await this.simuladoRepository.create({
nome: `${mainName}`,
categoria: prova.categoria,
questoes: [],
descricao: `${prova.categoria.exame.nome}`,
criadorId: prova.criadorId,
cursinhoId: prova.cursinhoId,
}),
);
}

public async createSimuladoDia2(prova: Prova) {
const mainName = `${prova.categoria.nome} ${prova.ano}`;
const exameId = prova.categoria.exame._id.toString();
await this.enemService.createSimuladoIdiomatica(prova);
prova.simulados.push(
await this.enemService.createSimuladoArea(mainName, EnemArea.Matematica, exameId),
await this.enemService.createSimuladoArea(prova, EnemArea.Matematica),
);
}

Expand Down
12 changes: 5 additions & 7 deletions src/modules/prova/factory/enem_2017_plus_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,33 +56,31 @@ export class Enem2017PlusFactory implements IProvaFactory {
}

public async createSimuladoDia1(prova: Prova) {
const mainName = `${prova.categoria.nome} ${prova.ano}`;
const exameId = prova.categoria.exame._id.toString();
await this.enemService.createSimuladoIdiomatica(prova);
prova.simulados.push(
await this.enemService.createSimuladoArea(
mainName,
prova,
EnemArea.CienciasHumanas,
exameId,
),
);
}

public async createSimuladoDia2(prova: Prova) {
const mainName = `${prova.categoria.nome} ${prova.ano}`;
const exameId = prova.categoria.exame._id.toString();
prova.simulados.push(
await this.enemService.createSimuladoArea(mainName, EnemArea.Matematica, exameId),
await this.enemService.createSimuladoArea(prova, EnemArea.Matematica),
);
prova.simulados.push(
await this.enemService.createSimuladoArea(mainName, EnemArea.BioExatas, exameId),
await this.enemService.createSimuladoArea(prova, EnemArea.BioExatas),
);
prova.simulados.push(
await this.simuladoRepository.create({
nome: `${mainName}`,
categoria: prova.categoria,
questoes: [],
descricao: `${prova.categoria.exame.nome}`,
criadorId: prova.criadorId,
cursinhoId: prova.cursinhoId,
}),
);
}
Expand Down
14 changes: 14 additions & 0 deletions src/modules/prova/prova.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export class ProvaController {
return await this.service.getAll(query);
}

@Get('cursinho/:cursinhoId')
@ApiResponse({
status: 200,
description: 'busca provas de um cursinho',
type: Prova,
isArray: true,
})
public async getAllByCursinho(
@Param('cursinhoId') cursinhoId: string,
@Query() query: GetAllDtoInput,
): Promise<GetAllDtoOutput<GetProvaDTOOutout>> {
return await this.service.getAllByCursinho(cursinhoId, query);
}

@Get('summary')
async getSummary() {
return await this.service.getSummary();
Expand Down
23 changes: 23 additions & 0 deletions src/modules/prova/prova.schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ describe('Prova schema — criadorId / cursinhoId', () => {
expect(prova.cursinhoId).toBeNull();
});

it('constructor lê cursinhoId do item quando presente', () => {
const item = {
criadorId: 'user-1',
cursinhoId: 'curs-1',
ano: 2023,
filename: 'f.pdf',
} as unknown as CreateProvaDTOInput;

const prova = new Prova(item, categoria);

expect(prova.cursinhoId).toBe('curs-1');
});

describe('validação do schema Mongoose', () => {
let Model: mongoose.Model<Prova>;

Expand All @@ -43,3 +56,13 @@ describe('Prova schema — criadorId / cursinhoId', () => {
});
});
});

describe('ProvaSchema — índices', () => {
it('declara índice em cursinhoId', () => {
const indexes = ProvaSchema.indexes();
const hasCursinhoIndex = indexes.some(
([fields]) => (fields as Record<string, number>).cursinhoId === 1,
);
expect(hasCursinhoIndex).toBe(true);
});
});
4 changes: 3 additions & 1 deletion src/modules/prova/prova.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class Prova extends BaseSchema {
this.gabarito = item.gabarito;
this.aplicacao = item.aplicacao;
this.criadorId = item.criadorId;
this.cursinhoId = null;
this.cursinhoId = item.cursinhoId ?? null;
this.simulados = [];
this.questoes = [];
}
Expand Down Expand Up @@ -76,3 +76,5 @@ export class Prova extends BaseSchema {
}

export const ProvaSchema = SchemaFactory.createForClass(Prova);

ProvaSchema.index({ cursinhoId: 1 });
77 changes: 77 additions & 0 deletions src/modules/prova/prova.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,80 @@ describe('ProvaService.selectQuestionsForSimulado — branch custom', () => {
expect(result).toHaveLength(2);
});
});

describe('ProvaService.getAllByCursinho', () => {
function makeProva(id: string) {
return {
_id: id,
edicao: 'Regular',
aplicacao: 1,
ano: 2024,
categoria: { nome: 'Personalizado', exame: { nome: 'Personalizado' } },
nome: `Prova ${id}`,
totalQuestao: 30,
gabarito: 'x',
totalQuestaoValidadas: 0,
filename: 'f.pdf',
enemAreas: [] as any[],
questoes: [] as any[],
createdAt: new Date('2024-01-01T00:00:00.000Z'),
};
}

function makeServiceWithGetAll(getAll: jest.Mock) {
const repository = { getAll };
const service = new ProvaService(
{} as any, // provaFactory
repository as any,
{} as any, // categoriaRepository
{} as any, // simuladoRepository
{} as any, // questaoRepository
{} as any, // frenteRepository
);
return { service, repository };
}

it('filtra por cursinhoId via where e mantém a paginação', async () => {
const getAll = jest.fn().mockResolvedValue({
data: [makeProva('p1')],
page: 1,
limit: 40,
totalItems: 1,
});
const { service } = makeServiceWithGetAll(getAll);

const result = await service.getAllByCursinho('curs-1', {
page: 1,
limit: 40,
});

expect(getAll).toHaveBeenCalledWith({
page: 1,
limit: 40,
where: { cursinhoId: 'curs-1' },
});
expect(result.totalItems).toBe(1);
expect(result.data[0]._id).toBe('p1');
expect(result.data[0].categoria).toBe('Personalizado');
expect(result.data[0].exame).toBe('Personalizado');
expect(result.data[0].totalQuestaoCadastradas).toBe(0);
});

it('cursinho sem provas retorna data vazia (não lança)', async () => {
const getAll = jest.fn().mockResolvedValue({
data: [],
page: 1,
limit: 40,
totalItems: 0,
});
const { service } = makeServiceWithGetAll(getAll);

const result = await service.getAllByCursinho('curs-x', {
page: 1,
limit: 40,
});

expect(result.data).toEqual([]);
expect(result.totalItems).toBe(0);
});
});
52 changes: 34 additions & 18 deletions src/modules/prova/prova.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,30 +72,46 @@ export class ProvaService {
return prova;
}

private toProvaDTO(prova: Prova): GetProvaDTOOutout {
return {
_id: prova._id,
edicao: prova.edicao,
aplicacao: prova.aplicacao,
ano: prova.ano,
categoria: prova.categoria.nome,
exame: prova.categoria.exame.nome,
nome: prova.nome,
totalQuestao: prova.totalQuestao,
gabarito: prova.gabarito,
totalQuestaoValidadas: prova.totalQuestaoValidadas,
filename: prova.filename,
enemAreas: prova.enemAreas,
totalQuestaoCadastradas: prova.questoes.length,
createdAt: prova.createdAt,
} as GetProvaDTOOutout;
}

public async getAll(
param: GetAllInput,
): Promise<GetAllOutput<GetProvaDTOOutout>> {
const provas = await this.repository.getAll(param);
return {
...provas,
data: provas.data.map((prova) => {
return {
_id: prova._id,
edicao: prova.edicao,
aplicacao: prova.aplicacao,
ano: prova.ano,
categoria: prova.categoria.nome,
exame: prova.categoria.exame.nome,
nome: prova.nome,
totalQuestao: prova.totalQuestao,
gabarito: prova.gabarito,
totalQuestaoValidadas: prova.totalQuestaoValidadas,
filename: prova.filename,
enemAreas: prova.enemAreas,
totalQuestaoCadastradas: prova.questoes.length,
createdAt: prova.createdAt,
};
}),
data: provas.data.map((prova) => this.toProvaDTO(prova)),
};
}

public async getAllByCursinho(
cursinhoId: string,
param: GetAllInput,
): Promise<GetAllOutput<GetProvaDTOOutout>> {
const provas = await this.repository.getAll({
...param,
where: { cursinhoId },
});
return {
...provas,
data: provas.data.map((prova) => this.toProvaDTO(prova)),
};
}

Expand Down
72 changes: 72 additions & 0 deletions src/modules/prova/services/enem_service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { EnemService } from './enem_service';

function makeService() {
const simuladoRepository = {
create: jest.fn().mockImplementation(async (s) => ({ ...s, _id: 's-new' })),
};
const categoriaRepository = {
getByFilter: jest.fn().mockResolvedValue({ _id: 'cat-area' }),
};
const provaRepository = {};
const service = new EnemService(
simuladoRepository as any,
categoriaRepository as any,
provaRepository as any,
);
return { service, simuladoRepository, categoriaRepository };
}

function makeProva() {
return {
ano: 2023,
criadorId: 'user-1',
cursinhoId: 'curs-1',
simulados: [] as any[],
categoria: {
nome: 'Enem Dia 1',
exame: { _id: 'e1', nome: 'ENEM' },
},
} as any;
}

describe('EnemService.createSimuladoArea', () => {
it('propaga criadorId e cursinhoId da prova para o simulado de área', async () => {
const { service, simuladoRepository } = makeService();
const prova = makeProva();

await service.createSimuladoArea(prova, 'Matemática');

const arg = simuladoRepository.create.mock.calls[0][0];
expect(arg.nome).toBe('Enem Dia 1 2023 Matemática');
expect(arg.criadorId).toBe('user-1');
expect(arg.cursinhoId).toBe('curs-1');
});

it('mantém cursinhoId null quando a prova não tem cursinho', async () => {
const { service, simuladoRepository } = makeService();
const prova = makeProva();
prova.cursinhoId = null;

await service.createSimuladoArea(prova, 'Matemática');

const arg = simuladoRepository.create.mock.calls[0][0];
expect(arg.criadorId).toBe('user-1');
expect(arg.cursinhoId).toBeNull();
});
});

describe('EnemService.createSimuladoIdiomatica', () => {
it('propaga criadorId e cursinhoId nos simulados Inglês e Espanhol', async () => {
const { service, simuladoRepository } = makeService();
const prova = makeProva();

await service.createSimuladoIdiomatica(prova);

const args = simuladoRepository.create.mock.calls.map((c) => c[0]);
expect(args.length).toBeGreaterThanOrEqual(2);
for (const arg of args) {
expect(arg.criadorId).toBe('user-1');
expect(arg.cursinhoId).toBe('curs-1');
}
});
});
Loading
Loading