Skip to content

Commit

Permalink
feat(api): Block the access to the school if the session is not started
Browse files Browse the repository at this point in the history
  • Loading branch information
aurelie-crouillebois committed Jul 5, 2024
1 parent bf60481 commit f0c1377
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 2 deletions.
5 changes: 4 additions & 1 deletion api/src/school/application/school-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const register = async function (server) {
method: 'GET',
path: '/api/pix1d/schools',
config: {
pre: [{ method: securityPreHandlers.checkPix1dActivated }],
pre: [
{ method: securityPreHandlers.checkPix1dActivated },
{ method: securityPreHandlers.checkSchoolSessionIsActive },
],
auth: false,
validate: {
query: Joi.object({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as schoolRepository from '../../infrastructure/repositories/school-repository.js';

const execute = async function ({ schoolCode, dependencies = { schoolRepository } }) {
const result = await dependencies.schoolRepository.getSessionExpirationDate({ code: schoolCode });

return result && new Date() < new Date(result) ? true : false;
};

export { execute };
18 changes: 17 additions & 1 deletion api/src/school/infrastructure/repositories/school-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,20 @@ const getDivisions = async function ({ organizationId, organizationLearnerApi })
return [...new Set(divisionLearners)].sort().map((divisionName) => new Division({ name: divisionName }));
};

export { getByCode, getById, getDivisions, isCodeAvailable, save, updateSessionExpirationDate };
const getSessionExpirationDate = async function ({ code }) {
const [sessionExpirationDate] = await knex('schools')
.select('sessionExpirationDate')
.where({ code })
.pluck('sessionExpirationDate');
return sessionExpirationDate;
};

export {
getByCode,
getById,
getDivisions,
getSessionExpirationDate,
isCodeAvailable,
save,
updateSessionExpirationDate,
};
20 changes: 20 additions & 0 deletions api/src/shared/application/security-pre-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Organization } from '../../../lib/domain/models/index.js';
import { PIX_ADMIN } from '../../authorization/domain/constants.js';
import * as checkUserIsCandidateUseCase from '../../certification/enrolment/application/usecases/checkUserIsCandidate.js';
import * as certificationIssueReportRepository from '../../certification/shared/infrastructure/repositories/certification-issue-report-repository.js';
import * as isSchoolSessionActive from '../../school/application/usecases/is-school-session-active.js';
import { ForbiddenAccess, NotFoundError } from '../domain/errors.js';
import * as organizationRepository from '../infrastructure/repositories/organization-repository.js';
import * as checkOrganizationHasFeatureUseCase from './usecases/checkOrganizationHasFeature.js';
Expand All @@ -49,6 +50,17 @@ function _replyForbiddenError(h) {
return h.response(jsonApiError).code(errorHttpStatusCode).takeover();
}

function _replyNotFoundError(h) {
const errorHttpStatusCode = 404;

const jsonApiError = new JSONAPIError({
code: errorHttpStatusCode,
title: 'Not found',
});

return h.response(jsonApiError).code(errorHttpStatusCode).takeover();
}

async function checkIfUserIsBlocked(
request,
h,
Expand Down Expand Up @@ -176,6 +188,13 @@ function checkRequestedUserIsAuthenticatedUser(request, h) {
return authenticatedUserId === requestedUserId ? h.response(true) : _replyForbiddenError(h);
}

async function checkSchoolSessionIsActive(request, h, dependencies = { isSchoolSessionActive }) {
if (await dependencies.isSchoolSessionActive.execute({ schoolCode: request.query.code })) {
return h.response(true);
}
return _replyNotFoundError(h);
}

function checkUserIsAdminInOrganization(request, h, dependencies = { checkUserIsAdminInOrganizationUseCase }) {
if (!request.auth.credentials || !request.auth.credentials.userId) {
return _replyForbiddenError(h);
Expand Down Expand Up @@ -742,6 +761,7 @@ const securityPreHandlers = {
checkIfUserIsBlocked,
checkPix1dActivated,
checkRequestedUserIsAuthenticatedUser,
checkSchoolSessionIsActive,
checkUserBelongsToLearnersOrganization,
checkUserBelongsToOrganization,
checkUserBelongsToOrganizationManagingStudents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,24 @@ describe('Integration | Repository | School', function () {
expect(divisions).to.be.empty;
});
});

describe('#getSessionExpirationDate', function () {
it('should return the session expiration date', async function () {
databaseBuilder.factory.buildSchool({ code: 'FRANCE998', sessionExpirationDate: '2022-07-04' });
await databaseBuilder.commit();
// when
const sessionExpirationDate = await repositories.schoolRepository.getSessionExpirationDate({ code: 'FRANCE998' });

// then
expect(sessionExpirationDate).to.deep.equal(new Date('2022-07-04'));
});

it('should return undefined when there is no school corresponding to the code', async function () {
// when
const sessionExpirationDate = await repositories.schoolRepository.getSessionExpirationDate({ code: 'FRANCE998' });

// then
expect(sessionExpirationDate).to.equal(undefined);
});
});
});
3 changes: 3 additions & 0 deletions api/tests/school/unit/application/school-route_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('Unit | Router | school-router', function () {
sinon.stub(schoolController, 'getSchool').callsFake((request, h) => h.response('ok'));

sinon.stub(securityPreHandlers, 'checkPix1dActivated').callsFake((request, h) => h.response());
sinon.stub(securityPreHandlers, 'checkSchoolSessionIsActive').callsFake((request, h) => h.response());
const httpTestServer = new HttpTestServer();
await httpTestServer.register(moduleUnderTest);

Expand All @@ -20,6 +21,8 @@ describe('Unit | Router | school-router', function () {

// then
expect(response.statusCode).to.equal(200);
expect(securityPreHandlers.checkPix1dActivated).to.have.been.called;
expect(securityPreHandlers.checkSchoolSessionIsActive).to.have.been.called;
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { execute as isSchoolSessionActive } from '../../../../../src/school/application/usecases/is-school-session-active.js';
import { expect, sinon } from '../../../../test-helper.js';

describe('Unit | Domain | Use Cases | is-session-active', function () {
let clock;

beforeEach(function () {
const now = new Date('2022-08-04');
clock = sinon.useFakeTimers({ now, toFake: ['Date'] });
});

afterEach(function () {
clock.restore();
});

it('should return false if no sessionExpirationDate found for school', async function () {
const dependencies = { schoolRepository: { getSessionExpirationDate: sinon.stub() } };
dependencies.schoolRepository.getSessionExpirationDate.resolves(undefined);

const result = await isSchoolSessionActive({ schoolCode: 'CODE1', dependencies });

expect(dependencies.schoolRepository.getSessionExpirationDate).to.have.been.calledWithExactly({ code: 'CODE1' });
expect(result).to.be.false;
});

it('should return false if date is passed', async function () {
const dependencies = { schoolRepository: { getSessionExpirationDate: sinon.stub() } };
dependencies.schoolRepository.getSessionExpirationDate.resolves('2022-07-04T00:00:00.000Z');

const result = await isSchoolSessionActive({ schoolCode: 'CODE1', dependencies });

expect(result).to.be.false;
});

it('should return true if session is still active', async function () {
const dependencies = { schoolRepository: { getSessionExpirationDate: sinon.stub() } };
dependencies.schoolRepository.getSessionExpirationDate.resolves('2022-08-04T01:00:00.000Z');

const result = await isSchoolSessionActive({ schoolCode: 'CODE1', dependencies });

expect(result).to.be.true;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -603,4 +603,82 @@ describe('Integration | Application | SecurityPreHandlers', function () {
expect(response.statusCode).to.equal(403);
});
});

describe('#checkSchoolSessionIsActive', function () {
let httpServerTest;

beforeEach(async function () {
const moduleUnderTest = {
name: 'has-feature-test',
register: async function (server) {
server.route([
{
method: 'GET',
path: '/api/test/schools/code',
handler: (r, h) => h.response().code(200),
config: {
auth: false,
pre: [
{
method: securityPreHandlers.checkSchoolSessionIsActive,
},
],
},
},
]);
},
};
httpServerTest = new HttpTestServer();
await httpServerTest.register(moduleUnderTest);
httpServerTest.setupAuthentication();
});

it('should return 200 when school session is active', async function () {
const sessionExpirationDate = new Date();
sessionExpirationDate.setHours(sessionExpirationDate.getHours() + 4);

const organization = databaseBuilder.factory.buildOrganization({ type: 'SCO-1D' });
const school = databaseBuilder.factory.buildSchool({
organizationId: organization.id,
code: 'SCHOOL',
sessionExpirationDate,
});
await databaseBuilder.commit();

const options = {
method: 'GET',
url: `/api/test/schools/code?code=${school.code}`,
};

// when
const response = await httpServerTest.requestObject(options);

// then
expect(response.statusCode).to.equal(200);
});

it('should return 404 when school session is not active', async function () {
const sessionExpirationDate = new Date();
sessionExpirationDate.setHours(sessionExpirationDate.getHours() - 4);

const organization = databaseBuilder.factory.buildOrganization({ type: 'SCO-1D' });
const school = databaseBuilder.factory.buildSchool({
organizationId: organization.id,
code: 'SCHOOL',
sessionExpirationDate,
});
await databaseBuilder.commit();

const options = {
method: 'GET',
url: `/api/test/schools/code?code=${school.code}`,
};

// when
const response = await httpServerTest.requestObject(options);

// then
expect(response.statusCode).to.equal(404);
});
});
});

0 comments on commit f0c1377

Please sign in to comment.