From 6305402ade53e2766e0436a5f9c1e5a3b348645d Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:14:28 +0200 Subject: [PATCH 01/39] Add campaign bundle (#298) * feat: Add basic design for dossiers * feat: Add basic structure for campaigns dossier * feat: Add device list * rework: Split to methods * ci: Add implicit dependency * feat: Add put campaign * feat: Add get dossier * feat: Add customer name * feat: Allow setting csm --- Dockerfile | 8 +- src/reference/openapi.yml | 238 +++++++++++++++ src/routes/dossiers/_post/creation.spec.ts | 273 +++++++++++++++++ src/routes/dossiers/_post/index.spec.ts | 96 ++++++ src/routes/dossiers/_post/index.ts | 116 ++++++++ .../dossiers/campaignId/_get/index.spec.ts | 199 +++++++++++++ src/routes/dossiers/campaignId/_get/index.ts | 152 ++++++++++ .../dossiers/campaignId/_put/index.spec.ts | 119 ++++++++ src/routes/dossiers/campaignId/_put/index.ts | 147 ++++++++++ .../dossiers/campaignId/_put/update.spec.ts | 274 ++++++++++++++++++ src/schema.ts | 109 +++++++ 11 files changed, 1724 insertions(+), 7 deletions(-) create mode 100644 src/routes/dossiers/_post/creation.spec.ts create mode 100644 src/routes/dossiers/_post/index.spec.ts create mode 100644 src/routes/dossiers/_post/index.ts create mode 100644 src/routes/dossiers/campaignId/_get/index.spec.ts create mode 100644 src/routes/dossiers/campaignId/_get/index.ts create mode 100644 src/routes/dossiers/campaignId/_put/index.spec.ts create mode 100644 src/routes/dossiers/campaignId/_put/index.ts create mode 100644 src/routes/dossiers/campaignId/_put/update.spec.ts diff --git a/Dockerfile b/Dockerfile index a4e37746a..1f0931ecd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,13 +21,7 @@ RUN yarn global add npm-run-all RUN yarn build -FROM alpine:3.16 as web - -COPY --from=node /usr/lib /usr/lib -COPY --from=node /usr/local/share /usr/local/share -COPY --from=node /usr/local/lib /usr/local/lib -COPY --from=node /usr/local/include /usr/local/include -COPY --from=node /usr/local/bin /usr/local/bin +FROM node:18-alpine3.16 AS web COPY --from=base /dist /app/build COPY package*.json /app/ diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index f3e923e4f..e1d11f53a 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9665,6 +9665,192 @@ paths: operationId: get-users-me-rank-list security: - JWT: [] + /dossiers: + parameters: [] + post: + summary: '' + operationId: post-dossiers + responses: + '201': + description: Created + content: + application/json: + schema: + type: object + properties: + id: + type: integer + x-stoplight: + id: 965rh7aagxj8x + examples: + Example 1: + value: + id: 1 + requestBody: + $ref: '#/components/requestBodies/DossierData' + security: + - JWT: [] + '/dossiers/{campaign}': + parameters: + - name: campaign + in: path + required: true + schema: + type: string + description: A campaign id + put: + summary: '' + operationId: put-dossiers-campaign + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: {} + description: '' + requestBody: + $ref: '#/components/requestBodies/DossierData' + security: + - JWT: [] + get: + summary: '' + operationId: get-dossiers-campaign + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + x-stoplight: + id: qqon32evab2o5 + title: + type: object + x-stoplight: + id: qfn9vuux1vjp1 + required: + - customer + - tester + properties: + customer: + type: string + x-stoplight: + id: tyx8mtant8jej + tester: + type: string + x-stoplight: + id: cupl63jal29yu + startDate: + type: string + x-stoplight: + id: lif1wyyh6miiy + format: date-time + endDate: + type: string + x-stoplight: + id: dhx6h9l494pyw + format: date-time + customer: + type: object + x-stoplight: + id: b6vdml702n2vr + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: hzxvga289ilzv + name: + type: string + x-stoplight: + id: zxk4wi6clmaxr + project: + type: object + x-stoplight: + id: 9dnwnxk98slrj + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: 795lyzmcn2f3i + name: + type: string + x-stoplight: + id: lonlyyu0xqpx9 + testType: + type: object + x-stoplight: + id: y2l96ujpgntcx + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: 5ut1dht9q3e5f + name: + type: string + x-stoplight: + id: 3zafe5jvg89hd + deviceList: + type: array + x-stoplight: + id: 8ac5l94cifolj + items: + x-stoplight: + id: p874do1llblvt + type: object + properties: + id: + type: integer + x-stoplight: + id: 64zrvfx7v23m8 + name: + type: string + x-stoplight: + id: 838zaws7zfcb6 + required: + - id + - name + csm: + type: object + x-stoplight: + id: ye4vllt9hwd40 + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: 29ini0mkuenx4 + name: + type: string + x-stoplight: + id: 0v4z9eu7jfeja + required: + - id + - title + - startDate + - endDate + - customer + - project + - testType + - deviceList + - csm + security: + - JWT: [] components: schemas: AdditionalField: @@ -10775,6 +10961,58 @@ components: schema: type: string examples: {} + requestBodies: + DossierData: + content: + application/json: + schema: + type: object + x-examples: + Example 1: + project: 1 + testType: 1 + title: + customer: Campaign Title for Customer + tester: Campaign Title for Tester + startDate: '2019-08-24T14:15:22Z' + endDate: '2019-08-24T14:15:22Z' + deviceList: + - 1 + - 5 + - 10 + - 36 + properties: + project: + type: integer + testType: + type: integer + title: + type: object + required: + - customer + properties: + customer: + type: string + tester: + type: string + startDate: + type: string + endDate: + type: string + deviceList: + type: array + items: + type: integer + csm: + type: number + x-stoplight: + id: tczilke0whidg + required: + - project + - testType + - title + - startDate + - deviceList tags: - name: Authentication - name: Campaign diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts new file mode 100644 index 000000000..218dfdaeb --- /dev/null +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -0,0 +1,273 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const baseRequest = { + project: 10, + testType: 10, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers", () => { + beforeAll(async () => { + await tryber.tables.WpAppqCustomer.do().insert({ + id: 1, + company: "Test Company", + pm_id: 1, + }); + await tryber.tables.WpAppqProject.do().insert({ + id: 10, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 10, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + { + id: 2, + name: "Test Type", + form_factor: 1, + architecture: 1, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should create a campaign", async () => { + const postResponse = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(postResponse.status).toBe(201); + expect(postResponse.body).toHaveProperty("id"); + + const getResponse = await request(app) + .get(`/campaigns/${postResponse.body.id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + }); + + it("Should create a campaign linked to the specified project", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, project: 10 }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("project_id", 10); + }); + + it("Should create a campaign linked to the specified test type", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, testType: 10 }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("campaign_type_id", 10); + }); + + it("Should create a campaign with the specified title", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + title: { ...baseRequest.title, tester: "new title" }, + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("title", "new title"); + }); + it("Should create a campaign with the specified customer title", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + title: { ...baseRequest.title, customer: "new title" }, + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("customer_title", "new title"); + }); + + it("Should create a campaign with the specified start date", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-24T14:15:22Z", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("start_date", "2021-08-24T14:15:22Z"); + }); + + it("Should create a campaign with the specified end date ", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + endDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("end_date", "2021-08-20T14:15:22Z"); + }); + + it("Should create a campaign with the end date as start date + 7 if left unspecified", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("end_date", "2021-08-27T14:15:22Z"); + }); + + it("Should create a campaign with current user as pm_id if left unspecified", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("pm_id", 1); + }); + + it("Should create a campaign with current user as pm_id if specified", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, csm: 2 }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("pm_id", 2); + }); + + it("Should create a campaign with the specified device list", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceList: [1, 2] }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("os", "1,2"); + expect(campaign).toHaveProperty("form_factor", "0,1"); + }); +}); diff --git a/src/routes/dossiers/_post/index.spec.ts b/src/routes/dossiers/_post/index.spec.ts new file mode 100644 index 000000000..356694715 --- /dev/null +++ b/src/routes/dossiers/_post/index.spec.ts @@ -0,0 +1,96 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should answer 403 if not logged in", async () => { + const response = await request(app).post("/dossiers").send(baseRequest); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if not admin", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer tester") + .send(baseRequest); + console.log(response.body); + expect(response.status).toBe(403); + }); + + it("Should answer 400 if project does not exists", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, project: 10 }); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if test type does not exists", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, testType: 10 }); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if device type does not exists", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceList: [10] }); + expect(response.status).toBe(400); + }); + + it("Should answer 201 if admin", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send(baseRequest); + expect(response.status).toBe(201); + }); +}); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts new file mode 100644 index 000000000..f9155f13b --- /dev/null +++ b/src/routes/dossiers/_post/index.ts @@ -0,0 +1,116 @@ +/** OPENAPI-CLASS: post-dossiers */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import AdminRoute from "@src/features/routes/AdminRoute"; + +export default class RouteItem extends AdminRoute<{ + response: StoplightOperations["post-dossiers"]["responses"]["201"]["content"]["application/json"]; + body: StoplightOperations["post-dossiers"]["requestBody"]["content"]["application/json"]; +}> { + protected async filter() { + if (!(await super.filter())) return false; + if (!(await this.projectExists())) { + this.setError(400, new OpenapiError("Project does not exist")); + return false; + } + if (!(await this.testTypeExists())) { + this.setError(400, new OpenapiError("Test type does not exist")); + return false; + } + if (!(await this.deviceExists())) { + this.setError(400, new OpenapiError("Invalid devices")); + return false; + } + + return true; + } + + private async projectExists(): Promise { + const { project: projectId } = this.getBody(); + const project = await tryber.tables.WpAppqProject.do() + .select() + .where({ + id: projectId, + }) + .first(); + return !!project; + } + + private async testTypeExists(): Promise { + const { testType: testTypeId } = this.getBody(); + const testType = await tryber.tables.WpAppqCampaignType.do() + .select() + .where({ + id: testTypeId, + }) + .first(); + return !!testType; + } + + private async deviceExists(): Promise { + const { deviceList } = this.getBody(); + const devices = await tryber.tables.WpAppqEvdPlatform.do() + .select() + .whereIn("id", deviceList); + return devices.length === deviceList.length; + } + + protected async prepare(): Promise { + try { + this.setSuccess(201, { + id: await this.createCampaign(), + }); + } catch (e) { + this.setError(500, e as OpenapiError); + } + } + + private async createCampaign() { + const { os, form_factor } = await this.getDevices(); + + const results = await tryber.tables.WpAppqEvdCampaign.do() + .insert({ + title: this.getBody().title.tester, + platform_id: 0, + start_date: this.getBody().startDate, + end_date: this.getEndDate(), + page_preview_id: 0, + page_manual_id: 0, + customer_id: 0, + pm_id: this.getCsmId(), + project_id: this.getBody().project, + campaign_type_id: this.getBody().testType, + customer_title: this.getBody().title.customer, + os: os.join(","), + form_factor: form_factor.join(","), + }) + .returning("id"); + + return results[0].id ?? results[0]; + } + + private getCsmId() { + const { csm } = this.getBody(); + return csm ? csm : this.getTesterId(); + } + + private getEndDate() { + if (this.getBody().endDate) return this.getBody().endDate; + + const startDate = new Date(this.getBody().startDate); + startDate.setDate(startDate.getDate() + 7); + return startDate.toISOString().replace(/\.\d+/, ""); + } + + private async getDevices() { + const devices = await tryber.tables.WpAppqEvdPlatform.do() + .select("id", "form_factor") + .whereIn("id", this.getBody().deviceList); + + const os = devices.map((device) => device.id); + const form_factor = devices.map((device) => device.form_factor); + + return { os, form_factor }; + } +} diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts new file mode 100644 index 000000000..f81b6c1e1 --- /dev/null +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -0,0 +1,199 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("Route GET /dossiers/:id", () => { + beforeAll(async () => { + await tryber.tables.WpAppqCustomer.do().insert({ + id: 1, + company: "Test Company", + pm_id: 1, + }); + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + name: "Test", + surname: "CSM", + email: "", + education_id: 1, + employment_id: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Device", + form_factor: 0, + architecture: 1, + }, + ]); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + project_id: 1, + campaign_type_id: 1, + title: "Test Campaign", + customer_title: "Test Customer Campaign", + start_date: "2019-08-24T14:15:22Z", + end_date: "2019-08-24T14:15:22Z", + platform_id: 0, + os: "1", + page_manual_id: 0, + page_preview_id: 0, + pm_id: 1, + customer_id: 0, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + + it("Should answer 403 if not logged in", async () => { + const response = await request(app).get("/dossiers/1"); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if campaign does not exists", async () => { + const response = await request(app).get("/dossiers/10"); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if not admin", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer tester"); + console.log(response.body); + expect(response.status).toBe(403); + }); + + it("Should answer 200 if admin", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + console.log(response.body); + expect(response.status).toBe(200); + }); + + it("Should return the campaign id", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("id"); + expect(response.body.id).toBe(1); + }); + it("Should return the campaign tester title", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("title"); + expect(response.body.title).toHaveProperty("tester", "Test Campaign"); + }); + + it("Should return the campaign customer title", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("title"); + expect(response.body.title).toHaveProperty( + "customer", + "Test Customer Campaign" + ); + }); + + it("Should return the project", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("project"); + expect(response.body.project).toHaveProperty("id", 1); + expect(response.body.project).toHaveProperty("name", "Test Project"); + }); + + it("Should return the test type", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("testType"); + expect(response.body.testType).toHaveProperty("id", 1); + expect(response.body.testType).toHaveProperty("name", "Test Type"); + }); + + it("Should return the start date", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("startDate", "2019-08-24T14:15:22Z"); + }); + + it("Should return the end date", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("endDate", "2019-08-24T14:15:22Z"); + }); + + it("Should return the device list", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("deviceList"); + expect(response.body.deviceList).toHaveLength(1); + expect(response.body.deviceList[0]).toHaveProperty("id", 1); + expect(response.body.deviceList[0]).toHaveProperty("name", "Test Device"); + }); + + it("Should return the csm", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("csm"); + expect(response.body.csm).toHaveProperty("id", 1); + expect(response.body.csm).toHaveProperty("name", "Test CSM"); + }); + it("Should return the customer", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("customer"); + expect(response.body.customer).toHaveProperty("id", 1); + expect(response.body.customer).toHaveProperty("name", "Test Company"); + }); +}); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts new file mode 100644 index 000000000..19034d493 --- /dev/null +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -0,0 +1,152 @@ +/** OPENAPI-CLASS: get-dossiers-campaign */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import AdminRoute from "@src/features/routes/AdminRoute"; + +export default class RouteItem extends AdminRoute<{ + response: StoplightOperations["get-dossiers-campaign"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["get-dossiers-campaign"]["parameters"]["path"]; +}> { + private campaignId: number; + private _campaign: Awaited> | undefined; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + this.campaignId = Number(this.getParameters().campaign); + } + + protected async init(): Promise { + await super.init(); + try { + this._campaign = await this.getCampaign(); + } catch (e) { + this.setError(500, e as OpenapiError); + throw e; + } + } + + private async getCampaign() { + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select( + "end_date", + tryber.ref("id").withSchema("wp_appq_evd_campaign"), + "title", + "customer_title", + "project_id", + "start_date", + "campaign_type_id", + "os", + tryber + .ref("display_name") + .withSchema("wp_appq_project") + .as("project_name"), + tryber + .ref("name") + .withSchema("wp_appq_campaign_type") + .as("campaign_type_name"), + tryber.ref("pm_id").withSchema("wp_appq_evd_campaign"), + tryber.ref("name").withSchema("wp_appq_evd_profile").as("pm_name"), + tryber + .ref("surname") + .withSchema("wp_appq_evd_profile") + .as("pm_surname"), + tryber + .ref("company") + .withSchema("wp_appq_customer") + .as("customer_name"), + tryber + .ref("customer_id") + .withSchema("wp_appq_project") + .as("customer_id") + ) + .join( + "wp_appq_project", + "wp_appq_project.id", + "wp_appq_evd_campaign.project_id" + ) + .join( + "wp_appq_customer", + "wp_appq_customer.id", + "wp_appq_project.customer_id" + ) + .join( + "wp_appq_campaign_type", + "wp_appq_campaign_type.id", + "wp_appq_evd_campaign.campaign_type_id" + ) + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.id", + "wp_appq_evd_campaign.pm_id" + ) + .where("wp_appq_evd_campaign.id", this.campaignId) + .first(); + + if (!campaign) return undefined; + + const devices = await tryber.tables.WpAppqEvdPlatform.do() + .select("id", "name") + .whereIn("id", campaign.os.split(",")); + + return { ...campaign, devices }; + } + + get campaign() { + if (!this._campaign) throw new Error("Campaign not found"); + return this._campaign; + } + + protected async filter() { + if (!(await super.filter())) return false; + + if (!(await this.campaignExists())) { + this.setError(403, new OpenapiError("Campaign does not exist")); + return false; + } + + return true; + } + + private async campaignExists(): Promise { + try { + this.campaign; + return true; + } catch (e) { + return false; + } + } + + protected async prepare(): Promise { + try { + this.setSuccess(200, { + id: this.campaign.id, + title: { + customer: this.campaign.customer_title, + tester: this.campaign.title, + }, + customer: { + id: this.campaign.customer_id, + name: this.campaign.customer_name, + }, + project: { + id: this.campaign.project_id, + name: this.campaign.project_name, + }, + testType: { + id: this.campaign.campaign_type_id, + name: this.campaign.campaign_type_name, + }, + startDate: this.campaign.start_date, + endDate: this.campaign.end_date, + deviceList: this.campaign.devices, + csm: { + id: this.campaign.pm_id, + name: `${this.campaign.pm_name} ${this.campaign.pm_surname}`, + }, + }); + } catch (e) { + this.setError(500, e as OpenapiError); + } + } +} diff --git a/src/routes/dossiers/campaignId/_put/index.spec.ts b/src/routes/dossiers/campaignId/_put/index.spec.ts new file mode 100644 index 000000000..297711776 --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/index.spec.ts @@ -0,0 +1,119 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route PUT /dossiers/:id", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + beforeEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + project_id: 1, + campaign_type_id: 1, + title: "Test Campaign", + customer_title: "Test Customer Campaign", + start_date: "2019-08-24T14:15:22Z", + end_date: "2019-08-24T14:15:22Z", + platform_id: 0, + os: "1", + page_manual_id: 0, + page_preview_id: 0, + pm_id: 1, + customer_id: 0, + }); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should answer 403 if not logged in", async () => { + const response = await request(app).put("/dossiers/1").send(baseRequest); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if campaign does not exists", async () => { + const response = await request(app).put("/dossiers/10").send(baseRequest); + expect(response.status).toBe(403); + }); + + it("Should answer 403 if not admin", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer tester") + .send(baseRequest); + console.log(response.body); + expect(response.status).toBe(403); + }); + + it("Should answer 400 if project does not exists", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, project: 10 }); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if test type does not exists", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, testType: 10 }); + expect(response.status).toBe(400); + }); + + it("Should answer 400 if device type does not exists", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceList: [10] }); + expect(response.status).toBe(400); + }); + + it("Should answer 200 if admin", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send(baseRequest); + expect(response.status).toBe(200); + }); +}); diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts new file mode 100644 index 000000000..ed6f736ea --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -0,0 +1,147 @@ +/** OPENAPI-CLASS: put-dossiers-campaign */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import AdminRoute from "@src/features/routes/AdminRoute"; + +export default class RouteItem extends AdminRoute<{ + response: StoplightOperations["put-dossiers-campaign"]["responses"]["200"]["content"]["application/json"]; + body: StoplightOperations["put-dossiers-campaign"]["requestBody"]["content"]["application/json"]; + parameters: StoplightOperations["put-dossiers-campaign"]["parameters"]["path"]; +}> { + private campaignId: number; + private _campaign: { end_date: string } | undefined; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + this.campaignId = Number(this.getParameters().campaign); + } + + protected async init(): Promise { + await super.init(); + this._campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("end_date") + .where({ + id: this.campaignId, + }) + .first(); + } + + get campaign() { + if (!this._campaign) throw new Error("Campaign not found"); + return this._campaign; + } + protected async filter() { + if (!(await super.filter())) return false; + + if (!(await this.campaignExists())) { + this.setError(403, new OpenapiError("Campaign does not exist")); + return false; + } + if (!(await this.projectExists())) { + this.setError(400, new OpenapiError("Project does not exist")); + return false; + } + if (!(await this.testTypeExists())) { + this.setError(400, new OpenapiError("Test type does not exist")); + return false; + } + if (!(await this.deviceExists())) { + this.setError(400, new OpenapiError("Invalid devices")); + return false; + } + + return true; + } + + private async campaignExists(): Promise { + try { + this.campaign; + return true; + } catch (e) { + return false; + } + } + + private async projectExists(): Promise { + const { project: projectId } = this.getBody(); + const project = await tryber.tables.WpAppqProject.do() + .select() + .where({ + id: projectId, + }) + .first(); + return !!project; + } + + private async testTypeExists(): Promise { + const { testType: testTypeId } = this.getBody(); + const testType = await tryber.tables.WpAppqCampaignType.do() + .select() + .where({ + id: testTypeId, + }) + .first(); + return !!testType; + } + + private async deviceExists(): Promise { + const { deviceList } = this.getBody(); + const devices = await tryber.tables.WpAppqEvdPlatform.do() + .select() + .whereIn("id", deviceList); + return devices.length === deviceList.length; + } + + protected async prepare(): Promise { + try { + await this.updateCampaign(); + this.setSuccess(200, { + id: this.campaignId, + }); + } catch (e) { + this.setError(500, e as OpenapiError); + } + } + + private async updateCampaign() { + const { os, form_factor } = await this.getDevices(); + + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + title: this.getBody().title.tester, + platform_id: 0, + start_date: this.getBody().startDate, + end_date: this.getEndDate(), + page_preview_id: 0, + page_manual_id: 0, + customer_id: 0, + pm_id: this.getTesterId(), + project_id: this.getBody().project, + campaign_type_id: this.getBody().testType, + customer_title: this.getBody().title.customer, + os: os.join(","), + form_factor: form_factor.join(","), + }) + .where({ + id: this.campaignId, + }); + } + + private getEndDate() { + if (this.getBody().endDate) return this.getBody().endDate; + + return this.campaign.end_date; + } + + private async getDevices() { + const devices = await tryber.tables.WpAppqEvdPlatform.do() + .select("id", "form_factor") + .whereIn("id", this.getBody().deviceList); + + const os = devices.map((device) => device.id); + const form_factor = devices.map((device) => device.form_factor); + + return { os, form_factor }; + } +} diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts new file mode 100644 index 000000000..c730ebf88 --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -0,0 +1,274 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const baseRequest = { + project: 10, + testType: 10, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers", () => { + beforeAll(async () => { + await tryber.tables.WpAppqCustomer.do().insert({ + id: 1, + company: "Test Company", + pm_id: 1, + }); + await tryber.tables.WpAppqProject.do().insert([ + { + id: 10, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }, + { + id: 11, + display_name: "Test Project 11", + customer_id: 1, + edited_by: 1, + }, + ]); + + await tryber.tables.WpAppqCampaignType.do().insert([ + { + id: 10, + name: "Test Type", + description: "Test Description", + category_id: 1, + }, + { + id: 11, + name: "Test Type", + description: "Test Description", + category_id: 1, + }, + ]); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + { + id: 2, + name: "Test Type", + form_factor: 1, + architecture: 1, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + project_id: 1, + campaign_type_id: 1, + title: "Test Campaign", + customer_title: "Test Customer Campaign", + start_date: "2019-08-24T14:15:22Z", + end_date: "2019-08-24T14:15:22Z", + platform_id: 0, + os: "1", + page_manual_id: 0, + page_preview_id: 0, + pm_id: 1, + customer_id: 0, + }); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should update the campaign to be linked to the specified project", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, project: 11 }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("project_id", 11); + }); + + it("Should updte the campaign to be linked to the specified test type", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, testType: 11 }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("campaign_type_id", 11); + }); + + it("Should update the campaign with the specified title", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + title: { ...baseRequest.title, tester: "new title" }, + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("title", "new title"); + }); + + it("Should update the campaign with the specified customer title", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + title: { ...baseRequest.title, customer: "new title" }, + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("customer_title", "new title"); + }); + + it("Should update the campaign with the specified start date", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-24T14:15:22Z", + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("start_date", "2021-08-24T14:15:22Z"); + }); + + it("Should update the campaign with the specified end date ", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + endDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("end_date", "2021-08-20T14:15:22Z"); + }); + + it("Should leave the end date of the campaign unedited if left unspecified", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("end_date", "2019-08-24T14:15:22Z"); + }); + + it("Should update the campaign with current user as pm_id", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("pm_id", 1); + }); + + it("Should update campaign with the specified device list", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceList: [1, 2] }); + + expect(response.status).toBe(200); + + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("os", "1,2"); + expect(campaign).toHaveProperty("form_factor", "0,1"); + }); +}); diff --git a/src/schema.ts b/src/schema.ts index 465a9b5b9..527ac21b9 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -540,6 +540,20 @@ export interface paths { "/users/me/rank/list": { get: operations["get-users-me-rank-list"]; }; + "/dossiers": { + post: operations["post-dossiers"]; + parameters: {}; + }; + "/dossiers/{campaign}": { + get: operations["get-dossiers-campaign"]; + put: operations["put-dossiers-campaign"]; + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + }; } export interface components { @@ -935,6 +949,24 @@ export interface components { search: string; testerId: string; }; + requestBodies: { + DossierData: { + content: { + "application/json": { + project: number; + testType: number; + title: { + customer: string; + tester?: string; + }; + startDate: string; + endDate?: string; + deviceList: number[]; + csm?: number; + }; + }; + }; + }; } export interface operations { @@ -3975,6 +4007,83 @@ export interface operations { 404: components["responses"]["NotFound"]; }; }; + "post-dossiers": { + parameters: {}; + responses: { + /** Created */ + 201: { + content: { + "application/json": { + id?: number; + }; + }; + }; + }; + requestBody: components["requestBodies"]["DossierData"]; + }; + "get-dossiers-campaign": { + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + id: number; + title: { + customer: string; + tester: string; + }; + /** Format: date-time */ + startDate: string; + /** Format: date-time */ + endDate: string; + customer: { + id: number; + name: string; + }; + project: { + id: number; + name: string; + }; + testType: { + id: number; + name: string; + }; + deviceList: { + id: number; + name: string; + }[]; + csm: { + id: number; + name: string; + }; + }; + }; + }; + }; + }; + "put-dossiers-campaign": { + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { [key: string]: unknown }; + }; + }; + }; + requestBody: components["requestBodies"]["DossierData"]; + }; } export interface external {} From a35bd4af55db6afbb3e04bc3f066dbdb67a340ae Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:22:10 +0200 Subject: [PATCH 02/39] feat: Get projects by customer (#300) * feat: Add basic design for dossiers * feat: Add basic structure for campaigns dossier * feat: Add device list * rework: Split to methods * ci: Add implicit dependency * feat: Add put campaign * feat: Add get dossier * feat: Get projects by customer * feat: Restore design --- src/reference/openapi.yml | 44 +++++++ .../customer/projects/_get/index.spec.ts | 118 ++++++++++++++++++ .../customers/customer/projects/_get/index.ts | 62 +++++++++ src/schema.ts | 28 +++++ 4 files changed, 252 insertions(+) create mode 100644 src/routes/customers/customer/projects/_get/index.spec.ts create mode 100644 src/routes/customers/customer/projects/_get/index.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index e1d11f53a..e5e9cca81 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9851,6 +9851,50 @@ paths: - csm security: - JWT: [] + '/customers/{customer}/projects': + parameters: + - schema: + type: string + name: customer + in: path + required: true + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + x-stoplight: + id: 9ccc89bhovco5 + items: + x-stoplight: + id: zog5qqnv5h7rb + type: object + properties: + id: + type: integer + x-stoplight: + id: xapq51u019ada + name: + type: string + x-stoplight: + id: 8fxvw6c3m46bv + required: + - id + - name + required: + - results + operationId: get-customers-customer-projects + description: '' + security: + - JWT: [] components: schemas: AdditionalField: diff --git a/src/routes/customers/customer/projects/_get/index.spec.ts b/src/routes/customers/customer/projects/_get/index.spec.ts new file mode 100644 index 000000000..66c158a7e --- /dev/null +++ b/src/routes/customers/customer/projects/_get/index.spec.ts @@ -0,0 +1,118 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const campaign = { + platform_id: 1, + start_date: "2023-01-13 10:10:10", + end_date: "2023-01-14 10:10:10", + title: "", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + customer_title: "", +}; +const project = { + display_name: "", + edited_by: 1, +}; +describe("GET /campaigns", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...campaign, + id: 1, + project_id: 1, + }, + { + ...campaign, + id: 2, + project_id: 2, + }, + { + ...campaign, + id: 3, + project_id: 3, + }, + ]); + await tryber.tables.WpAppqProject.do().insert([ + { + ...project, + display_name: "Project 1", + id: 1, + customer_id: 1, + }, + { + ...project, + display_name: "Project 2", + id: 2, + customer_id: 1, + }, + { + ...project, + display_name: "Project 3", + id: 3, + customer_id: 2, + }, + ]); + await tryber.tables.WpAppqCustomer.do().insert([ + { + id: 1, + company: "Company 1", + pm_id: 1, + }, + { + id: 2, + company: "Company 2", + pm_id: 1, + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + }); + + it("Should answer 403 if not logged in", () => { + return request(app).get("/customers/1/projects").expect(403); + }); + it("Should answer 403 if logged in without permissions", async () => { + const response = await request(app) + .get("/customers/1/projects") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + it("Should answer 403 if customer does not exists", async () => { + const response = await request(app) + .get("/customers/100/projects") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(403); + }); + it("Should answer 200 if logged as user with full access on campaigns", async () => { + const response = await request(app) + .get("/customers/1/projects") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + }); + it("Should answer 403 if logged as user with access to some campaigns", async () => { + const response = await request(app) + .get("/customers/1/projects") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1,2]}'); + expect(response.status).toBe(403); + }); + it("Should answer with a list of all projects of the customer if has full access", async () => { + const response = await request(app) + .get("/customers/1/projects") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("results"); + expect(Array.isArray(response.body.results)).toBe(true); + expect(response.body.results.length).toBe(2); + expect(response.body.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 1, name: "Project 1" }), + expect.objectContaining({ id: 2, name: "Project 2" }), + ]) + ); + }); +}); diff --git a/src/routes/customers/customer/projects/_get/index.ts b/src/routes/customers/customer/projects/_get/index.ts new file mode 100644 index 000000000..f1dc3e01e --- /dev/null +++ b/src/routes/customers/customer/projects/_get/index.ts @@ -0,0 +1,62 @@ +/** OPENAPI-CLASS : get-customers-customer-projects */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +class RouteItem extends UserRoute<{ + response: StoplightOperations["get-customers-customer-projects"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["get-customers-customer-projects"]["parameters"]["path"]; +}> { + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + private customerId: number; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + this.customerId = Number(this.getParameters().customer); + } + + protected async filter() { + if ((await super.filter()) === false) return false; + if (this.doesNotHaveAccessToCampaigns()) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + if (await this.customerDoesNotExist()) { + this.setError(403, new OpenapiError("Customer does not exist")); + return false; + } + return true; + } + + private doesNotHaveAccessToCampaigns() { + return this.accessibleCampaigns !== true; + } + + private async customerDoesNotExist() { + const results = await tryber.tables.WpAppqCustomer.do() + .select("id") + .where("id", this.customerId); + + return results.length === 0; + } + + protected async prepare(): Promise { + return this.setSuccess(200, { + results: await this.getProjects(), + }); + } + + private getProjects() { + return tryber.tables.WpAppqProject.do() + .select( + tryber.ref("id").withSchema("wp_appq_project"), + tryber.ref("display_name").withSchema("wp_appq_project").as("name") + ) + .where("customer_id", this.customerId); + } +} + +export default RouteItem; diff --git a/src/schema.ts b/src/schema.ts index 527ac21b9..74c5a8e93 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -554,6 +554,14 @@ export interface paths { }; }; }; + "/customers/{customer}/projects": { + get: operations["get-customers-customer-projects"]; + parameters: { + path: { + customer: string; + }; + }; + }; } export interface components { @@ -4084,6 +4092,26 @@ export interface operations { }; requestBody: components["requestBodies"]["DossierData"]; }; + "get-customers-customer-projects": { + parameters: { + path: { + customer: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + }[]; + }; + }; + }; + }; + }; } export interface external {} From 420deb89913f2915af4ea4a42ba24311c83a7676 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 11 Apr 2024 15:25:30 +0200 Subject: [PATCH 03/39] rework: Refactor to class os --- .../operating_systems/_get/index.spec.ts | 157 ++++++++++++++++++ .../operating_systems/_get/index.ts | 156 +++++++++-------- 2 files changed, 241 insertions(+), 72 deletions(-) create mode 100644 src/routes/device/device_type/operating_systems/_get/index.spec.ts diff --git a/src/routes/device/device_type/operating_systems/_get/index.spec.ts b/src/routes/device/device_type/operating_systems/_get/index.spec.ts new file mode 100644 index 000000000..d32da0838 --- /dev/null +++ b/src/routes/device/device_type/operating_systems/_get/index.spec.ts @@ -0,0 +1,157 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /devices/{type}/operating_systems", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Android", + form_factor: 0, + architecture: 0, + }, + { + id: 2, + name: "Android (Tablet)", + form_factor: 1, + architecture: 0, + }, + { + id: 3, + name: "iOS", + form_factor: 0, + architecture: 0, + }, + { + id: 4, + name: "Android (Farlocco)", + form_factor: 0, + architecture: 0, + }, + ]); + + await tryber.tables.WpDcAppqDevices.do().insert([ + { + id: 1, + device_type: 0, + manufacturer: "Samsung", + model: "Galaxy S10", + platform_id: 1, + }, + { + id: 2, + device_type: 0, + manufacturer: "Apple", + model: "iPhone 11", + platform_id: 3, + }, + { + id: 3, + device_type: 1, + manufacturer: "Samsung", + model: "Galaxy Tab S6", + platform_id: 2, + }, + { + id: 4, + device_type: 0, + manufacturer: "Samsung", + model: "Galaxy S10 Farlocco", + platform_id: 4, + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + it("Should return 403 if user is not logged in", async () => { + const response = await request(app).get("/devices/0/operating_systems"); + expect(response.status).toBe(403); + }); + + it("Should return 200 if user is logged in", async () => { + const response = await request(app) + .get("/devices/0/operating_systems") + .set("authorization", "Bearer tester"); + expect(response.status).toBe(200); + }); + + it("Should return the os by form factor", async () => { + const responseFF0 = await request(app) + .get("/devices/0/operating_systems") + .set("authorization", "Bearer tester"); + expect(responseFF0.body).toHaveLength(3); + expect(responseFF0.body).toEqual([ + { + id: 1, + name: "Android", + }, + { + id: 3, + name: "iOS", + }, + { + id: 4, + name: "Android (Farlocco)", + }, + ]); + const responseFF1 = await request(app) + .get("/devices/1/operating_systems") + .set("authorization", "Bearer tester"); + expect(responseFF1.body).toHaveLength(1); + expect(responseFF1.body).toEqual([ + { + id: 2, + name: "Android (Tablet)", + }, + ]); + }); + + it("Should return the os by manufacturer", async () => { + const response = await request(app) + .get("/devices/0/operating_systems?filterBy[manufacturer]=Samsung") + .set("authorization", "Bearer tester"); + + expect(response.body).toHaveLength(2); + expect(response.body).toEqual([ + { + id: 1, + name: "Android", + }, + { + id: 4, + name: "Android (Farlocco)", + }, + ]); + }); + it("Should return the os by model", async () => { + const response = await request(app) + .get("/devices/0/operating_systems?filterBy[model]=Galaxy S10") + .set("authorization", "Bearer tester"); + + expect(response.body).toHaveLength(1); + expect(response.body).toEqual([ + { + id: 1, + name: "Android", + }, + ]); + }); + + it("Should return the os by model and manufacturer", async () => { + const response = await request(app) + .get( + "/devices/0/operating_systems?filterBy[model]=Galaxy S10&filterBy[manufacturer]=Samsung" + ) + .set("authorization", "Bearer tester"); + + expect(response.body).toHaveLength(1); + expect(response.body).toEqual([ + { + id: 1, + name: "Android", + }, + ]); + }); +}); diff --git a/src/routes/device/device_type/operating_systems/_get/index.ts b/src/routes/device/device_type/operating_systems/_get/index.ts index de22f7f14..11c02bb88 100644 --- a/src/routes/device/device_type/operating_systems/_get/index.ts +++ b/src/routes/device/device_type/operating_systems/_get/index.ts @@ -1,84 +1,96 @@ -import * as db from "@src/features/db"; -import { Context } from "openapi-backend"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; -/** OPENAPI-ROUTE: get-devices-operating-systems */ -export default async ( - c: Context, - req: OpenapiRequest, - res: OpenapiResponse -) => { - try { - let device_type = 1; - if (typeof c.request.params.device_type == "string") { - device_type = parseInt(c.request.params.device_type); +/** OPENAPI-CLASS: get-devices-operating-systems */ + +export default class Route extends UserRoute<{ + response: StoplightOperations["get-devices-operating-systems"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["get-devices-operating-systems"]["parameters"]["path"]; + query: StoplightOperations["get-devices-operating-systems"]["parameters"]["query"]; +}> { + private deviceType: number; + private filterBy: + | { + manufacturer?: string | string[]; + model?: string | string[]; + } + | false = false; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + const { device_type } = this.getParameters(); + this.deviceType = Number(device_type); + const { filterBy } = this.getQuery(); + if (filterBy) { + this.filterBy = { + ...("manufacturer" in filterBy + ? { manufacturer: filterBy.manufacturer as string | string[] } + : {}), + ...("model" in filterBy + ? { model: filterBy.model as string | string[] } + : {}), + }; } - const filter = - req.query && req.query.filterBy - ? (req.query.filterBy as { [key: string]: string | string[] }) - : false; + } - let fallbackSql = `SELECT DISTINCT id, name FROM wp_appq_evd_platform WHERE form_factor = ?`; - fallbackSql = db.format(fallbackSql, [device_type]); - let sql = `SELECT DISTINCT id, name FROM wp_appq_evd_platform `; - let subQuery = `SELECT DISTINCT platform_id - FROM wp_dc_appq_devices - `; - let subWhere = ` device_type=? `; - let subQueryData: (string | number)[] = [device_type]; + protected async prepare(): Promise { + try { + const results = await this.getData(); - let acceptedFilters = ["manufacturer", "model"].filter((f) => - Object.keys(filter).includes(f) - ); + if (!results.length) { + const fallbackResults = await this.getFallback(); + if (!fallbackResults.length) throw Error("Error on finding devices"); + this.setSuccess(200, fallbackResults); + return; + } - if (acceptedFilters.length && filter) { - acceptedFilters = acceptedFilters.map((k) => { - const filterItem = filter[k]; - if (typeof filterItem === "string") { - subQueryData.push(filterItem); - return `${k}=?`; - } - const orQuery = filterItem - .map((el) => { - subQueryData.push(el); - return `${k}=?`; - }) - .join(" OR "); - return ` ( ${orQuery} ) `; - }); - subWhere += " AND " + Object.values(acceptedFilters).join(" AND "); + return this.setSuccess(200, results); + } catch (error) { + this.setError(404, error as OpenapiError); } + } + + private async getData() { + const subQueryIds = await this.getSubQuery(); + if (!subQueryIds.length) return []; - subQuery += ` WHERE ${subWhere}`; - sql += ` WHERE id IN (${subQuery})`; - sql = db.format(sql, subQueryData); + const results = tryber.tables.WpAppqEvdPlatform.do() + .distinct("id") + .select("name") + .whereIn("id", subQueryIds); - const results = await db.query(sql); + return results; + } + + private async getSubQuery() { + const query = tryber.tables.WpDcAppqDevices.do() + .distinct("platform_id") + .where("device_type", this.deviceType); - if (!results.length) { - const fallbackResults = await db.query(fallbackSql); - if (!fallbackResults.length) throw Error("Error on finding devices"); - res.status_code = 200; - return fallbackResults.map((row: { id: string; name: string }) => { - return { - id: row.id, - name: row.name, - }; - }); + if (this.filterBy) { + if (this.filterBy.manufacturer) { + if (typeof this.filterBy.manufacturer === "string") { + query.where("manufacturer", this.filterBy.manufacturer); + } else { + query.whereIn("manufacturer", this.filterBy.manufacturer); + } + } + if (this.filterBy.model) { + if (typeof this.filterBy.model === "string") { + query.where("model", this.filterBy.model); + } else { + query.whereIn("model", this.filterBy.model); + } + } } - res.status_code = 200; - return results.map((row: { id: string; name: string }) => { - return { - id: row.id, - name: row.name, - }; - }); - } catch (error) { - res.status_code = 404; - return { - element: "devices", - id: 0, - message: (error as OpenapiError).message, - }; + return (await query).map((row: { platform_id: number }) => row.platform_id); + } + + private async getFallback() { + return await tryber.tables.WpAppqEvdPlatform.do() + .distinct("id") + .select("name") + .where("form_factor", this.deviceType); } -}; +} From 92861fc5dd095ce149cdee58bb44f8a251359616 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 11 Apr 2024 16:39:37 +0200 Subject: [PATCH 04/39] feat: Allow listing all device types --- src/reference/openapi.yml | 8 ++ .../operating_systems/_get/formFactor.spec.ts | 101 ++++++++++++++++++ .../operating_systems/_get/index.spec.ts | 58 +++------- .../operating_systems/_get/index.ts | 79 ++++++++++---- src/schema.ts | 5 +- 5 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 src/routes/device/device_type/operating_systems/_get/formFactor.spec.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index e5e9cca81..c9f6346d3 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -4237,6 +4237,14 @@ paths: type: integer name: type: string + type: + type: string + x-stoplight: + id: 6yohqfohiv47u + required: + - id + - name + - type '403': $ref: '#/components/responses/NotAuthorized' '404': diff --git a/src/routes/device/device_type/operating_systems/_get/formFactor.spec.ts b/src/routes/device/device_type/operating_systems/_get/formFactor.spec.ts new file mode 100644 index 000000000..45af7eb9b --- /dev/null +++ b/src/routes/device/device_type/operating_systems/_get/formFactor.spec.ts @@ -0,0 +1,101 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /devices/{type}/operating_systems - form factor", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Android", + form_factor: 0, + architecture: 0, + }, + { + id: 2, + name: "Android", + form_factor: 1, + architecture: 0, + }, + { + id: 3, + name: "Windows", + form_factor: 2, + architecture: 0, + }, + { + id: 4, + name: "WearOS", + form_factor: 3, + architecture: 0, + }, + { + id: 5, + name: "PlayStation", + form_factor: 4, + architecture: 0, + }, + { + id: 6, + name: "TvOS", + form_factor: 5, + architecture: 0, + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + + it("Should return device type ", async () => { + const response = await request(app) + .get("/devices/0/operating_systems") + .set("authorization", "Bearer tester"); + expect(response.status).toBe(200); + + expect(response.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + type: "Smartphone", + }), + ]) + ); + }); + + it("Should return device type for all types", async () => { + const response = await request(app) + .get("/devices/all/operating_systems") + .set("authorization", "Bearer tester"); + expect(response.status).toBe(200); + + expect(response.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + type: "Smartphone", + }), + expect.objectContaining({ + id: 2, + type: "Tablet", + }), + expect.objectContaining({ + id: 3, + type: "PC", + }), + expect.objectContaining({ + id: 4, + type: "Smartwatch", + }), + expect.objectContaining({ + id: 5, + type: "Console", + }), + expect.objectContaining({ + id: 6, + type: "SmartTV", + }), + ]) + ); + }); +}); diff --git a/src/routes/device/device_type/operating_systems/_get/index.spec.ts b/src/routes/device/device_type/operating_systems/_get/index.spec.ts index d32da0838..2e5e64eaf 100644 --- a/src/routes/device/device_type/operating_systems/_get/index.spec.ts +++ b/src/routes/device/device_type/operating_systems/_get/index.spec.ts @@ -82,30 +82,18 @@ describe("GET /devices/{type}/operating_systems", () => { .get("/devices/0/operating_systems") .set("authorization", "Bearer tester"); expect(responseFF0.body).toHaveLength(3); - expect(responseFF0.body).toEqual([ - { - id: 1, - name: "Android", - }, - { - id: 3, - name: "iOS", - }, - { - id: 4, - name: "Android (Farlocco)", - }, - ]); + expect(responseFF0.body[0]).toHaveProperty("id", 1); + expect(responseFF0.body[0]).toHaveProperty("name", "Android"); + expect(responseFF0.body[1]).toHaveProperty("id", 3); + expect(responseFF0.body[1]).toHaveProperty("name", "iOS"); + expect(responseFF0.body[2]).toHaveProperty("id", 4); + expect(responseFF0.body[2]).toHaveProperty("name", "Android (Farlocco)"); const responseFF1 = await request(app) .get("/devices/1/operating_systems") .set("authorization", "Bearer tester"); expect(responseFF1.body).toHaveLength(1); - expect(responseFF1.body).toEqual([ - { - id: 2, - name: "Android (Tablet)", - }, - ]); + expect(responseFF1.body[0]).toHaveProperty("id", 2); + expect(responseFF1.body[0]).toHaveProperty("name", "Android (Tablet)"); }); it("Should return the os by manufacturer", async () => { @@ -114,16 +102,10 @@ describe("GET /devices/{type}/operating_systems", () => { .set("authorization", "Bearer tester"); expect(response.body).toHaveLength(2); - expect(response.body).toEqual([ - { - id: 1, - name: "Android", - }, - { - id: 4, - name: "Android (Farlocco)", - }, - ]); + expect(response.body[0]).toHaveProperty("id", 1); + expect(response.body[0]).toHaveProperty("name", "Android"); + expect(response.body[1]).toHaveProperty("id", 4); + expect(response.body[1]).toHaveProperty("name", "Android (Farlocco)"); }); it("Should return the os by model", async () => { const response = await request(app) @@ -131,12 +113,8 @@ describe("GET /devices/{type}/operating_systems", () => { .set("authorization", "Bearer tester"); expect(response.body).toHaveLength(1); - expect(response.body).toEqual([ - { - id: 1, - name: "Android", - }, - ]); + expect(response.body[0]).toHaveProperty("id", 1); + expect(response.body[0]).toHaveProperty("name", "Android"); }); it("Should return the os by model and manufacturer", async () => { @@ -147,11 +125,7 @@ describe("GET /devices/{type}/operating_systems", () => { .set("authorization", "Bearer tester"); expect(response.body).toHaveLength(1); - expect(response.body).toEqual([ - { - id: 1, - name: "Android", - }, - ]); + expect(response.body[0]).toHaveProperty("id", 1); + expect(response.body[0]).toHaveProperty("name", "Android"); }); }); diff --git a/src/routes/device/device_type/operating_systems/_get/index.ts b/src/routes/device/device_type/operating_systems/_get/index.ts index 11c02bb88..707f793ed 100644 --- a/src/routes/device/device_type/operating_systems/_get/index.ts +++ b/src/routes/device/device_type/operating_systems/_get/index.ts @@ -8,18 +8,26 @@ export default class Route extends UserRoute<{ parameters: StoplightOperations["get-devices-operating-systems"]["parameters"]["path"]; query: StoplightOperations["get-devices-operating-systems"]["parameters"]["query"]; }> { - private deviceType: number; + private deviceType: "all" | number; private filterBy: | { manufacturer?: string | string[]; model?: string | string[]; } | false = false; + private readonly DEVICE_TYPES = { + 0: "Smartphone", + 1: "Tablet", + 2: "PC", + 3: "Smartwatch", + 4: "Console", + 5: "SmartTV", + }; constructor(configuration: RouteClassConfiguration) { super(configuration); const { device_type } = this.getParameters(); - this.deviceType = Number(device_type); + this.deviceType = device_type === "all" ? device_type : Number(device_type); const { filterBy } = this.getQuery(); if (filterBy) { this.filterBy = { @@ -37,35 +45,51 @@ export default class Route extends UserRoute<{ try { const results = await this.getData(); - if (!results.length) { - const fallbackResults = await this.getFallback(); - if (!fallbackResults.length) throw Error("Error on finding devices"); - this.setSuccess(200, fallbackResults); - return; - } + if (results.length) + return this.setSuccess( + 200, + results.map((row) => ({ + id: row.id, + name: row.name, + type: row.type, + })) + ); - return this.setSuccess(200, results); + const fallbackResults = await this.getFallback(); + if (!fallbackResults.length) throw Error("Error on finding devices"); + this.setSuccess( + 200, + fallbackResults.map((row) => ({ + id: row.id, + name: row.name, + type: row.type, + })) + ); } catch (error) { this.setError(404, error as OpenapiError); } } private async getData() { - const subQueryIds = await this.getSubQuery(); - if (!subQueryIds.length) return []; + const osFromFilters = await this.getOsFromFilters(); + if (!osFromFilters.length) return []; - const results = tryber.tables.WpAppqEvdPlatform.do() + const results = await tryber.tables.WpAppqEvdPlatform.do() .distinct("id") .select("name") - .whereIn("id", subQueryIds); + .select("form_factor") + .whereIn("id", osFromFilters); - return results; + return results.map((row) => ({ + ...row, + type: this.mapDeviceTypeToFormFactor(row.form_factor), + })); } - private async getSubQuery() { - const query = tryber.tables.WpDcAppqDevices.do() - .distinct("platform_id") - .where("device_type", this.deviceType); + private async getOsFromFilters() { + const query = tryber.tables.WpDcAppqDevices.do().distinct("platform_id"); + + if (this.deviceType !== "all") query.where("device_type", this.deviceType); if (this.filterBy) { if (this.filterBy.manufacturer) { @@ -88,9 +112,24 @@ export default class Route extends UserRoute<{ } private async getFallback() { - return await tryber.tables.WpAppqEvdPlatform.do() + const query = tryber.tables.WpAppqEvdPlatform.do() .distinct("id") .select("name") - .where("form_factor", this.deviceType); + .select("form_factor"); + + if (this.deviceType !== "all") query.where("form_factor", this.deviceType); + + const results = await query; + + return results.map((row) => ({ + ...row, + type: this.mapDeviceTypeToFormFactor(row.form_factor), + })); + } + + private mapDeviceTypeToFormFactor(deviceType: number) { + if (deviceType in this.DEVICE_TYPES) + return this.DEVICE_TYPES[deviceType as keyof typeof this.DEVICE_TYPES]; + return "Unknown"; } } diff --git a/src/schema.ts b/src/schema.ts index 74c5a8e93..872e4ff6b 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2283,8 +2283,9 @@ export interface operations { 200: { content: { "application/json": { - id?: number; - name?: string; + id: number; + name: string; + type: string; }[]; }; }; From 58ebe748615690cd244811e040b9ae626f155f27 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 12 Apr 2024 11:45:51 +0200 Subject: [PATCH 05/39] feat: Return all devices --- .../operating_systems/_get/index.spec.ts | 12 ++++ .../operating_systems/_get/index.ts | 57 ++++++++++--------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/routes/device/device_type/operating_systems/_get/index.spec.ts b/src/routes/device/device_type/operating_systems/_get/index.spec.ts index 2e5e64eaf..89842caa1 100644 --- a/src/routes/device/device_type/operating_systems/_get/index.spec.ts +++ b/src/routes/device/device_type/operating_systems/_get/index.spec.ts @@ -128,4 +128,16 @@ describe("GET /devices/{type}/operating_systems", () => { expect(response.body[0]).toHaveProperty("id", 1); expect(response.body[0]).toHaveProperty("name", "Android"); }); + + it("Should return 406 if device type = all and filterBy is present", async () => { + const responseModel = await request(app) + .get("/devices/all/operating_systems?filterBy[model]=Galaxy S10") + .set("authorization", "Bearer tester"); + expect(responseModel.status).toBe(406); + + const responseManufacturer = await request(app) + .get("/devices/all/operating_systems?filterBy[manufacturer]=Samsung") + .set("authorization", "Bearer tester"); + expect(responseManufacturer.status).toBe(406); + }); }); diff --git a/src/routes/device/device_type/operating_systems/_get/index.ts b/src/routes/device/device_type/operating_systems/_get/index.ts index 707f793ed..1865dbed8 100644 --- a/src/routes/device/device_type/operating_systems/_get/index.ts +++ b/src/routes/device/device_type/operating_systems/_get/index.ts @@ -1,3 +1,4 @@ +import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import UserRoute from "@src/features/routes/UserRoute"; @@ -41,28 +42,28 @@ export default class Route extends UserRoute<{ } } + protected async filter() { + if (!(await super.filter())) return false; + if (this.deviceType === "all" && this.filterBy) { + this.setError( + 406, + new OpenapiError("Filtering is not allowed for all devices") + ); + return false; + } + return true; + } + protected async prepare(): Promise { try { - const results = await this.getData(); - - if (results.length) - return this.setSuccess( - 200, - results.map((row) => ({ - id: row.id, - name: row.name, - type: row.type, - })) - ); - - const fallbackResults = await this.getFallback(); - if (!fallbackResults.length) throw Error("Error on finding devices"); - this.setSuccess( + const results = await this.getOperativeSystems(); + + return this.setSuccess( 200, - fallbackResults.map((row) => ({ + results.map((row) => ({ id: row.id, name: row.name, - type: row.type, + type: this.mapDeviceTypeToFormFactor(row.form_factor), })) ); } catch (error) { @@ -70,9 +71,16 @@ export default class Route extends UserRoute<{ } } - private async getData() { + private async getOperativeSystems() { + if (this.deviceType === "all") { + return await tryber.tables.WpAppqEvdPlatform.do() + .distinct("id") + .select("name") + .select("form_factor"); + } + const osFromFilters = await this.getOsFromFilters(); - if (!osFromFilters.length) return []; + if (!osFromFilters.length) return await this.getFallback(); const results = await tryber.tables.WpAppqEvdPlatform.do() .distinct("id") @@ -80,10 +88,8 @@ export default class Route extends UserRoute<{ .select("form_factor") .whereIn("id", osFromFilters); - return results.map((row) => ({ - ...row, - type: this.mapDeviceTypeToFormFactor(row.form_factor), - })); + if (!results.length) await this.getFallback(); + return results; } private async getOsFromFilters() { @@ -121,10 +127,7 @@ export default class Route extends UserRoute<{ const results = await query; - return results.map((row) => ({ - ...row, - type: this.mapDeviceTypeToFormFactor(row.form_factor), - })); + return results; } private mapDeviceTypeToFormFactor(deviceType: number) { From b21f94432edd328fb97f15a494d3d6abd4fd0cff Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:18:32 +0200 Subject: [PATCH 06/39] Handle custom roles (#306) * feat: Add roles designs * feat: Allow linking roles to campaign * feat: Assign olps * feat: Add roles to get * feat: Allow changing roles * feat: Delete only custom roles olps * chore: Update database * fix: Format datetime * chore: Remove x-stoplight meta * test: Format date --- src/reference/openapi.yml | 132 ++++++++++-------- src/routes/dossiers/_post/creation.spec.ts | 66 +++++++++ src/routes/dossiers/_post/index.ts | 74 +++++++++- .../dossiers/campaignId/_get/index.spec.ts | 54 ++++++- src/routes/dossiers/campaignId/_get/index.ts | 57 +++++++- src/routes/dossiers/campaignId/_put/index.ts | 90 ++++++++++++ .../dossiers/campaignId/_put/update.spec.ts | 123 ++++++++++++++++ src/schema.ts | 15 ++ 8 files changed, 539 insertions(+), 72 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index c9f6346d3..5c097bc70 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -1176,8 +1176,6 @@ paths: type: string metal: type: string - x-stoplight: - id: 9afe0zlf0hr2b devices: type: array items: @@ -4239,8 +4237,6 @@ paths: type: string type: type: string - x-stoplight: - id: 6yohqfohiv47u required: - id - name @@ -9688,8 +9684,6 @@ paths: properties: id: type: integer - x-stoplight: - id: 965rh7aagxj8x examples: Example 1: value: @@ -9735,118 +9729,102 @@ paths: properties: id: type: integer - x-stoplight: - id: qqon32evab2o5 title: type: object - x-stoplight: - id: qfn9vuux1vjp1 required: - customer - tester properties: customer: type: string - x-stoplight: - id: tyx8mtant8jej tester: type: string - x-stoplight: - id: cupl63jal29yu startDate: type: string - x-stoplight: - id: lif1wyyh6miiy format: date-time endDate: type: string - x-stoplight: - id: dhx6h9l494pyw format: date-time customer: type: object - x-stoplight: - id: b6vdml702n2vr required: - id - name properties: id: type: integer - x-stoplight: - id: hzxvga289ilzv name: type: string - x-stoplight: - id: zxk4wi6clmaxr project: type: object - x-stoplight: - id: 9dnwnxk98slrj required: - id - name properties: id: type: integer - x-stoplight: - id: 795lyzmcn2f3i name: type: string - x-stoplight: - id: lonlyyu0xqpx9 testType: type: object - x-stoplight: - id: y2l96ujpgntcx required: - id - name properties: id: type: integer - x-stoplight: - id: 5ut1dht9q3e5f name: type: string - x-stoplight: - id: 3zafe5jvg89hd deviceList: type: array - x-stoplight: - id: 8ac5l94cifolj items: - x-stoplight: - id: p874do1llblvt type: object properties: id: type: integer - x-stoplight: - id: 64zrvfx7v23m8 name: type: string - x-stoplight: - id: 838zaws7zfcb6 required: - id - name csm: type: object - x-stoplight: - id: ye4vllt9hwd40 required: - id - name properties: id: type: integer - x-stoplight: - id: 29ini0mkuenx4 name: type: string - x-stoplight: - id: 0v4z9eu7jfeja + roles: + type: array + items: + type: object + properties: + role: + type: object + properties: + id: + type: integer + name: + type: string + required: + - id + - name + user: + type: object + properties: + id: + type: integer + name: + type: string + surname: + type: string + required: + - id + - name + - surname required: - id - title @@ -9857,6 +9835,38 @@ paths: - testType - deviceList - csm + examples: + Example 1: + value: + id: 1 + title: + customer: Customer Title + tester: Tester Title + startDate: '2019-08-24T14:15:22Z' + endDate: '2019-08-24T14:15:22Z' + customer: + id: 1 + name: My Customer + project: + id: 1 + name: My Project + testType: + id: 1 + name: Bughunting + deviceList: + - id: 1 + name: Android + csm: + id: 1 + name: Name Surname + roles: + - role: + id: 1 + name: PM + user: + id: 1 + name: Name + surname: Surname security: - JWT: [] '/customers/{customer}/projects': @@ -9879,21 +9889,13 @@ paths: properties: results: type: array - x-stoplight: - id: 9ccc89bhovco5 items: - x-stoplight: - id: zog5qqnv5h7rb type: object properties: id: type: integer - x-stoplight: - id: xapq51u019ada name: type: string - x-stoplight: - id: 8fxvw6c3m46bv required: - id - name @@ -11057,8 +11059,18 @@ components: type: integer csm: type: number - x-stoplight: - id: tczilke0whidg + roles: + type: array + items: + type: object + properties: + role: + type: number + user: + type: number + required: + - role + - user required: - project - testType diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 218dfdaeb..6aa350cfc 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -15,6 +15,14 @@ const baseRequest = { describe("Route POST /dossiers", () => { beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 100, + name: "", + email: "", + education_id: 1, + employment_id: 1, + }); await tryber.tables.WpAppqCustomer.do().insert({ id: 1, company: "Test Company", @@ -48,6 +56,10 @@ describe("Route POST /dossiers", () => { architecture: 1, }, ]); + + await tryber.tables.CustomRoles.do().insert([ + { id: 1, name: "Test Role", olp: '["appq_bugs"]' }, + ]); }); afterAll(async () => { @@ -55,9 +67,12 @@ describe("Route POST /dossiers", () => { await tryber.tables.WpAppqProject.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); await tryber.tables.WpAppqEvdPlatform.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CustomRoles.do().delete(); }); afterEach(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignCustomRoles.do().delete(); }); it("Should create a campaign", async () => { @@ -270,4 +285,55 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("os", "1,2"); expect(campaign).toHaveProperty("form_factor", "0,1"); }); + + it("Should return 406 if adding a role that does not exist", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 100, user: 1 }] }); + + expect(response.status).toBe(406); + }); + + it("Should return 406 if adding a role to a user that does not exist", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 1, user: 100 }] }); + + expect(response.status).toBe(406); + }); + + it("Should link the roles to the campaign", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 1, user: 1 }] }); + + const id = response.body.id; + + const roles = await tryber.tables.CampaignCustomRoles.do() + .select() + .where({ campaign_id: id }); + expect(roles).toHaveLength(1); + expect(roles[0]).toHaveProperty("custom_role_id", 1); + expect(roles[0]).toHaveProperty("tester_id", 1); + }); + + it("Should set the olp roles to the campaign", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 1, user: 1 }] }); + + const id = response.body.id; + + const olps = await tryber.tables.WpAppqOlpPermissions.do() + .select() + .where({ main_id: id }); + expect(olps).toHaveLength(1); + expect(olps[0]).toHaveProperty("type", "appq_bugs"); + expect(olps[0]).toHaveProperty("main_type", "campaign"); + expect(olps[0]).toHaveProperty("wp_user_id", 100); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index f9155f13b..69cf3cb42 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -10,6 +10,10 @@ export default class RouteItem extends AdminRoute<{ }> { protected async filter() { if (!(await super.filter())) return false; + if (await this.invalidRolesSubmitted()) { + this.setError(406, new OpenapiError("Invalid roles submitted")); + return false; + } if (!(await this.projectExists())) { this.setError(400, new OpenapiError("Project does not exist")); return false; @@ -26,6 +30,23 @@ export default class RouteItem extends AdminRoute<{ return true; } + private async invalidRolesSubmitted() { + const { roles } = this.getBody(); + if (!roles) return false; + const roleIds = [...new Set(roles.map((role) => role.role))]; + const rolesExist = await tryber.tables.CustomRoles.do() + .select() + .whereIn("id", roleIds); + if (rolesExist.length !== roleIds.length) return true; + + const userIds = [...new Set(roles.map((role) => role.user))]; + const usersExist = await tryber.tables.WpAppqEvdProfile.do() + .select() + .whereIn("id", userIds); + if (usersExist.length !== userIds.length) return true; + return false; + } + private async projectExists(): Promise { const { project: projectId } = this.getBody(); const project = await tryber.tables.WpAppqProject.do() @@ -58,8 +79,11 @@ export default class RouteItem extends AdminRoute<{ protected async prepare(): Promise { try { + const campaignId = await this.createCampaign(); + await this.linkRolesToCampaign(campaignId); + this.setSuccess(201, { - id: await this.createCampaign(), + id: campaignId, }); } catch (e) { this.setError(500, e as OpenapiError); @@ -90,6 +114,54 @@ export default class RouteItem extends AdminRoute<{ return results[0].id ?? results[0]; } + private async linkRolesToCampaign(campaignId: number) { + const roles = this.getBody().roles; + if (!roles) return; + + await tryber.tables.CampaignCustomRoles.do().insert( + roles.map((role) => ({ + campaign_id: campaignId, + custom_role_id: role.role, + tester_id: role.user, + })) + ); + + await this.assignOlps(campaignId); + } + + private async assignOlps(campaignId: number) { + const roles = this.getBody().roles; + if (!roles) return; + + const roleOlps = await tryber.tables.CustomRoles.do() + .select("id", "olp") + .whereIn( + "id", + roles.map((role) => role.role) + ); + const wpUserIds = await tryber.tables.WpAppqEvdProfile.do() + .select("id", "wp_user_id") + .whereIn( + "id", + roles.map((role) => role.user) + ); + for (const role of roles) { + const olp = roleOlps.find((r) => r.id === role.role)?.olp; + const wpUserId = wpUserIds.find((r) => r.id === role.user); + if (olp && wpUserId) { + const olpObject = JSON.parse(olp); + await tryber.tables.WpAppqOlpPermissions.do().insert( + olpObject.map((olpType: string) => ({ + main_id: campaignId, + main_type: "campaign", + type: olpType, + wp_user_id: wpUserId.wp_user_id, + })) + ); + } + } + } + private getCsmId() { const { csm } = this.getBody(); return csm ? csm : this.getTesterId(); diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index f81b6c1e1..dfd6971ae 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -16,15 +16,27 @@ describe("Route GET /dossiers/:id", () => { edited_by: 1, }); - await tryber.tables.WpAppqEvdProfile.do().insert({ - id: 1, - wp_user_id: 1, + const profile = { name: "Test", - surname: "CSM", + surname: "Profile", email: "", education_id: 1, employment_id: 1, - }); + }; + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + ...profile, + id: 1, + wp_user_id: 1, + surname: "CSM", + }, + { + ...profile, + id: 2, + wp_user_id: 2, + surname: "PM", + }, + ]); await tryber.tables.WpAppqCampaignType.do().insert({ id: 1, @@ -57,6 +69,20 @@ describe("Route GET /dossiers/:id", () => { pm_id: 1, customer_id: 0, }); + + await tryber.tables.CustomRoles.do().insert([ + { + id: 1, + name: "Test Role", + olp: '["appq_bug"]', + }, + ]); + + await tryber.tables.CampaignCustomRoles.do().insert({ + campaign_id: 1, + custom_role_id: 1, + tester_id: 2, + }); }); afterAll(async () => { @@ -82,7 +108,6 @@ describe("Route GET /dossiers/:id", () => { const response = await request(app) .get("/dossiers/1") .set("authorization", "Bearer tester"); - console.log(response.body); expect(response.status).toBe(403); }); @@ -196,4 +221,21 @@ describe("Route GET /dossiers/:id", () => { expect(response.body.customer).toHaveProperty("id", 1); expect(response.body.customer).toHaveProperty("name", "Test Company"); }); + + it("Should return the roles", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("roles"); + expect(response.body.roles).toHaveLength(1); + expect(response.body.roles[0]).toHaveProperty("role"); + expect(response.body.roles[0].role).toHaveProperty("id", 1); + expect(response.body.roles[0].role).toHaveProperty("name", "Test Role"); + expect(response.body.roles[0]).toHaveProperty("user"); + expect(response.body.roles[0].user).toHaveProperty("id", 2); + expect(response.body.roles[0].user).toHaveProperty("name", "Test"); + expect(response.body.roles[0].user).toHaveProperty("surname", "PM"); + }); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 19034d493..440921c18 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -29,12 +29,12 @@ export default class RouteItem extends AdminRoute<{ private async getCampaign() { const campaign = await tryber.tables.WpAppqEvdCampaign.do() .select( - "end_date", + tryber.fn.charDate("start_date"), + tryber.fn.charDate("end_date"), tryber.ref("id").withSchema("wp_appq_evd_campaign"), "title", "customer_title", "project_id", - "start_date", "campaign_type_id", "os", tryber @@ -89,7 +89,30 @@ export default class RouteItem extends AdminRoute<{ .select("id", "name") .whereIn("id", campaign.os.split(",")); - return { ...campaign, devices }; + const roles = await tryber.tables.CustomRoles.do() + .join( + "campaign_custom_roles", + "campaign_custom_roles.custom_role_id", + "custom_roles.id" + ) + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.id", + "campaign_custom_roles.tester_id" + ) + .select( + tryber.ref("id").withSchema("wp_appq_evd_profile").as("tester_id"), + tryber.ref("name").withSchema("wp_appq_evd_profile").as("tester_name"), + tryber + .ref("surname") + .withSchema("wp_appq_evd_profile") + .as("tester_surname"), + tryber.ref("id").withSchema("custom_roles").as("role_id"), + tryber.ref("name").withSchema("custom_roles").as("role_name") + ) + .where("campaign_custom_roles.campaign_id", this.campaignId); + + return { ...campaign, devices, roles }; } get campaign() { @@ -118,6 +141,7 @@ export default class RouteItem extends AdminRoute<{ } protected async prepare(): Promise { + console.log(this.campaign.start_date); try { this.setSuccess(200, { id: this.campaign.id, @@ -137,16 +161,39 @@ export default class RouteItem extends AdminRoute<{ id: this.campaign.campaign_type_id, name: this.campaign.campaign_type_name, }, - startDate: this.campaign.start_date, - endDate: this.campaign.end_date, + startDate: this.formatDate(this.campaign.start_date), + endDate: this.formatDate(this.campaign.end_date), deviceList: this.campaign.devices, csm: { id: this.campaign.pm_id, name: `${this.campaign.pm_name} ${this.campaign.pm_surname}`, }, + ...(this.campaign.roles.length + ? { + roles: this.campaign.roles.map((item) => { + return { + role: { + id: item.role_id, + name: item.role_name, + }, + user: { + id: item.tester_id, + name: item.tester_name, + surname: item.tester_surname, + }, + }; + }), + } + : {}), }); } catch (e) { this.setError(500, e as OpenapiError); } } + + private formatDate(dateTime: string) { + const [date, time] = dateTime.split(" "); + if (!date || !time) return dateTime; + return `${date}T${time}Z`; + } } diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index ed6f736ea..5cf416bcc 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -126,6 +126,96 @@ export default class RouteItem extends AdminRoute<{ .where({ id: this.campaignId, }); + + await this.linkRolesToCampaign(); + } + + private async linkRolesToCampaign() { + await this.cleanupCurrentRoles(); + const roles = this.getBody().roles; + if (!roles) return; + + await tryber.tables.CampaignCustomRoles.do().insert( + roles.map((role) => ({ + campaign_id: this.campaignId, + custom_role_id: role.role, + tester_id: role.user, + })) + ); + + await this.assignOlps(); + } + + private async cleanupCurrentRoles() { + const currentRoles = await tryber.tables.CampaignCustomRoles.do() + .select( + "tester_id", + "custom_role_id", + tryber.ref("olp").withSchema("custom_roles"), + "wp_user_id" + ) + .join( + "custom_roles", + "custom_roles.id", + "campaign_custom_roles.custom_role_id" + ) + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.id", + "campaign_custom_roles.tester_id" + ) + .where({ + campaign_id: this.campaignId, + }); + if (!currentRoles.length) return; + await tryber.tables.CampaignCustomRoles.do().delete().where({ + campaign_id: this.campaignId, + }); + + for (const role of currentRoles) { + const olpObject = JSON.parse(role.olp); + await tryber.tables.WpAppqOlpPermissions.do() + .delete() + .where({ + main_id: this.campaignId, + main_type: "campaign", + wp_user_id: role.wp_user_id, + }) + .whereIn("type", olpObject); + } + } + + private async assignOlps() { + const roles = this.getBody().roles; + if (!roles) return; + + const roleOlps = await tryber.tables.CustomRoles.do() + .select("id", "olp") + .whereIn( + "id", + roles.map((role) => role.role) + ); + const wpUserIds = await tryber.tables.WpAppqEvdProfile.do() + .select("id", "wp_user_id") + .whereIn( + "id", + roles.map((role) => role.user) + ); + for (const role of roles) { + const olp = roleOlps.find((r) => r.id === role.role)?.olp; + const wpUserId = wpUserIds.find((r) => r.id === role.user); + if (olp && wpUserId) { + const olpObject = JSON.parse(olp); + await tryber.tables.WpAppqOlpPermissions.do().insert( + olpObject.map((olpType: string) => ({ + main_id: this.campaignId, + main_type: "campaign", + type: olpType, + wp_user_id: wpUserId.wp_user_id, + })) + ); + } + } } private getEndDate() { diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index c730ebf88..8fe6e5451 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -271,4 +271,127 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("os", "1,2"); expect(campaign).toHaveProperty("form_factor", "0,1"); }); + + describe("Role handling", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 2, + wp_user_id: 2, + name: "Test User", + surname: "Test Surname", + education_id: 1, + employment_id: 1, + email: "", + }); + + await tryber.tables.CustomRoles.do().insert([ + { + id: 1, + name: "Test Role", + olp: '["appq_bugs"]', + }, + { + id: 2, + name: "Another Role", + olp: '["appq_bugs_2"]', + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CustomRoles.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + afterEach(async () => { + await tryber.tables.CampaignCustomRoles.do().delete(); + await tryber.tables.WpAppqOlpPermissions.do().delete(); + }); + describe("When campaign has no roles", () => { + it("Should link the roles to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 1, user: 1 }] }); + + const id = response.body.id; + + const roles = await tryber.tables.CampaignCustomRoles.do() + .select() + .where({ campaign_id: id }); + expect(roles).toHaveLength(1); + expect(roles[0]).toHaveProperty("custom_role_id", 1); + expect(roles[0]).toHaveProperty("tester_id", 1); + }); + + it("Should set the olp roles to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 1, user: 2 }] }); + + const olps = await tryber.tables.WpAppqOlpPermissions.do() + .select() + .where({ main_id: 1 }); + expect(olps).toHaveLength(1); + expect(olps[0]).toHaveProperty("type", "appq_bugs"); + expect(olps[0]).toHaveProperty("main_type", "campaign"); + expect(olps[0]).toHaveProperty("wp_user_id", 2); + }); + }); + + describe("When campaign has roles", () => { + beforeEach(async () => { + await tryber.tables.CampaignCustomRoles.do().insert([ + { + campaign_id: 1, + custom_role_id: 1, + tester_id: 2, + }, + ]); + await tryber.tables.WpAppqOlpPermissions.do().insert([ + { + main_id: 1, + main_type: "campaign", + type: "appq_bugs", + wp_user_id: 2, + }, + ]); + }); + + afterEach(async () => { + await tryber.tables.CampaignCustomRoles.do().delete(); + await tryber.tables.WpAppqOlpPermissions.do().delete(); + }); + it("Should link the roles to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 2, user: 2 }] }); + + const id = response.body.id; + + const roles = await tryber.tables.CampaignCustomRoles.do() + .select() + .where({ campaign_id: id }); + expect(roles).toHaveLength(1); + expect(roles[0]).toHaveProperty("custom_role_id", 2); + expect(roles[0]).toHaveProperty("tester_id", 2); + }); + + it("Should set the olp roles to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, roles: [{ role: 2, user: 2 }] }); + + const olps = await tryber.tables.WpAppqOlpPermissions.do() + .select() + .where({ main_id: 1 }); + expect(olps).toHaveLength(1); + expect(olps[0]).toHaveProperty("type", "appq_bugs_2"); + expect(olps[0]).toHaveProperty("main_type", "campaign"); + expect(olps[0]).toHaveProperty("wp_user_id", 2); + }); + }); + }); }); diff --git a/src/schema.ts b/src/schema.ts index 872e4ff6b..3fe50fd7e 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -971,6 +971,10 @@ export interface components { endDate?: string; deviceList: number[]; csm?: number; + roles?: { + role: number; + user: number; + }[]; }; }; }; @@ -4071,6 +4075,17 @@ export interface operations { id: number; name: string; }; + roles?: { + role?: { + id: number; + name: string; + }; + user?: { + id: number; + name: string; + surname: string; + }; + }[]; }; }; }; From 7b56af72c434883bfd730fab823f1d3951e2c1b6 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:32:25 +0200 Subject: [PATCH 07/39] Add additional campaign data (#307) * feat: Add close date to edit * feat: Add close date to cp creation * feat: Add types to dates * feat: Add closedate to get campaigns * feat: Allow settting dossier data * feat: Add dossier data to get * feat: Add country codes to get * fix: Allow partial data for get dossier * feat: Allow creating dossier with country * feat: Add countries put * feat: Add languages to get * feat: Add language to post * feat: Add language to put * feat: Add get browsers * feat: Add browser to post * feat: Add browsers to put * feat: Add product type * feat: Add product type to post * feat: Add product type to put --- package.json | 2 +- src/reference/openapi.yml | 135 +++++++ src/routes/dossiers/_post/creation.spec.ts | 345 +++++++++++++++++ src/routes/dossiers/_post/index.ts | 65 +++- .../dossiers/campaignId/_get/index.spec.ts | 239 ++++++++++++ src/routes/dossiers/campaignId/_get/index.ts | 110 +++++- src/routes/dossiers/campaignId/_put/index.ts | 128 ++++++- .../dossiers/campaignId/_put/update.spec.ts | 346 ++++++++++++++++++ src/schema.ts | 43 +++ yarn.lock | 8 +- 10 files changed, 1411 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3c11f2f08..79b0ce618 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.30.0", + "@appquality/tryber-database": "^0.33.0", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 5c097bc70..404e52127 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9745,6 +9745,9 @@ paths: endDate: type: string format: date-time + closeDate: + type: string + format: date-time customer: type: object required: @@ -9825,11 +9828,91 @@ paths: - id - name - surname + description: + type: string + productLink: + type: string + goal: + type: string + outOfScope: + type: string + deviceRequirements: + type: string + target: + type: object + properties: + notes: + type: string + size: + type: integer + countries: + type: array + minItems: 1 + uniqueItems: true + items: + $ref: '#/components/schemas/CountryCode' + languages: + type: array + x-stoplight: + id: vnqv4spymw4ki + items: + x-stoplight: + id: 3vr40snkowkto + type: object + properties: + id: + type: integer + x-stoplight: + id: oshj1a0l38q2h + name: + type: string + x-stoplight: + id: w3yg9dpyxvdpg + required: + - id + - name + browsers: + type: array + x-stoplight: + id: drm3b91p7l2h0 + items: + x-stoplight: + id: 58z8i5w06gdi6 + type: object + properties: + id: + type: integer + x-stoplight: + id: 12u8hxpxa3kvp + name: + type: string + x-stoplight: + id: lpgpv16kv5okp + required: + - id + - name + productType: + type: object + x-stoplight: + id: w34jw9gfix4od + properties: + id: + type: number + x-stoplight: + id: pxc44zqtp5bot + name: + type: string + x-stoplight: + id: ygrkmgv5bib6y + required: + - id + - name required: - id - title - startDate - endDate + - closeDate - customer - project - testType @@ -10717,6 +10800,12 @@ components: - draft - confirmed - done + CountryCode: + title: CountryCode + type: string + pattern: '^[A-Z][A-Z]$' + minLength: 2 + maxLength: 2 securitySchemes: JWT: type: http @@ -11051,8 +11140,13 @@ components: type: string startDate: type: string + format: date-time endDate: type: string + format: date-time + closeDate: + type: string + format: date-time deviceList: type: array items: @@ -11071,6 +11165,47 @@ components: required: - role - user + description: + type: string + productLink: + type: string + goal: + type: string + outOfScope: + type: string + deviceRequirements: + type: string + target: + type: object + properties: + notes: + type: string + size: + type: integer + countries: + type: array + items: + $ref: '#/components/schemas/CountryCode' + languages: + type: array + x-stoplight: + id: dy0yofmqy4ayn + items: + x-stoplight: + id: bko78gf0sdhjz + type: integer + browsers: + type: array + x-stoplight: + id: jc5nfd988spfc + items: + x-stoplight: + id: 9wdgspthmxvhf + type: integer + productType: + type: integer + x-stoplight: + id: ilm9662wwxef3 required: - project - testType diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 6aa350cfc..402ce6a41 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -60,6 +60,36 @@ describe("Route POST /dossiers", () => { await tryber.tables.CustomRoles.do().insert([ { id: 1, name: "Test Role", olp: '["appq_bugs"]' }, ]); + + await tryber.tables.ProductTypes.do().insert([ + { + id: 1, + name: "App", + }, + { + id: 2, + name: "Web", + }, + ]); + + await tryber.tables.Browsers.do().insert([ + { + id: 1, + name: "Test Browser", + }, + { + id: 2, + name: "Other Browser", + }, + ]); + + await tryber.tables.WpAppqLang.do().insert([ + { + id: 1, + display_name: "Test Language", + lang_code: "te-ST", + }, + ]); }); afterAll(async () => { @@ -69,10 +99,17 @@ describe("Route POST /dossiers", () => { await tryber.tables.WpAppqEvdPlatform.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); await tryber.tables.CustomRoles.do().delete(); + await tryber.tables.ProductTypes.do().delete(); + await tryber.tables.Browsers.do().delete(); + await tryber.tables.WpAppqLang.do().delete(); }); afterEach(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.CampaignCustomRoles.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); }); it("Should create a campaign", async () => { @@ -210,6 +247,27 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("end_date", "2021-08-20T14:15:22Z"); }); + it("Should create a campaign with the specified close date ", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + closeDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("close_date", "2021-08-20T14:15:22Z"); + }); + it("Should create a campaign with the end date as start date + 7 if left unspecified", async () => { const response = await request(app) .post("/dossiers") @@ -231,6 +289,27 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("end_date", "2021-08-27T14:15:22Z"); }); + it("Should create a campaign with the close date as start date + 14 if left unspecified", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("close_date", "2021-09-03T14:15:22Z"); + }); + it("Should create a campaign with current user as pm_id if left unspecified", async () => { const response = await request(app) .post("/dossiers") @@ -336,4 +415,270 @@ describe("Route POST /dossiers", () => { expect(olps[0]).toHaveProperty("main_type", "campaign"); expect(olps[0]).toHaveProperty("wp_user_id", 100); }); + + it("Should create a dossier data even if no additional data is provided", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + + expect(dossierData).toHaveLength(1); + }); + it("Should save description in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, description: "Test description" }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("description", "Test description"); + }); + + it("Should save productLink in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, productLink: "https://example.com" }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("link", "https://example.com"); + }); + + it("Should save goal in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, goal: "Having no bugs" }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("goal", "Having no bugs"); + }); + + it("Should save outOfScope in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, outOfScope: "Login page" }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("out_of_scope", "Login page"); + }); + + it("Should save target notes in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { notes: "New testers" } }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_audience", "New testers"); + }); + + it("Should save device requirements in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceRequirements: "New devices" }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_devices", "New devices"); + }); + + it("Should save target size in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { size: 10 } }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_size", 10); + }); + + it("Should save the tester id in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("created_by", 1); + expect(dossierData[0]).toHaveProperty("updated_by", 1); + }); + + it("Should save the countries in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + countries: ["IT", "FR"], + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("countries"); + expect(getResponse.body.countries).toHaveLength(2); + expect(getResponse.body.countries).toContain("IT"); + expect(getResponse.body.countries).toContain("FR"); + }); + + it("Should save the languages in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + languages: [1], + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("languages"); + expect(getResponse.body.languages).toHaveLength(1); + expect(getResponse.body.languages[0]).toEqual({ + id: 1, + name: "Test Language", + }); + }); + + it("Should save the browsers in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + browsers: [1], + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("browsers"); + expect(getResponse.body.browsers).toHaveLength(1); + expect(getResponse.body.browsers[0]).toEqual({ + id: 1, + name: "Test Browser", + }); + }); + it("Should save the product type in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + productType: 1, + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("productType"); + expect(getResponse.body.productType).toEqual({ + id: 1, + name: "App", + }); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 69cf3cb42..fe73a9164 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -99,6 +99,7 @@ export default class RouteItem extends AdminRoute<{ platform_id: 0, start_date: this.getBody().startDate, end_date: this.getEndDate(), + close_date: this.getCloseDate(), page_preview_id: 0, page_manual_id: 0, customer_id: 0, @@ -111,7 +112,61 @@ export default class RouteItem extends AdminRoute<{ }) .returning("id"); - return results[0].id ?? results[0]; + const campaignId = results[0].id ?? results[0]; + + const dossier = await tryber.tables.CampaignDossierData.do() + .insert({ + campaign_id: campaignId, + description: this.getBody().description, + ...(this.getBody().productLink && { + link: this.getBody().productLink, + }), + goal: this.getBody().goal, + out_of_scope: this.getBody().outOfScope, + target_audience: this.getBody().target?.notes, + ...(this.getBody().target?.size && { + target_size: this.getBody().target?.size, + }), + product_type_id: this.getBody().productType, + target_devices: this.getBody().deviceRequirements, + created_by: this.getTesterId(), + updated_by: this.getTesterId(), + }) + .returning("id"); + + const dossierId = dossier[0].id ?? dossier[0]; + + const countries = this.getBody().countries; + if (countries) { + await tryber.tables.CampaignDossierDataCountries.do().insert( + countries.map((country) => ({ + campaign_dossier_data_id: dossierId, + country_code: country, + })) + ); + } + + const languages = this.getBody().languages; + if (languages) { + await tryber.tables.CampaignDossierDataLanguages.do().insert( + languages.map((language) => ({ + campaign_dossier_data_id: dossierId, + language_id: language, + })) + ); + } + + const browsers = this.getBody().browsers; + if (browsers) { + await tryber.tables.CampaignDossierDataBrowsers.do().insert( + browsers.map((browser) => ({ + campaign_dossier_data_id: dossierId, + browser_id: browser, + })) + ); + } + + return campaignId; } private async linkRolesToCampaign(campaignId: number) { @@ -175,6 +230,14 @@ export default class RouteItem extends AdminRoute<{ return startDate.toISOString().replace(/\.\d+/, ""); } + private getCloseDate() { + if (this.getBody().closeDate) return this.getBody().closeDate; + + const startDate = new Date(this.getBody().startDate); + startDate.setDate(startDate.getDate() + 14); + return startDate.toISOString().replace(/\.\d+/, ""); + } + private async getDevices() { const devices = await tryber.tables.WpAppqEvdPlatform.do() .select("id", "form_factor") diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index dfd6971ae..0df641dd3 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -62,6 +62,7 @@ describe("Route GET /dossiers/:id", () => { customer_title: "Test Customer Campaign", start_date: "2019-08-24T14:15:22Z", end_date: "2019-08-24T14:15:22Z", + close_date: "2019-08-27T14:15:22Z", platform_id: 0, os: "1", page_manual_id: 0, @@ -188,6 +189,14 @@ describe("Route GET /dossiers/:id", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("endDate", "2019-08-24T14:15:22Z"); }); + it("Should return the close date", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("closeDate", "2019-08-27T14:15:22Z"); + }); it("Should return the device list", async () => { const response = await request(app) @@ -238,4 +247,234 @@ describe("Route GET /dossiers/:id", () => { expect(response.body.roles[0].user).toHaveProperty("name", "Test"); expect(response.body.roles[0].user).toHaveProperty("surname", "PM"); }); + + describe("With dossier data", () => { + beforeAll(async () => { + await tryber.tables.CampaignDossierData.do().insert({ + id: 1, + campaign_id: 1, + description: "Original description", + link: "Original link", + goal: "Original goal", + out_of_scope: "Original out of scope", + target_audience: "Original target audience", + target_size: 7, + target_devices: "Original target devices", + created_by: 100, + updated_by: 100, + product_type_id: 1, + }); + await tryber.tables.CampaignDossierDataCountries.do().insert([ + { + campaign_dossier_data_id: 1, + country_code: "IT", + }, + { + campaign_dossier_data_id: 1, + country_code: "FR", + }, + ]); + + await tryber.tables.WpAppqLang.do().insert([ + { + id: 1, + display_name: "English", + lang_code: "en", + }, + { + id: 2, + display_name: "Italiano", + lang_code: "it", + }, + ]); + + await tryber.tables.CampaignDossierDataLanguages.do().insert([ + { + campaign_dossier_data_id: 1, + language_id: 1, + }, + { + campaign_dossier_data_id: 1, + language_id: 2, + }, + ]); + + await tryber.tables.Browsers.do().insert([ + { + id: 1, + name: "Chrome", + }, + { + id: 2, + name: "Edge", + }, + ]); + + await tryber.tables.CampaignDossierDataBrowsers.do().insert([ + { + campaign_dossier_data_id: 1, + browser_id: 1, + }, + { + campaign_dossier_data_id: 1, + browser_id: 2, + }, + ]); + + await tryber.tables.ProductTypes.do().insert([ + { + id: 1, + name: "Test Product", + }, + { + id: 2, + name: "Another Product", + }, + ]); + }); + afterAll(async () => { + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.WpAppqLang.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.Browsers.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.ProductTypes.do().delete(); + }); + + it("Should return description", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + "description", + "Original description" + ); + }); + + it("Should return link", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("productLink", "Original link"); + }); + + it("Should return goal", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("goal", "Original goal"); + }); + + it("Should return out of scope", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + "outOfScope", + "Original out of scope" + ); + }); + + it("Should return target audience", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + "target", + expect.objectContaining({ notes: "Original target audience" }) + ); + }); + + it("Should return target size", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("target"); + expect(response.body.target).toHaveProperty("size", 7); + }); + + it("Should return devices requirements", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + "deviceRequirements", + "Original target devices" + ); + }); + + it("Should return countries", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("countries"); + expect(response.body.countries).toHaveLength(2); + expect(response.body.countries).toContainEqual("IT"); + expect(response.body.countries).toContainEqual("FR"); + }); + + it("Should return languages", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("languages"); + expect(response.body.languages).toHaveLength(2); + expect(response.body.languages).toEqual( + expect.arrayContaining([ + { id: 1, name: "English" }, + { + id: 2, + name: "Italiano", + }, + ]) + ); + }); + it("Should return browsers", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("browsers"); + expect(response.body.browsers).toHaveLength(2); + expect(response.body.browsers).toEqual( + expect.arrayContaining([ + { id: 1, name: "Chrome" }, + { + id: 2, + name: "Edge", + }, + ]) + ); + }); + it("Should return product type", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("productType"); + expect(response.body.productType).toHaveProperty("id", 1); + expect(response.body.productType).toHaveProperty("name", "Test Product"); + }); + }); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 440921c18..748a2895b 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -31,6 +31,7 @@ export default class RouteItem extends AdminRoute<{ .select( tryber.fn.charDate("start_date"), tryber.fn.charDate("end_date"), + tryber.fn.charDate("close_date"), tryber.ref("id").withSchema("wp_appq_evd_campaign"), "title", "customer_title", @@ -112,7 +113,66 @@ export default class RouteItem extends AdminRoute<{ ) .where("campaign_custom_roles.campaign_id", this.campaignId); - return { ...campaign, devices, roles }; + const dossierData = await tryber.tables.CampaignDossierData.do() + .select( + tryber.ref("id").withSchema("campaign_dossier_data"), + "description", + "link", + "goal", + "out_of_scope", + "target_audience", + "target_size", + "target_devices", + "product_type_id", + tryber.ref("name").withSchema("product_types").as("product_type_name") + ) + .leftJoin( + "product_types", + "product_types.id", + "campaign_dossier_data.product_type_id" + ) + .where("campaign_id", this.campaignId) + .first(); + + const targetCountries = dossierData + ? await tryber.tables.CampaignDossierDataCountries.do() + .select("country_code") + .where("campaign_dossier_data_id", dossierData.id) + : []; + + const targetLanguages = dossierData + ? await tryber.tables.CampaignDossierDataLanguages.do() + .join( + "wp_appq_lang", + "wp_appq_lang.id", + "campaign_dossier_data_languages.language_id" + ) + .select("language_id") + .select("display_name") + .where("campaign_dossier_data_id", dossierData.id) + : []; + + const targetBrowsers = dossierData + ? await tryber.tables.CampaignDossierDataBrowsers.do() + .join( + "browsers", + "browsers.id", + "campaign_dossier_data_browsers.browser_id" + ) + .select("browser_id") + .select("name") + .where("campaign_dossier_data_id", dossierData.id) + : []; + + return { + ...campaign, + devices, + roles, + ...dossierData, + countries: targetCountries, + languages: targetLanguages, + browsers: targetBrowsers, + }; } get campaign() { @@ -141,7 +201,6 @@ export default class RouteItem extends AdminRoute<{ } protected async prepare(): Promise { - console.log(this.campaign.start_date); try { this.setSuccess(200, { id: this.campaign.id, @@ -163,6 +222,7 @@ export default class RouteItem extends AdminRoute<{ }, startDate: this.formatDate(this.campaign.start_date), endDate: this.formatDate(this.campaign.end_date), + closeDate: this.formatDate(this.campaign.close_date), deviceList: this.campaign.devices, csm: { id: this.campaign.pm_id, @@ -185,6 +245,52 @@ export default class RouteItem extends AdminRoute<{ }), } : {}), + ...(this.campaign.description && { + description: this.campaign.description, + }), + productLink: this.campaign.link, + ...(this.campaign.goal && { + goal: this.campaign.goal, + }), + ...(this.campaign.out_of_scope && { + outOfScope: this.campaign.out_of_scope, + }), + ...((this.campaign.target_audience || this.campaign.target_size) && { + target: { + ...(this.campaign.target_audience && { + notes: this.campaign.target_audience, + }), + ...(this.campaign.target_size && { + size: this.campaign.target_size, + }), + }, + }), + ...(this.campaign.target_devices && { + deviceRequirements: this.campaign.target_devices, + }), + ...(this.campaign.countries.length > 0 && { + countries: this.campaign.countries?.map((item) => item.country_code), + }), + ...(this.campaign.languages.length > 0 && { + languages: this.campaign.languages?.map((item) => ({ + id: item.language_id, + name: item.display_name, + })), + }), + ...(this.campaign.browsers.length > 0 && { + browsers: this.campaign.browsers?.map((item) => ({ + id: item.browser_id, + name: item.name, + })), + }), + + ...(this.campaign.product_type_id && + this.campaign.product_type_name && { + productType: { + id: this.campaign.product_type_id, + name: this.campaign.product_type_name, + }, + }), }); } catch (e) { this.setError(500, e as OpenapiError); diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index 5cf416bcc..c03924b80 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -10,7 +10,7 @@ export default class RouteItem extends AdminRoute<{ parameters: StoplightOperations["put-dossiers-campaign"]["parameters"]["path"]; }> { private campaignId: number; - private _campaign: { end_date: string } | undefined; + private _campaign: { end_date: string; close_date: string } | undefined; constructor(configuration: RouteClassConfiguration) { super(configuration); @@ -20,7 +20,7 @@ export default class RouteItem extends AdminRoute<{ protected async init(): Promise { await super.init(); this._campaign = await tryber.tables.WpAppqEvdCampaign.do() - .select("end_date") + .select("end_date", "close_date") .where({ id: this.campaignId, }) @@ -113,6 +113,7 @@ export default class RouteItem extends AdminRoute<{ platform_id: 0, start_date: this.getBody().startDate, end_date: this.getEndDate(), + close_date: this.getCloseDate(), page_preview_id: 0, page_manual_id: 0, customer_id: 0, @@ -128,6 +129,123 @@ export default class RouteItem extends AdminRoute<{ }); await this.linkRolesToCampaign(); + + await this.updateCampaignDossierData(); + + await this.updateCampaignDossierDataCountries(); + await this.updateCampaignDossierDataLanguages(); + await this.updateCampaignDossierDataBrowsers(); + } + + private async updateCampaignDossierData() { + const dossierExists = await tryber.tables.CampaignDossierData.do() + .select("id") + .where({ + campaign_id: this.campaignId, + }) + .first(); + if (!dossierExists) { + await tryber.tables.CampaignDossierData.do().insert({ + campaign_id: this.campaignId, + created_by: this.getTesterId(), + updated_by: this.getTesterId(), + }); + } + + await tryber.tables.CampaignDossierData.do() + .update({ + description: this.getBody().description, + ...(this.getBody().productLink && { + link: this.getBody().productLink, + }), + goal: this.getBody().goal, + out_of_scope: this.getBody().outOfScope, + target_audience: this.getBody().target?.notes, + ...(this.getBody().target?.size && { + target_size: this.getBody().target?.size, + }), + product_type_id: this.getBody().productType, + target_devices: this.getBody().deviceRequirements, + updated_by: this.getTesterId(), + }) + .where({ + campaign_id: this.campaignId, + }); + } + + private async updateCampaignDossierDataCountries() { + const dossier = await tryber.tables.CampaignDossierData.do() + .select("id") + .where({ + campaign_id: this.campaignId, + }) + .first(); + if (!dossier) return; + + const dossierId = dossier.id; + await tryber.tables.CampaignDossierDataCountries.do() + .delete() + .where("campaign_dossier_data_id", dossierId); + + const countries = this.getBody().countries; + if (!countries) return; + + await tryber.tables.CampaignDossierDataCountries.do().insert( + countries.map((country) => ({ + campaign_dossier_data_id: dossierId, + country_code: country, + })) + ); + } + + private async updateCampaignDossierDataLanguages() { + const dossier = await tryber.tables.CampaignDossierData.do() + .select("id") + .where({ + campaign_id: this.campaignId, + }) + .first(); + if (!dossier) return; + + const dossierId = dossier.id; + await tryber.tables.CampaignDossierDataLanguages.do() + .delete() + .where("campaign_dossier_data_id", dossierId); + + const languages = this.getBody().languages; + if (!languages) return; + + await tryber.tables.CampaignDossierDataLanguages.do().insert( + languages.map((lang) => ({ + campaign_dossier_data_id: dossierId, + language_id: lang, + })) + ); + } + + private async updateCampaignDossierDataBrowsers() { + const dossier = await tryber.tables.CampaignDossierData.do() + .select("id") + .where({ + campaign_id: this.campaignId, + }) + .first(); + if (!dossier) return; + + const dossierId = dossier.id; + await tryber.tables.CampaignDossierDataBrowsers.do() + .delete() + .where("campaign_dossier_data_id", dossierId); + + const browsers = this.getBody().browsers; + if (!browsers) return; + + await tryber.tables.CampaignDossierDataBrowsers.do().insert( + browsers.map((browser) => ({ + campaign_dossier_data_id: dossierId, + browser_id: browser, + })) + ); } private async linkRolesToCampaign() { @@ -224,6 +342,12 @@ export default class RouteItem extends AdminRoute<{ return this.campaign.end_date; } + private getCloseDate() { + if (this.getBody().closeDate) return this.getBody().closeDate; + + return this.campaign.close_date; + } + private async getDevices() { const devices = await tryber.tables.WpAppqEvdPlatform.do() .select("id", "form_factor") diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 8fe6e5451..8e014691b 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -64,6 +64,30 @@ describe("Route POST /dossiers", () => { architecture: 1, }, ]); + + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + name: "Test User", + email: "", + education_id: 1, + employment_id: 1, + }); + + await tryber.tables.WpAppqLang.do().insert([ + { id: 1, display_name: "Test Language", lang_code: "TL" }, + { id: 2, display_name: "Other Language", lang_code: "OL" }, + ]); + + await tryber.tables.Browsers.do().insert([ + { id: 1, name: "Test Browser" }, + { id: 2, name: "Other Browser" }, + ]); + + await tryber.tables.ProductTypes.do().insert([ + { id: 1, name: "Test Product" }, + { id: 2, name: "Other Product" }, + ]); }); afterAll(async () => { @@ -71,6 +95,10 @@ describe("Route POST /dossiers", () => { await tryber.tables.WpAppqProject.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); await tryber.tables.WpAppqEvdPlatform.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqLang.do().delete(); + await tryber.tables.Browsers.do().delete(); + await tryber.tables.ProductTypes.do().delete(); }); beforeEach(async () => { @@ -82,6 +110,7 @@ describe("Route POST /dossiers", () => { customer_title: "Test Customer Campaign", start_date: "2019-08-24T14:15:22Z", end_date: "2019-08-24T14:15:22Z", + close_date: "2019-08-25T14:15:22Z", platform_id: 0, os: "1", page_manual_id: 0, @@ -92,6 +121,10 @@ describe("Route POST /dossiers", () => { }); afterEach(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); }); it("Should update the campaign to be linked to the specified project", async () => { @@ -214,6 +247,27 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("end_date", "2021-08-20T14:15:22Z"); }); + it("Should update the campaign with the specified close date ", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + closeDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("close_date", "2021-08-20T14:15:22Z"); + }); + it("Should leave the end date of the campaign unedited if left unspecified", async () => { const response = await request(app) .put("/dossiers/1") @@ -235,6 +289,27 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("end_date", "2019-08-24T14:15:22Z"); }); + it("Should leave the close date of the campaign unedited if left unspecified", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + startDate: "2021-08-20T14:15:22Z", + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select() + .where({ id }) + .first(); + + expect(campaign).toHaveProperty("close_date", "2019-08-25T14:15:22Z"); + }); + it("Should update the campaign with current user as pm_id", async () => { const response = await request(app) .put("/dossiers/1") @@ -272,6 +347,277 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("form_factor", "0,1"); }); + describe("Without dossier data", () => { + it("Should create dossier data for the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, description: "Test description" }); + + expect(response.status).toBe(200); + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: 1 }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("description", "Test description"); + }); + }); + + describe("With dossier data", () => { + beforeEach(async () => { + await tryber.tables.CampaignDossierData.do().insert({ + id: 1, + campaign_id: 1, + description: "Original description", + link: "Original link", + goal: "Original goal", + out_of_scope: "Original out of scope", + target_audience: "Original target audience", + target_size: 0, + target_devices: "Original target devices", + product_type_id: 2, + created_by: 100, + updated_by: 100, + }); + + await tryber.tables.CampaignDossierDataCountries.do().insert([ + { campaign_dossier_data_id: 1, country_code: "US" }, + { campaign_dossier_data_id: 1, country_code: "GB" }, + ]); + + await tryber.tables.CampaignDossierDataLanguages.do().insert([ + { campaign_dossier_data_id: 1, language_id: 2 }, + ]); + await tryber.tables.CampaignDossierDataBrowsers.do().insert([ + { campaign_dossier_data_id: 1, browser_id: 2 }, + ]); + }); + + it("Should save description in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, description: "Test description" }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("description", "Test description"); + }); + + it("Should save productLink in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, productLink: "https://example.com" }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("link", "https://example.com"); + }); + + it("Should save goal in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, goal: "Having no bugs" }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("goal", "Having no bugs"); + }); + + it("Should save outOfScope in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, outOfScope: "Login page" }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("out_of_scope", "Login page"); + }); + + it("Should save target notes in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { notes: "New testers" } }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_audience", "New testers"); + }); + + it("Should save device requirements in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, deviceRequirements: "New devices" }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_devices", "New devices"); + }); + + it("Should save target size in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, target: { size: 10 } }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("target_size", 10); + }); + + it("Should save the tester id in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send(baseRequest); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + + const id = 1; + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select() + .where({ campaign_id: id }); + expect(dossierData).toHaveLength(1); + expect(dossierData[0]).toHaveProperty("created_by", 100); + expect(dossierData[0]).toHaveProperty("updated_by", 1); + }); + + it("Should update the countries in the dossier data", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + countries: ["DE", "FR"], + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + console.log(responseGet.body); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("countries", ["DE", "FR"]); + }); + it("Should update the languages in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + languages: [1], + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + console.log(responseGet.body); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("languages"); + expect(responseGet.body.languages).toHaveLength(1); + expect(responseGet.body.languages[0]).toEqual({ + id: 1, + name: "Test Language", + }); + }); + + it("Should update the browsers in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + browsers: [1], + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("browsers"); + expect(responseGet.body.browsers).toHaveLength(1); + expect(responseGet.body.browsers[0]).toEqual({ + id: 1, + name: "Test Browser", + }); + }); + it("Should update the product type in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + productType: 1, + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("productType"); + expect(responseGet.body.productType).toEqual({ + id: 1, + name: "Test Product", + }); + }); + }); + describe("Role handling", () => { beforeAll(async () => { await tryber.tables.WpAppqEvdProfile.do().insert({ diff --git a/src/schema.ts b/src/schema.ts index 3fe50fd7e..3d5d886ec 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -869,6 +869,8 @@ export interface components { * @enum {string} */ ProspectStatus: "draft" | "confirmed" | "done"; + /** CountryCode */ + CountryCode: string; }; responses: { /** A user */ @@ -967,14 +969,31 @@ export interface components { customer: string; tester?: string; }; + /** Format: date-time */ startDate: string; + /** Format: date-time */ endDate?: string; + /** Format: date-time */ + closeDate?: string; deviceList: number[]; csm?: number; roles?: { role: number; user: number; }[]; + description?: string; + productLink?: string; + goal?: string; + outOfScope?: string; + deviceRequirements?: string; + target?: { + notes?: string; + size?: number; + }; + countries?: components["schemas"]["CountryCode"][]; + languages?: number[]; + browsers?: number[]; + productType?: number; }; }; }; @@ -4055,6 +4074,8 @@ export interface operations { startDate: string; /** Format: date-time */ endDate: string; + /** Format: date-time */ + closeDate: string; customer: { id: number; name: string; @@ -4086,6 +4107,28 @@ export interface operations { surname: string; }; }[]; + description?: string; + productLink?: string; + goal?: string; + outOfScope?: string; + deviceRequirements?: string; + target?: { + notes?: string; + size?: number; + }; + countries?: components["schemas"]["CountryCode"][]; + languages?: { + id: number; + name: string; + }[]; + browsers?: { + id: number; + name: string; + }[]; + productType?: { + id: number; + name: string; + }; }; }; }; diff --git a/yarn.lock b/yarn.lock index 0f6dbe885..d85782dd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.30.0": - version "0.30.0" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.30.0.tgz#6bb4e1ad996685d2f5b79251cf98c2291bb56c6b" - integrity sha512-Medf+jBxBIoYus6IpkdfO4qfyZcSsMt7U1UuvgrgSuPRVTYXSgnVWrzuoI5Nqx4xQlF7i0wbVH+ptQOgw+v/+Q== +"@appquality/tryber-database@^0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.0.tgz#4f46e44824fad37139aa1f1c2fec50b1f658245c" + integrity sha512-U5zcgF/VlrAnD1pk4WFxc7oAdRDHxjiSlhhYpURweGtot2fwIxwJ28vqEgisv/zXETeXEMVspOOMfkyrg+EfyQ== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From f04ff2af377463de92e3f9ba7307423b04d05867 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:45:26 +0200 Subject: [PATCH 08/39] Add selects (#308) * feat: Add close date to edit * feat: Add close date to cp creation * feat: Add types to dates * feat: Add closedate to get campaigns * feat: Allow settting dossier data * feat: Add dossier data to get * feat: Add country codes to get * fix: Allow partial data for get dossier * feat: Allow creating dossier with country * feat: Add countries put * feat: Add languages to get * feat: Add language to post * feat: Add language to put * feat: Add get browsers * feat: Add browser to post * feat: Add browsers to put * feat: Add product type * feat: Add product type to post * feat: Add product type to put * feat: Add project managers get * feat: Add role selects * feat: Add basic structure for browsers and product types * fix: Remove empty requestbody * feat: Add assistants api * feat: Add errors * fix: Search capabilities only on correct meta_keys --- src/reference/openapi.yml | 149 +++++++++++++++ .../users/by-role/role/_get/index.spec.ts | 173 ++++++++++++++++++ src/routes/users/by-role/role/_get/index.ts | 161 ++++++++++++++++ src/schema.ts | 67 +++++++ 4 files changed, 550 insertions(+) create mode 100644 src/routes/users/by-role/role/_get/index.spec.ts create mode 100644 src/routes/users/by-role/role/_get/index.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 404e52127..6e3b039aa 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9988,6 +9988,155 @@ paths: description: '' security: - JWT: [] + '/users/by-role/{role}': + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + x-stoplight: + id: bit4usrip6upm + items: + x-stoplight: + id: o7tw7533e3qes + type: object + properties: + id: + type: integer + x-stoplight: + id: 03fxvfejydtdr + name: + type: string + x-stoplight: + id: b9qd1xctx2kcy + surname: + type: string + x-stoplight: + id: j4j7zt1laq54b + required: + - id + - name + - surname + required: + - results + operationId: get-users-by-role-role + security: + - JWT: [] + parameters: + - schema: + type: string + enum: + - tester_lead + - quality_leader + - ux_researcher + - assistants + name: role + in: path + required: true + /browsers: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + x-stoplight: + id: 5wcw8uwqeljlf + items: + x-stoplight: + id: xxr1fwn9kxcp4 + type: object + properties: + id: + type: integer + x-stoplight: + id: cx3i7ryqsjmrq + name: + type: string + x-stoplight: + id: 93yftbsxaqmdm + required: + - id + - name + required: + - results + examples: + Example 1: + value: + results: + - id: 1 + name: Chrome + - id: 2 + name: Firefox + - id: 3 + name: Safari + - id: 4 + name: Edge + - id: 100 + name: Other + operationId: get-browsers + /productTypes: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + x-stoplight: + id: 5wcw8uwqeljlf + items: + x-stoplight: + id: xxr1fwn9kxcp4 + type: object + properties: + id: + type: integer + x-stoplight: + id: cx3i7ryqsjmrq + name: + type: string + x-stoplight: + id: 93yftbsxaqmdm + required: + - id + - name + required: + - results + examples: + Example 1: + value: + results: + - id: 1 + name: Website / Webapp + - id: 2 + name: Mobile App + - id: 100 + name: Other + operationId: get-productTypes + description: '' + parameters: [] components: schemas: AdditionalField: diff --git a/src/routes/users/by-role/role/_get/index.spec.ts b/src/routes/users/by-role/role/_get/index.spec.ts new file mode 100644 index 000000000..9ee43d116 --- /dev/null +++ b/src/routes/users/by-role/role/_get/index.spec.ts @@ -0,0 +1,173 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("Route GET /users/by-role/quality_leader", () => { + beforeAll(async () => { + const profile = { + surname: "User", + email: "", + education_id: 1, + employment_id: 1, + }; + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + ...profile, + id: 1, + wp_user_id: 10, + name: "CSM", + }, + { + ...profile, + id: 2, + wp_user_id: 20, + name: "Testissimo", + }, + { + ...profile, + id: 3, + wp_user_id: 30, + name: "Contributor", + }, + { + ...profile, + id: 4, + wp_user_id: 40, + name: "Tester Leader", + }, + { + ...profile, + id: 5, + wp_user_id: 50, + name: "UX Researcher", + }, + ]); + + await tryber.tables.WpUsers.do().insert([ + { ID: 10 }, + { ID: 20 }, + { ID: 30 }, + { ID: 40 }, + { ID: 50 }, + ]); + + await tryber.tables.WpUsermeta.do().insert([ + { + user_id: 10, + meta_key: "wp_capabilities", + meta_value: 'a:1:{s:14:"quality_leader";b:1;}', + }, + { + user_id: 20, + meta_key: "wp_capabilities", + meta_value: 'a:1:{s:20:"quality_leaderissimo";b:1;}', + }, + { + user_id: 30, + meta_key: "wp_capabilities", + meta_value: 'a:1:{s:11:"contributor";b:1;}', + }, + { + user_id: 40, + meta_key: "wp_capabilities", + meta_value: 'a:1:{s:11:"tester_lead";b:1;}', + }, + { + user_id: 50, + meta_key: "wp_capabilities", + meta_value: 'a:1:{s:13:"ux_researcher";b:1;}', + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpUsers.do().delete(); + }); + it("Should answer 403 if not logged in", async () => { + const response = await request(app).get("/users/by-role/quality_leader"); + + expect(response.status).toBe(403); + }); + + it("Should answer 200 if logged ad admin", async () => { + const response = await request(app) + .get("/users/by-role/quality_leader") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + }); + + it("Should answer 403 if logged in as tester", async () => { + const response = await request(app) + .get("/users/by-role/quality_leader") + .set("authorization", "Bearer tester"); + + expect(response.status).toBe(403); + }); + + it("Should answer 200 if logged with full access to campaign", async () => { + const response = await request(app) + .get("/users/by-role/quality_leader") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + + expect(response.status).toBe(200); + }); + it("Should answer 403 if logged with access to a single campaign", async () => { + const response = await request(app) + .get("/users/by-role/quality_leader") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + + expect(response.status).toBe(403); + }); + + it("Should answer with list of quality leaders", async () => { + const response = await request(app) + .get("/users/by-role/quality_leader") + .set("authorization", "Bearer admin"); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toEqual([ + { + id: 1, + name: "CSM", + surname: "User", + }, + ]); + }); + it("Should answer with list of tester leader", async () => { + const response = await request(app) + .get("/users/by-role/tester_lead") + .set("authorization", "Bearer admin"); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toEqual([ + { + id: 4, + name: "Tester Leader", + surname: "User", + }, + ]); + }); + + it("Should answer with list of ux researcher", async () => { + const response = await request(app) + .get("/users/by-role/ux_researcher") + .set("authorization", "Bearer admin"); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toEqual([ + { + id: 5, + name: "UX Researcher", + surname: "User", + }, + ]); + }); + it("Should answer 400 when asking for contributor", async () => { + const response = await request(app) + .get("/users/by-role/contributor") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(400); + }); +}); diff --git a/src/routes/users/by-role/role/_get/index.ts b/src/routes/users/by-role/role/_get/index.ts new file mode 100644 index 000000000..55e3007e1 --- /dev/null +++ b/src/routes/users/by-role/role/_get/index.ts @@ -0,0 +1,161 @@ +/** OPENAPI-CLASS: get-users-by-role-role */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; +import PHPUnserialize from "php-unserialize"; + +export default class Route extends UserRoute<{ + response: StoplightOperations["get-users-by-role-role"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["get-users-by-role-role"]["parameters"]["path"]; +}> { + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + private roleName: string; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + const { role } = this.getParameters(); + this.roleName = role; + } + + protected async filter() { + if ((await super.filter()) === false) return false; + if (this.doesNotHaveAccessToCampaigns()) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + return true; + } + + private doesNotHaveAccessToCampaigns() { + return this.accessibleCampaigns !== true; + } + + protected async prepare() { + const results = + this.roleName === "assistants" + ? await this.getAssistants() + : await this.getUsersByRole(); + + this.setSuccess(200, { + results, + }); + } + + private async getAssistants() { + const roles = [ + ...(await this.getRolesWithVisibility()), + "wp_admin_visibility", + ]; + + const query = tryber.tables.WpUsermeta.do() + .select("id", "name", "surname", "meta_value") + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.wp_user_id", + "wp_usermeta.user_id" + ) + .where({ + meta_key: "wp_capabilities", + }) + .andWhere((query) => { + for (const role of roles) { + query.orWhereLike("meta_value", `%${role}%`); + } + }); + + console.log(query.toString()); + + const users = await query; + + const results = users + .filter((user) => { + let value; + try { + value = PHPUnserialize.unserialize(user.meta_value); + } catch (error) { + throw new Error(`Error unserializing ${user.meta_value}`); + } + if (!value) return false; + if (typeof value !== "object") return false; + + return Object.keys(value).some((key) => { + return roles.includes(key); + }); + }) + .map((user) => { + return { + id: user.id, + name: user.name, + surname: user.surname, + }; + }); + + return results; + } + + private async getRolesWithVisibility() { + try { + const roles = await tryber.tables.WpOptions.do() + .select("option_value") + .where({ + option_name: "wp_user_roles", + }) + .first(); + + if (!roles) return []; + + let value; + try { + value = PHPUnserialize.unserialize(roles.option_value); + } catch (error) { + throw new Error(`Error unserializing ${roles.option_value}`); + } + + const results = Object.entries(value) + .filter(([, value]) => { + return Object.keys( + (value as { capabilities: Record }).capabilities + ).includes("wp_admin_visibility"); + }) + .map(([key]) => key); + return results; + } catch (error) { + return []; + } + } + + private async getUsersByRole() { + const users = await tryber.tables.WpUsermeta.do() + .select("id", "name", "surname", "meta_value") + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.wp_user_id", + "wp_usermeta.user_id" + ) + .where({ + meta_key: "wp_capabilities", + }) + .whereLike("meta_value", `%${this.roleName}%`); + + const results = users + .filter((user) => { + const value = PHPUnserialize.unserialize(user.meta_value); + if (!value) return false; + if (typeof value !== "object") return false; + + return Object.keys(value).includes(this.roleName); + }) + .map((user) => { + return { + id: user.id, + name: user.name, + surname: user.surname, + }; + }); + + return results; + } +} diff --git a/src/schema.ts b/src/schema.ts index 3d5d886ec..307f6beaa 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -562,6 +562,21 @@ export interface paths { }; }; }; + "/users/by-role/{role}": { + get: operations["get-users-by-role-role"]; + parameters: { + path: { + role: "tester_lead" | "quality_leader" | "ux_researcher" | "assistants"; + }; + }; + }; + "/browsers": { + get: operations["get-browsers"]; + }; + "/productTypes": { + get: operations["get-productTypes"]; + parameters: {}; + }; } export interface components { @@ -4171,6 +4186,58 @@ export interface operations { }; }; }; + "get-users-by-role-role": { + parameters: { + path: { + role: "tester_lead" | "quality_leader" | "ux_researcher" | "assistants"; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + surname: string; + }[]; + }; + }; + }; + }; + }; + "get-browsers": { + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + }[]; + }; + }; + }; + }; + }; + "get-productTypes": { + parameters: {}; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + }[]; + }; + }; + }; + }; + }; } export interface external {} From 61dc399b2d2486ec066ccdf4cc3e59d882c02c32 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:22:07 +0200 Subject: [PATCH 09/39] Allow duplication options (#309) * feat: Allow duplication * rework: Refactor polylang handling * feat: Add tester duplication * feat: Send new cp to wordpress regeneration api * fix: Aggiungi header User-Agent alle richieste GET al servizio di rigenerazione di Wordpress * feat: Add generate tasks * fix: Allow posting empty data --- deployment/after-install.sh | 1 + .../wp/WordpressJsonApiTrigger/index.spec.ts | 96 +++ .../wp/WordpressJsonApiTrigger/index.ts | 36 ++ src/reference/openapi.yml | 264 +++++---- src/routes/dossiers/_post/creation.spec.ts | 1 + src/routes/dossiers/_post/duplication.spec.ts | 545 ++++++++++++++++++ src/routes/dossiers/_post/index.spec.ts | 1 + src/routes/dossiers/_post/index.ts | 352 ++++++++++- src/routes/dossiers/_post/triggerWp.spec.ts | 134 +++++ src/schema.ts | 93 +-- 10 files changed, 1365 insertions(+), 158 deletions(-) create mode 100644 src/features/wp/WordpressJsonApiTrigger/index.spec.ts create mode 100644 src/features/wp/WordpressJsonApiTrigger/index.ts create mode 100644 src/routes/dossiers/_post/duplication.spec.ts create mode 100644 src/routes/dossiers/_post/triggerWp.spec.ts diff --git a/deployment/after-install.sh b/deployment/after-install.sh index 952cbd89e..ce148178a 100644 --- a/deployment/after-install.sh +++ b/deployment/after-install.sh @@ -86,6 +86,7 @@ services: SENTRY_SAMPLE_RATE: ${SENTRY_SAMPLE_RATE:-1} CLOUDFRONT_KEY_ID: ${CLOUDFRONT_KEY_ID} JOTFORM_APIKEY: ${JOTFORM_APIKEY} + WORDPRESS_API_URL: ${WORDPRESS_API_URL} volumes: - /var/docker/keys:/app/keys logging: diff --git a/src/features/wp/WordpressJsonApiTrigger/index.spec.ts b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts new file mode 100644 index 000000000..0c9e888d6 --- /dev/null +++ b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts @@ -0,0 +1,96 @@ +import axios from "axios"; +import WordpressJsonApiTrigger from "."; + +// mock axios + +jest.mock("axios"); + +describe("WordpressJsonApiTrigger", () => { + beforeAll(() => { + process.env = Object.assign(process.env, { + WORDPRESS_API_URL: "https://example.com", + }); + }); + + afterAll(() => { + jest.resetAllMocks(); + process.env = Object.assign(process.env, { + WORDPRESS_API_URL: "", + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it("Should create a new instance of WordpressJsonApiTrigger", () => { + const instance = new WordpressJsonApiTrigger(1); + expect(instance).toBeInstanceOf(WordpressJsonApiTrigger); + }); + + it("Should call axios on generateUseCase", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateUseCase(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, + method: "GET", + url: "https://example.com/regenerate-campaign-use-cases/1", + }); + }); + + it("Should call axios on generateMailmerges", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateMailMerges(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, + method: "GET", + url: "https://example.com/regenerate-campaign-crons/1", + }); + }); + it("Should call axios on generatePages", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generatePages(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, + method: "GET", + url: "https://example.com/regenerate-campaign-pages/1", + }); + }); + + it("Should call axios on generateTasks", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateTasks(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, + method: "GET", + url: "https://example.com/regenerate-campaign-tasks/1", + }); + }); +}); diff --git a/src/features/wp/WordpressJsonApiTrigger/index.ts b/src/features/wp/WordpressJsonApiTrigger/index.ts new file mode 100644 index 000000000..a5a94e142 --- /dev/null +++ b/src/features/wp/WordpressJsonApiTrigger/index.ts @@ -0,0 +1,36 @@ +import axios from "axios"; + +class WordpressJsonApiTrigger { + constructor(private campaign: number) {} + + public async generateUseCase() { + await this.postToWordpress( + `regenerate-campaign-use-cases/${this.campaign}` + ); + } + + public async generateMailMerges() { + await this.postToWordpress(`regenerate-campaign-crons/${this.campaign}`); + } + + public async generatePages() { + await this.postToWordpress(`regenerate-campaign-pages/${this.campaign}`); + } + + public async generateTasks() { + await this.postToWordpress(`regenerate-campaign-tasks/${this.campaign}`); + } + + private async postToWordpress(url: string) { + await axios({ + method: "GET", + url: `${process.env.WORDPRESS_API_URL}/${url}`, + headers: { + "User-Agent": "Tryber API", + "Content-Type": "application/json", + }, + }); + } +} + +export default WordpressJsonApiTrigger; diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 6e3b039aa..7bb5f5e84 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9688,10 +9688,43 @@ paths: Example 1: value: id: 1 - requestBody: - $ref: '#/components/requestBodies/DossierData' security: - JWT: [] + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/DossierCreationData' + - type: object + x-stoplight: + id: 7ewqh8piy4dc8 + properties: + duplicate: + type: object + x-stoplight: + id: qjlhi5gbkesdj + properties: + fields: + type: integer + x-stoplight: + id: eynp882a0yi85 + useCases: + type: integer + x-stoplight: + id: aeis659jristy + mailMerges: + type: integer + x-stoplight: + id: uzgqkctl150pz + pages: + type: integer + x-stoplight: + id: qhyggxgrgajc1 + testers: + type: integer + x-stoplight: + id: lobzb13vjwkln '/dossiers/{campaign}': parameters: - name: campaign @@ -9712,10 +9745,13 @@ paths: type: object properties: {} description: '' - requestBody: - $ref: '#/components/requestBodies/DossierData' security: - JWT: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DossierCreationData' get: summary: '' operationId: get-dossiers-campaign @@ -10955,6 +10991,117 @@ components: pattern: '^[A-Z][A-Z]$' minLength: 2 maxLength: 2 + DossierCreationData: + type: object + x-examples: + Example 1: + project: 0 + testType: 0 + title: + customer: string + tester: string + startDate: '2019-08-24T14:15:22Z' + endDate: '2019-08-24T14:15:22Z' + closeDate: '2019-08-24T14:15:22Z' + deviceList: + - 0 + csm: 0 + roles: + - role: 0 + user: 0 + description: string + productLink: string + goal: string + outOfScope: string + deviceRequirements: string + target: + notes: string + size: 0 + countries: + - st + languages: + - 0 + browsers: + - 0 + productType: 0 + properties: + project: + type: integer + testType: + type: integer + title: + type: object + required: + - customer + properties: + customer: + type: string + tester: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + closeDate: + type: string + format: date-time + deviceList: + type: array + items: + type: integer + csm: + type: integer + roles: + type: array + items: + type: object + properties: + role: + type: integer + user: + type: integer + required: + - role + - user + description: + type: string + productLink: + type: string + goal: + type: string + outOfScope: + type: string + deviceRequirements: + type: string + target: + type: object + properties: + notes: + type: string + size: + type: integer + countries: + type: array + items: + $ref: '#/components/schemas/CountryCode' + languages: + type: array + items: + type: integer + browsers: + type: array + items: + type: integer + productType: + type: integer + required: + - project + - testType + - title + - startDate + - deviceList securitySchemes: JWT: type: http @@ -11253,114 +11400,7 @@ components: schema: type: string examples: {} - requestBodies: - DossierData: - content: - application/json: - schema: - type: object - x-examples: - Example 1: - project: 1 - testType: 1 - title: - customer: Campaign Title for Customer - tester: Campaign Title for Tester - startDate: '2019-08-24T14:15:22Z' - endDate: '2019-08-24T14:15:22Z' - deviceList: - - 1 - - 5 - - 10 - - 36 - properties: - project: - type: integer - testType: - type: integer - title: - type: object - required: - - customer - properties: - customer: - type: string - tester: - type: string - startDate: - type: string - format: date-time - endDate: - type: string - format: date-time - closeDate: - type: string - format: date-time - deviceList: - type: array - items: - type: integer - csm: - type: number - roles: - type: array - items: - type: object - properties: - role: - type: number - user: - type: number - required: - - role - - user - description: - type: string - productLink: - type: string - goal: - type: string - outOfScope: - type: string - deviceRequirements: - type: string - target: - type: object - properties: - notes: - type: string - size: - type: integer - countries: - type: array - items: - $ref: '#/components/schemas/CountryCode' - languages: - type: array - x-stoplight: - id: dy0yofmqy4ayn - items: - x-stoplight: - id: bko78gf0sdhjz - type: integer - browsers: - type: array - x-stoplight: - id: jc5nfd988spfc - items: - x-stoplight: - id: 9wdgspthmxvhf - type: integer - productType: - type: integer - x-stoplight: - id: ilm9662wwxef3 - required: - - project - - testType - - title - - startDate - - deviceList + requestBodies: {} tags: - name: Authentication - name: Campaign diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 402ce6a41..07a42fb2b 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -2,6 +2,7 @@ import app from "@src/app"; import { tryber } from "@src/features/database"; import request from "supertest"; +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); const baseRequest = { project: 10, testType: 10, diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts new file mode 100644 index 000000000..794099397 --- /dev/null +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -0,0 +1,545 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; +import request from "supertest"; + +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers - duplication", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + + await tryber.tables.WpOptions.do().insert({ + option_name: "polylang", + option_value: + 'a:16:{s:7:"browser";b:1;s:7:"rewrite";i:1;s:12:"hide_default";i:1;s:10:"force_lang";i:1;s:13:"redirect_lang";i:1;s:13:"media_support";i:1;s:9:"uninstall";i:0;s:4:"sync";a:0:{}s:10:"post_types";a:2:{i:0;s:6:"manual";i:1;s:7:"preview";}s:10:"taxonomies";a:0:{}s:7:"domains";a:0:{}s:7:"version";s:5:"3.1.1";s:16:"first_activation";i:1562051895;s:12:"default_lang";s:2:"en";s:9:"nav_menus";a:1:{s:15:"crowdappquality";a:1:{s:7:"primary";a:2:{s:2:"en";i:2;s:2:"it";i:163;}}}s:16:"previous_version";s:3:"3.1";}', + }); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + platform_id: 1, + start_date: "1970-01-01", + end_date: "1970-01-01", + title: "Origin Campaign", + customer_title: "Origin Campaign", + page_manual_id: 1, + page_preview_id: 2, + customer_id: 1, + project_id: 1, + pm_id: 1, + }); + + await tryber.tables.WpAppqCampaignAdditionalFields.do().insert({ + cp_id: 1, + slug: "field", + title: "Field", + type: "regex", + validation: ".*", + error_message: "Invalid format", + }); + + await tryber.tables.WpAppqCampaignTask.do().insert({ + id: 1, + campaign_id: 1, + title: "Task", + content: "Task content", + simple_title: "Task", + info: "Task info", + prefix: "T", + language: "en", + optimize_media: 1, + is_required: 1, + jf_code: "T", + jf_text: "JF", + }); + + await tryber.tables.WpAppqCampaignTaskGroup.do().insert({ + task_id: 1, + group_id: 4, + }); + + await tryber.tables.WpAppqCronJobs.do().insert({ + id: 1, + campaign_id: 1, + display_name: "Test Mail Merge", + email_template_id: 1, + template_text: "Test Template", + template_json: "{}", + last_editor_id: 1, + creation_date: "1970-01-01", + template_id: 1, + update_date: "1970-01-01", + executed_on: "", + }); + + const page = { + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + to_ping: "", + pinged: "", + post_content_filtered: "", + }; + + await tryber.tables.WpPosts.do().insert([ + { + ...page, + ID: 1, + post_title: "CP1 - Test Manual", + post_type: "manual", + }, + { + ...page, + ID: 2, + post_title: "CP1 - Test Preview", + post_type: "preview", + }, + { + ...page, + ID: 3, + post_title: "CP1 - Test Manual (it)", + post_type: "manual", + }, + { + ...page, + ID: 4, + post_title: "CP1 - Test Preview (it)", + post_type: "preview", + }, + ]); + + await tryber.tables.WpTermRelationships.do().insert([ + { object_id: 1, term_taxonomy_id: 1 }, + { object_id: 2, term_taxonomy_id: 2 }, + ]); + + await tryber.tables.WpTermTaxonomy.do().insert([ + { + term_taxonomy_id: 1, + term_id: 1, + taxonomy: "post_translations", + description: 'a:2:{s:2:"en";i:1;s:2:"it";i:3;}', + }, + { + term_taxonomy_id: 2, + term_id: 2, + taxonomy: "post_translations", + description: 'a:2:{s:2:"en";i:2;s:2:"it";i:4;}', + }, + ]); + + await tryber.tables.WpPostmeta.do().insert([ + { + post_id: 1, + meta_key: "_wp_page_template", + meta_value: "page-manual.php", + }, + { post_id: 1, meta_key: "acf_field", meta_value: "value" }, + { + post_id: 2, + meta_key: "_wp_page_template", + meta_value: "page-preview.php", + }, + { post_id: 2, meta_key: "acf_field", meta_value: "value" }, + ]); + await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ + user_id: 1, + campaign_id: 1, + subscription_date: "1970-01-01", + accepted: 1, + devices: "1", + selected_device: 1, + modified: "1970-01-01", + group_id: 1, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.WpAppqCampaignAdditionalFields.do().delete(); + await tryber.tables.WpAppqCampaignTask.do().delete(); + await tryber.tables.WpAppqCampaignTaskGroup.do().delete(); + await tryber.tables.WpAppqCronJobs.do().delete(); + await tryber.tables.WpPosts.do().delete(); + await tryber.tables.WpPostmeta.do().delete(); + await tryber.tables.WpTermRelationships.do().delete(); + await tryber.tables.WpTermTaxonomy.do().delete(); + await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); + + jest.clearAllMocks(); + }); + + it("Should return 400 if campaign to duplicate does not exist", async () => { + const fields = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { fields: 100 } }); + + expect(fields.status).toBe(400); + + const useCases = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { useCases: 100 } }); + + expect(useCases.status).toBe(400); + + const mailMerges = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { mailMerges: 100 } }); + + expect(mailMerges.status).toBe(400); + + const pages = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { pages: 100 } }); + + expect(pages.status).toBe(400); + + const testers = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { testers: 100 } }); + + expect(testers.status).toBe(400); + }); + + it("Should duplicate additional fields", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { fields: 1 } }); + expect(response.status).toBe(201); + + const id = response.body.id; + + const fields = await tryber.tables.WpAppqCampaignAdditionalFields.do() + .select() + .where({ cp_id: id }); + + expect(fields).toHaveLength(1); + expect(fields[0]).toMatchObject({ + slug: "field", + title: "Field", + type: "regex", + validation: ".*", + error_message: "Invalid format", + }); + }); + + it("Should duplicate usecases", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { useCases: 1 } }); + expect(response.status).toBe(201); + + const id = response.body.id; + + const tasks = await tryber.tables.WpAppqCampaignTask.do() + .select() + .where({ campaign_id: id }); + + expect(tasks).toHaveLength(1); + expect(tasks[0]).toMatchObject({ + title: "Task", + content: "Task content", + simple_title: "Task", + info: "Task info", + prefix: "T", + language: "en", + optimize_media: 1, + is_required: 1, + jf_code: "T", + jf_text: "JF", + }); + + const groups = await tryber.tables.WpAppqCampaignTaskGroup.do() + .select() + .where({ task_id: tasks[0].id }); + + expect(groups).toHaveLength(1); + expect(groups[0]).toMatchObject({ group_id: 4 }); + }); + + it("Should duplicate mail merges", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const mailMerges = await tryber.tables.WpAppqCronJobs.do() + .select() + .where({ campaign_id: id }); + + expect(mailMerges).toHaveLength(1); + expect(mailMerges[0]).toMatchObject({ + display_name: "Test Mail Merge", + email_template_id: 1, + template_text: "Test Template", + template_json: "{}", + last_editor_id: 1, + }); + }); + + it("Should duplicate pages", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + + .send({ ...baseRequest, duplicate: { pages: 1 } }); + + const posts = await tryber.tables.WpPosts.do().select(); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const pages = await tryber.tables.WpAppqEvdCampaign.do() + .select("page_manual_id", "page_preview_id") + .where({ id }); + + expect(pages).toHaveLength(1); + + const manualId = pages[0].page_manual_id; + const previewId = pages[0].page_preview_id; + + const manual = await tryber.tables.WpPosts.do() + .select() + .where({ ID: manualId }); + + expect(manual).toHaveLength(1); + expect(manual[0]).toMatchObject({ + post_title: `CP${id} - Test Manual`, + post_type: "manual", + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + }); + + const preview = await tryber.tables.WpPosts.do() + .select() + .where({ ID: previewId }); + + expect(preview).toHaveLength(1); + + expect(preview[0]).toMatchObject({ + post_title: `CP${id} - Test Preview`, + post_type: "preview", + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + }); + + const manualMeta = await tryber.tables.WpPostmeta.do() + .select() + .where({ post_id: manualId }); + + expect(manualMeta).toHaveLength(2); + expect(manualMeta[0]).toMatchObject({ + meta_key: "_wp_page_template", + meta_value: "page-manual.php", + }); + + expect(manualMeta[1]).toMatchObject({ + meta_key: "acf_field", + meta_value: "value", + }); + + const previewMeta = await tryber.tables.WpPostmeta.do() + .select() + .where({ post_id: previewId }); + + expect(previewMeta).toHaveLength(2); + expect(previewMeta[0]).toMatchObject({ + meta_key: "_wp_page_template", + meta_value: "page-preview.php", + }); + + expect(previewMeta[1]).toMatchObject({ + meta_key: "acf_field", + meta_value: "value", + }); + }); + + it("Should duplicate all languages", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + + .send({ ...baseRequest, duplicate: { pages: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const posts = await tryber.tables.WpPosts.do().select(); + + expect(posts).toHaveLength(8); + + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Manual`, + post_type: "manual", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Preview`, + post_type: "preview", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Manual (it)`, + post_type: "manual", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Preview (it)`, + post_type: "preview", + }) + ); + }); + + it("Should duplicate testers", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { testers: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const testers = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select() + .where({ campaign_id: id }); + + expect(testers).toHaveLength(1); + + expect(testers[0]).toMatchObject({ + user_id: 1, + campaign_id: id, + subscription_date: "1970-01-01", + accepted: 1, + devices: "1", + selected_device: 1, + modified: "1970-01-01", + group_id: 1, + }); + }); + + it("Should not post to wordpress if usecase is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { useCases: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateUseCase + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if mailmerge is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if mailmerge is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if pages is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { pages: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generatePages + ).not.toHaveBeenCalled(); + }); +}); diff --git a/src/routes/dossiers/_post/index.spec.ts b/src/routes/dossiers/_post/index.spec.ts index 356694715..a3465aa61 100644 --- a/src/routes/dossiers/_post/index.spec.ts +++ b/src/routes/dossiers/_post/index.spec.ts @@ -2,6 +2,7 @@ import app from "@src/app"; import { tryber } from "@src/features/database"; import request from "supertest"; +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); const baseRequest = { project: 1, testType: 1, diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index fe73a9164..e849bc811 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -3,11 +3,36 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import AdminRoute from "@src/features/routes/AdminRoute"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; +import { unserialize } from "php-unserialize"; export default class RouteItem extends AdminRoute<{ response: StoplightOperations["post-dossiers"]["responses"]["201"]["content"]["application/json"]; body: StoplightOperations["post-dossiers"]["requestBody"]["content"]["application/json"]; }> { + private duplicate: { + fieldsFrom?: number; + useCasesFrom?: number; + mailMergesFrom?: number; + pagesFrom?: number; + testersFrom?: number; + } = {}; + + constructor(config: RouteClassConfiguration) { + super(config); + + const { duplicate } = this.getBody(); + + if (duplicate) { + if (duplicate.fields) this.duplicate.fieldsFrom = duplicate.fields; + if (duplicate.useCases) this.duplicate.useCasesFrom = duplicate.useCases; + if (duplicate.mailMerges) + this.duplicate.mailMergesFrom = duplicate.mailMerges; + if (duplicate.pages) this.duplicate.pagesFrom = duplicate.pages; + if (duplicate.testers) this.duplicate.testersFrom = duplicate.testers; + } + } + protected async filter() { if (!(await super.filter())) return false; if (await this.invalidRolesSubmitted()) { @@ -26,10 +51,33 @@ export default class RouteItem extends AdminRoute<{ this.setError(400, new OpenapiError("Invalid devices")); return false; } + if (await this.campaignToDuplicateDoesNotExist()) { + this.setError(400, new OpenapiError("Invalid campaign to duplicate")); + return false; + } return true; } + private async campaignToDuplicateDoesNotExist() { + const ids = [ + ...new Set([ + ...(this.duplicate.fieldsFrom ? [this.duplicate.fieldsFrom] : []), + ...(this.duplicate.useCasesFrom ? [this.duplicate.useCasesFrom] : []), + ...(this.duplicate.mailMergesFrom + ? [this.duplicate.mailMergesFrom] + : []), + ...(this.duplicate.pagesFrom ? [this.duplicate.pagesFrom] : []), + ...(this.duplicate.testersFrom ? [this.duplicate.testersFrom] : []), + ]), + ]; + const campaigns = await tryber.tables.WpAppqEvdCampaign.do() + .select("id") + .whereIn("id", ids); + + return campaigns.length !== ids.length; + } + private async invalidRolesSubmitted() { const { roles } = this.getBody(); if (!roles) return false; @@ -82,6 +130,8 @@ export default class RouteItem extends AdminRoute<{ const campaignId = await this.createCampaign(); await this.linkRolesToCampaign(campaignId); + await this.generateLinkedData(campaignId); + this.setSuccess(201, { id: campaignId, }); @@ -137,7 +187,7 @@ export default class RouteItem extends AdminRoute<{ const dossierId = dossier[0].id ?? dossier[0]; const countries = this.getBody().countries; - if (countries) { + if (countries?.length) { await tryber.tables.CampaignDossierDataCountries.do().insert( countries.map((country) => ({ campaign_dossier_data_id: dossierId, @@ -147,7 +197,7 @@ export default class RouteItem extends AdminRoute<{ } const languages = this.getBody().languages; - if (languages) { + if (languages?.length) { await tryber.tables.CampaignDossierDataLanguages.do().insert( languages.map((language) => ({ campaign_dossier_data_id: dossierId, @@ -157,7 +207,7 @@ export default class RouteItem extends AdminRoute<{ } const browsers = this.getBody().browsers; - if (browsers) { + if (browsers?.length) { await tryber.tables.CampaignDossierDataBrowsers.do().insert( browsers.map((browser) => ({ campaign_dossier_data_id: dossierId, @@ -171,7 +221,7 @@ export default class RouteItem extends AdminRoute<{ private async linkRolesToCampaign(campaignId: number) { const roles = this.getBody().roles; - if (!roles) return; + if (!roles?.length) return; await tryber.tables.CampaignCustomRoles.do().insert( roles.map((role) => ({ @@ -184,9 +234,301 @@ export default class RouteItem extends AdminRoute<{ await this.assignOlps(campaignId); } + private async generateLinkedData(campaignId: number) { + const apiTrigger = new WordpressJsonApiTrigger(campaignId); + + await apiTrigger.generateTasks(); + + if (this.duplicate.fieldsFrom) await this.duplicateFields(campaignId); + + if (this.duplicate.useCasesFrom) await this.duplicateUsecases(campaignId); + else await apiTrigger.generateUseCase(); + + if (this.duplicate.mailMergesFrom) + await this.duplicateMailMerge(campaignId); + else await apiTrigger.generateMailMerges(); + + if (this.duplicate.pagesFrom) await this.duplicatePages(campaignId); + else await apiTrigger.generatePages(); + + if (this.duplicate.testersFrom) await this.duplicateTesters(campaignId); + } + + private async duplicateFields(campaignId: number) { + if (!this.duplicate.fieldsFrom) return; + + const fields = await tryber.tables.WpAppqCampaignAdditionalFields.do() + .select() + .where({ + cp_id: this.duplicate.fieldsFrom, + }); + + if (!fields.length) return; + + await tryber.tables.WpAppqCampaignAdditionalFields.do().insert( + fields.map((field) => { + const { id, ...rest } = field; + return { + ...rest, + cp_id: campaignId, + }; + }) + ); + } + + private async duplicateUsecases(campaignId: number) { + if (!this.duplicate.useCasesFrom) return; + + const tasks = await tryber.tables.WpAppqCampaignTask.do().select().where({ + campaign_id: this.duplicate.useCasesFrom, + }); + + const taskMap = new Map(); + + for (const task of tasks) { + const { id, ...rest } = task; + const newItem = await tryber.tables.WpAppqCampaignTask.do() + .insert({ + ...rest, + campaign_id: campaignId, + }) + .returning("id"); + taskMap.set(id, newItem[0].id ?? newItem[0]); + } + + const groups = await tryber.tables.WpAppqCampaignTaskGroup.do() + .select() + .whereIn( + "task_id", + tasks.map((task) => task.id) + ); + + if (!groups.length) return; + + await tryber.tables.WpAppqCampaignTaskGroup.do().insert( + groups.map((group) => ({ + ...group, + task_id: taskMap.get(group.task_id), + })) + ); + } + + private async duplicateMailMerge(campaignId: number) { + if (!this.duplicate.mailMergesFrom) return; + + const mailMerges = await tryber.tables.WpAppqCronJobs.do() + .select( + "display_name", + "email_template_id", + "template_text", + "template_json", + "last_editor_id", + "template_id" + ) + .where("campaign_id", this.duplicate.mailMergesFrom) + .where("email_template_id", ">", 0); + + if (!mailMerges.length) return; + + await tryber.tables.WpAppqCronJobs.do().insert( + mailMerges.map((mailMerge) => ({ + ...mailMerge, + campaign_id: campaignId, + creation_date: tryber.fn.now(), + update_date: tryber.fn.now(), + executed_on: "", + })) + ); + } + + private async duplicatePages(campaignId: number) { + if (!this.duplicate.pagesFrom) return; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("page_manual_id", "page_preview_id") + .where("id", this.duplicate.pagesFrom); + + if (!campaign.length) return; + + const { page_manual_id, page_preview_id } = campaign[0]; + + const manualId = await this.duplicatePage({ + pageId: page_manual_id, + campaignId, + }); + + if (manualId) { + await tryber.tables.WpAppqEvdCampaign.do().update({ + page_manual_id: manualId, + }); + } + + const previewId = await this.duplicatePage({ + pageId: page_preview_id, + campaignId, + }); + + if (previewId) { + await tryber.tables.WpAppqEvdCampaign.do().update({ + page_preview_id: previewId, + }); + } + + if (manualId || previewId) { + const defaultLanguage = await this.getDefaultLanguage(); + if (!defaultLanguage) return; + if (manualId) { + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_manual_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + } + } + + if (previewId) { + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_preview_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + } + } + } + } + + private async duplicatePage({ + pageId, + campaignId, + }: { + pageId: number; + campaignId: number; + }) { + if (!this.duplicate.pagesFrom) return; + + const page = await tryber.tables.WpPosts.do() + .select() + .where("ID", pageId) + .first(); + + if (!page) return null; + + const { ID, ...rest } = page; + const newPage = await tryber.tables.WpPosts.do() + .insert({ + ...rest, + post_title: rest.post_title.replace( + this.duplicate.pagesFrom.toString(), + campaignId.toString() + ), + }) + .returning("ID"); + + const meta = await tryber.tables.WpPostmeta.do() + .select() + .where("post_id", ID); + + if (meta.length) { + await tryber.tables.WpPostmeta.do().insert( + meta.map((metaItem) => { + const { meta_id, ...rest } = metaItem; + return { + ...rest, + post_id: newPage[0].ID ?? newPage[0], + }; + }) + ); + } + + const newId = newPage[0].ID ?? newPage[0]; + return newId; + } + + private async duplicateTesters(campaignId: number) { + if (!this.duplicate.testersFrom) return; + + const testers = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select( + "user_id", + "subscription_date", + "accepted", + "devices", + "selected_device", + "modified", + "group_id" + ) + .where("campaign_id", this.duplicate.testersFrom); + + if (!testers.length) return; + + await tryber.tables.WpCrowdAppqHasCandidate.do().insert( + testers.map((tester) => ({ + ...tester, + campaign_id: campaignId, + })) + ); + } + + private async getDefaultLanguage() { + const polylangOptions = await tryber.tables.WpOptions.do() + .select("option_value") + .where("option_name", "polylang") + .first(); + + if (!polylangOptions) return false; + let parsedOptions: { [key: string]: any } = {}; + try { + parsedOptions = unserialize(polylangOptions.option_value); + } catch (e) { + return false; + } + if (!("default_lang" in parsedOptions)) return false; + return parsedOptions.default_lang; + } + + private async getPageTranslationIds({ + pageId, + defaultLanguage, + }: { + pageId: number; + defaultLanguage: string; + }) { + const translations = await tryber.tables.WpTermRelationships.do() + .select("object_id", "description") + .join( + "wp_term_taxonomy", + "wp_term_taxonomy.term_taxonomy_id", + "wp_term_relationships.term_taxonomy_id" + ) + .where("object_id", pageId) + .where("taxonomy", "post_translations") + .first(); + + if (!translations) return {}; + + let parsedTranslations: { [key: string]: any } = {}; + try { + parsedTranslations = unserialize(translations.description); + } catch (e) { + return; + } + + if (defaultLanguage in parsedTranslations) + delete parsedTranslations[defaultLanguage]; + return parsedTranslations; + } + private async assignOlps(campaignId: number) { const roles = this.getBody().roles; - if (!roles) return; + if (!roles?.length) return; const roleOlps = await tryber.tables.CustomRoles.do() .select("id", "olp") diff --git a/src/routes/dossiers/_post/triggerWp.spec.ts b/src/routes/dossiers/_post/triggerWp.spec.ts new file mode 100644 index 000000000..53710322c --- /dev/null +++ b/src/routes/dossiers/_post/triggerWp.spec.ts @@ -0,0 +1,134 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; +import request from "supertest"; + +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers - duplication", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + }); + + beforeEach(async () => {}); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.WpAppqCampaignAdditionalFields.do().delete(); + + jest.clearAllMocks(); + }); + + it("Should post to wordpress if usecase is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateUseCase + ).toHaveBeenCalledTimes(1); + }); + + it("Should post to wordpress if pages is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generatePages + ).toHaveBeenCalledTimes(1); + }); + + it("Should post to wordpress if mailmerge is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).toHaveBeenCalledTimes(1); + }); + + it("Should post to wordpress to generate tasks", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateTasks + ).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/schema.ts b/src/schema.ts index 307f6beaa..223330016 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -886,6 +886,39 @@ export interface components { ProspectStatus: "draft" | "confirmed" | "done"; /** CountryCode */ CountryCode: string; + DossierCreationData: { + project: number; + testType: number; + title: { + customer: string; + tester?: string; + }; + /** Format: date-time */ + startDate: string; + /** Format: date-time */ + endDate?: string; + /** Format: date-time */ + closeDate?: string; + deviceList: number[]; + csm?: number; + roles?: { + role: number; + user: number; + }[]; + description?: string; + productLink?: string; + goal?: string; + outOfScope?: string; + deviceRequirements?: string; + target?: { + notes?: string; + size?: number; + }; + countries?: components["schemas"]["CountryCode"][]; + languages?: number[]; + browsers?: number[]; + productType?: number; + }; }; responses: { /** A user */ @@ -974,45 +1007,7 @@ export interface components { search: string; testerId: string; }; - requestBodies: { - DossierData: { - content: { - "application/json": { - project: number; - testType: number; - title: { - customer: string; - tester?: string; - }; - /** Format: date-time */ - startDate: string; - /** Format: date-time */ - endDate?: string; - /** Format: date-time */ - closeDate?: string; - deviceList: number[]; - csm?: number; - roles?: { - role: number; - user: number; - }[]; - description?: string; - productLink?: string; - goal?: string; - outOfScope?: string; - deviceRequirements?: string; - target?: { - notes?: string; - size?: number; - }; - countries?: components["schemas"]["CountryCode"][]; - languages?: number[]; - browsers?: number[]; - productType?: number; - }; - }; - }; - }; + requestBodies: {}; } export interface operations { @@ -4066,7 +4061,19 @@ export interface operations { }; }; }; - requestBody: components["requestBodies"]["DossierData"]; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"] & { + duplicate?: { + fields?: number; + useCases?: number; + mailMerges?: number; + pages?: number; + testers?: number; + }; + }; + }; + }; }; "get-dossiers-campaign": { parameters: { @@ -4164,7 +4171,11 @@ export interface operations { }; }; }; - requestBody: components["requestBodies"]["DossierData"]; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"]; + }; + }; }; "get-customers-customer-projects": { parameters: { From 186b62eb0dbc38dcbfbcc033b4e7a6851b1bd65c Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 24 Apr 2024 17:49:53 +0200 Subject: [PATCH 10/39] feat: Always use campaign id as id --- .../dossiers/campaignId/_get/index.spec.ts | 23 +++++++++++++------ src/routes/dossiers/campaignId/_get/index.ts | 8 +++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index 0df641dd3..90bc4d489 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -251,7 +251,7 @@ describe("Route GET /dossiers/:id", () => { describe("With dossier data", () => { beforeAll(async () => { await tryber.tables.CampaignDossierData.do().insert({ - id: 1, + id: 100, campaign_id: 1, description: "Original description", link: "Original link", @@ -266,11 +266,11 @@ describe("Route GET /dossiers/:id", () => { }); await tryber.tables.CampaignDossierDataCountries.do().insert([ { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, country_code: "IT", }, { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, country_code: "FR", }, ]); @@ -290,11 +290,11 @@ describe("Route GET /dossiers/:id", () => { await tryber.tables.CampaignDossierDataLanguages.do().insert([ { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, language_id: 1, }, { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, language_id: 2, }, ]); @@ -312,11 +312,11 @@ describe("Route GET /dossiers/:id", () => { await tryber.tables.CampaignDossierDataBrowsers.do().insert([ { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, browser_id: 1, }, { - campaign_dossier_data_id: 1, + campaign_dossier_data_id: 100, browser_id: 2, }, ]); @@ -476,5 +476,14 @@ describe("Route GET /dossiers/:id", () => { expect(response.body.productType).toHaveProperty("id", 1); expect(response.body.productType).toHaveProperty("name", "Test Product"); }); + + it("Should return id", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id", 1); + }); }); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 748a2895b..30e22c683 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -115,7 +115,7 @@ export default class RouteItem extends AdminRoute<{ const dossierData = await tryber.tables.CampaignDossierData.do() .select( - tryber.ref("id").withSchema("campaign_dossier_data"), + tryber.ref("id").withSchema("campaign_dossier_data").as("dossier_id"), "description", "link", "goal", @@ -137,7 +137,7 @@ export default class RouteItem extends AdminRoute<{ const targetCountries = dossierData ? await tryber.tables.CampaignDossierDataCountries.do() .select("country_code") - .where("campaign_dossier_data_id", dossierData.id) + .where("campaign_dossier_data_id", dossierData.dossier_id) : []; const targetLanguages = dossierData @@ -149,7 +149,7 @@ export default class RouteItem extends AdminRoute<{ ) .select("language_id") .select("display_name") - .where("campaign_dossier_data_id", dossierData.id) + .where("campaign_dossier_data_id", dossierData.dossier_id) : []; const targetBrowsers = dossierData @@ -161,7 +161,7 @@ export default class RouteItem extends AdminRoute<{ ) .select("browser_id") .select("name") - .where("campaign_dossier_data_id", dossierData.id) + .where("campaign_dossier_data_id", dossierData.dossier_id) : []; return { From e320f408ab3aa8dcb63deeb043e5b11df46e6827 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:04:04 +0200 Subject: [PATCH 11/39] feat: Add create project (#310) --- src/reference/openapi.yml | 36 +++++++ .../customer/projects/_post/index.spec.ts | 99 +++++++++++++++++++ .../customer/projects/_post/index.ts | 69 +++++++++++++ src/schema.ts | 26 +++++ 4 files changed, 230 insertions(+) create mode 100644 src/routes/customers/customer/projects/_post/index.spec.ts create mode 100644 src/routes/customers/customer/projects/_post/index.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 7bb5f5e84..44e2e06fd 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10024,6 +10024,42 @@ paths: description: '' security: - JWT: [] + post: + summary: '' + operationId: post-customers-customer-projects + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: number + x-stoplight: + id: 23mwsogkyrhe8 + name: + type: string + x-stoplight: + id: zj6433e2mxne6 + required: + - id + - name + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + x-stoplight: + id: 7dx62s7fiaz1z + required: + - name '/users/by-role/{role}': get: summary: Your GET endpoint diff --git a/src/routes/customers/customer/projects/_post/index.spec.ts b/src/routes/customers/customer/projects/_post/index.spec.ts new file mode 100644 index 000000000..291e6951f --- /dev/null +++ b/src/routes/customers/customer/projects/_post/index.spec.ts @@ -0,0 +1,99 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const campaign = { + platform_id: 1, + start_date: "2023-01-13 10:10:10", + end_date: "2023-01-14 10:10:10", + title: "", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + customer_title: "", +}; +describe("GET /campaigns", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...campaign, + id: 1, + project_id: 1, + }, + ]); + await tryber.tables.WpAppqCustomer.do().insert([ + { + id: 1, + company: "Company 1", + pm_id: 1, + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCustomer.do().delete(); + }); + + afterEach(async () => { + await tryber.tables.WpAppqProject.do().delete(); + }); + + it("Should answer 403 if not logged in", () => { + return request(app) + .post("/customers/1/projects") + .send({ name: "New project" }) + .expect(403); + }); + it("Should answer 403 if logged in without permissions", async () => { + const response = await request(app) + .post("/customers/1/projects") + .send({ name: "New project" }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + it("Should answer 403 if customer does not exists", async () => { + const response = await request(app) + .post("/customers/100/projects") + .send({ name: "New project" }) + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(403); + }); + it("Should answer 201 if logged as user with full access on campaigns", async () => { + const response = await request(app) + .post("/customers/1/projects") + .send({ name: "New project" }) + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(201); + }); + it("Should answer 403 if logged as user with access to some campaigns", async () => { + const response = await request(app) + .post("/customers/1/projects") + .send({ name: "New project" }) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1,2]}'); + expect(response.status).toBe(403); + }); + + it("Should add projects to the customer", async () => { + const postResponse = await request(app) + .post("/customers/1/projects") + .send({ name: "New project" }) + .set("Authorization", "Bearer admin"); + + expect(postResponse.status).toBe(201); + expect(postResponse.body).toHaveProperty("id"); + expect(postResponse.body).toHaveProperty("name"); + const { id, name } = postResponse.body; + + const getResponse = await request(app) + .get("/customers/1/projects") + .set("Authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + + const projects = getResponse.body.results; + expect(projects).toHaveLength(1); + expect(projects[0].id).toBe(id); + expect(projects[0].name).toBe(name); + }); +}); diff --git a/src/routes/customers/customer/projects/_post/index.ts b/src/routes/customers/customer/projects/_post/index.ts new file mode 100644 index 000000000..c7b7de5ab --- /dev/null +++ b/src/routes/customers/customer/projects/_post/index.ts @@ -0,0 +1,69 @@ +/** OPENAPI-CLASS : post-customers-customer-projects */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +class RouteItem extends UserRoute<{ + response: StoplightOperations["post-customers-customer-projects"]["responses"]["200"]["content"]["application/json"]; + body: StoplightOperations["post-customers-customer-projects"]["requestBody"]["content"]["application/json"]; + parameters: StoplightOperations["post-customers-customer-projects"]["parameters"]["path"]; +}> { + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + private customerId: number; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + this.customerId = Number(this.getParameters().customer); + } + + protected async filter() { + if ((await super.filter()) === false) return false; + if (this.doesNotHaveAccessToCampaigns()) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + if (await this.customerDoesNotExist()) { + this.setError(403, new OpenapiError("Customer does not exist")); + return false; + } + return true; + } + + private doesNotHaveAccessToCampaigns() { + return this.accessibleCampaigns !== true; + } + + private async customerDoesNotExist() { + const results = await tryber.tables.WpAppqCustomer.do() + .select("id") + .where("id", this.customerId); + + return results.length === 0; + } + + protected async prepare(): Promise { + const project = await this.createProject(); + return this.setSuccess(201, project); + } + + private async createProject() { + const project = await tryber.tables.WpAppqProject.do() + .insert({ + customer_id: this.customerId, + display_name: this.getBody().name, + edited_by: this.getTesterId(), + }) + .returning("id"); + const id = project[0].id ?? project[0]; + + return { + id: id, + name: this.getBody().name, + }; + } +} + +export default RouteItem; diff --git a/src/schema.ts b/src/schema.ts index 223330016..eeb13b76f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -556,6 +556,7 @@ export interface paths { }; "/customers/{customer}/projects": { get: operations["get-customers-customer-projects"]; + post: operations["post-customers-customer-projects"]; parameters: { path: { customer: string; @@ -4197,6 +4198,31 @@ export interface operations { }; }; }; + "post-customers-customer-projects": { + parameters: { + path: { + customer: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + id: number; + name: string; + }; + }; + }; + }; + requestBody: { + content: { + "application/json": { + name: string; + }; + }; + }; + }; "get-users-by-role-role": { parameters: { path: { From 240a8c59231c11b6369b46a9f0d4d3ac8fcb5f2e Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:40:25 +0200 Subject: [PATCH 12/39] feat: Add post customer (#311) --- src/reference/openapi.yml | 36 ++++++++++++++ src/routes/customers/_post/index.spec.ts | 60 ++++++++++++++++++++++++ src/routes/customers/_post/index.ts | 49 +++++++++++++++++++ src/schema.ts | 22 +++++++++ 4 files changed, 167 insertions(+) create mode 100644 src/routes/customers/_post/index.spec.ts create mode 100644 src/routes/customers/_post/index.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 44e2e06fd..2377d5c16 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -4136,6 +4136,42 @@ paths: security: - JWT: [] parameters: [] + post: + summary: '' + operationId: post-customers + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + x-stoplight: + id: zxshcfdjbugtr + name: + type: string + x-stoplight: + id: s32ugpcfzhrmp + required: + - id + - name + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + x-stoplight: + id: 5gyi3swml7tyw + required: + - name /custom_user_fields: get: summary: Get all custom user fields diff --git a/src/routes/customers/_post/index.spec.ts b/src/routes/customers/_post/index.spec.ts new file mode 100644 index 000000000..7ebe878c2 --- /dev/null +++ b/src/routes/customers/_post/index.spec.ts @@ -0,0 +1,60 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("POST /customers", () => { + afterEach(async () => { + await tryber.tables.WpAppqCustomer.do().delete(); + }); + + it("Should answer 403 if not logged in", () => { + return request(app) + .post("/customers") + .send({ name: "New project" }) + .expect(403); + }); + it("Should answer 403 if logged in without permissions", async () => { + const response = await request(app) + .post("/customers") + .send({ name: "New project" }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(403); + }); + it("Should answer 201 if logged as user with full access on campaigns", async () => { + const response = await request(app) + .post("/customers") + .send({ name: "New project" }) + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(201); + }); + it("Should answer 403 if logged as user with access to some campaigns", async () => { + const response = await request(app) + .post("/customers") + .send({ name: "New project" }) + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1,2]}'); + expect(response.status).toBe(403); + }); + + it("Should add customer", async () => { + const postResponse = await request(app) + .post("/customers") + .send({ name: "New project" }) + .set("Authorization", "Bearer admin"); + + expect(postResponse.status).toBe(201); + expect(postResponse.body).toHaveProperty("id"); + expect(postResponse.body).toHaveProperty("name"); + const { id, name } = postResponse.body; + + const getResponse = await request(app) + .get("/customers") + .set("Authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + + const customers = getResponse.body; + expect(customers).toHaveLength(1); + expect(customers[0].id).toBe(id); + expect(customers[0].name).toBe(name); + }); +}); diff --git a/src/routes/customers/_post/index.ts b/src/routes/customers/_post/index.ts new file mode 100644 index 000000000..7dccd76f4 --- /dev/null +++ b/src/routes/customers/_post/index.ts @@ -0,0 +1,49 @@ +/** OPENAPI-CLASS : post-customers */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +class RouteItem extends UserRoute<{ + response: StoplightOperations["post-customers"]["responses"]["200"]["content"]["application/json"]; + body: StoplightOperations["post-customers"]["requestBody"]["content"]["application/json"]; +}> { + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + + protected async filter() { + if ((await super.filter()) === false) return false; + if (this.doesNotHaveAccessToCampaigns()) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + return true; + } + + private doesNotHaveAccessToCampaigns() { + return this.accessibleCampaigns !== true; + } + + protected async prepare(): Promise { + const customer = await this.createCustomer(); + return this.setSuccess(201, customer); + } + + private async createCustomer() { + const customer = await tryber.tables.WpAppqCustomer.do() + .insert({ + company: this.getBody().name, + pm_id: 0, + }) + .returning("id"); + const id = customer[0].id ?? customer[0]; + + return { + id: id, + name: this.getBody().name, + }; + } +} + +export default RouteItem; diff --git a/src/schema.ts b/src/schema.ts index eeb13b76f..c90b2c4df 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -224,6 +224,7 @@ export interface paths { "/customers": { /** Get all the customers you have access to */ get: operations["get-customers"]; + post: operations["post-customers"]; parameters: {}; }; "/custom_user_fields": { @@ -2255,6 +2256,27 @@ export interface operations { 403: components["responses"]["NotFound"]; }; }; + "post-customers": { + parameters: {}; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + id: number; + name: string; + }; + }; + }; + }; + requestBody: { + content: { + "application/json": { + name: string; + }; + }; + }; + }; "get-customUserFields": { parameters: {}; responses: { From d3e1fe2f5df47083b88bbdc64838380751f2a004 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:23:27 +0200 Subject: [PATCH 13/39] feat: Add browsers and product types (#312) --- package.json | 2 +- src/routes/browsers/index.spec.ts | 40 +++++++++++++++++++++++++++ src/routes/browsers/index.ts | 15 ++++++++++ src/routes/productTypes/index.spec.ts | 40 +++++++++++++++++++++++++++ src/routes/productTypes/index.ts | 15 ++++++++++ yarn.lock | 8 +++--- 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/routes/browsers/index.spec.ts create mode 100644 src/routes/browsers/index.ts create mode 100644 src/routes/productTypes/index.spec.ts create mode 100644 src/routes/productTypes/index.ts diff --git a/package.json b/package.json index 79b0ce618..015e25c6f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.33.0", + "@appquality/tryber-database": "^0.33.1", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/routes/browsers/index.spec.ts b/src/routes/browsers/index.spec.ts new file mode 100644 index 000000000..bded66dda --- /dev/null +++ b/src/routes/browsers/index.spec.ts @@ -0,0 +1,40 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /browsers", () => { + beforeAll(async () => { + await tryber.tables.Browsers.do().insert([ + { + id: 1, + name: "Browser 1", + }, + { + id: 2, + name: "Browser 2", + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.Browsers.do().delete(); + }); + + it("should return all browsers", async () => { + const response = await request(app).get("/browsers"); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(2); + expect(response.body.results).toEqual([ + { + id: 1, + name: "Browser 1", + }, + { + id: 2, + name: "Browser 2", + }, + ]); + }); +}); diff --git a/src/routes/browsers/index.ts b/src/routes/browsers/index.ts new file mode 100644 index 000000000..d82ce32b7 --- /dev/null +++ b/src/routes/browsers/index.ts @@ -0,0 +1,15 @@ +/** OPENAPI-CLASS: get-browsers */ + +import { tryber } from "@src/features/database"; +import Route from "@src/features/routes/Route"; + +export default class Browsers extends Route<{ + response: StoplightOperations["get-browsers"]["responses"]["200"]["content"]["application/json"]; +}> { + protected async prepare(): Promise { + const browsers = await tryber.tables.Browsers.do().select(); + this.setSuccess(200, { + results: browsers, + }); + } +} diff --git a/src/routes/productTypes/index.spec.ts b/src/routes/productTypes/index.spec.ts new file mode 100644 index 000000000..3e06f8847 --- /dev/null +++ b/src/routes/productTypes/index.spec.ts @@ -0,0 +1,40 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /productTypes", () => { + beforeAll(async () => { + await tryber.tables.ProductTypes.do().insert([ + { + id: 1, + name: "Type 1", + }, + { + id: 2, + name: "Type 2", + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.Browsers.do().delete(); + }); + + it("should return all productTypes", async () => { + const response = await request(app).get("/productTypes"); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(2); + expect(response.body.results).toEqual([ + { + id: 1, + name: "Type 1", + }, + { + id: 2, + name: "Type 2", + }, + ]); + }); +}); diff --git a/src/routes/productTypes/index.ts b/src/routes/productTypes/index.ts new file mode 100644 index 000000000..a35d31e00 --- /dev/null +++ b/src/routes/productTypes/index.ts @@ -0,0 +1,15 @@ +/** OPENAPI-CLASS: get-productTypes */ + +import { tryber } from "@src/features/database"; +import Route from "@src/features/routes/Route"; + +export default class Browsers extends Route<{ + response: StoplightOperations["get-productTypes"]["responses"]["200"]["content"]["application/json"]; +}> { + protected async prepare(): Promise { + const productTypes = await tryber.tables.ProductTypes.do().select(); + this.setSuccess(200, { + results: productTypes, + }); + } +} diff --git a/yarn.lock b/yarn.lock index d85782dd8..104bfc8ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.0.tgz#4f46e44824fad37139aa1f1c2fec50b1f658245c" - integrity sha512-U5zcgF/VlrAnD1pk4WFxc7oAdRDHxjiSlhhYpURweGtot2fwIxwJ28vqEgisv/zXETeXEMVspOOMfkyrg+EfyQ== +"@appquality/tryber-database@^0.33.1": + version "0.33.1" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.1.tgz#47921b71cfeacb3d428d6fab381576a8e3b2e1ed" + integrity sha512-wN2gcx/QcWhxUUItCdF8+DDVWufN3ep9J1HPktAPyTiMy6GwN1jIE3PMkHb/ik77d5XwjsV9Om2Ea0tSpues2g== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From 2b580b247be2fc7f3227768811cea60ce38bde82 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 30 Apr 2024 15:26:43 +0200 Subject: [PATCH 14/39] fix: Prevent working with empty data --- src/routes/dossiers/campaignId/_put/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index c03924b80..f3fdcf46f 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -188,7 +188,7 @@ export default class RouteItem extends AdminRoute<{ .where("campaign_dossier_data_id", dossierId); const countries = this.getBody().countries; - if (!countries) return; + if (!countries || !countries.length) return; await tryber.tables.CampaignDossierDataCountries.do().insert( countries.map((country) => ({ @@ -213,7 +213,7 @@ export default class RouteItem extends AdminRoute<{ .where("campaign_dossier_data_id", dossierId); const languages = this.getBody().languages; - if (!languages) return; + if (!languages || !languages.length) return; await tryber.tables.CampaignDossierDataLanguages.do().insert( languages.map((lang) => ({ @@ -238,7 +238,7 @@ export default class RouteItem extends AdminRoute<{ .where("campaign_dossier_data_id", dossierId); const browsers = this.getBody().browsers; - if (!browsers) return; + if (!browsers || !browsers.length) return; await tryber.tables.CampaignDossierDataBrowsers.do().insert( browsers.map((browser) => ({ @@ -305,7 +305,7 @@ export default class RouteItem extends AdminRoute<{ private async assignOlps() { const roles = this.getBody().roles; - if (!roles) return; + if (!roles || !roles.length) return; const roleOlps = await tryber.tables.CustomRoles.do() .select("id", "olp") @@ -324,6 +324,7 @@ export default class RouteItem extends AdminRoute<{ const wpUserId = wpUserIds.find((r) => r.id === role.user); if (olp && wpUserId) { const olpObject = JSON.parse(olp); + if (!olpObject || !olpObject.length) continue; await tryber.tables.WpAppqOlpPermissions.do().insert( olpObject.map((olpType: string) => ({ main_id: this.campaignId, From 61edbb466c5ee53937804495ca6a400caedb5030 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 30 Apr 2024 15:27:53 +0200 Subject: [PATCH 15/39] feat: Order campaign types by name --- src/routes/campaignTypes/_get/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/routes/campaignTypes/_get/index.ts b/src/routes/campaignTypes/_get/index.ts index 2e013d7e7..0358bfb96 100644 --- a/src/routes/campaignTypes/_get/index.ts +++ b/src/routes/campaignTypes/_get/index.ts @@ -27,10 +27,12 @@ class RouteItem extends UserRoute<{ } protected async prepare(): Promise { - const types = await tryber.tables.WpAppqCampaignType.do().select( - tryber.ref("id").withSchema("wp_appq_campaign_type"), - tryber.ref("name").withSchema("wp_appq_campaign_type") - ); + const types = await tryber.tables.WpAppqCampaignType.do() + .select( + tryber.ref("id").withSchema("wp_appq_campaign_type"), + tryber.ref("name").withSchema("wp_appq_campaign_type") + ) + .orderBy("name", "asc"); return this.setSuccess(200, types); } } From 9aaf9b4db14c51850c0c9849c3717599a9e04a08 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 30 Apr 2024 15:55:44 +0200 Subject: [PATCH 16/39] fix: Allow empty roles --- src/routes/dossiers/campaignId/_put/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index f3fdcf46f..d9e63358a 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -251,7 +251,7 @@ export default class RouteItem extends AdminRoute<{ private async linkRolesToCampaign() { await this.cleanupCurrentRoles(); const roles = this.getBody().roles; - if (!roles) return; + if (!roles || !roles.length) return; await tryber.tables.CampaignCustomRoles.do().insert( roles.map((role) => ({ From cca2b1075e24a256b60e59c511206ebb4432f367 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Thu, 2 May 2024 11:23:38 +0200 Subject: [PATCH 17/39] Add phases (#313) * feat: Return phase in get * feat: Add get phases * feat: Add basic structure for phases * feat: Add basic structure for phase * test: Fix tests * feat: Add status change handler * test: Fix tests --- package.json | 2 +- src/reference/openapi.yml | 99 ++++++++++ src/routes/dossiers/_post/creation.spec.ts | 4 + .../dossiers/campaignId/_get/index.spec.ts | 21 +++ src/routes/dossiers/campaignId/_get/index.ts | 17 +- .../dossiers/campaignId/_put/update.spec.ts | 5 + .../_put/StatusChangeHandler/index.spec.ts | 117 ++++++++++++ .../phases/_put/StatusChangeHandler/index.ts | 73 ++++++++ .../campaignId/phases/_put/index.spec.ts | 171 ++++++++++++++++++ .../dossiers/campaignId/phases/_put/index.ts | 110 +++++++++++ src/routes/phases/_get/index.spec.ts | 64 +++++++ src/routes/phases/_get/index.ts | 34 ++++ src/schema.ts | 57 ++++++ yarn.lock | 8 +- 14 files changed, 776 insertions(+), 6 deletions(-) create mode 100644 src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts create mode 100644 src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts create mode 100644 src/routes/dossiers/campaignId/phases/_put/index.spec.ts create mode 100644 src/routes/dossiers/campaignId/phases/_put/index.ts create mode 100644 src/routes/phases/_get/index.spec.ts create mode 100644 src/routes/phases/_get/index.ts diff --git a/package.json b/package.json index 015e25c6f..54f045d10 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.33.1", + "@appquality/tryber-database": "^0.33.3", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 2377d5c16..9382face0 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9979,6 +9979,22 @@ paths: required: - id - name + phase: + type: object + x-stoplight: + id: rs89ajijla1kn + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: q9ebzl51ln517 + name: + type: string + x-stoplight: + id: dm8wvzioqfoid required: - id - title @@ -9990,6 +10006,7 @@ paths: - testType - deviceList - csm + - phase examples: Example 1: value: @@ -10245,6 +10262,88 @@ paths: operationId: get-productTypes description: '' parameters: [] + /phases: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + x-stoplight: + id: v3nmpsqm6avkm + items: + x-stoplight: + id: t8xafvy0ygkk2 + type: object + properties: + id: + type: integer + x-stoplight: + id: gvx5dzxct64br + name: + type: string + x-stoplight: + id: e7bpgmwgih5mb + required: + - id + - name + required: + - results + operationId: get-phases + security: + - JWT: [] + '/dossiers/{campaign}/phases': + parameters: + - name: campaign + in: path + required: true + schema: + type: string + description: A campaign id + put: + summary: Your PUT endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + x-stoplight: + id: 3ir2671nik0ot + name: + type: string + x-stoplight: + id: 12by584z698iq + required: + - id + - name + operationId: put-dossiers-campaign-phases + requestBody: + content: + application/json: + schema: + type: object + properties: + phase: + type: integer + x-stoplight: + id: 0jmutc81ya7sa + required: + - phase + security: + - JWT: [] components: schemas: AdditionalField: diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 07a42fb2b..8cd31eb5f 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -16,6 +16,9 @@ const baseRequest = { describe("Route POST /dossiers", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Test Phase", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert({ id: 1, wp_user_id: 100, @@ -94,6 +97,7 @@ describe("Route POST /dossiers", () => { }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqCustomer.do().delete(); await tryber.tables.WpAppqProject.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index 90bc4d489..2aa46e65a 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -4,6 +4,13 @@ import request from "supertest"; describe("Route GET /dossiers/:id", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { + id: 1, + name: "Active", + type_id: 1, + }, + ]); await tryber.tables.WpAppqCustomer.do().insert({ id: 1, company: "Test Company", @@ -93,6 +100,9 @@ describe("Route GET /dossiers/:id", () => { await tryber.tables.WpAppqEvdPlatform.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CustomRoles.do().delete(); + await tryber.tables.CampaignCustomRoles.do().delete(); + await tryber.tables.CampaignPhase.do().delete(); }); it("Should answer 403 if not logged in", async () => { @@ -248,6 +258,17 @@ describe("Route GET /dossiers/:id", () => { expect(response.body.roles[0].user).toHaveProperty("surname", "PM"); }); + it("Should return the phase", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("phase"); + expect(response.body.phase).toHaveProperty("id", 1); + expect(response.body.phase).toHaveProperty("name", "Active"); + }); + describe("With dossier data", () => { beforeAll(async () => { await tryber.tables.CampaignDossierData.do().insert({ diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 30e22c683..38168deae 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -59,7 +59,13 @@ export default class RouteItem extends AdminRoute<{ tryber .ref("customer_id") .withSchema("wp_appq_project") - .as("customer_id") + .as("customer_id"), + tryber + .ref("id") + + .withSchema("campaign_phase") + .as("phase_id"), + tryber.ref("name").withSchema("campaign_phase").as("phase_name") ) .join( "wp_appq_project", @@ -81,6 +87,11 @@ export default class RouteItem extends AdminRoute<{ "wp_appq_evd_profile.id", "wp_appq_evd_campaign.pm_id" ) + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) .where("wp_appq_evd_campaign.id", this.campaignId) .first(); @@ -228,6 +239,10 @@ export default class RouteItem extends AdminRoute<{ id: this.campaign.pm_id, name: `${this.campaign.pm_name} ${this.campaign.pm_surname}`, }, + phase: { + id: this.campaign.phase_id, + name: this.campaign.phase_name, + }, ...(this.campaign.roles.length ? { roles: this.campaign.roles.map((item) => { diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 8e014691b..889a88727 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -15,6 +15,11 @@ const baseRequest = { describe("Route POST /dossiers", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert({ + id: 1, + name: "Draft", + type_id: 1, + }); await tryber.tables.WpAppqCustomer.do().insert({ id: 1, company: "Test Company", diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts new file mode 100644 index 000000000..9d0b2b706 --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts @@ -0,0 +1,117 @@ +import { tryber } from "@src/features/database"; +import { StatusChangeHandler } from "."; + +describe("StatusChangeHandler", () => { + beforeAll(async () => { + const campaign = { + title: "Campaign 1", + customer_title: "Customer 1", + platform_id: 1, + pm_id: 1, + campaign_type_id: 1, + start_date: "2019-08-24T14:15:22Z", + end_date: "2019-08-24T14:15:22Z", + page_manual_id: 1, + page_preview_id: 1, + customer_id: 1, + project_id: 1, + }; + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...campaign, + id: 1, + phase_id: 1, + status_id: 1, + }, + { + ...campaign, + id: 2, + phase_id: 3, + status_id: 2, + }, + ]); + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + { id: 2, name: "Running", type_id: 2 }, + { id: 3, name: "Closed", type_id: 3 }, + ]); + + await tryber.tables.CampaignPhaseType.do().insert([ + { id: 1, name: "unavailable" }, + { id: 2, name: "running" }, + { id: 3, name: "closed" }, + ]); + }); + + afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); + await tryber.tables.CampaignPhaseType.do().delete(); + }); + + afterEach(async () => { + await tryber.tables.CampaignPhaseHistory.do().delete(); + }); + it("should have a constructor", () => { + const handler = new StatusChangeHandler({ + newPhase: 2, + campaignId: 1, + creator: 1, + }); + expect(handler).toBeDefined(); + }); + + it("Should save the oldPhase, newPhase and campaignId", async () => { + const handler = new StatusChangeHandler({ + newPhase: 2, + campaignId: 1, + creator: 1, + }); + + await handler.run(); + + const history = await tryber.tables.CampaignPhaseHistory.do() + .select("phase_id", "created_by") + .where("campaign_id", 1); + + expect(history).toHaveLength(1); + + expect(history[0].phase_id).toBe(2); + expect(history[0].created_by).toBe(1); + }); + + it("Should change the status_id when changing to a closed phase", async () => { + const handler = new StatusChangeHandler({ + newPhase: 3, + campaignId: 1, + creator: 1, + }); + + await handler.run(); + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("status_id") + .where("id", 1) + .first(); + + if (!campaign) throw new Error("Campaign not found"); + expect(campaign.status_id).toBe(2); + }); + + it("Should change the status_id when changing from closed phase", async () => { + const handler = new StatusChangeHandler({ + newPhase: 1, + campaignId: 2, + creator: 1, + }); + + await handler.run(); + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("status_id") + .where("id", 2) + .first(); + + if (!campaign) throw new Error("Campaign not found"); + expect(campaign.status_id).toBe(1); + }); +}); diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts new file mode 100644 index 000000000..7f4f60bea --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts @@ -0,0 +1,73 @@ +import { tryber } from "@src/features/database"; + +class StatusChangeHandler { + private oldPhase: number = 0; + private newPhase: number; + private campaignId: number; + private creator: number; + + constructor({ + newPhase, + campaignId, + creator, + }: { + newPhase: number; + campaignId: number; + creator: number; + }) { + this.newPhase = newPhase; + this.campaignId = campaignId; + this.creator = creator; + } + + public async run() { + this.oldPhase = await this.getOldPhase(); + + const type = await tryber.tables.CampaignPhaseType.do() + .select(tryber.ref("name").withSchema("campaign_phase_type")) + .join( + "campaign_phase", + "campaign_phase.type_id", + "campaign_phase_type.id" + ) + .where("campaign_phase.id", this.newPhase) + .first(); + if (!type) return; + + await this.handleStatusChange(type.name); + + await this.saveHistory(); + console.log("Status changed from", this.oldPhase, "to", this.newPhase); + } + + private async getOldPhase() { + const oldPhase = await tryber.tables.WpAppqEvdCampaign.do() + .select("phase_id") + .where("id", this.campaignId) + .first(); + if (!oldPhase || !oldPhase.phase_id) throw new Error("Old phase not found"); + return oldPhase.phase_id; + } + + private async saveHistory() { + await tryber.tables.CampaignPhaseHistory.do().insert({ + phase_id: this.newPhase, + campaign_id: this.campaignId, + created_by: this.creator, + }); + } + + private async handleStatusChange(type: string) { + if (type === "closed") { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ status_id: 2 }) + .where("id", this.campaignId); + } else { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ status_id: 1 }) + .where("id", this.campaignId); + } + } +} + +export { StatusChangeHandler }; diff --git a/src/routes/dossiers/campaignId/phases/_put/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/index.spec.ts new file mode 100644 index 000000000..a869f5ccc --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/index.spec.ts @@ -0,0 +1,171 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import { StatusChangeHandler } from "@src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler"; +import request from "supertest"; + +jest.mock("@src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler"); + +describe("Route PUT /dossiers/:id/phases", () => { + beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + { id: 2, name: "Running", type_id: 2 }, + { id: 3, name: "Closed", type_id: 3 }, + ]); + + await tryber.tables.CampaignPhaseType.do().insert([ + { id: 1, name: "unavailable" }, + { id: 2, name: "running" }, + { id: 3, name: "closed" }, + ]); + + await tryber.tables.WpAppqProject.do().insert([ + { id: 1, customer_id: 1, display_name: "Project 1", edited_by: 1 }, + ]); + await tryber.tables.WpAppqCustomer.do().insert([ + { id: 1, company: "Customer 1", pm_id: 1 }, + ]); + await tryber.tables.WpAppqCampaignType.do().insert([ + { id: 1, name: "Type 1", category_id: 1 }, + ]); + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 1, + wp_user_id: 1, + name: "CSM", + surname: "", + email: "", + education_id: 1, + employment_id: 1, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); + await tryber.tables.CampaignPhaseType.do().delete(); + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCustomer.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + id: 1, + title: "Campaign 1", + customer_title: "Customer 1", + platform_id: 1, + pm_id: 1, + campaign_type_id: 1, + page_manual_id: 1, + page_preview_id: 1, + start_date: "2021-01-01T00:00:00.000Z", + end_date: "2021-12-31T00:00:00.000Z", + close_date: "2022-01-01T00:00:00.000Z", + customer_id: 1, + project_id: 1, + phase_id: 1, + os: "", + }, + ]); + }); + + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should return 403 if not logged in", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .send({ phase: 2 }); + expect(response.status).toBe(403); + }); + + it("Should return 200 if logged in as admin", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer admin") + .send({ phase: 2 }); + + expect(response.status).toBe(200); + }); + + it("Should return 403 if logged in as tester", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer tester") + .send({ phase: 2 }); + + expect(response.status).toBe(403); + }); + + it("Should return 200 if has access to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}') + .send({ phase: 2 }); + + expect(response.status).toBe(200); + }); + + it("Should return 403 if campaign does not exists", async () => { + const response = await request(app) + .put("/dossiers/100/phases") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[100]}') + .send({ phase: 2 }); + + expect(response.status).toBe(403); + }); + + it("Should return 400 if sending the same phase_id", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer admin") + .send({ phase: 1 }); + + expect(response.status).toBe(400); + }); + + it("Should change the phase", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer admin") + .send({ phase: 2 }); + + expect(response.status).toBe(200); + + const campaign = await request(app) + .get("/dossiers/1") + .set("Authorization", "Bearer admin"); + + expect(campaign.body.phase.id).toBe(2); + expect(campaign.body.phase.name).toBe("Running"); + }); + + it("Should return the new phase", async () => { + const response = await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer admin") + + .send({ phase: 2 }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(2); + expect(response.body.name).toBe("Running"); + }); + + it("Should handle the status change", async () => { + await request(app) + .put("/dossiers/1/phases") + .set("Authorization", "Bearer admin") + .send({ phase: 2 }); + + expect(StatusChangeHandler).toHaveBeenCalledWith({ + newPhase: 2, + campaignId: 1, + creator: 1, + }); + expect(StatusChangeHandler.prototype.run).toHaveBeenCalled(); + }); +}); diff --git a/src/routes/dossiers/campaignId/phases/_put/index.ts b/src/routes/dossiers/campaignId/phases/_put/index.ts new file mode 100644 index 000000000..9a128503e --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/index.ts @@ -0,0 +1,110 @@ +/** OPENAPI-CLASS: put-dossiers-campaign-phases */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; +import { StatusChangeHandler } from "./StatusChangeHandler"; + +export default class RouteItem extends UserRoute<{ + response: StoplightOperations["put-dossiers-campaign-phases"]["responses"]["200"]["content"]["application/json"]; + parameters: StoplightOperations["put-dossiers-campaign-phases"]["parameters"]["path"]; + body: StoplightOperations["put-dossiers-campaign-phases"]["requestBody"]["content"]["application/json"]; +}> { + private campaignId: number; + private newPhaseId: number; + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + private _campaign?: { id: number; phase_id: number }; + + constructor(configuration: RouteClassConfiguration) { + super(configuration); + this.campaignId = Number(this.getParameters().campaign); + this.newPhaseId = this.getBody().phase; + } + + get campaign() { + if (!this._campaign) throw new Error("Campaign not loaded"); + return this._campaign; + } + + protected async init() { + this._campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("id", "phase_id") + .where("id", this.campaignId) + .first(); + } + + protected async filter() { + if (!(await super.filter())) return false; + + if ( + (await this.doesNotHaveAccess()) || + (await this.campaignDoesNotExists()) + ) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + + if (this.newPhaseId === this.campaign.phase_id) { + this.setError( + 400, + new OpenapiError("The campaign is already in this phase") + ); + return false; + } + + return true; + } + + private async doesNotHaveAccess() { + if (this.accessibleCampaigns === true) return false; + if (Array.isArray(this.accessibleCampaigns)) + return !this.accessibleCampaigns.includes(this.campaignId); + return true; + } + + private async campaignDoesNotExists() { + try { + this.campaign; + return false; + } catch (error) { + return true; + } + } + + protected async prepare() { + const newPhase = await this.updatePhase(); + + this.setSuccess(200, newPhase); + } + + private async updatePhase() { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ phase_id: this.newPhaseId }) + .where("id", this.campaignId); + + const result = await tryber.tables.WpAppqEvdCampaign.do() + .select(tryber.ref("id").withSchema("campaign_phase"), "name") + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) + .where("wp_appq_evd_campaign.id", this.campaignId) + .first(); + + await this.triggerStatusChange(); + return result; + } + + private async triggerStatusChange() { + const handler = new StatusChangeHandler({ + newPhase: this.newPhaseId, + campaignId: this.campaignId, + creator: this.getTesterId(), + }); + + await handler.run(); + } +} diff --git a/src/routes/phases/_get/index.spec.ts b/src/routes/phases/_get/index.spec.ts new file mode 100644 index 000000000..3b14bbf2b --- /dev/null +++ b/src/routes/phases/_get/index.spec.ts @@ -0,0 +1,64 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("Route GET /phases", () => { + beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Phase 1", type_id: 1 }, + { id: 2, name: "Phase 2", type_id: 2 }, + ]); + }); + + afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); + }); + + it("Should answer 403 if not logged in", async () => { + const response = await request(app).get("/phases"); + expect(response.status).toBe(403); + }); + + it("Should answer 200 if logged in as admin", async () => { + const response = await request(app) + .get("/phases") + .set("authorization", "Bearer admin"); + expect(response.status).toBe(200); + }); + + it("Should answer 403 if logged in as user", async () => { + const response = await request(app) + .get("/phases") + .set("authorization", "Bearer tester"); + + expect(response.status).toBe(403); + }); + + it("Should answer 200 if logged in with full access", async () => { + const response = await request(app) + .get("/phases") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + }); + + it("Should answer 200 if logged in at least one campaign", async () => { + const response = await request(app) + .get("/phases") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + }); + + it("should return all phases", async () => { + const response = await request(app) + .get("/phases") + .set("Authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(2); + expect(response.body.results).toEqual([ + { id: 1, name: "Phase 1" }, + { id: 2, name: "Phase 2" }, + ]); + }); +}); diff --git a/src/routes/phases/_get/index.ts b/src/routes/phases/_get/index.ts new file mode 100644 index 000000000..98e921cf5 --- /dev/null +++ b/src/routes/phases/_get/index.ts @@ -0,0 +1,34 @@ +/** OPENAPI-CLASS: get-phases */ +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +export default class Route extends UserRoute<{ + response: StoplightOperations["get-phases"]["responses"]["200"]["content"]["application/json"]; +}> { + private accessibleCampaigns: true | number[] = this.campaignOlps + ? this.campaignOlps + : []; + + protected async filter() { + if (!(await super.filter())) return false; + + if (this.doesNotHaveAccessToCampaigns()) { + this.setError(403, new OpenapiError("You are not authorized to do this")); + return false; + } + return true; + } + + private doesNotHaveAccessToCampaigns() { + if (Array.isArray(this.accessibleCampaigns)) + return this.accessibleCampaigns.length === 0; + return this.accessibleCampaigns !== true; + } + protected async prepare(): Promise { + const results = await tryber.tables.CampaignPhase.do().select("id", "name"); + this.setSuccess(200, { + results, + }); + } +} diff --git a/src/schema.ts b/src/schema.ts index c90b2c4df..8625b803e 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -579,6 +579,18 @@ export interface paths { get: operations["get-productTypes"]; parameters: {}; }; + "/phases": { + get: operations["get-phases"]; + }; + "/dossiers/{campaign}/phases": { + put: operations["put-dossiers-campaign-phases"]; + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + }; } export interface components { @@ -4174,6 +4186,10 @@ export interface operations { id: number; name: string; }; + phase: { + id: number; + name: string; + }; }; }; }; @@ -4297,6 +4313,47 @@ export interface operations { }; }; }; + "get-phases": { + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + }[]; + }; + }; + }; + }; + }; + "put-dossiers-campaign-phases": { + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + id: number; + name: string; + }; + }; + }; + }; + requestBody: { + content: { + "application/json": { + phase: number; + }; + }; + }; + }; } export interface external {} diff --git a/yarn.lock b/yarn.lock index 104bfc8ab..7c8ed3169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.1.tgz#47921b71cfeacb3d428d6fab381576a8e3b2e1ed" - integrity sha512-wN2gcx/QcWhxUUItCdF8+DDVWufN3ep9J1HPktAPyTiMy6GwN1jIE3PMkHb/ik77d5XwjsV9Om2Ea0tSpues2g== +"@appquality/tryber-database@^0.33.3": + version "0.33.3" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.3.tgz#4c3cdcd47f9c247944c805ee5f1da2034048d28b" + integrity sha512-Sg90QyXqcME3uaV/1WSt3Poy9PgUPaK2dineMFPizcu76MYmyAgx0/eaTyRUmk1y6shV5iuBWig0pUPCsZ3U2Q== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From f09bd558f57bd2c3ee2632b23d09e8270c989819 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 6 May 2024 10:31:46 +0200 Subject: [PATCH 18/39] Update permissions dossiers (#314) * feat: Allow getting/updating dossier by tl * feat: Return phase from campaigns * fix: Add empty description --- src/reference/openapi.yml | 16 ++++ src/routes/campaigns/_get/fields.spec.ts | 6 ++ src/routes/campaigns/_get/filterBy.spec.ts | 4 + src/routes/campaigns/_get/filterByCsm.spec.ts | 4 + src/routes/campaigns/_get/index.spec.ts | 8 ++ src/routes/campaigns/_get/index.ts | 29 +++++++ src/routes/campaigns/_get/order.spec.ts | 4 + src/routes/campaigns/_get/phases.spec.ts | 78 +++++++++++++++++++ src/routes/campaigns/_get/resultType.spec.ts | 4 + src/routes/campaigns/_get/search.spec.ts | 4 + src/routes/campaigns/_get/visibility.spec.ts | 4 + src/routes/dossiers/_post/index.ts | 1 + .../dossiers/campaignId/_get/index.spec.ts | 15 +++- src/routes/dossiers/campaignId/_get/index.ts | 13 +++- .../dossiers/campaignId/_put/index.spec.ts | 16 ++++ src/routes/dossiers/campaignId/_put/index.ts | 13 +++- 16 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 src/routes/campaigns/_get/phases.spec.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 9382face0..656f865bc 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -719,6 +719,22 @@ paths: type: string required: - name + phase: + type: object + x-stoplight: + id: 99cgph0clnzjb + properties: + id: + type: integer + x-stoplight: + id: 5c61zclzvbcds + name: + type: string + x-stoplight: + id: 192pfrpi90clp + required: + - id + - name - $ref: '#/components/schemas/PaginationData' '403': $ref: '#/components/responses/NotAuthorized' diff --git a/src/routes/campaigns/_get/fields.spec.ts b/src/routes/campaigns/_get/fields.spec.ts index e71e15554..d8e8e21ae 100644 --- a/src/routes/campaigns/_get/fields.spec.ts +++ b/src/routes/campaigns/_get/fields.spec.ts @@ -17,6 +17,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqCustomer.do().insert([ { id: 1, @@ -92,6 +95,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); }); @@ -115,6 +119,7 @@ describe("GET /campaigns", () => { type: { name: "CampaignType 1", area: "quality" }, visibility: "admin", resultType: "bug", + phase: { id: 1, name: "Draft" }, }, { id: 3, @@ -129,6 +134,7 @@ describe("GET /campaigns", () => { type: { name: "CampaignType 2", area: "experience" }, visibility: "admin", resultType: "bug", + phase: { id: 1, name: "Draft" }, }, ]) ); diff --git a/src/routes/campaigns/_get/filterBy.spec.ts b/src/routes/campaigns/_get/filterBy.spec.ts index 644d5e2b2..1fa385446 100644 --- a/src/routes/campaigns/_get/filterBy.spec.ts +++ b/src/routes/campaigns/_get/filterBy.spec.ts @@ -21,6 +21,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqCampaignType.do().insert([ { id: 1, @@ -104,6 +107,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); diff --git a/src/routes/campaigns/_get/filterByCsm.spec.ts b/src/routes/campaigns/_get/filterByCsm.spec.ts index 104a1bf9a..6a4186c7e 100644 --- a/src/routes/campaigns/_get/filterByCsm.spec.ts +++ b/src/routes/campaigns/_get/filterByCsm.spec.ts @@ -21,6 +21,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -50,6 +53,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); diff --git a/src/routes/campaigns/_get/index.spec.ts b/src/routes/campaigns/_get/index.spec.ts index cdd6d9931..c1e55a912 100644 --- a/src/routes/campaigns/_get/index.spec.ts +++ b/src/routes/campaigns/_get/index.spec.ts @@ -22,6 +22,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -51,6 +54,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); @@ -109,6 +113,9 @@ describe("GET /campaigns", () => { describe("GET /campaigns with start and limit query params", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -141,6 +148,7 @@ describe("GET /campaigns with start and limit query params", () => { }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); diff --git a/src/routes/campaigns/_get/index.ts b/src/routes/campaigns/_get/index.ts index ce0561884..92d127389 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -17,6 +17,7 @@ const ACCEPTABLE_FIELDS = [ "resultType" as const, "status" as const, "type" as const, + "phase" as const, ]; type CampaignSelect = ReturnType; @@ -152,6 +153,7 @@ class RouteItem extends UserRoute<{ this.addTypeTo(query); this.addVisibilityTo(query); this.addResultTypeTo(query); + this.addPhaseTo(query); if (this.limit) { query.limit(this.limit); @@ -181,6 +183,8 @@ class RouteItem extends UserRoute<{ type_area?: 0 | 1; visibility?: 0 | 1 | 2 | 3; resultType?: -1 | 0 | 1; + phase_id?: number; + phase_name?: string; }[]; } @@ -233,6 +237,14 @@ class RouteItem extends UserRoute<{ }, } : {}), + ...(this.fields.includes("phase") + ? { + phase: { + id: campaign.phase_id, + name: campaign.phase_name, + }, + } + : {}), visibility: this.getVisibilityName(campaign.visibility), resultType: this.getResultTypeName(campaign.resultType), })); @@ -498,6 +510,23 @@ class RouteItem extends UserRoute<{ } }); } + + private addPhaseTo(query: CampaignSelect) { + query.modify((query) => { + if (this.fields.includes("phase")) { + query + .leftJoin( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) + .select( + tryber.ref("campaign_phase.id").as("phase_id"), + tryber.ref("campaign_phase.name").as("phase_name") + ); + } + }); + } } export default RouteItem; diff --git a/src/routes/campaigns/_get/order.spec.ts b/src/routes/campaigns/_get/order.spec.ts index 329a9e7bb..5e8c2f270 100644 --- a/src/routes/campaigns/_get/order.spec.ts +++ b/src/routes/campaigns/_get/order.spec.ts @@ -26,6 +26,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -62,6 +65,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); diff --git a/src/routes/campaigns/_get/phases.spec.ts b/src/routes/campaigns/_get/phases.spec.ts new file mode 100644 index 000000000..812dd4eae --- /dev/null +++ b/src/routes/campaigns/_get/phases.spec.ts @@ -0,0 +1,78 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; +const campaign = { + id: 1, + platform_id: 1, + start_date: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end_date: new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + title: "This is the title", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "", + campaign_pts: 200, +}; +describe("GET /campaigns", () => { + beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + { id: 2, name: "Running", type_id: 2 }, + ]); + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 1, + name: "name", + surname: "surname", + email: "", + wp_user_id: 1, + education_id: 1, + employment_id: 1, + }, + ]); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { ...campaign, id: 1, title: "First test campaign", phase_id: 1 }, + { ...campaign, id: 2, title: "Second test campaign", phase_id: 2 }, + ]); + }); + afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + + it("Should return the phase of the campaigns", async () => { + const response = await request(app) + .get("/campaigns?fields=id,phase") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + + expect(response.status).toBe(200); + + expect(response.body.items).toHaveLength(2); + + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + phase: { + id: 1, + name: "Draft", + }, + }), + expect.objectContaining({ + id: 2, + phase: { + id: 2, + name: "Running", + }, + }), + ]) + ); + }); +}); diff --git a/src/routes/campaigns/_get/resultType.spec.ts b/src/routes/campaigns/_get/resultType.spec.ts index 02b86b3ed..891218ef8 100644 --- a/src/routes/campaigns/_get/resultType.spec.ts +++ b/src/routes/campaigns/_get/resultType.spec.ts @@ -17,6 +17,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdCampaign.do().insert([ { ...campaign, @@ -39,6 +42,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); }); diff --git a/src/routes/campaigns/_get/search.spec.ts b/src/routes/campaigns/_get/search.spec.ts index 641ca5335..557820391 100644 --- a/src/routes/campaigns/_get/search.spec.ts +++ b/src/routes/campaigns/_get/search.spec.ts @@ -21,6 +21,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -40,6 +43,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqEvdProfile.do().delete(); }); diff --git a/src/routes/campaigns/_get/visibility.spec.ts b/src/routes/campaigns/_get/visibility.spec.ts index b101bb1e2..9cf5a3cfa 100644 --- a/src/routes/campaigns/_get/visibility.spec.ts +++ b/src/routes/campaigns/_get/visibility.spec.ts @@ -17,6 +17,9 @@ const campaign = { }; describe("GET /campaigns", () => { beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); await tryber.tables.WpAppqEvdCampaign.do().insert([ { ...campaign, @@ -45,6 +48,7 @@ describe("GET /campaigns", () => { ]); }); afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); await tryber.tables.WpAppqEvdCampaign.do().delete(); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index e849bc811..966118587 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -153,6 +153,7 @@ export default class RouteItem extends AdminRoute<{ page_preview_id: 0, page_manual_id: 0, customer_id: 0, + description: "", pm_id: this.getCsmId(), project_id: this.getBody().project, campaign_type_id: this.getBody().testType, diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index 2aa46e65a..c2760782e 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -126,7 +126,20 @@ describe("Route GET /dossiers/:id", () => { const response = await request(app) .get("/dossiers/1") .set("authorization", "Bearer admin"); - console.log(response.body); + expect(response.status).toBe(200); + }); + + it("Should answer 200 if user has access to the campaign", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + }); + + it("Should answer 200 if user has access to the campaign", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", 'Bearer tester olp {"appq_campaign":true}'); expect(response.status).toBe(200); }); diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 38168deae..622d59008 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -2,9 +2,9 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; -import AdminRoute from "@src/features/routes/AdminRoute"; +import UserRoute from "@src/features/routes/UserRoute"; -export default class RouteItem extends AdminRoute<{ +export default class RouteItem extends UserRoute<{ response: StoplightOperations["get-dossiers-campaign"]["responses"]["200"]["content"]["application/json"]; parameters: StoplightOperations["get-dossiers-campaign"]["parameters"]["path"]; }> { @@ -194,6 +194,11 @@ export default class RouteItem extends AdminRoute<{ protected async filter() { if (!(await super.filter())) return false; + if (await this.doesNotHaveAccessToCampaign()) { + this.setError(403, new OpenapiError("No access to campaign")); + return false; + } + if (!(await this.campaignExists())) { this.setError(403, new OpenapiError("Campaign does not exist")); return false; @@ -202,6 +207,10 @@ export default class RouteItem extends AdminRoute<{ return true; } + private async doesNotHaveAccessToCampaign() { + return !this.hasAccessToCampaign(this.campaignId); + } + private async campaignExists(): Promise { try { this.campaign; diff --git a/src/routes/dossiers/campaignId/_put/index.spec.ts b/src/routes/dossiers/campaignId/_put/index.spec.ts index 297711776..7a26f08cf 100644 --- a/src/routes/dossiers/campaignId/_put/index.spec.ts +++ b/src/routes/dossiers/campaignId/_put/index.spec.ts @@ -116,4 +116,20 @@ describe("Route PUT /dossiers/:id", () => { .send(baseRequest); expect(response.status).toBe(200); }); + + it("Should answer 200 if user has access to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .send(baseRequest) + .set("authorization", 'Bearer tester olp {"appq_campaign":[1]}'); + expect(response.status).toBe(200); + }); + + it("Should answer 200 if user has access to the campaign", async () => { + const response = await request(app) + .put("/dossiers/1") + .send(baseRequest) + .set("authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + }); }); diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index d9e63358a..019b94d0c 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -2,9 +2,9 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; -import AdminRoute from "@src/features/routes/AdminRoute"; +import UserRoute from "@src/features/routes/UserRoute"; -export default class RouteItem extends AdminRoute<{ +export default class RouteItem extends UserRoute<{ response: StoplightOperations["put-dossiers-campaign"]["responses"]["200"]["content"]["application/json"]; body: StoplightOperations["put-dossiers-campaign"]["requestBody"]["content"]["application/json"]; parameters: StoplightOperations["put-dossiers-campaign"]["parameters"]["path"]; @@ -34,6 +34,11 @@ export default class RouteItem extends AdminRoute<{ protected async filter() { if (!(await super.filter())) return false; + if (await this.doesNotHaveAccessToCampaign()) { + this.setError(403, new OpenapiError("No access to campaign")); + return false; + } + if (!(await this.campaignExists())) { this.setError(403, new OpenapiError("Campaign does not exist")); return false; @@ -54,6 +59,10 @@ export default class RouteItem extends AdminRoute<{ return true; } + private async doesNotHaveAccessToCampaign() { + return !this.hasAccessToCampaign(this.campaignId); + } + private async campaignExists(): Promise { try { this.campaign; From 0a9c3ffe44fc067e88a4a582c36f32ef91d00394 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 6 May 2024 14:27:58 +0200 Subject: [PATCH 19/39] Add roles to campaigns (#315) * feat: Add api design * feat: Update get-campaigns endpoint to include roles --- src/reference/openapi.yml | 190 +++++++++++------------ src/routes/campaigns/_get/fields.spec.ts | 2 + src/routes/campaigns/_get/index.ts | 76 ++++++++- src/routes/campaigns/_get/roles.spec.ts | 109 +++++++++++++ src/schema.ts | 15 ++ 5 files changed, 289 insertions(+), 103 deletions(-) create mode 100644 src/routes/campaigns/_get/roles.spec.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 656f865bc..e88c39982 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -721,20 +721,57 @@ paths: - name phase: type: object - x-stoplight: - id: 99cgph0clnzjb + properties: id: type: integer - x-stoplight: - id: 5c61zclzvbcds + name: type: string - x-stoplight: - id: 192pfrpi90clp + required: - id - name + roles: + type: array + + items: + + type: object + properties: + role: + type: object + + required: + - id + - name + properties: + id: + type: integer + + name: + type: string + + user: + type: object + + required: + - id + - name + - surname + properties: + id: + type: integer + + name: + type: string + + surname: + type: string + + required: + - role + - user - $ref: '#/components/schemas/PaginationData' '403': $ref: '#/components/responses/NotAuthorized' @@ -4165,12 +4202,10 @@ paths: properties: id: type: integer - x-stoplight: - id: zxshcfdjbugtr + name: type: string - x-stoplight: - id: s32ugpcfzhrmp + required: - id - name @@ -4184,8 +4219,7 @@ paths: properties: name: type: string - x-stoplight: - id: 5gyi3swml7tyw + required: - name /custom_user_fields: @@ -9749,34 +9783,27 @@ paths: allOf: - $ref: '#/components/schemas/DossierCreationData' - type: object - x-stoplight: - id: 7ewqh8piy4dc8 + properties: duplicate: type: object - x-stoplight: - id: qjlhi5gbkesdj + properties: fields: type: integer - x-stoplight: - id: eynp882a0yi85 + useCases: type: integer - x-stoplight: - id: aeis659jristy + mailMerges: type: integer - x-stoplight: - id: uzgqkctl150pz + pages: type: integer - x-stoplight: - id: qhyggxgrgajc1 + testers: type: integer - x-stoplight: - id: lobzb13vjwkln + '/dossiers/{campaign}': parameters: - name: campaign @@ -9941,76 +9968,62 @@ paths: $ref: '#/components/schemas/CountryCode' languages: type: array - x-stoplight: - id: vnqv4spymw4ki + items: - x-stoplight: - id: 3vr40snkowkto + type: object properties: id: type: integer - x-stoplight: - id: oshj1a0l38q2h + name: type: string - x-stoplight: - id: w3yg9dpyxvdpg + required: - id - name browsers: type: array - x-stoplight: - id: drm3b91p7l2h0 + items: - x-stoplight: - id: 58z8i5w06gdi6 + type: object properties: id: type: integer - x-stoplight: - id: 12u8hxpxa3kvp + name: type: string - x-stoplight: - id: lpgpv16kv5okp + required: - id - name productType: type: object - x-stoplight: - id: w34jw9gfix4od + properties: id: type: number - x-stoplight: - id: pxc44zqtp5bot + name: type: string - x-stoplight: - id: ygrkmgv5bib6y + required: - id - name phase: type: object - x-stoplight: - id: rs89ajijla1kn + required: - id - name properties: id: type: integer - x-stoplight: - id: q9ebzl51ln517 + name: type: string - x-stoplight: - id: dm8wvzioqfoid + required: - id - title @@ -10106,12 +10119,10 @@ paths: properties: id: type: number - x-stoplight: - id: 23mwsogkyrhe8 + name: type: string - x-stoplight: - id: zj6433e2mxne6 + required: - id - name @@ -10125,8 +10136,7 @@ paths: properties: name: type: string - x-stoplight: - id: 7dx62s7fiaz1z + required: - name '/users/by-role/{role}': @@ -10143,25 +10153,20 @@ paths: properties: results: type: array - x-stoplight: - id: bit4usrip6upm + items: - x-stoplight: - id: o7tw7533e3qes + type: object properties: id: type: integer - x-stoplight: - id: 03fxvfejydtdr + name: type: string - x-stoplight: - id: b9qd1xctx2kcy + surname: type: string - x-stoplight: - id: j4j7zt1laq54b + required: - id - name @@ -10196,21 +10201,17 @@ paths: properties: results: type: array - x-stoplight: - id: 5wcw8uwqeljlf + items: - x-stoplight: - id: xxr1fwn9kxcp4 + type: object properties: id: type: integer - x-stoplight: - id: cx3i7ryqsjmrq + name: type: string - x-stoplight: - id: 93yftbsxaqmdm + required: - id - name @@ -10245,21 +10246,17 @@ paths: properties: results: type: array - x-stoplight: - id: 5wcw8uwqeljlf + items: - x-stoplight: - id: xxr1fwn9kxcp4 + type: object properties: id: type: integer - x-stoplight: - id: cx3i7ryqsjmrq + name: type: string - x-stoplight: - id: 93yftbsxaqmdm + required: - id - name @@ -10292,21 +10289,17 @@ paths: properties: results: type: array - x-stoplight: - id: v3nmpsqm6avkm + items: - x-stoplight: - id: t8xafvy0ygkk2 + type: object properties: id: type: integer - x-stoplight: - id: gvx5dzxct64br + name: type: string - x-stoplight: - id: e7bpgmwgih5mb + required: - id - name @@ -10336,12 +10329,10 @@ paths: properties: id: type: integer - x-stoplight: - id: 3ir2671nik0ot + name: type: string - x-stoplight: - id: 12by584z698iq + required: - id - name @@ -10354,8 +10345,7 @@ paths: properties: phase: type: integer - x-stoplight: - id: 0jmutc81ya7sa + required: - phase security: diff --git a/src/routes/campaigns/_get/fields.spec.ts b/src/routes/campaigns/_get/fields.spec.ts index d8e8e21ae..d418c77aa 100644 --- a/src/routes/campaigns/_get/fields.spec.ts +++ b/src/routes/campaigns/_get/fields.spec.ts @@ -120,6 +120,7 @@ describe("GET /campaigns", () => { visibility: "admin", resultType: "bug", phase: { id: 1, name: "Draft" }, + roles: [], }, { id: 3, @@ -135,6 +136,7 @@ describe("GET /campaigns", () => { visibility: "admin", resultType: "bug", phase: { id: 1, name: "Draft" }, + roles: [], }, ]) ); diff --git a/src/routes/campaigns/_get/index.ts b/src/routes/campaigns/_get/index.ts index 92d127389..3da5d682b 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -18,9 +18,15 @@ const ACCEPTABLE_FIELDS = [ "status" as const, "type" as const, "phase" as const, + "roles" as const, ]; type CampaignSelect = ReturnType; +type Roles = NonNullable< + NonNullable< + StoplightOperations["get-campaigns"]["responses"]["200"]["content"]["application/json"]["items"] + >[0]["roles"] +>; class RouteItem extends UserRoute<{ response: StoplightOperations["get-campaigns"]["responses"]["200"]["content"]["application/json"]; @@ -165,7 +171,7 @@ class RouteItem extends UserRoute<{ query.orderBy(this.orderBy, this.order); - return (await query) as { + const results: { id?: number; name?: string; startDate?: string; @@ -185,7 +191,65 @@ class RouteItem extends UserRoute<{ resultType?: -1 | 0 | 1; phase_id?: number; phase_name?: string; - }[]; + }[] = await query; + + const withRoles = this.addRoles(results); + + return withRoles; + } + + private async addRoles( + campaigns: T[] + ): Promise<(T & { roles?: Roles })[]> { + if (!this.fields.includes("roles")) return campaigns; + const roles = await tryber.tables.CampaignCustomRoles.do() + .select( + "campaign_id", + "custom_role_id", + tryber.ref("name").withSchema("custom_roles").as("custom_role_name"), + "tester_id", + tryber.ref("name").withSchema("wp_appq_evd_profile").as("tester_name"), + tryber + .ref("surname") + .withSchema("wp_appq_evd_profile") + .as("tester_surname") + ) + .join( + "custom_roles", + "custom_roles.id", + "campaign_custom_roles.custom_role_id" + ) + .join( + "wp_appq_evd_profile", + "wp_appq_evd_profile.id", + "campaign_custom_roles.tester_id" + ) + .whereIn( + "campaign_id", + campaigns.map((result) => result.id || 0) + ); + + return campaigns.map((campaign) => { + const rolesForCampaign = roles.filter( + (role) => role.campaign_id === campaign.id + ); + const results = rolesForCampaign.map((role) => ({ + role: { + id: role.custom_role_id, + name: role.custom_role_name, + }, + user: { + id: role.tester_id, + name: role.tester_name, + surname: role.tester_surname, + }, + })); + + return { + ...campaign, + roles: results, + }; + }); } private formatCampaigns( @@ -237,7 +301,9 @@ class RouteItem extends UserRoute<{ }, } : {}), - ...(this.fields.includes("phase") + ...(this.fields.includes("phase") && + campaign.phase_id && + campaign.phase_name ? { phase: { id: campaign.phase_id, @@ -247,6 +313,10 @@ class RouteItem extends UserRoute<{ : {}), visibility: this.getVisibilityName(campaign.visibility), resultType: this.getResultTypeName(campaign.resultType), + + ...(this.fields.includes("roles") && { + roles: campaign.roles, + }), })); } diff --git a/src/routes/campaigns/_get/roles.spec.ts b/src/routes/campaigns/_get/roles.spec.ts new file mode 100644 index 000000000..a9e99db1b --- /dev/null +++ b/src/routes/campaigns/_get/roles.spec.ts @@ -0,0 +1,109 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /campaigns - roles", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + platform_id: 1, + start_date: "2023-01-13 10:10:10", + end_date: "2023-01-14 10:10:10", + title: "This is the title", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "", + campaign_pts: 200, + }); + + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + name: "User", + surname: "Name", + email: "", + wp_user_id: 1, + education_id: 1, + employment_id: 1, + }); + + await tryber.tables.CustomRoles.do().insert({ + id: 1, + name: "Role", + olp: "", + }); + + await tryber.tables.CampaignCustomRoles.do().insert({ + id: 1, + campaign_id: 1, + custom_role_id: 1, + tester_id: 1, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CustomRoles.do().delete(); + await tryber.tables.CampaignCustomRoles.do().delete(); + }); + + it("Should return roles", async () => { + const response = await request(app) + .get("/campaigns") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("items"); + const { items } = response.body; + expect(items).toHaveLength(1); + + expect(items[0]).toHaveProperty("roles"); + + const { roles } = items[0]; + + expect(roles).toEqual([ + { + role: { id: 1, name: "Role" }, + user: { id: 1, name: "User", surname: "Name" }, + }, + ]); + }); + + it("Should not return roles if not in fields", async () => { + const response = await request(app) + .get("/campaigns?fields=id") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("items"); + const { items } = response.body; + expect(items).toHaveLength(1); + + expect(items[0]).not.toHaveProperty("roles"); + }); + + it("Should return roles if in fields", async () => { + const response = await request(app) + .get("/campaigns?fields=roles") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty("items"); + const { items } = response.body; + expect(items).toHaveLength(1); + + expect(items[0]).toHaveProperty("roles"); + + const { roles } = items[0]; + + expect(roles).toEqual([ + { + role: { id: 1, name: "Role" }, + user: { id: 1, name: "User", surname: "Name" }, + }, + ]); + }); +}); diff --git a/src/schema.ts b/src/schema.ts index 8625b803e..b61ab1111 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1249,6 +1249,21 @@ export interface operations { id?: number; name: string; }; + phase?: { + id: number; + name: string; + }; + roles?: { + role: { + id: number; + name: string; + }; + user: { + id: number; + name: string; + surname: string; + }; + }[]; }[]; } & components["schemas"]["PaginationData"]; }; From a80efba90f68ba8bf051161ad0996c1f10cbf316 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 6 May 2024 14:37:37 +0200 Subject: [PATCH 20/39] feat: Add filter by role (#316) * feat: Add filter by role * test: Handle no role items --- .../campaigns/_get/filterByRole.spec.ts | 125 ++++++++++++++++++ src/routes/campaigns/_get/index.ts | 25 ++++ 2 files changed, 150 insertions(+) create mode 100644 src/routes/campaigns/_get/filterByRole.spec.ts diff --git a/src/routes/campaigns/_get/filterByRole.spec.ts b/src/routes/campaigns/_get/filterByRole.spec.ts new file mode 100644 index 000000000..f56da92dc --- /dev/null +++ b/src/routes/campaigns/_get/filterByRole.spec.ts @@ -0,0 +1,125 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; +const campaign = { + id: 1, + platform_id: 1, + start_date: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end_date: new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + title: "This is the title", + page_preview_id: 1, + page_manual_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "", + campaign_pts: 200, +}; +describe("GET /campaigns", () => { + beforeAll(async () => { + await tryber.tables.CampaignPhase.do().insert([ + { id: 1, name: "Draft", type_id: 1 }, + ]); + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 1, + name: "name", + surname: "surname", + email: "", + wp_user_id: 1, + education_id: 1, + employment_id: 1, + }, + ]); + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 2, + name: "name", + surname: "surname", + email: "", + wp_user_id: 2, + education_id: 1, + employment_id: 1, + }, + ]); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { ...campaign, id: 1, title: "First campaign" }, + { ...campaign, id: 2, title: "Second campaign" }, + { ...campaign, id: 3, title: "Third campaign" }, + { ...campaign, id: 4, title: "Fourth campaign" }, + ]); + + await tryber.tables.CustomRoles.do().insert([ + { id: 1, name: "PM", olp: "" }, + { id: 2, name: "Other", olp: "" }, + ]); + + await tryber.tables.CampaignCustomRoles.do().insert([ + { campaign_id: 1, custom_role_id: 1, tester_id: 1 }, + { campaign_id: 2, custom_role_id: 1, tester_id: 2 }, + { campaign_id: 3, custom_role_id: 1, tester_id: 1 }, + ]); + }); + afterAll(async () => { + await tryber.tables.CampaignPhase.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.CustomRoles.do().delete(); + await tryber.tables.CampaignCustomRoles.do().delete(); + }); + + it("Should return only campaigns with custom role with id 1 when filterBy[role_1]=1", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=1") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(2); + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 1, name: "First campaign" }), + expect.objectContaining({ id: 3, name: "Third campaign" }), + ]) + ); + }); + + it("Should return filtered campaign total", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=1&limit=10") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(2); + expect(response.body.total).toBe(2); + }); + + it("Should return only campaigns with filtered csm", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=2") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(1); + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 2, name: "Second campaign" }), + ]) + ); + }); + + it("Should return all campaigns filtered by filterBy[role_1]", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=1,2") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(3); + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 1, name: "First campaign" }), + expect.objectContaining({ id: 2, name: "Second campaign" }), + expect.objectContaining({ id: 3, name: "Third campaign" }), + ]) + ); + }); +}); diff --git a/src/routes/campaigns/_get/index.ts b/src/routes/campaigns/_get/index.ts index 3da5d682b..d13391fad 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -50,6 +50,10 @@ class RouteItem extends UserRoute<{ type?: number[]; status?: "closed" | "running" | "incoming"; csm?: number; + roles?: { + id: number; + value: number[]; + }[]; } = {}; constructor(configuration: RouteClassConfiguration) { @@ -109,6 +113,15 @@ class RouteItem extends UserRoute<{ const csmId = (query.filterBy as any).csm; this.filterBy.csm = Number(csmId); } + const roles = Object.entries( + query.filterBy as { [key: string]: string } + ).filter(([key]) => key.startsWith("role_")); + if (roles.length) { + this.filterBy.roles = roles.map(([key, value]) => ({ + id: parseInt(key.split("_")[1]), + value: value.split(",").map((id: string) => parseInt(id)), + })); + } } } @@ -437,6 +450,18 @@ class RouteItem extends UserRoute<{ .where("wp_appq_evd_campaign.start_date", ">", tryber.fn.now()); } } + + if (this.filterBy.roles) { + this.filterBy.roles.forEach((role) => { + query = query.whereIn( + "wp_appq_evd_campaign.id", + tryber.tables.CampaignCustomRoles.do() + .select("campaign_id") + .where("custom_role_id", role.id) + .whereIn("tester_id", role.value) + ); + }); + } }); } From 552b837a02d28d0d7e1812b864f0065345a6b5cb Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Mon, 6 May 2024 15:17:49 +0200 Subject: [PATCH 21/39] feat: Allow filtering by phase (#317) --- src/routes/campaigns/_get/index.ts | 13 +++++++++++++ src/routes/campaigns/_get/phases.spec.ts | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/routes/campaigns/_get/index.ts b/src/routes/campaigns/_get/index.ts index d13391fad..b91818594 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -54,6 +54,7 @@ class RouteItem extends UserRoute<{ id: number; value: number[]; }[]; + phase?: number; } = {}; constructor(configuration: RouteClassConfiguration) { @@ -122,6 +123,11 @@ class RouteItem extends UserRoute<{ value: value.split(",").map((id: string) => parseInt(id)), })); } + + if ((query.filterBy as any).phase) { + const phaseId = (query.filterBy as any).phase; + this.filterBy.phase = Number(phaseId); + } } } @@ -462,6 +468,13 @@ class RouteItem extends UserRoute<{ ); }); } + + if (this.filterBy.phase) { + query = query.where( + "wp_appq_evd_campaign.phase_id", + this.filterBy.phase + ); + } }); } diff --git a/src/routes/campaigns/_get/phases.spec.ts b/src/routes/campaigns/_get/phases.spec.ts index 812dd4eae..1a6bf4ef2 100644 --- a/src/routes/campaigns/_get/phases.spec.ts +++ b/src/routes/campaigns/_get/phases.spec.ts @@ -75,4 +75,26 @@ describe("GET /campaigns", () => { ]) ); }); + + it("Should allow filtering by phase", async () => { + const response = await request(app) + .get("/campaigns?filterBy[phase]=1") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + + expect(response.status).toBe(200); + + expect(response.body.items).toHaveLength(1); + + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + phase: { + id: 1, + name: "Draft", + }, + }), + ]) + ); + }); }); From cc6d737bea4758737ab53dcd1aafd843b2e05813 Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Tue, 7 May 2024 15:30:53 +0200 Subject: [PATCH 22/39] feat: Add phase type (#318) --- src/reference/openapi.yml | 76 +++++++--------------------- src/routes/phases/_get/index.spec.ts | 8 ++- src/routes/phases/_get/index.ts | 19 ++++++- src/schema.ts | 4 ++ 4 files changed, 44 insertions(+), 63 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index e88c39982..0b409f9e5 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -721,40 +721,31 @@ paths: - name phase: type: object - properties: id: type: integer - name: type: string - required: - id - name roles: type: array - items: - type: object properties: role: type: object - required: - id - name properties: id: type: integer - name: type: string - user: type: object - required: - id - name @@ -762,13 +753,10 @@ paths: properties: id: type: integer - name: type: string - surname: type: string - required: - role - user @@ -4202,10 +4190,8 @@ paths: properties: id: type: integer - name: type: string - required: - id - name @@ -4219,7 +4205,6 @@ paths: properties: name: type: string - required: - name /custom_user_fields: @@ -9783,27 +9768,20 @@ paths: allOf: - $ref: '#/components/schemas/DossierCreationData' - type: object - properties: duplicate: type: object - properties: fields: type: integer - useCases: type: integer - mailMerges: type: integer - pages: type: integer - testers: type: integer - '/dossiers/{campaign}': parameters: - name: campaign @@ -9968,62 +9946,48 @@ paths: $ref: '#/components/schemas/CountryCode' languages: type: array - items: - type: object properties: id: type: integer - name: type: string - required: - id - name browsers: type: array - items: - type: object properties: id: type: integer - name: type: string - required: - id - name productType: type: object - properties: id: type: number - name: type: string - required: - id - name phase: type: object - required: - id - name properties: id: type: integer - name: type: string - required: - id - title @@ -10119,10 +10083,8 @@ paths: properties: id: type: number - name: type: string - required: - id - name @@ -10136,7 +10098,6 @@ paths: properties: name: type: string - required: - name '/users/by-role/{role}': @@ -10153,20 +10114,15 @@ paths: properties: results: type: array - items: - type: object properties: id: type: integer - name: type: string - surname: type: string - required: - id - name @@ -10201,17 +10157,13 @@ paths: properties: results: type: array - items: - type: object properties: id: type: integer - name: type: string - required: - id - name @@ -10246,17 +10198,13 @@ paths: properties: results: type: array - items: - type: object properties: id: type: integer - name: type: string - required: - id - name @@ -10289,20 +10237,33 @@ paths: properties: results: type: array - items: - type: object properties: id: type: integer - name: type: string - + type: + type: object + x-stoplight: + id: jmjdmy2wzbrvx + required: + - id + - name + properties: + id: + type: integer + x-stoplight: + id: qlltod4as9yce + name: + type: string + x-stoplight: + id: lokz0tin3f5ad required: - id - name + - type required: - results operationId: get-phases @@ -10329,10 +10290,8 @@ paths: properties: id: type: integer - name: type: string - required: - id - name @@ -10345,7 +10304,6 @@ paths: properties: phase: type: integer - required: - phase security: diff --git a/src/routes/phases/_get/index.spec.ts b/src/routes/phases/_get/index.spec.ts index 3b14bbf2b..c0b69a467 100644 --- a/src/routes/phases/_get/index.spec.ts +++ b/src/routes/phases/_get/index.spec.ts @@ -8,6 +8,10 @@ describe("Route GET /phases", () => { { id: 1, name: "Phase 1", type_id: 1 }, { id: 2, name: "Phase 2", type_id: 2 }, ]); + await tryber.tables.CampaignPhaseType.do().insert([ + { id: 1, name: "Type 1" }, + { id: 2, name: "Type 2" }, + ]); }); afterAll(async () => { @@ -57,8 +61,8 @@ describe("Route GET /phases", () => { expect(response.body).toHaveProperty("results"); expect(response.body.results).toHaveLength(2); expect(response.body.results).toEqual([ - { id: 1, name: "Phase 1" }, - { id: 2, name: "Phase 2" }, + { id: 1, name: "Phase 1", type: { id: 1, name: "Type 1" } }, + { id: 2, name: "Phase 2", type: { id: 2, name: "Type 2" } }, ]); }); }); diff --git a/src/routes/phases/_get/index.ts b/src/routes/phases/_get/index.ts index 98e921cf5..1ed82d361 100644 --- a/src/routes/phases/_get/index.ts +++ b/src/routes/phases/_get/index.ts @@ -26,9 +26,24 @@ export default class Route extends UserRoute<{ return this.accessibleCampaigns !== true; } protected async prepare(): Promise { - const results = await tryber.tables.CampaignPhase.do().select("id", "name"); + const results = await tryber.tables.CampaignPhase.do() + .join( + "campaign_phase_type", + "campaign_phase_type.id", + "campaign_phase.type_id" + ) + .select( + tryber.ref("id").withSchema("campaign_phase"), + tryber.ref("name").withSchema("campaign_phase"), + tryber.ref("id").withSchema("campaign_phase_type").as("type_id"), + tryber.ref("name").withSchema("campaign_phase_type").as("type_name") + ); this.setSuccess(200, { - results, + results: results.map((phase) => ({ + id: phase.id, + name: phase.name, + type: { id: phase.type_id, name: phase.type_name }, + })), }); } } diff --git a/src/schema.ts b/src/schema.ts index b61ab1111..2044b0aa4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -4337,6 +4337,10 @@ export interface operations { results: { id: number; name: string; + type: { + id: number; + name: string; + }; }[]; }; }; From 20b27ed95d36a951b02f46fe56f3ae41cec5bc1c Mon Sep 17 00:00:00 2001 From: d-beezee <59012086+d-beezee@users.noreply.github.com> Date: Wed, 8 May 2024 12:27:03 +0200 Subject: [PATCH 23/39] Use phase visibility (#319) * feat: Do not show unavailable cp on get campaigns * feat: Prevent posting bugs/ showing cp draft --- package.json | 2 +- .../users/me/campaigns/_get/filters.spec.ts | 6 +- .../users/me/campaigns/_get/index.spec.ts | 71 +++++++++----- src/routes/users/me/campaigns/_get/index.ts | 17 +++- .../me/campaigns/_get/manualpreview.spec.ts | 70 +++++++------- .../users/me/campaigns/_get/order.spec.ts | 73 ++++++++------ .../me/campaigns/_get/pagination.spec.ts | 94 ++++++++++--------- .../campaigns/campaignId/_get/index.spec.ts | 3 +- .../me/campaigns/campaignId/_get/index.ts | 26 +++++ .../campaigns/campaignId/_get/phase.spec.ts | 28 ++++++ .../campaigns/campaignId/_get/useBasicData.ts | 3 + .../campaigns/campaignId/bugs/_post/index.ts | 17 +++- .../campaignId/bugs/_post/phase.spec.ts | 44 +++++++++ .../campaignId/bugs/_post/useBasicData.ts | 5 + yarn.lock | 8 +- 15 files changed, 322 insertions(+), 145 deletions(-) create mode 100644 src/routes/users/me/campaigns/campaignId/_get/phase.spec.ts create mode 100644 src/routes/users/me/campaigns/campaignId/bugs/_post/phase.spec.ts diff --git a/package.json b/package.json index 54f045d10..6cbca2373 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.33.3", + "@appquality/tryber-database": "^0.35.0", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/routes/users/me/campaigns/_get/filters.spec.ts b/src/routes/users/me/campaigns/_get/filters.spec.ts index 98d586575..caf69d36f 100644 --- a/src/routes/users/me/campaigns/_get/filters.spec.ts +++ b/src/routes/users/me/campaigns/_get/filters.spec.ts @@ -1,12 +1,13 @@ -import request from "supertest"; import app from "@src/app"; -import resolvePermalinks from "@src/features/wp/resolvePermalinks"; import { tryber } from "@src/features/database"; +import resolvePermalinks from "@src/features/wp/resolvePermalinks"; +import request from "supertest"; jest.mock("@src/features/wp/resolvePermalinks"); describe("GET /users/me/campaigns - filters", () => { beforeAll(async () => { + await tryber.seeds().campaign_statuses(); await tryber.tables.WpAppqEvdProfile.do().insert([ { id: 1, @@ -47,6 +48,7 @@ describe("GET /users/me/campaigns - filters", () => { pm_id: 1, project_id: 1, customer_title: "Customer title", + phase_id: 10, }; await tryber.tables.WpAppqCampaignType.do().insert({ id: 1, diff --git a/src/routes/users/me/campaigns/_get/index.spec.ts b/src/routes/users/me/campaigns/_get/index.spec.ts index ae0f0b342..dd58f4ac1 100644 --- a/src/routes/users/me/campaigns/_get/index.spec.ts +++ b/src/routes/users/me/campaigns/_get/index.spec.ts @@ -1,7 +1,6 @@ -import campaigns from "@src/__mocks__/mockedDb/campaign"; -import campaignTypes from "@src/__mocks__/mockedDb/campaignType"; import candidature from "@src/__mocks__/mockedDb/cpHasCandidates"; import app from "@src/app"; +import { tryber } from "@src/features/database"; import resolvePermalinks from "@src/features/wp/resolvePermalinks"; import request from "supertest"; @@ -13,32 +12,21 @@ const fourteenDaysFromNow = new Date().setDate(new Date().getDate() + 14); const closeDate = new Date(fourteenDaysFromNow).toISOString().split("T")[0]; describe("GET /users/me/campaigns", () => { - beforeAll(() => { + beforeAll(async () => { (resolvePermalinks as jest.Mock).mockImplementation(() => { return { 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, }; }); - campaignTypes.insert({ + await tryber.tables.WpAppqCampaignType.do().insert({ id: 1, + name: "Type", + category_id: 1, }); - campaigns.insert({ - id: 1, - title: "Public campaign", - start_date: new Date().toISOString().split("T")[0], - end_date: endDate, - close_date: closeDate, - campaign_type_id: 1, - page_preview_id: 1, - page_manual_id: 2, - os: "1", - is_public: 1, - }); - campaigns.insert({ - id: 2, - title: "Small Group campaign", + await tryber.seeds().campaign_statuses(); + const campaign = { start_date: new Date().toISOString().split("T")[0], end_date: endDate, close_date: closeDate, @@ -46,12 +34,47 @@ describe("GET /users/me/campaigns", () => { page_preview_id: 1, page_manual_id: 2, os: "1", - is_public: 0, - }); + platform_id: 1, + pm_id: 1, + customer_id: 1, + project_id: 1, + customer_title: "Customer", + }; + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...campaign, + id: 1, + title: "Public campaign", + is_public: 1, + phase_id: 10, + }, + { + ...campaign, + id: 2, + title: "Small Group campaign", + is_public: 0, + phase_id: 10, + }, + { + ...campaign, + id: 3, + title: "Public campaign - draft", + is_public: 1, + phase_id: 1, + }, + { + ...campaign, + id: 4, + title: "Small Group campaign - draft", + is_public: 0, + phase_id: 1, + }, + ]); }); - afterAll(() => { - campaigns.clear(); - campaignTypes.clear(); + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.CampaignPhase.do().delete(); jest.resetAllMocks(); }); describe("GET /users/me/campaigns", () => { diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index ad816f399..df8468720 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -1,9 +1,9 @@ import * as db from "@src/features/db"; -import UserRoute from "@src/features/routes/UserRoute"; import Campaigns from "@src/features/db/class/Campaigns"; +import UserRoute from "@src/features/routes/UserRoute"; -import resolvePermalinks from "../../../../../features/wp/resolvePermalinks"; import { tryber } from "@src/features/database"; +import resolvePermalinks from "../../../../../features/wp/resolvePermalinks"; /** OPENAPI-CLASS: get-users-me-campaigns */ @@ -150,7 +150,18 @@ class RouteItem extends UserRoute<{ "wp_appq_campaign_type", "wp_appq_campaign_type.id", "wp_appq_evd_campaign.campaign_type_id" - ); + ) + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) + .join( + "campaign_phase_type", + "campaign_phase_type.id", + "campaign_phase.type_id" + ) + .whereNot("campaign_phase_type.name", "unavailable"); if (!this.filterByAccepted()) { const pageAccess = await this.getPageAccess(); diff --git a/src/routes/users/me/campaigns/_get/manualpreview.spec.ts b/src/routes/users/me/campaigns/_get/manualpreview.spec.ts index 372ccc135..5721916c4 100644 --- a/src/routes/users/me/campaigns/_get/manualpreview.spec.ts +++ b/src/routes/users/me/campaigns/_get/manualpreview.spec.ts @@ -1,20 +1,20 @@ -import request from "supertest"; -import app from "@src/app"; import campaigns from "@src/__mocks__/mockedDb/campaign"; -import candidature from "@src/__mocks__/mockedDb/cpHasCandidates"; import campaignTypes from "@src/__mocks__/mockedDb/campaignType"; +import candidature from "@src/__mocks__/mockedDb/cpHasCandidates"; +import app from "@src/app"; +import { tryber } from "@src/features/database"; import resolvePermalinks from "@src/features/wp/resolvePermalinks"; +import request from "supertest"; jest.mock("@src/features/wp/resolvePermalinks"); describe("GET /users/me/campaigns ", () => { - beforeAll(() => { + beforeAll(async () => { + await tryber.seeds().campaign_statuses(); campaignTypes.insert({ id: 1, }); - campaigns.insert({ - id: 1, - title: "Campaign with manual not public in all languages", + const campaign = { start_date: new Date().toISOString().split("T")[0], end_date: new Date().toISOString().split("T")[0], close_date: new Date().toISOString().split("T")[0], @@ -23,31 +23,37 @@ describe("GET /users/me/campaigns ", () => { page_manual_id: 2, os: "1", is_public: 1, - }); - campaigns.insert({ - id: 2, - title: "Campaign with preview not public in all language", - start_date: new Date().toISOString().split("T")[0], - end_date: new Date().toISOString().split("T")[0], - close_date: new Date().toISOString().split("T")[0], - campaign_type_id: 1, - page_preview_id: 2, - page_manual_id: 3, - os: "1", - is_public: 1, - }); - campaigns.insert({ - id: 3, - title: "Campaign with preview not public in a single language", - start_date: new Date().toISOString().split("T")[0], - end_date: new Date().toISOString().split("T")[0], - close_date: new Date().toISOString().split("T")[0], - campaign_type_id: 1, - page_preview_id: 1, - page_manual_id: 3, - os: "1", - is_public: 1, - }); + pm_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, + phase_id: 10, + customer_title: "Customer title", + }; + + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...campaign, + id: 1, + title: "Campaign with manual not public in all languages", + page_preview_id: 3, + page_manual_id: 2, + }, + { + ...campaign, + id: 2, + title: "Campaign with preview not public in all language", + page_preview_id: 2, + page_manual_id: 3, + }, + { + ...campaign, + id: 3, + title: "Campaign with preview not public in a single language", + page_preview_id: 1, + page_manual_id: 3, + }, + ]); }); afterAll(() => { campaigns.clear(); diff --git a/src/routes/users/me/campaigns/_get/order.spec.ts b/src/routes/users/me/campaigns/_get/order.spec.ts index c0e9c4790..11d968b07 100644 --- a/src/routes/users/me/campaigns/_get/order.spec.ts +++ b/src/routes/users/me/campaigns/_get/order.spec.ts @@ -1,6 +1,7 @@ import campaigns from "@src/__mocks__/mockedDb/campaign"; import campaignTypes from "@src/__mocks__/mockedDb/campaignType"; import app from "@src/app"; +import { tryber } from "@src/features/database"; import resolvePermalinks from "@src/features/wp/resolvePermalinks"; import request from "supertest"; @@ -12,13 +13,15 @@ const dayFromNow = (days: number) => { .split("T")[0]; }; describe("GET /users/me/campaigns ", () => { - beforeAll(() => { + beforeAll(async () => { (resolvePermalinks as jest.Mock).mockImplementation(() => { return { 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, }; }); + + await tryber.seeds().campaign_statuses(); const basicCampaignObject = { title: "My campaign", start_date: new Date().toISOString().split("T")[0], @@ -28,40 +31,48 @@ describe("GET /users/me/campaigns ", () => { page_preview_id: 1, page_manual_id: 2, os: "1", - is_public: 1 as 0, - status_id: 1 as 1, + is_public: 1, + status_id: 1, + pm_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, + customer_title: "Customer title", + phase_id: 10, }; campaignTypes.insert({ id: 1, }); - campaigns.insert({ - ...basicCampaignObject, - id: 1, - start_date: dayFromNow(2), - end_date: dayFromNow(3), - close_date: dayFromNow(2), - }); - campaigns.insert({ - ...basicCampaignObject, - id: 2, - start_date: dayFromNow(1), - end_date: dayFromNow(2), - close_date: dayFromNow(3), - }); - campaigns.insert({ - ...basicCampaignObject, - id: 3, - start_date: dayFromNow(3), - end_date: dayFromNow(4), - close_date: dayFromNow(4), - }); - campaigns.insert({ - ...basicCampaignObject, - id: 4, - start_date: dayFromNow(4), - end_date: dayFromNow(1), - close_date: dayFromNow(1), - }); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...basicCampaignObject, + id: 1, + start_date: dayFromNow(2), + end_date: dayFromNow(3), + close_date: dayFromNow(2), + }, + { + ...basicCampaignObject, + id: 2, + start_date: dayFromNow(1), + end_date: dayFromNow(2), + close_date: dayFromNow(3), + }, + { + ...basicCampaignObject, + id: 3, + start_date: dayFromNow(3), + end_date: dayFromNow(4), + close_date: dayFromNow(4), + }, + { + ...basicCampaignObject, + id: 4, + start_date: dayFromNow(4), + end_date: dayFromNow(1), + close_date: dayFromNow(1), + }, + ]); }); afterAll(() => { campaigns.clear(); diff --git a/src/routes/users/me/campaigns/_get/pagination.spec.ts b/src/routes/users/me/campaigns/_get/pagination.spec.ts index 23882e39d..a5e3ff468 100644 --- a/src/routes/users/me/campaigns/_get/pagination.spec.ts +++ b/src/routes/users/me/campaigns/_get/pagination.spec.ts @@ -2,18 +2,25 @@ import campaigns from "@src/__mocks__/mockedDb/campaign"; import campaignTypes from "@src/__mocks__/mockedDb/campaignType"; import candidature from "@src/__mocks__/mockedDb/cpHasCandidates"; import app from "@src/app"; +import { tryber } from "@src/features/database"; import resolvePermalinks from "@src/features/wp/resolvePermalinks"; import request from "supertest"; jest.mock("@src/features/wp/resolvePermalinks"); describe("GET /users/me/campaigns - pagination ", () => { - beforeAll(() => { + beforeAll(async () => { (resolvePermalinks as jest.Mock).mockImplementation(() => { return { 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, }; }); + + await tryber.seeds().campaign_statuses(); + + campaignTypes.insert({ + id: 1, + }); const basicCampaignObject = { title: "My campaign", start_date: new Date().toISOString().split("T")[0], @@ -23,49 +30,50 @@ describe("GET /users/me/campaigns - pagination ", () => { page_preview_id: 1, page_manual_id: 2, os: "1", - is_public: 1 as 0, - status_id: 1 as 1, + is_public: 1, + status_id: 1, + pm_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, + customer_title: "Customer title", + phase_id: 10, }; - campaignTypes.insert({ - id: 1, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 1, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 2, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 3, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 4, - status_id: 2, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 5, - status_id: 2, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 6, - status_id: 2, - }); - campaigns.insert({ - ...basicCampaignObject, - id: 7, - status_id: 2, - }); - - campaigns.insert({ - ...basicCampaignObject, - id: 8, - }); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...basicCampaignObject, + id: 1, + }, + { + ...basicCampaignObject, + id: 2, + }, + { + ...basicCampaignObject, + id: 3, + }, + { + ...basicCampaignObject, + id: 4, + status_id: 2, + }, + { + ...basicCampaignObject, + id: 5, + status_id: 2, + }, + { + ...basicCampaignObject, + id: 6, + status_id: 2, + }, + { + ...basicCampaignObject, + id: 7, + status_id: 2, + }, + { ...basicCampaignObject, id: 8 }, + ]); candidature.insert({ campaign_id: 8, user_id: 1, diff --git a/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts b/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts index 0216ad236..252cc6108 100644 --- a/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts +++ b/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts @@ -1,6 +1,6 @@ import app from "@src/app"; -import request from "supertest"; import { tryber } from "@src/features/database"; +import request from "supertest"; import useBasicData from "./useBasicData"; describe("Route GET /users/me/campaigns/{campaignId}/", () => { @@ -239,6 +239,7 @@ describe("Route GET /users/me/campaigns/{campaignId}/ - bug language set", () => pm_id: 1, project_id: 1, customer_title: "My campaign", + phase_id: 10, }); await tryber.tables.WpAppqCpMeta.do().insert([ { diff --git a/src/routes/users/me/campaigns/campaignId/_get/index.ts b/src/routes/users/me/campaigns/campaignId/_get/index.ts index 32acd8e6b..ec55adf04 100644 --- a/src/routes/users/me/campaigns/campaignId/_get/index.ts +++ b/src/routes/users/me/campaigns/campaignId/_get/index.ts @@ -2,6 +2,7 @@ import OpenapiError from "@src/features/OpenapiError"; import Campaign from "@src/features/class/Campaign"; +import { tryber } from "@src/features/database"; import UserRoute from "@src/features/routes/UserRoute"; export default class UserSingleCampaignRoute extends UserRoute<{ @@ -12,10 +13,35 @@ export default class UserSingleCampaignRoute extends UserRoute<{ protected async filter(): Promise { if (await this.testerIsNotCandidate()) return false; + if (await this.campaignIsUnavailable()) return false; return true; } + private async campaignIsUnavailable() { + const phaseType = await tryber.tables.WpAppqEvdCampaign.do() + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) + .join( + "campaign_phase_type", + "campaign_phase_type.id", + "campaign_phase.type_id" + ) + .select("campaign_phase_type.name") + .where("wp_appq_evd_campaign.id", this.campaignId) + .first(); + + if (!phaseType || phaseType.name === "unavailable") { + this.setError(404, new OpenapiError("This campaign does not exist")); + return true; + } + + return false; + } + protected async prepare() { const campaign = new Campaign(this.campaignId, false); campaign.init(); diff --git a/src/routes/users/me/campaigns/campaignId/_get/phase.spec.ts b/src/routes/users/me/campaigns/campaignId/_get/phase.spec.ts new file mode 100644 index 000000000..83b3b371a --- /dev/null +++ b/src/routes/users/me/campaigns/campaignId/_get/phase.spec.ts @@ -0,0 +1,28 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; +import useBasicData from "./useBasicData"; + +describe("Route GET /users/me/campaigns/{campaignId}/ - bug language set", () => { + useBasicData(); + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + phase_id: 1, + }) + .where({ + id: 1, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should return 404 if campaign is unavailable", async () => { + const response = await request(app) + .get("/users/me/campaigns/1") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(404); + }); +}); diff --git a/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts b/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts index 21a60a9ac..8413b8a82 100644 --- a/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts +++ b/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts @@ -2,6 +2,7 @@ import { tryber } from "@src/features/database"; const useBasicData = () => { beforeAll(async () => { + await tryber.seeds().campaign_statuses(); await tryber.tables.WpAppqEvdProfile.do().insert({ id: 1, name: "tester", @@ -208,6 +209,7 @@ const useBasicData = () => { pm_id: 1, project_id: 1, customer_title: "My campaign", + phase_id: 10, }, { id: 2, @@ -223,6 +225,7 @@ const useBasicData = () => { pm_id: 1, project_id: 1, customer_title: "My campaign", + phase_id: 10, }, ]); await tryber.tables.WpOptions.do().insert({ diff --git a/src/routes/users/me/campaigns/campaignId/bugs/_post/index.ts b/src/routes/users/me/campaigns/campaignId/bugs/_post/index.ts index ddf142b47..3252d40d8 100644 --- a/src/routes/users/me/campaigns/campaignId/bugs/_post/index.ts +++ b/src/routes/users/me/campaigns/campaignId/bugs/_post/index.ts @@ -80,10 +80,19 @@ export default class PostBugsOnCampaignRoute extends UserRoute<{ private async campaignDoesNotExists() { const result = await tryber.tables.WpAppqEvdCampaign.do() - .select("id") - .where({ - id: this.campaignId, - }) + .select(tryber.ref("id").withSchema("wp_appq_evd_campaign")) + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_id" + ) + .join( + "campaign_phase_type", + "campaign_phase_type.id", + "campaign_phase.type_id" + ) + .where("wp_appq_evd_campaign.id", this.campaignId) + .whereNot("campaign_phase_type.name", "unavailable") .first(); if (result) return false; this.setError( diff --git a/src/routes/users/me/campaigns/campaignId/bugs/_post/phase.spec.ts b/src/routes/users/me/campaigns/campaignId/bugs/_post/phase.spec.ts new file mode 100644 index 000000000..25d6d7fbb --- /dev/null +++ b/src/routes/users/me/campaigns/campaignId/bugs/_post/phase.spec.ts @@ -0,0 +1,44 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; +import useBasicData from "./useBasicData"; + +const bug = { + title: "Campaign Title", + description: "Camapign Description", + expected: "The expected to reproduce the bug", + current: "Current case", + severity: "LOW", + replicability: "ONCE", + type: "CRASH", + notes: "The bug notes", + lastSeen: "2022-07-01T13:44:00.000+02:00", + usecase: 1, + device: 1, + media: ["www.example.com/media69.jpg", "www.example.com/media6969.jpg"], +}; + +describe("Route POST a bug to a specific campaign - unavailable", () => { + useBasicData(); + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + phase_id: 1, + }) + .where({ + id: 1, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + }); + + it("Should return 404 if campaign is unavailable", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/bugs") + .set("authorization", "Bearer tester") + .send(bug); + expect(response.status).toBe(404); + }); +}); diff --git a/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts b/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts index 276ec632a..25a12a8fb 100644 --- a/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts +++ b/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts @@ -2,6 +2,7 @@ import { tryber } from "@src/features/database"; const useBasicData = () => { beforeAll(async () => { + await tryber.seeds().campaign_statuses(); await tryber.tables.WpUsers.do().insert({ ID: 1, }); @@ -26,6 +27,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 10, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ selected_device: 1, @@ -76,6 +78,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 10, }); await tryber.tables.WpAppqEvdCampaign.do().insert({ @@ -92,6 +95,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 10, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ @@ -148,6 +152,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 10, }); await tryber.tables.WpAppqCampaignTask.do().insert({ diff --git a/yarn.lock b/yarn.lock index 7c8ed3169..625d3963e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.33.3": - version "0.33.3" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.33.3.tgz#4c3cdcd47f9c247944c805ee5f1da2034048d28b" - integrity sha512-Sg90QyXqcME3uaV/1WSt3Poy9PgUPaK2dineMFPizcu76MYmyAgx0/eaTyRUmk1y6shV5iuBWig0pUPCsZ3U2Q== +"@appquality/tryber-database@^0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.35.0.tgz#16b2d0c7049e6f91a9378e546ad51a4b79c62884" + integrity sha512-8p/vqGjOqmtPcupPAK6VJEYdd2eXN+E9u5vTtVpdGDhsg/6QUZQqIXVfOP+2e+dx6GRvDyMrvY+uQmAZisHHDw== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From 23163da4755c4845181ec29f0fe010d84bba13b0 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 12:29:50 +0200 Subject: [PATCH 24/39] fix: Update update with where --- src/routes/dossiers/_post/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 966118587..ac7357d00 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -359,9 +359,11 @@ export default class RouteItem extends AdminRoute<{ }); if (manualId) { - await tryber.tables.WpAppqEvdCampaign.do().update({ - page_manual_id: manualId, - }); + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + page_manual_id: manualId, + }) + .where("id", campaignId); } const previewId = await this.duplicatePage({ @@ -370,9 +372,11 @@ export default class RouteItem extends AdminRoute<{ }); if (previewId) { - await tryber.tables.WpAppqEvdCampaign.do().update({ - page_preview_id: previewId, - }); + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + page_preview_id: previewId, + }) + .where("id", campaignId); } if (manualId || previewId) { From 64ee69a34a89ba118acefe7fa1a7f5667de736c4 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 13:21:09 +0200 Subject: [PATCH 25/39] fix: Link translations --- package.json | 3 +- src/routes/dossiers/_post/index.ts | 55 ++++++++++++++++++++++++++++-- yarn.lock | 5 +++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6cbca2373..4cd8058dc 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "body-parser": "^1.19.1", "codice-fiscale-js": "^2.3.13", "connect-busboy": "^1.0.0", + "core-js": "^3.6.5", "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.18.1", @@ -43,10 +44,10 @@ "mysql": "^2.18.1", "openapi-backend": "^3.9.2", "parse-comments": "^1.0.0", + "php-serialize": "^4.1.1", "php-unserialize": "^0.0.1", "spark-md5": "^3.0.2", "uuid": "^9.0.0", - "core-js": "^3.6.5", "wordpress-hash-node": "^1.0.0" }, "devDependencies": { diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index ac7357d00..770f78cf3 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -4,6 +4,8 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import AdminRoute from "@src/features/routes/AdminRoute"; import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; +import crypto from "crypto"; +import { serialize } from "php-serialize"; import { unserialize } from "php-unserialize"; export default class RouteItem extends AdminRoute<{ @@ -383,33 +385,82 @@ export default class RouteItem extends AdminRoute<{ const defaultLanguage = await this.getDefaultLanguage(); if (!defaultLanguage) return; if (manualId) { + const manualTranslations: { [key: string]: number } = { + [defaultLanguage]: manualId, + }; const parsedTranslations = await this.getPageTranslationIds({ pageId: page_manual_id, defaultLanguage, }); for (const t in parsedTranslations) { - await this.duplicatePage({ + const transId = await this.duplicatePage({ pageId: parsedTranslations[t], campaignId, }); + if (transId) manualTranslations[t] = transId; } + await this.createPolylangTranslations({ + id: manualId, + translations: manualTranslations, + }); } if (previewId) { + const previewTranslations: { [key: string]: number } = { + [defaultLanguage]: previewId, + }; const parsedTranslations = await this.getPageTranslationIds({ pageId: page_preview_id, defaultLanguage, }); for (const t in parsedTranslations) { - await this.duplicatePage({ + const transId = await this.duplicatePage({ pageId: parsedTranslations[t], campaignId, }); + if (transId) previewTranslations[t] = transId; } + await this.createPolylangTranslations({ + id: previewId, + translations: previewTranslations, + }); } } } + private async createPolylangTranslations({ + id, + translations, + }: { + id: number; + translations: { [key: string]: number }; + }) { + const term_name = crypto + .createHash("md5") + .update(id.toString()) + .digest("hex"); + const term = await tryber.tables.WpTerms.do() + .insert({ + name: `pll_${term_name}`, + slug: `pll_${term_name}`, + }) + .returning("term_id"); + + const term_id = term[0].term_id ?? term[0]; + + await tryber.tables.WpTermTaxonomy.do().insert({ + term_id: term_id, + term_taxonomy_id: term_id, + taxonomy: "post_translations", + description: serialize(translations), + }); + + await tryber.tables.WpTermRelationships.do().insert({ + object_id: id, + term_taxonomy_id: term_id, + }); + } + private async duplicatePage({ pageId, campaignId, diff --git a/yarn.lock b/yarn.lock index 625d3963e..fc3d3dde5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4713,6 +4713,11 @@ pg-connection-string@2.6.1: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== +php-serialize@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/php-serialize/-/php-serialize-4.1.1.tgz#1a614fde3da42361af05afffbaf967fb6556591e" + integrity sha512-7drCrSZdJ05UdG3hyYEIRW0XyKyUFkxa5A3dpIp3NTjUHpI080pkdBAvqaBtkA+kBkMeXX3XnaSnaLGJRz071A== + php-unserialize@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/php-unserialize/-/php-unserialize-0.0.1.tgz" From 0380aa50eed580923c47c74b38085dda8f6a919d Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 13:56:13 +0200 Subject: [PATCH 26/39] test: Fix tests --- src/routes/dossiers/_post/duplication.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts index 794099397..d398d59a6 100644 --- a/src/routes/dossiers/_post/duplication.spec.ts +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -148,6 +148,11 @@ describe("Route POST /dossiers - duplication", () => { { object_id: 2, term_taxonomy_id: 2 }, ]); + await tryber.tables.WpTerms.do().insert([ + { term_id: 1, name: "pll_1234567", slug: "pll_1234567" }, + { term_id: 2, name: "pll_1234567", slug: "pll_1234567" }, + ]); + await tryber.tables.WpTermTaxonomy.do().insert([ { term_taxonomy_id: 1, @@ -209,6 +214,7 @@ describe("Route POST /dossiers - duplication", () => { await tryber.tables.WpTermRelationships.do().delete(); await tryber.tables.WpTermTaxonomy.do().delete(); await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); + await tryber.tables.WpTerms.do().delete(); jest.clearAllMocks(); }); From 874c27cbaaf22f9dcb9ac87c1f3321d91709119f Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 14:01:45 +0200 Subject: [PATCH 27/39] feat: Update campaign id in duplicated manuals --- src/routes/dossiers/_post/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 770f78cf3..3b3db23d3 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -403,6 +403,14 @@ export default class RouteItem extends AdminRoute<{ id: manualId, translations: manualTranslations, }); + for (const transId of Object.values(manualTranslations)) { + await tryber.tables.WpPostmeta.do() + .update({ + meta_value: campaignId.toString(), + }) + .where("meta_key", "man_campaign_id") + .where("post_id", transId); + } } if (previewId) { @@ -447,6 +455,7 @@ export default class RouteItem extends AdminRoute<{ .returning("term_id"); const term_id = term[0].term_id ?? term[0]; + console.log(term_id); await tryber.tables.WpTermTaxonomy.do().insert({ term_id: term_id, From 316c3681a123cf74dea30eab1177b030110cd1f1 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 14:27:47 +0200 Subject: [PATCH 28/39] fix: Do not break data on save --- src/routes/dossiers/campaignId/_put/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index 019b94d0c..1b3eca39e 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -119,13 +119,9 @@ export default class RouteItem extends UserRoute<{ await tryber.tables.WpAppqEvdCampaign.do() .update({ title: this.getBody().title.tester, - platform_id: 0, start_date: this.getBody().startDate, end_date: this.getEndDate(), close_date: this.getCloseDate(), - page_preview_id: 0, - page_manual_id: 0, - customer_id: 0, pm_id: this.getTesterId(), project_id: this.getBody().project, campaign_type_id: this.getBody().testType, From e3a23467020f6b0fe3445d83cc329ad0fa296d69 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 9 May 2024 15:06:16 +0200 Subject: [PATCH 29/39] fix: Add terms for translations --- src/routes/dossiers/_post/index.ts | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 3b3db23d3..b92cfc788 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -384,6 +384,7 @@ export default class RouteItem extends AdminRoute<{ if (manualId || previewId) { const defaultLanguage = await this.getDefaultLanguage(); if (!defaultLanguage) return; + const languages = await this.getPolylangLanguages(); if (manualId) { const manualTranslations: { [key: string]: number } = { [defaultLanguage]: manualId, @@ -402,6 +403,7 @@ export default class RouteItem extends AdminRoute<{ await this.createPolylangTranslations({ id: manualId, translations: manualTranslations, + languages, }); for (const transId of Object.values(manualTranslations)) { await tryber.tables.WpPostmeta.do() @@ -431,6 +433,7 @@ export default class RouteItem extends AdminRoute<{ await this.createPolylangTranslations({ id: previewId, translations: previewTranslations, + languages, }); } } @@ -439,9 +442,11 @@ export default class RouteItem extends AdminRoute<{ private async createPolylangTranslations({ id, translations, + languages, }: { id: number; translations: { [key: string]: number }; + languages: { [key: string]: any }; }) { const term_name = crypto .createHash("md5") @@ -455,7 +460,6 @@ export default class RouteItem extends AdminRoute<{ .returning("term_id"); const term_id = term[0].term_id ?? term[0]; - console.log(term_id); await tryber.tables.WpTermTaxonomy.do().insert({ term_id: term_id, @@ -468,6 +472,17 @@ export default class RouteItem extends AdminRoute<{ object_id: id, term_taxonomy_id: term_id, }); + + for (const trans of Object.keys(translations)) { + const transId = translations[trans]; + const langId = trans in languages ? languages[trans] : false; + if (langId) { + await tryber.tables.WpTermRelationships.do().insert({ + object_id: transId, + term_taxonomy_id: langId, + }); + } + } } private async duplicatePage({ @@ -559,6 +574,24 @@ export default class RouteItem extends AdminRoute<{ return parsedOptions.default_lang; } + private async getPolylangLanguages() { + const languages = await tryber.tables.WpTermTaxonomy.do() + .select("term_id", "description") + .where("taxonomy", "language"); + + let parsedLanguages: { [key: string]: number } = {}; + for (const language of languages) { + try { + const lang = unserialize(language.description); + if ("locale" in lang) + parsedLanguages[lang.locale.split("_")[0]] = language.term_id; + } catch (e) { + continue; + } + } + return parsedLanguages; + } + private async getPageTranslationIds({ pageId, defaultLanguage, From 8cca6d6db798f3a8b6eee3d4a9cf9ab4cdc8ab5f Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 10 May 2024 09:18:00 +0200 Subject: [PATCH 30/39] feat: Set close date on status change --- .../_put/StatusChangeHandler/index.spec.ts | 19 +++++++++++++++++++ .../phases/_put/StatusChangeHandler/index.ts | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts index 9d0b2b706..2ca8b8fb7 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts @@ -97,6 +97,25 @@ describe("StatusChangeHandler", () => { expect(campaign.status_id).toBe(2); }); + it("Should change the close date when changing to a closed phase", async () => { + const handler = new StatusChangeHandler({ + newPhase: 3, + campaignId: 1, + creator: 1, + }); + + await handler.run(); + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("close_date") + .where("id", 1) + .first(); + + if (!campaign) throw new Error("Campaign not found"); + const now = new Date().toISOString().replace("T", " ").split(".")[0]; + expect(campaign.close_date).toBe(now); + }); + it("Should change the status_id when changing from closed phase", async () => { const handler = new StatusChangeHandler({ newPhase: 1, diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts index 7f4f60bea..61b4ec38c 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts @@ -60,7 +60,7 @@ class StatusChangeHandler { private async handleStatusChange(type: string) { if (type === "closed") { await tryber.tables.WpAppqEvdCampaign.do() - .update({ status_id: 2 }) + .update({ status_id: 2, close_date: tryber.fn.now() }) .where("id", this.campaignId); } else { await tryber.tables.WpAppqEvdCampaign.do() From c2dbaacb9b217fce3a0128bfb7d2d46b2dd0d482 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 13 May 2024 12:18:59 +0200 Subject: [PATCH 31/39] feat: Add notes field --- package.json | 2 +- src/reference/openapi.yml | 8 +++++++ src/routes/dossiers/_post/creation.spec.ts | 21 +++++++++++++++++++ src/routes/dossiers/_post/index.ts | 1 + .../dossiers/campaignId/_get/index.spec.ts | 10 +++++++++ src/routes/dossiers/campaignId/_get/index.ts | 4 ++++ src/routes/dossiers/campaignId/_put/index.ts | 1 + .../dossiers/campaignId/_put/update.spec.ts | 15 +++++++++++++ src/schema.ts | 2 ++ yarn.lock | 8 +++---- 10 files changed, 67 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4cd8058dc..9b76a4b0a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.35.0", + "@appquality/tryber-database": "^0.37.0", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 0b409f9e5..17bcc15c9 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9988,6 +9988,10 @@ paths: type: integer name: type: string + notes: + type: string + x-stoplight: + id: nep7qag1smkcp required: - id - title @@ -11231,6 +11235,10 @@ components: type: integer productType: type: integer + notes: + type: string + x-stoplight: + id: xmrb08t4qc2fv required: - project - testType diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 8cd31eb5f..a60d38ae2 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -686,4 +686,25 @@ describe("Route POST /dossiers", () => { name: "App", }); }); + it("Should save the notes in the dossier data", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + notes: "Notes", + }); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty("id"); + + const id = response.body.id; + + const getResponse = await request(app) + .get(`/dossiers/${id}`) + .set("authorization", "Bearer admin"); + + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty("notes", "Notes"); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index b92cfc788..1a1c0c21a 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -184,6 +184,7 @@ export default class RouteItem extends AdminRoute<{ target_devices: this.getBody().deviceRequirements, created_by: this.getTesterId(), updated_by: this.getTesterId(), + notes: this.getBody().notes, }) .returning("id"); diff --git a/src/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts index c2760782e..f779f37ea 100644 --- a/src/routes/dossiers/campaignId/_get/index.spec.ts +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -297,6 +297,7 @@ describe("Route GET /dossiers/:id", () => { created_by: 100, updated_by: 100, product_type_id: 1, + notes: "Notes", }); await tryber.tables.CampaignDossierDataCountries.do().insert([ { @@ -511,6 +512,15 @@ describe("Route GET /dossiers/:id", () => { expect(response.body.productType).toHaveProperty("name", "Test Product"); }); + it("Should return notes", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("notes", "Notes"); + }); + it("Should return id", async () => { const response = await request(app) .get("/dossiers/1") diff --git a/src/routes/dossiers/campaignId/_get/index.ts b/src/routes/dossiers/campaignId/_get/index.ts index 622d59008..c3474e466 100644 --- a/src/routes/dossiers/campaignId/_get/index.ts +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -135,6 +135,7 @@ export default class RouteItem extends UserRoute<{ "target_size", "target_devices", "product_type_id", + "notes", tryber.ref("name").withSchema("product_types").as("product_type_name") ) .leftJoin( @@ -292,6 +293,9 @@ export default class RouteItem extends UserRoute<{ ...(this.campaign.target_devices && { deviceRequirements: this.campaign.target_devices, }), + ...(this.campaign.notes && { + notes: this.campaign.notes, + }), ...(this.campaign.countries.length > 0 && { countries: this.campaign.countries?.map((item) => item.country_code), }), diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index 1b3eca39e..277473b3c 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -171,6 +171,7 @@ export default class RouteItem extends UserRoute<{ }), product_type_id: this.getBody().productType, target_devices: this.getBody().deviceRequirements, + notes: this.getBody().notes, updated_by: this.getTesterId(), }) .where({ diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 889a88727..9528c193e 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -621,6 +621,21 @@ describe("Route POST /dossiers", () => { name: "Test Product", }); }); + it("Should update the notes in the dossier data", async () => { + await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + notes: "Notes", + }); + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("notes", "Notes"); + }); }); describe("Role handling", () => { diff --git a/src/schema.ts b/src/schema.ts index 2044b0aa4..f2ad1ddae 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -932,6 +932,7 @@ export interface components { languages?: number[]; browsers?: number[]; productType?: number; + notes?: string; }; }; responses: { @@ -4205,6 +4206,7 @@ export interface operations { id: number; name: string; }; + notes?: string; }; }; }; diff --git a/yarn.lock b/yarn.lock index fc3d3dde5..a8520b4a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.35.0": - version "0.35.0" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.35.0.tgz#16b2d0c7049e6f91a9378e546ad51a4b79c62884" - integrity sha512-8p/vqGjOqmtPcupPAK6VJEYdd2eXN+E9u5vTtVpdGDhsg/6QUZQqIXVfOP+2e+dx6GRvDyMrvY+uQmAZisHHDw== +"@appquality/tryber-database@^0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.37.0.tgz#cf68eeae7f26f3089c83dc7e657cd3369db4452d" + integrity sha512-wizquFsjXWPzDe3xYfC8MSzzuX4tSQpdO+4sLBsGTekaf0N4gwfzn9b0xrjq16aiQSAr84Y01elgiiJWC5J9+w== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From 2bc97fe154fbcc083d266efc331a7e4a260b26ba Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 13 May 2024 16:43:32 +0200 Subject: [PATCH 32/39] feat: Add phase history --- src/routes/dossiers/_post/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 1a1c0c21a..3bc4184ad 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -190,6 +190,12 @@ export default class RouteItem extends AdminRoute<{ const dossierId = dossier[0].id ?? dossier[0]; + await tryber.tables.CampaignPhaseHistory.do().insert({ + campaign_id: campaignId, + phase_id: 1, + created_by: this.getTesterId(), + }); + const countries = this.getBody().countries; if (countries?.length) { await tryber.tables.CampaignDossierDataCountries.do().insert( From 4c7545e10bb043dc25ffa546b572dfdc1b5cc631 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 14 May 2024 14:47:03 +0200 Subject: [PATCH 33/39] feat: Add zapier triggers --- .env.template | 6 ++- deployment/after-install.sh | 3 ++ src/features/webhookTrigger/index.ts | 49 +++++++++++++++++++ src/routes/dossiers/_post/index.ts | 9 ++++ .../phases/_put/StatusChangeHandler/index.ts | 16 ++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/features/webhookTrigger/index.ts diff --git a/.env.template b/.env.template index 821bd205e..92e99255a 100644 --- a/.env.template +++ b/.env.template @@ -54,4 +54,8 @@ GOOGLE_API_KEY=AIzaserejejadejedejebetudejeberesebiuno PAYMENT_INVOICE_RECAP_CC_EMAIL=it+administration@unguess.io -SENTRY_ENVIRONMENT=local \ No newline at end of file +SENTRY_ENVIRONMENT=local + + +CAMPAIGN_CREATION_WEBHOOK=https://webhook.site/11111111-1111-1111-1111-11111111111 +STATUS_CHANGE_WEBHOOK=https://webhook.site/11111111-1111-1111-1111-11111111111 \ No newline at end of file diff --git a/deployment/after-install.sh b/deployment/after-install.sh index ce148178a..12b06657a 100644 --- a/deployment/after-install.sh +++ b/deployment/after-install.sh @@ -81,12 +81,15 @@ services: GOOGLE_API_KEY: '${GOOGLE_API_KEY}' PAYMENT_INVOICE_RECAP_CC_EMAIL: '${PAYMENT_INVOICE_RECAP_CC_EMAIL}' SENTRY_ENVIRONMENT: ${ENVIRONMENT} + ENVIRONMENT: ${ENVIRONMENT} SENTRY_RELEASE: ${DOCKER_IMAGE} SENTRY_DSN: ${SENTRY_DSN} SENTRY_SAMPLE_RATE: ${SENTRY_SAMPLE_RATE:-1} CLOUDFRONT_KEY_ID: ${CLOUDFRONT_KEY_ID} JOTFORM_APIKEY: ${JOTFORM_APIKEY} WORDPRESS_API_URL: ${WORDPRESS_API_URL} + CAMPAIGN_CREATION_WEBHOOK: ${CAMPAIGN_CREATION_WEBHOOK} + STATUS_CHANGE_WEBHOOK: ${STATUS_CHANGE_WEBHOOK} volumes: - /var/docker/keys:/app/keys logging: diff --git a/src/features/webhookTrigger/index.ts b/src/features/webhookTrigger/index.ts new file mode 100644 index 000000000..83373f870 --- /dev/null +++ b/src/features/webhookTrigger/index.ts @@ -0,0 +1,49 @@ +import axios from "axios"; + +type StatusChangeWebhook = { + type: "status_change"; + data: { + campaignId: number; + newPhase: number; + oldPhase: number; + }; +}; + +type CampaignCreatedWebhook = { + type: "campaign_created"; + data: { + campaignId: number; + }; +}; + +type WebhookTypes = StatusChangeWebhook | CampaignCreatedWebhook; + +export class WebhookTrigger { + private webhookUrl: string; + private data: WebhookTypes["data"]; + + constructor({ + type, + data, + }: { + type: T; + data: Extract["data"]; + }) { + if (type === "status_change") { + this.webhookUrl = process.env.STATUS_CHANGE_WEBHOOK_URL || ""; + } else if (type === "campaign_created") { + this.webhookUrl = process.env.CAMPAIGN_CREATED_WEBHOOK_URL || ""; + } else { + throw new Error("Invalid webhook type"); + } + + this.data = data; + } + + async trigger() { + await axios.post(this.webhookUrl, { + ...this.data, + environment: process.env.ENVIROMENT, + }); + } +} diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 3bc4184ad..a530d0c7d 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -3,6 +3,7 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import AdminRoute from "@src/features/routes/AdminRoute"; +import { WebhookTrigger } from "@src/features/webhookTrigger"; import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; import crypto from "crypto"; import { serialize } from "php-serialize"; @@ -134,6 +135,14 @@ export default class RouteItem extends AdminRoute<{ await this.generateLinkedData(campaignId); + const webhook = new WebhookTrigger({ + type: "campaign_created", + data: { + campaignId, + }, + }); + await webhook.trigger(); + this.setSuccess(201, { id: campaignId, }); diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts index 61b4ec38c..bea5f732f 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts @@ -1,4 +1,5 @@ import { tryber } from "@src/features/database"; +import { WebhookTrigger } from "@src/features/webhookTrigger"; class StatusChangeHandler { private oldPhase: number = 0; @@ -67,6 +68,21 @@ class StatusChangeHandler { .update({ status_id: 1 }) .where("id", this.campaignId); } + + await this.triggerWebhook(); + } + + private async triggerWebhook() { + const webhook = new WebhookTrigger({ + type: "status_change", + data: { + campaignId: this.campaignId, + oldPhase: this.oldPhase, + newPhase: this.newPhase, + }, + }); + + await webhook.trigger(); } } From a8edecd192a6105422fe39e73a1da5a7bf37a7b2 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 14 May 2024 15:28:57 +0200 Subject: [PATCH 34/39] feat: Allow filterby role empty --- .../campaigns/_get/filterByRole.spec.ts | 27 +++++++++++++++++ src/routes/campaigns/_get/index.ts | 30 +++++++++++++------ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/routes/campaigns/_get/filterByRole.spec.ts b/src/routes/campaigns/_get/filterByRole.spec.ts index f56da92dc..79fc7b911 100644 --- a/src/routes/campaigns/_get/filterByRole.spec.ts +++ b/src/routes/campaigns/_get/filterByRole.spec.ts @@ -51,6 +51,7 @@ describe("GET /campaigns", () => { { ...campaign, id: 2, title: "Second campaign" }, { ...campaign, id: 3, title: "Third campaign" }, { ...campaign, id: 4, title: "Fourth campaign" }, + { ...campaign, id: 5, title: "Fifth campaign" }, ]); await tryber.tables.CustomRoles.do().insert([ @@ -62,6 +63,7 @@ describe("GET /campaigns", () => { { campaign_id: 1, custom_role_id: 1, tester_id: 1 }, { campaign_id: 2, custom_role_id: 1, tester_id: 2 }, { campaign_id: 3, custom_role_id: 1, tester_id: 1 }, + { campaign_id: 5, custom_role_id: 2, tester_id: 1 }, ]); }); afterAll(async () => { @@ -122,4 +124,29 @@ describe("GET /campaigns", () => { ]) ); }); + it("Should return all campaigns without the role assigned if filterBy[role_1]=empty", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=empty") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(2); + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 4, name: "Fourth campaign" }), + expect.objectContaining({ id: 5, name: "Fifth campaign" }), + ]) + ); + }); + it("Should return allow filtering by multiple roles", async () => { + const response = await request(app) + .get("/campaigns?filterBy[role_1]=empty&filterBy[role_2]=1") + .set("Authorization", 'Bearer tester olp {"appq_campaign":true}'); + expect(response.status).toBe(200); + expect(response.body.items).toHaveLength(1); + expect(response.body.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 5, name: "Fifth campaign" }), + ]) + ); + }); }); diff --git a/src/routes/campaigns/_get/index.ts b/src/routes/campaigns/_get/index.ts index b91818594..ddcd6b0e3 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -52,7 +52,7 @@ class RouteItem extends UserRoute<{ csm?: number; roles?: { id: number; - value: number[]; + value: number[] | "empty"; }[]; phase?: number; } = {}; @@ -120,7 +120,10 @@ class RouteItem extends UserRoute<{ if (roles.length) { this.filterBy.roles = roles.map(([key, value]) => ({ id: parseInt(key.split("_")[1]), - value: value.split(",").map((id: string) => parseInt(id)), + value: + value === "empty" + ? "empty" + : value.split(",").map((id: string) => parseInt(id)), })); } @@ -459,13 +462,22 @@ class RouteItem extends UserRoute<{ if (this.filterBy.roles) { this.filterBy.roles.forEach((role) => { - query = query.whereIn( - "wp_appq_evd_campaign.id", - tryber.tables.CampaignCustomRoles.do() - .select("campaign_id") - .where("custom_role_id", role.id) - .whereIn("tester_id", role.value) - ); + if (role.value === "empty") { + query = query.whereNotIn( + "wp_appq_evd_campaign.id", + tryber.tables.CampaignCustomRoles.do() + .select("campaign_id") + .where("custom_role_id", role.id) + ); + } else { + query = query.whereIn( + "wp_appq_evd_campaign.id", + tryber.tables.CampaignCustomRoles.do() + .select("campaign_id") + .where("custom_role_id", role.id) + .whereIn("tester_id", role.value) + ); + } }); } From a0d4d466fa960133b2590e4045b2e2380ffd45b3 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 14 May 2024 15:56:58 +0200 Subject: [PATCH 35/39] test: Mock zap trigger --- .../webhookTrigger/__mocks__/index.ts | 7 ++++++ src/features/webhookTrigger/index.ts | 23 ++++--------------- src/features/webhookTrigger/types.d.ts | 21 +++++++++++++++++ src/routes/dossiers/_post/creation.spec.ts | 1 + src/routes/dossiers/_post/duplication.spec.ts | 1 + src/routes/dossiers/_post/index.spec.ts | 1 + src/routes/dossiers/_post/triggerWp.spec.ts | 1 + .../_put/StatusChangeHandler/index.spec.ts | 1 + .../campaignId/phases/_put/index.spec.ts | 1 + 9 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 src/features/webhookTrigger/__mocks__/index.ts create mode 100644 src/features/webhookTrigger/types.d.ts diff --git a/src/features/webhookTrigger/__mocks__/index.ts b/src/features/webhookTrigger/__mocks__/index.ts new file mode 100644 index 000000000..8353ecf2b --- /dev/null +++ b/src/features/webhookTrigger/__mocks__/index.ts @@ -0,0 +1,7 @@ +import { iWebhookTrigger } from "../types"; + +export class WebhookTrigger implements iWebhookTrigger { + async trigger() { + return; + } +} diff --git a/src/features/webhookTrigger/index.ts b/src/features/webhookTrigger/index.ts index 83373f870..6568a9089 100644 --- a/src/features/webhookTrigger/index.ts +++ b/src/features/webhookTrigger/index.ts @@ -1,24 +1,9 @@ import axios from "axios"; +import { WebhookTypes, iWebhookTrigger } from "./types"; -type StatusChangeWebhook = { - type: "status_change"; - data: { - campaignId: number; - newPhase: number; - oldPhase: number; - }; -}; - -type CampaignCreatedWebhook = { - type: "campaign_created"; - data: { - campaignId: number; - }; -}; - -type WebhookTypes = StatusChangeWebhook | CampaignCreatedWebhook; - -export class WebhookTrigger { +export class WebhookTrigger + implements iWebhookTrigger +{ private webhookUrl: string; private data: WebhookTypes["data"]; diff --git a/src/features/webhookTrigger/types.d.ts b/src/features/webhookTrigger/types.d.ts new file mode 100644 index 000000000..a5fe49d50 --- /dev/null +++ b/src/features/webhookTrigger/types.d.ts @@ -0,0 +1,21 @@ +type StatusChangeWebhook = { + type: "status_change"; + data: { + campaignId: number; + newPhase: number; + oldPhase: number; + }; +}; + +type CampaignCreatedWebhook = { + type: "campaign_created"; + data: { + campaignId: number; + }; +}; + +export type WebhookTypes = StatusChangeWebhook | CampaignCreatedWebhook; + +export interface iWebhookTrigger { + trigger(): Promise; +} diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index a60d38ae2..fbf87febb 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -3,6 +3,7 @@ import { tryber } from "@src/features/database"; import request from "supertest"; jest.mock("@src/features/wp/WordpressJsonApiTrigger"); +jest.mock("@src/features/webhookTrigger"); const baseRequest = { project: 10, testType: 10, diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts index d398d59a6..241961856 100644 --- a/src/routes/dossiers/_post/duplication.spec.ts +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -4,6 +4,7 @@ import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; import request from "supertest"; jest.mock("@src/features/wp/WordpressJsonApiTrigger"); +jest.mock("@src/features/webhookTrigger"); const baseRequest = { project: 1, diff --git a/src/routes/dossiers/_post/index.spec.ts b/src/routes/dossiers/_post/index.spec.ts index a3465aa61..e56e61d20 100644 --- a/src/routes/dossiers/_post/index.spec.ts +++ b/src/routes/dossiers/_post/index.spec.ts @@ -3,6 +3,7 @@ import { tryber } from "@src/features/database"; import request from "supertest"; jest.mock("@src/features/wp/WordpressJsonApiTrigger"); +jest.mock("@src/features/webhookTrigger"); const baseRequest = { project: 1, testType: 1, diff --git a/src/routes/dossiers/_post/triggerWp.spec.ts b/src/routes/dossiers/_post/triggerWp.spec.ts index 53710322c..022df8269 100644 --- a/src/routes/dossiers/_post/triggerWp.spec.ts +++ b/src/routes/dossiers/_post/triggerWp.spec.ts @@ -4,6 +4,7 @@ import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; import request from "supertest"; jest.mock("@src/features/wp/WordpressJsonApiTrigger"); +jest.mock("@src/features/webhookTrigger"); const baseRequest = { project: 1, diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts index 2ca8b8fb7..cabb1171e 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts @@ -1,6 +1,7 @@ import { tryber } from "@src/features/database"; import { StatusChangeHandler } from "."; +jest.mock("@src/features/webhookTrigger"); describe("StatusChangeHandler", () => { beforeAll(async () => { const campaign = { diff --git a/src/routes/dossiers/campaignId/phases/_put/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/index.spec.ts index a869f5ccc..63d82330d 100644 --- a/src/routes/dossiers/campaignId/phases/_put/index.spec.ts +++ b/src/routes/dossiers/campaignId/phases/_put/index.spec.ts @@ -4,6 +4,7 @@ import { StatusChangeHandler } from "@src/routes/dossiers/campaignId/phases/_put import request from "supertest"; jest.mock("@src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler"); +jest.mock("@src/features/webhookTrigger"); describe("Route PUT /dossiers/:id/phases", () => { beforeAll(async () => { From c09f3a61b214e6cc6d2dbda573d32a52d20d56c7 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 15 May 2024 12:15:18 +0200 Subject: [PATCH 36/39] fix: Use correct webhook env --- src/features/webhookTrigger/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/webhookTrigger/index.ts b/src/features/webhookTrigger/index.ts index 6568a9089..c9d026fd4 100644 --- a/src/features/webhookTrigger/index.ts +++ b/src/features/webhookTrigger/index.ts @@ -17,7 +17,7 @@ export class WebhookTrigger if (type === "status_change") { this.webhookUrl = process.env.STATUS_CHANGE_WEBHOOK_URL || ""; } else if (type === "campaign_created") { - this.webhookUrl = process.env.CAMPAIGN_CREATED_WEBHOOK_URL || ""; + this.webhookUrl = process.env.CAMPAIGN_CREATION_WEBHOOK || ""; } else { throw new Error("Invalid webhook type"); } From a1899b1200cab5ce9fa5b8ac024fb09f49192939 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 15 May 2024 12:48:50 +0200 Subject: [PATCH 37/39] fix: Allow changing csm --- src/routes/dossiers/campaignId/_put/index.ts | 2 +- .../dossiers/campaignId/_put/update.spec.ts | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/routes/dossiers/campaignId/_put/index.ts b/src/routes/dossiers/campaignId/_put/index.ts index 277473b3c..07e840032 100644 --- a/src/routes/dossiers/campaignId/_put/index.ts +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -122,7 +122,7 @@ export default class RouteItem extends UserRoute<{ start_date: this.getBody().startDate, end_date: this.getEndDate(), close_date: this.getCloseDate(), - pm_id: this.getTesterId(), + pm_id: this.getBody().csm ?? this.getTesterId(), project_id: this.getBody().project, campaign_type_id: this.getBody().testType, customer_title: this.getBody().title.customer, diff --git a/src/routes/dossiers/campaignId/_put/update.spec.ts b/src/routes/dossiers/campaignId/_put/update.spec.ts index 9528c193e..8f2c68d90 100644 --- a/src/routes/dossiers/campaignId/_put/update.spec.ts +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -70,14 +70,26 @@ describe("Route POST /dossiers", () => { }, ]); - await tryber.tables.WpAppqEvdProfile.do().insert({ - id: 1, - wp_user_id: 1, - name: "Test User", + const user = { email: "", education_id: 1, employment_id: 1, - }); + }; + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + ...user, + id: 1, + wp_user_id: 1, + name: "Test User", + }, + { + ...user, + id: 2, + wp_user_id: 2, + name: "Other", + surname: "User", + }, + ]); await tryber.tables.WpAppqLang.do().insert([ { id: 1, display_name: "Test Language", lang_code: "TL" }, @@ -168,6 +180,31 @@ describe("Route POST /dossiers", () => { expect(campaign).toHaveProperty("campaign_type_id", 11); }); + it("Should update the campaign with the specified csm", async () => { + const response = await request(app) + .put("/dossiers/1") + .set("authorization", "Bearer admin") + .send({ + ...baseRequest, + csm: 2, + }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("id"); + const id = response.body.id; + + const responseGet = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + + expect(responseGet.status).toBe(200); + expect(responseGet.body).toHaveProperty("csm"); + expect(responseGet.body.csm).toEqual({ + id: 2, + name: "Other User", + }); + }); + it("Should update the campaign with the specified title", async () => { const response = await request(app) .put("/dossiers/1") @@ -640,16 +677,6 @@ describe("Route POST /dossiers", () => { describe("Role handling", () => { beforeAll(async () => { - await tryber.tables.WpAppqEvdProfile.do().insert({ - id: 2, - wp_user_id: 2, - name: "Test User", - surname: "Test Surname", - education_id: 1, - employment_id: 1, - email: "", - }); - await tryber.tables.CustomRoles.do().insert([ { id: 1, @@ -664,9 +691,7 @@ describe("Route POST /dossiers", () => { ]); }); afterAll(async () => { - await tryber.tables.WpAppqEvdProfile.do().delete(); await tryber.tables.CustomRoles.do().delete(); - await tryber.tables.WpAppqEvdProfile.do().delete(); }); afterEach(async () => { await tryber.tables.CampaignCustomRoles.do().delete(); From 03d4af7ba3b0a5288c09c1472e470f101ef065c6 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 17 May 2024 10:39:17 +0200 Subject: [PATCH 38/39] feat: Update legacy status details --- package.json | 2 +- .../_put/StatusChangeHandler/index.spec.ts | 24 ++++++++++++++++--- .../phases/_put/StatusChangeHandler/index.ts | 14 +++++++++++ yarn.lock | 8 +++---- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 9b76a4b0a..16b6631eb 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.37.0", + "@appquality/tryber-database": "^0.39.0", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts index cabb1171e..f9149b3ab 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts @@ -32,9 +32,9 @@ describe("StatusChangeHandler", () => { }, ]); await tryber.tables.CampaignPhase.do().insert([ - { id: 1, name: "Draft", type_id: 1 }, - { id: 2, name: "Running", type_id: 2 }, - { id: 3, name: "Closed", type_id: 3 }, + { id: 1, name: "Draft", type_id: 1, status_details: "Planned" }, + { id: 2, name: "Running", type_id: 2, status_details: "Running" }, + { id: 3, name: "Closed", type_id: 3, status_details: "Successful" }, ]); await tryber.tables.CampaignPhaseType.do().insert([ @@ -134,4 +134,22 @@ describe("StatusChangeHandler", () => { if (!campaign) throw new Error("Campaign not found"); expect(campaign.status_id).toBe(1); }); + + it("Should change the status details of the campaign", async () => { + const handler = new StatusChangeHandler({ + newPhase: 1, + campaignId: 2, + creator: 1, + }); + + await handler.run(); + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("status_details") + .where("id", 2) + .first(); + + if (!campaign) throw new Error("Campaign not found"); + expect(campaign.status_details).toBe("Planned"); + }); }); diff --git a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts index bea5f732f..4adae728f 100644 --- a/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts @@ -37,6 +37,8 @@ class StatusChangeHandler { await this.handleStatusChange(type.name); + await this.legacySetStatusDetails(); + await this.saveHistory(); console.log("Status changed from", this.oldPhase, "to", this.newPhase); } @@ -71,6 +73,18 @@ class StatusChangeHandler { await this.triggerWebhook(); } + private async legacySetStatusDetails() { + const phase = await tryber.tables.CampaignPhase.do() + .select("status_details") + .where("id", this.newPhase) + .first(); + + if (!phase || !phase.status_details) return; + + await tryber.tables.WpAppqEvdCampaign.do() + .update({ status_details: phase.status_details }) + .where("id", this.campaignId); + } private async triggerWebhook() { const webhook = new WebhookTrigger({ diff --git a/yarn.lock b/yarn.lock index a8520b4a2..4e2f22b05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.37.0.tgz#cf68eeae7f26f3089c83dc7e657cd3369db4452d" - integrity sha512-wizquFsjXWPzDe3xYfC8MSzzuX4tSQpdO+4sLBsGTekaf0N4gwfzn9b0xrjq16aiQSAr84Y01elgiiJWC5J9+w== +"@appquality/tryber-database@^0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.39.0.tgz#b0b0e4a6c6d456f5cc7d6b9d0348e60795fac8ea" + integrity sha512-SQaETcgQml5qBhkJUJNRdk+POjQx8zRLTNPnRlR5/ZUdN6qzsrhUfmP+2mA1SHx0AIqP4Gfh2hKV1QJZj7FV/w== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From 4d10b25128c807e7aaa20e2e2221ba11f2a79310 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 17 May 2024 15:13:34 +0200 Subject: [PATCH 39/39] test: Update phase ids --- src/routes/users/me/campaigns/_get/filters.spec.ts | 2 +- src/routes/users/me/campaigns/_get/index.spec.ts | 4 ++-- src/routes/users/me/campaigns/_get/manualpreview.spec.ts | 2 +- src/routes/users/me/campaigns/_get/order.spec.ts | 2 +- src/routes/users/me/campaigns/_get/pagination.spec.ts | 2 +- .../users/me/campaigns/campaignId/_get/index.spec.ts | 2 +- .../users/me/campaigns/campaignId/_get/useBasicData.ts | 4 ++-- .../me/campaigns/campaignId/bugs/_post/useBasicData.ts | 8 ++++---- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/routes/users/me/campaigns/_get/filters.spec.ts b/src/routes/users/me/campaigns/_get/filters.spec.ts index caf69d36f..323f3c911 100644 --- a/src/routes/users/me/campaigns/_get/filters.spec.ts +++ b/src/routes/users/me/campaigns/_get/filters.spec.ts @@ -48,7 +48,7 @@ describe("GET /users/me/campaigns - filters", () => { pm_id: 1, project_id: 1, customer_title: "Customer title", - phase_id: 10, + phase_id: 20, }; await tryber.tables.WpAppqCampaignType.do().insert({ id: 1, diff --git a/src/routes/users/me/campaigns/_get/index.spec.ts b/src/routes/users/me/campaigns/_get/index.spec.ts index dd58f4ac1..b77961f4c 100644 --- a/src/routes/users/me/campaigns/_get/index.spec.ts +++ b/src/routes/users/me/campaigns/_get/index.spec.ts @@ -46,14 +46,14 @@ describe("GET /users/me/campaigns", () => { id: 1, title: "Public campaign", is_public: 1, - phase_id: 10, + phase_id: 20, }, { ...campaign, id: 2, title: "Small Group campaign", is_public: 0, - phase_id: 10, + phase_id: 20, }, { ...campaign, diff --git a/src/routes/users/me/campaigns/_get/manualpreview.spec.ts b/src/routes/users/me/campaigns/_get/manualpreview.spec.ts index 5721916c4..5032688c0 100644 --- a/src/routes/users/me/campaigns/_get/manualpreview.spec.ts +++ b/src/routes/users/me/campaigns/_get/manualpreview.spec.ts @@ -27,7 +27,7 @@ describe("GET /users/me/campaigns ", () => { platform_id: 1, customer_id: 1, project_id: 1, - phase_id: 10, + phase_id: 20, customer_title: "Customer title", }; diff --git a/src/routes/users/me/campaigns/_get/order.spec.ts b/src/routes/users/me/campaigns/_get/order.spec.ts index 11d968b07..a13702e54 100644 --- a/src/routes/users/me/campaigns/_get/order.spec.ts +++ b/src/routes/users/me/campaigns/_get/order.spec.ts @@ -38,7 +38,7 @@ describe("GET /users/me/campaigns ", () => { customer_id: 1, project_id: 1, customer_title: "Customer title", - phase_id: 10, + phase_id: 20, }; campaignTypes.insert({ id: 1, diff --git a/src/routes/users/me/campaigns/_get/pagination.spec.ts b/src/routes/users/me/campaigns/_get/pagination.spec.ts index a5e3ff468..bd9958be6 100644 --- a/src/routes/users/me/campaigns/_get/pagination.spec.ts +++ b/src/routes/users/me/campaigns/_get/pagination.spec.ts @@ -37,7 +37,7 @@ describe("GET /users/me/campaigns - pagination ", () => { customer_id: 1, project_id: 1, customer_title: "Customer title", - phase_id: 10, + phase_id: 20, }; await tryber.tables.WpAppqEvdCampaign.do().insert([ { diff --git a/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts b/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts index 252cc6108..0747ebb52 100644 --- a/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts +++ b/src/routes/users/me/campaigns/campaignId/_get/index.spec.ts @@ -239,7 +239,7 @@ describe("Route GET /users/me/campaigns/{campaignId}/ - bug language set", () => pm_id: 1, project_id: 1, customer_title: "My campaign", - phase_id: 10, + phase_id: 20, }); await tryber.tables.WpAppqCpMeta.do().insert([ { diff --git a/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts b/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts index 8413b8a82..120910061 100644 --- a/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts +++ b/src/routes/users/me/campaigns/campaignId/_get/useBasicData.ts @@ -209,7 +209,7 @@ const useBasicData = () => { pm_id: 1, project_id: 1, customer_title: "My campaign", - phase_id: 10, + phase_id: 20, }, { id: 2, @@ -225,7 +225,7 @@ const useBasicData = () => { pm_id: 1, project_id: 1, customer_title: "My campaign", - phase_id: 10, + phase_id: 20, }, ]); await tryber.tables.WpOptions.do().insert({ diff --git a/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts b/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts index 25a12a8fb..d7c11ff0f 100644 --- a/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts +++ b/src/routes/users/me/campaigns/campaignId/bugs/_post/useBasicData.ts @@ -27,7 +27,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, - phase_id: 10, + phase_id: 20, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ selected_device: 1, @@ -78,7 +78,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, - phase_id: 10, + phase_id: 20, }); await tryber.tables.WpAppqEvdCampaign.do().insert({ @@ -95,7 +95,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, - phase_id: 10, + phase_id: 20, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ @@ -152,7 +152,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, - phase_id: 10, + phase_id: 20, }); await tryber.tables.WpAppqCampaignTask.do().insert({