Skip to content

Commit 03dda9f

Browse files
feat: added paged getByStatus (#1428)
1 parent b2c9345 commit 03dda9f

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed

src/controllers/suggestions.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,65 @@ function SuggestionsController(ctx, sqs, env) {
214214
return ok(suggestions);
215215
};
216216

217+
/**
218+
* Gets all suggestions for a given site, opportunity and status
219+
* @param {Object} context of the request
220+
* @returns {Promise<Response>} Array of suggestions response.
221+
*/
222+
const getByStatusPaged = async (context) => {
223+
const siteId = context.params?.siteId;
224+
const opptyId = context.params?.opportunityId;
225+
const status = context.params?.status || undefined;
226+
const limit = parseInt(context.params?.limit, 10) || DEFAULT_PAGE_SIZE;
227+
const cursor = context.params?.cursor || null;
228+
229+
if (!isValidUUID(siteId)) {
230+
return badRequest('Site ID required');
231+
}
232+
if (!isValidUUID(opptyId)) {
233+
return badRequest('Opportunity ID required');
234+
}
235+
if (!hasText(status)) {
236+
return badRequest('Status is required');
237+
}
238+
239+
if (!isInteger(limit) || limit < 1) {
240+
return badRequest('Page size must be greater than 0');
241+
}
242+
243+
const site = await Site.findById(siteId);
244+
if (!site) {
245+
return notFound('Site not found');
246+
}
247+
248+
if (!await accessControlUtil.hasAccess(site)) {
249+
return forbidden('User does not belong to the organization');
250+
}
251+
252+
const results = await Suggestion.allByOpportunityIdAndStatus(opptyId, status, {
253+
limit,
254+
cursor,
255+
returnCursor: true,
256+
});
257+
const { data: suggestionEntities = [], cursor: newCursor = null } = results;
258+
// Check if the opportunity belongs to the site
259+
if (suggestionEntities.length > 0) {
260+
const oppty = await suggestionEntities[0].getOpportunity();
261+
if (!oppty || oppty.getSiteId() !== siteId) {
262+
return notFound('Opportunity not found');
263+
}
264+
}
265+
const suggestions = suggestionEntities.map((sugg) => SuggestionDto.toJSON(sugg));
266+
return ok({
267+
suggestions,
268+
pagination: {
269+
limit,
270+
cursor: newCursor ?? null,
271+
hasMore: !!newCursor,
272+
},
273+
});
274+
};
275+
217276
/**
218277
* Get a suggestion given a site, opportunity and suggestion ID
219278
* @param {Object} context of the request
@@ -934,6 +993,7 @@ function SuggestionsController(ctx, sqs, env) {
934993
getAllForOpportunityPaged,
935994
getByID,
936995
getByStatus,
996+
getByStatusPaged,
937997
getSuggestionFixes,
938998
patchSuggestion,
939999
patchSuggestionsStatus,

src/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ export default function getRouteHandlers(
187187
'PATCH /sites/:siteId/opportunities/:opportunityId/suggestions/auto-fix': suggestionsController.autofixSuggestions,
188188
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-deploy': suggestionsController.deploySuggestionToEdge,
189189
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus,
190+
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit/:cursor': suggestionsController.getByStatusPaged,
191+
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit': suggestionsController.getByStatusPaged,
190192
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId': suggestionsController.getByID,
191193
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/:suggestionId/fixes': suggestionsController.getSuggestionFixes,
192194
'POST /sites/:siteId/opportunities/:opportunityId/suggestions': suggestionsController.createSuggestions,

test/controllers/suggestions.test.js

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ describe('Suggestions Controller', () => {
147147
'deploySuggestionToEdge',
148148
'getByID',
149149
'getByStatus',
150+
'getByStatusPaged',
150151
'getSuggestionFixes',
151152
'patchSuggestion',
152153
'patchSuggestionsStatus',
@@ -913,6 +914,297 @@ describe('Suggestions Controller', () => {
913914
expect(error).to.have.property('message', 'Opportunity not found');
914915
});
915916

917+
it('gets paged suggestions by status returns bad request if site ID is missing', async () => {
918+
const response = await suggestionsController.getByStatusPaged({
919+
params: {
920+
opportunityId: OPPORTUNITY_ID,
921+
status: 'NEW',
922+
},
923+
...context,
924+
});
925+
expect(response.status).to.equal(400);
926+
const error = await response.json();
927+
expect(error).to.have.property('message', 'Site ID required');
928+
});
929+
930+
it('gets paged suggestions by status returns bad request if opportunity ID is missing', async () => {
931+
const response = await suggestionsController.getByStatusPaged({
932+
params: {
933+
siteId: SITE_ID,
934+
status: 'NEW',
935+
},
936+
...context,
937+
});
938+
expect(response.status).to.equal(400);
939+
const error = await response.json();
940+
expect(error).to.have.property('message', 'Opportunity ID required');
941+
});
942+
943+
it('gets paged suggestions by status returns bad request if status is missing', async () => {
944+
const response = await suggestionsController.getByStatusPaged({
945+
params: {
946+
siteId: SITE_ID,
947+
opportunityId: OPPORTUNITY_ID,
948+
},
949+
...context,
950+
});
951+
expect(response.status).to.equal(400);
952+
const error = await response.json();
953+
expect(error).to.have.property('message', 'Status is required');
954+
});
955+
956+
it('gets paged suggestions by status returns bad request if limit is less than 1', async () => {
957+
const response = await suggestionsController.getByStatusPaged({
958+
params: {
959+
siteId: SITE_ID,
960+
opportunityId: OPPORTUNITY_ID,
961+
status: 'NEW',
962+
limit: -1,
963+
},
964+
...context,
965+
});
966+
expect(response.status).to.equal(400);
967+
const error = await response.json();
968+
expect(error).to.have.property('message', 'Page size must be greater than 0');
969+
});
970+
971+
it('gets paged suggestions by status returns not found if site does not exist', async () => {
972+
const response = await suggestionsController.getByStatusPaged({
973+
params: {
974+
siteId: SITE_ID_NOT_FOUND,
975+
opportunityId: OPPORTUNITY_ID,
976+
status: 'NEW',
977+
},
978+
...context,
979+
});
980+
expect(response.status).to.equal(404);
981+
const error = await response.json();
982+
expect(error).to.have.property('message', 'Site not found');
983+
});
984+
985+
it('gets paged suggestions by status returns forbidden if user does not have access', async () => {
986+
sandbox.stub(AccessControlUtil.prototype, 'hasAccess').returns(false);
987+
const response = await suggestionsController.getByStatusPaged({
988+
params: {
989+
siteId: SITE_ID,
990+
opportunityId: OPPORTUNITY_ID,
991+
status: 'NEW',
992+
},
993+
...context,
994+
});
995+
expect(response.status).to.equal(403);
996+
const error = await response.json();
997+
expect(error).to.have.property('message', 'User does not belong to the organization');
998+
});
999+
1000+
it('gets paged suggestions by status returns not found if opportunity does not belong to site', async () => {
1001+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1002+
if (options) {
1003+
return Promise.resolve({
1004+
data: [mockSuggestionEntity(suggs[0])],
1005+
cursor: undefined,
1006+
});
1007+
}
1008+
return Promise.resolve([mockSuggestionEntity(suggs[0])]);
1009+
});
1010+
const response = await suggestionsController.getByStatusPaged({
1011+
params: {
1012+
siteId: SITE_ID_NOT_ENABLED,
1013+
opportunityId: OPPORTUNITY_ID,
1014+
status: 'NEW',
1015+
},
1016+
...context,
1017+
});
1018+
expect(response.status).to.equal(404);
1019+
const error = await response.json();
1020+
expect(error).to.have.property('message', 'Opportunity not found');
1021+
});
1022+
1023+
it('gets paged suggestions by status successfully', async () => {
1024+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1025+
if (options) {
1026+
return Promise.resolve({
1027+
data: [mockSuggestionEntity(suggs[0])],
1028+
cursor: undefined,
1029+
});
1030+
}
1031+
return Promise.resolve([mockSuggestionEntity(suggs[0])]);
1032+
});
1033+
const response = await suggestionsController.getByStatusPaged({
1034+
params: {
1035+
siteId: SITE_ID,
1036+
opportunityId: OPPORTUNITY_ID,
1037+
status: 'NEW',
1038+
},
1039+
...context,
1040+
});
1041+
expect(mockSuggestionDataAccess.Suggestion.allByOpportunityIdAndStatus.calledOnce).to.be.true;
1042+
expect(response.status).to.equal(200);
1043+
const result = await response.json();
1044+
expect(result).to.have.property('suggestions');
1045+
expect(result.suggestions).to.be.an('array').with.lengthOf(1);
1046+
expect(result).to.have.property('pagination');
1047+
expect(result.pagination).to.deep.equal({
1048+
limit: 100,
1049+
cursor: null,
1050+
hasMore: false,
1051+
});
1052+
});
1053+
1054+
it('gets paged suggestions by status with empty results', async () => {
1055+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1056+
if (options) {
1057+
return Promise.resolve({
1058+
data: [],
1059+
cursor: undefined,
1060+
});
1061+
}
1062+
return Promise.resolve([]);
1063+
});
1064+
const response = await suggestionsController.getByStatusPaged({
1065+
params: {
1066+
siteId: SITE_ID,
1067+
opportunityId: OPPORTUNITY_ID,
1068+
status: 'NEW',
1069+
},
1070+
...context,
1071+
});
1072+
expect(response.status).to.equal(200);
1073+
const result = await response.json();
1074+
expect(result.suggestions).to.be.an('array').with.lengthOf(0);
1075+
expect(result.pagination).to.deep.equal({
1076+
limit: 100,
1077+
cursor: null,
1078+
hasMore: false,
1079+
});
1080+
});
1081+
1082+
it('gets paged suggestions by status successfully when parameters come as strings from URL', async () => {
1083+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1084+
if (options) {
1085+
return Promise.resolve({
1086+
data: [mockSuggestionEntity(suggs[0])],
1087+
cursor: 'next-cursor-value',
1088+
});
1089+
}
1090+
return Promise.resolve([mockSuggestionEntity(suggs[0])]);
1091+
});
1092+
const response = await suggestionsController.getByStatusPaged({
1093+
params: {
1094+
siteId: SITE_ID,
1095+
opportunityId: OPPORTUNITY_ID,
1096+
status: 'NEW',
1097+
limit: '50',
1098+
cursor: 'some-cursor',
1099+
},
1100+
...context,
1101+
});
1102+
expect(mockSuggestionDataAccess.Suggestion.allByOpportunityIdAndStatus.calledOnce).to.be.true;
1103+
expect(response.status).to.equal(200);
1104+
const result = await response.json();
1105+
expect(result.suggestions).to.be.an('array').with.lengthOf(1);
1106+
expect(result.pagination).to.deep.equal({
1107+
limit: 50,
1108+
cursor: 'next-cursor-value',
1109+
hasMore: true,
1110+
});
1111+
});
1112+
1113+
it('gets paged suggestions by status with cursor parameter calls with correct options', async () => {
1114+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1115+
if (options) {
1116+
return Promise.resolve({
1117+
data: [mockSuggestionEntity(suggs[0])],
1118+
cursor: 'next-cursor',
1119+
});
1120+
}
1121+
return Promise.resolve([mockSuggestionEntity(suggs[0])]);
1122+
});
1123+
1124+
await suggestionsController.getByStatusPaged({
1125+
params: {
1126+
siteId: SITE_ID,
1127+
opportunityId: OPPORTUNITY_ID,
1128+
status: 'NEW',
1129+
limit: 25,
1130+
cursor: 'previous-cursor',
1131+
},
1132+
...context,
1133+
});
1134+
1135+
expect(mockSuggestion.allByOpportunityIdAndStatus).to.have.been.calledWith(
1136+
OPPORTUNITY_ID,
1137+
'NEW',
1138+
{
1139+
limit: 25,
1140+
cursor: 'previous-cursor',
1141+
returnCursor: true,
1142+
},
1143+
);
1144+
});
1145+
1146+
it('gets paged suggestions by status handles null opportunity correctly', async () => {
1147+
const mockSuggestionWithNullOpportunity = {
1148+
...mockSuggestionEntity(suggs[0]),
1149+
getOpportunity: sandbox.stub().returns(null),
1150+
};
1151+
1152+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1153+
if (options) {
1154+
return Promise.resolve({
1155+
data: [mockSuggestionWithNullOpportunity],
1156+
cursor: null,
1157+
});
1158+
}
1159+
return Promise.resolve([mockSuggestionWithNullOpportunity]);
1160+
});
1161+
1162+
const response = await suggestionsController.getByStatusPaged({
1163+
params: {
1164+
siteId: SITE_ID,
1165+
opportunityId: OPPORTUNITY_ID,
1166+
status: 'NEW',
1167+
},
1168+
...context,
1169+
});
1170+
1171+
expect(response.status).to.equal(404);
1172+
const error = await response.json();
1173+
expect(error).to.have.property('message', 'Opportunity not found');
1174+
});
1175+
1176+
it('gets paged suggestions by status handles opportunity with wrong site ID', async () => {
1177+
const mockSuggestionWithWrongSite = {
1178+
...mockSuggestionEntity(suggs[0]),
1179+
getOpportunity: sandbox.stub().returns({
1180+
getSiteId: () => SITE_ID_NOT_ENABLED,
1181+
}),
1182+
};
1183+
1184+
mockSuggestion.allByOpportunityIdAndStatus.callsFake((opptyId, status, options) => {
1185+
if (options) {
1186+
return Promise.resolve({
1187+
data: [mockSuggestionWithWrongSite],
1188+
cursor: null,
1189+
});
1190+
}
1191+
return Promise.resolve([mockSuggestionWithWrongSite]);
1192+
});
1193+
1194+
const response = await suggestionsController.getByStatusPaged({
1195+
params: {
1196+
siteId: SITE_ID,
1197+
opportunityId: OPPORTUNITY_ID,
1198+
status: 'NEW',
1199+
},
1200+
...context,
1201+
});
1202+
1203+
expect(response.status).to.equal(404);
1204+
const error = await response.json();
1205+
expect(error).to.have.property('message', 'Opportunity not found');
1206+
});
1207+
9161208
it('gets suggestion by ID', async () => {
9171209
const response = await suggestionsController.getByID({
9181210
params: {

0 commit comments

Comments
 (0)