Skip to content

Commit

Permalink
[TECH] Exposer une API afin de récupérer les informations d'une campa…
Browse files Browse the repository at this point in the history
…gne (Pix-10062)

 #7519
  • Loading branch information
pix-service-auto-merge authored Nov 24, 2023
2 parents 8cc615b + dce0e13 commit 5620190
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 83 deletions.
1 change: 1 addition & 0 deletions api/lib/application/campaigns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const register = async function (server) {
method: 'GET',
path: '/api/campaigns/{id}',
config: {
pre: [{ method: securityPreHandlers.checkAuthorizationToAccessCampaign }],
validate: {
params: Joi.object({
id: identifiersType.campaignId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class CampaignAuthorization {
static isAllowedToManage({ prescriberRole }) {
return prescriberRole === prescriberRoles.ADMIN || prescriberRole === prescriberRoles.OWNER;
}

static isAllowedToAccess({ prescriberRole }) {
return Object.values(prescriberRoles).includes(prescriberRole);
}
}

export { CampaignAuthorization, prescriberRoles };
18 changes: 18 additions & 0 deletions api/lib/application/security-pre-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as checkUserIsAdminOfCertificationCenterUsecase from './usecases/checkU
import * as checkUserIsMemberOfCertificationCenterUsecase from './usecases/checkUserIsMemberOfCertificationCenter.js';
import * as checkUserIsMemberOfCertificationCenterSessionUsecase from './usecases/checkUserIsMemberOfCertificationCenterSession.js';
import * as checkAuthorizationToManageCampaignUsecase from './usecases/checkAuthorizationToManageCampaign.js';
import * as checkAuthorizationToAccessCampaignUsecase from './usecases/checkAuthorizationToAccessCampaign.js';
import * as checkOrganizationIsScoAndManagingStudentUsecase from './usecases/checkOrganizationIsScoAndManagingStudent.js';
import * as checkPix1dEnabled from './usecases/checkPix1dEnabled.js';
import * as certificationIssueReportRepository from '../../src/certification/shared/infrastructure/repositories/certification-issue-report-repository.js';
Expand Down Expand Up @@ -548,6 +549,22 @@ async function checkAuthorizationToManageCampaign(
return _replyForbiddenError(h);
}

async function checkAuthorizationToAccessCampaign(
request,
h,
dependencies = { checkAuthorizationToAccessCampaignUsecase },
) {
const userId = request.auth.credentials.userId;
const campaignId = request.params.id;
const belongsToOrganization = await dependencies.checkAuthorizationToAccessCampaignUsecase.execute({
userId,
campaignId,
});

if (belongsToOrganization) return h.response(true);
return _replyForbiddenError(h);
}

function adminMemberHasAtLeastOneAccessOf(securityChecks) {
return async (request, h) => {
const responses = await bluebird.map(securityChecks, (securityCheck) => securityCheck(request, h));
Expand Down Expand Up @@ -626,6 +643,7 @@ const securityPreHandlers = {
checkAdminMemberHasRoleSuperAdmin,
checkAdminMemberHasRoleSupport,
checkAuthorizationToManageCampaign,
checkAuthorizationToAccessCampaign,
checkCertificationCenterIsNotScoManagingStudents,
checkIfUserIsBlocked,
checkPix1dActivated,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as prescriberRoleRepository from '../../infrastructure/repositories/prescriber-role-repository.js';
import { CampaignAuthorization } from '../preHandlers/models/CampaignAuthorization.js';

const execute = async function ({ userId, campaignId }) {
const prescriberRole = await prescriberRoleRepository.getForCampaign({ userId, campaignId });
return CampaignAuthorization.isAllowedToAccess({ prescriberRole });
};

export { execute };
25 changes: 4 additions & 21 deletions api/lib/domain/usecases/get-campaign.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
import { NotFoundError, UserNotAuthorizedToAccessEntityError } from '../../domain/errors.js';

const getCampaign = async function ({
campaignId,
userId,
badgeRepository,
campaignRepository,
campaignReportRepository,
stageCollectionRepository,
}) {
const integerCampaignId = parseInt(campaignId);
if (!Number.isFinite(integerCampaignId)) {
throw new NotFoundError(`Campaign not found for ID ${campaignId}`);
}

const userHasAccessToCampaign = await campaignRepository.checkIfUserOrganizationHasAccessToCampaign(
campaignId,
userId,
);
if (!userHasAccessToCampaign) {
throw new UserNotAuthorizedToAccessEntityError('User does not belong to the organization that owns the campaign');
}

const campaignReport = await campaignReportRepository.get(integerCampaignId);
const campaignReport = await campaignReportRepository.get(campaignId);

if (campaignReport.isAssessment) {
const [badges, stageCollection, aggregatedResults] = await Promise.all([
badgeRepository.findByCampaignId(integerCampaignId),
stageCollectionRepository.findStageCollection({ campaignId: integerCampaignId }),
campaignReportRepository.findMasteryRatesAndValidatedSkillsCount(integerCampaignId),
badgeRepository.findByCampaignId(campaignId),
stageCollectionRepository.findStageCollection({ campaignId: campaignId }),
campaignReportRepository.findMasteryRatesAndValidatedSkillsCount(campaignId),
]);

campaignReport.setBadges(badges);
Expand Down
11 changes: 11 additions & 0 deletions api/src/prescription/campaigns/application/api/Campaign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class Campaign {
constructor({ id, code, name, title, createdAt, archivedAt, customLandingPageText }) {
this.id = id;
this.code = code;
this.name = name;
this.title = title;
this.createdAt = createdAt;
this.archivedAt = archivedAt;
this.customLandingPageText = customLandingPageText;
}
}
17 changes: 15 additions & 2 deletions api/src/prescription/campaigns/application/api/campaigns-api.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { usecases } from '../../../../../lib/domain/usecases/index.js';
import { SavedCampaign } from './SavedCampaign.js';

import { Campaign } from './Campaign.js';
/**
* @typedef CampaignApi
* @type {object}
* @function save
* @function get
*/

/**
Expand All @@ -29,7 +30,7 @@ import { SavedCampaign } from './SavedCampaign.js';
* @name save
*
* @param {CampaignPayload} campaign
* @returns {Promise<SavedCampaignResponse>}
* @returns {Promise<SavedCampaign>}
* @throws {UserNotAuthorizedToCreateCampaignError} to be improved to handle different error types
*/
export const save = async (campaign) => {
Expand All @@ -44,3 +45,15 @@ export const save = async (campaign) => {

return new SavedCampaign(savedCampaign);
};

/**
* @function
* @name get
*
* @param {number} campaignId
* @returns {Promise<Campaign>}
*/
export const get = async (campaignId) => {
const getCampaign = await usecases.getCampaign({ campaignId });
return new Campaign(getCampaign);
};
Original file line number Diff line number Diff line change
Expand Up @@ -1360,21 +1360,6 @@ describe('Acceptance | API | Campaign Controller', function () {

// then
expect(response.statusCode).to.equal(200);
expect(response.result.data.id).to.equal(campaign.id.toString());
expect(response.result.data.attributes.name).to.equal(campaign.name);
});

it('should return HTTP code 403 if the authenticated user is not authorize to access the campaign', async function () {
// given
userId = databaseBuilder.factory.buildUser().id;
options.headers.authorization = generateValidRequestAuthorizationHeader(userId);
await databaseBuilder.commit();

// when
const response = await server.inject(options);

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

Expand Down
10 changes: 0 additions & 10 deletions api/tests/integration/application/campaigns/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ describe('Integration | Application | Route | campaignRouter', function () {
});
});

describe('GET /api/campaigns/{id}', function () {
it('should return a 200', async function () {
// when
const response = await httpTestServer.request('GET', '/api/campaigns/1');

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

describe('GET /api/campaigns/{id}/analyses', function () {
it('should return 200', async function () {
// given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, databaseBuilder, catchErr } from '../../../test-helper.js';
import * as checkAuthorizationToAccessCampaign from '../../../../lib/application/usecases/checkAuthorizationToAccessCampaign.js';

describe('Integration | API | checkAuthorizationToAccessCampaign', function () {
describe('when the user belongs to organization', function () {
it('returns true', async function () {
// given
const organization = databaseBuilder.factory.buildOrganization();
const user = databaseBuilder.factory.buildUser();
databaseBuilder.factory.buildMembership({ userId: user.id, organizationId: organization.id });
const campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id });
await databaseBuilder.commit();

// when
const hasAccess = await checkAuthorizationToAccessCampaign.execute({ campaignId: campaign.id, userId: user.id });

// then
expect(hasAccess).to.be.true;
});
});

describe('when the user does not belong to organization', function () {
it('throws', async function () {
// given
const organization = databaseBuilder.factory.buildOrganization();
const user = databaseBuilder.factory.buildUser();
const campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id });
await databaseBuilder.commit();

// when
const hasAccess = await catchErr(checkAuthorizationToAccessCampaign.execute)({
campaignId: campaign.id,
userId: user.id,
});

// then
expect(hasAccess).to.throws;
});
});
});
35 changes: 1 addition & 34 deletions api/tests/integration/domain/usecases/get-campaign_test.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
import { expect, databaseBuilder, catchErr, mockLearningContent } from '../../../test-helper.js';
import { NotFoundError, UserNotAuthorizedToAccessEntityError } from '../../../../lib/domain/errors.js';
import { expect, databaseBuilder, mockLearningContent } from '../../../test-helper.js';
import { usecases } from '../../../../lib/domain/usecases/index.js';
import * as badgeRepository from '../../../../lib/infrastructure/repositories/badge-repository.js';
import * as campaignRepository from '../../../../lib/infrastructure/repositories/campaign-repository.js';
import * as campaignReportRepository from '../../../../lib/infrastructure/repositories/campaign-report-repository.js';

describe('Integration | UseCase | get-campaign', function () {
context('Error case', function () {
it('should throw a NotFoundError when the campaign not exist', async function () {
const error = await catchErr(usecases.getCampaign)({
campaignId: 'invalid Campaign Id',
userId: 'whateverId',
badgeRepository,
campaignRepository,
campaignReportRepository,
});

expect(error).to.be.instanceOf(NotFoundError);
});

it("UserNotAuthorizedToAccessEntityError when user does not belong to organization's campaign", async function () {
const userId = databaseBuilder.factory.buildUser().id;
const organizationId = databaseBuilder.factory.buildOrganization().id;
const campaignId = databaseBuilder.factory.buildCampaign({ organizationId }).id;

await databaseBuilder.commit();

const error = await catchErr(usecases.getCampaign)({
campaignId,
userId,
badgeRepository,
campaignRepository,
campaignReportRepository,
});

expect(error).to.be.instanceOf(UserNotAuthorizedToAccessEntityError);
});
});

context('Type Assessment', function () {
let userId;
let campaign;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { usecases } from '../../../../../../lib/domain/usecases/index.js';
import * as campaignApi from '../../../../../../src/prescription/campaigns/application/api/campaigns-api.js';
import { expect, sinon, catchErr } from '../../../../../test-helper.js';
import { domainBuilder } from '../../../../../tooling/domain-builder/domain-builder.js';
import { Campaign } from '../../../../../../lib/domain/models/index.js';
import { UserNotAuthorizedToCreateCampaignError } from '../../../../../../lib/domain/errors.js';
import { Campaign } from '../../../../../../lib/domain/models/Campaign.js';

describe('Unit | API | Campaigns', function () {
describe('#save', function () {
Expand Down Expand Up @@ -77,4 +77,34 @@ describe('Unit | API | Campaigns', function () {
});
});
});

describe('#get', function () {
it('should return campaign informations', async function () {
const campaignInformation = domainBuilder.buildCampaign({
id: '777',
code: 'SOMETHING',
name: 'Godzilla',
title: 'is Biohazard',
customLandingPageText: 'Pika pika pikaCHUUUUUUUUUUUUUUUUUU',
createdAt: new Date('2020-01-01'),
archivedAt: new Date('2023-01-01'),
});

const getCampaignStub = sinon.stub(usecases, 'getCampaign');
getCampaignStub.withArgs({ campaignId: campaignInformation.id }).resolves(campaignInformation);

// when
const result = await campaignApi.get(campaignInformation.id);

// then
expect(result.id).to.equal(campaignInformation.id);
expect(result.code).to.equal(campaignInformation.code);
expect(result.name).to.equal(campaignInformation.name);
expect(result.title).to.equal(campaignInformation.title);
expect(result.createdAt).to.equal(campaignInformation.createdAt);
expect(result.archivedAt).to.equal(campaignInformation.archivedAt);
expect(result.customLandingPageText).to.equal(campaignInformation.customLandingPageText);
expect(result).not.to.be.instanceOf(Campaign);
});
});
});
17 changes: 17 additions & 0 deletions api/tests/unit/application/campaign/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('Unit | Application | Router | campaign-router ', function () {
it('should return 200', async function () {
// given
sinon.stub(campaignController, 'getById').callsFake((request, h) => h.response('ok').code(200));
sinon.stub(securityPreHandlers, 'checkAuthorizationToAccessCampaign').callsFake((request, h) => h.response(true));
const httpTestServer = new HttpTestServer();
await httpTestServer.register(moduleUnderTest);

Expand All @@ -67,6 +68,22 @@ describe('Unit | Application | Router | campaign-router ', function () {
expect(response.statusCode).to.equal(200);
});

it('should return 403', async function () {
// given
sinon.stub(campaignController, 'getById').callsFake((request, h) => h.response('ok').code(200));
sinon
.stub(securityPreHandlers, 'checkAuthorizationToAccessCampaign')
.callsFake((request, h) => h.response().code(403).takeover());
const httpTestServer = new HttpTestServer();
await httpTestServer.register(moduleUnderTest);

// when
const response = await httpTestServer.request('GET', '/api/campaigns/1');

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

it('should return 400 with an invalid campaign id', async function () {
// given
const httpTestServer = new HttpTestServer();
Expand Down
Loading

0 comments on commit 5620190

Please sign in to comment.