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/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/deployment/after-install.sh b/deployment/after-install.sh index 952cbd89e..12b06657a 100644 --- a/deployment/after-install.sh +++ b/deployment/after-install.sh @@ -81,11 +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/package.json b/package.json index c508e1f40..16b6631eb 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.27.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", @@ -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,6 +44,7 @@ "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", 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 new file mode 100644 index 000000000..c9d026fd4 --- /dev/null +++ b/src/features/webhookTrigger/index.ts @@ -0,0 +1,34 @@ +import axios from "axios"; +import { WebhookTypes, iWebhookTrigger } from "./types"; + +export class WebhookTrigger + implements iWebhookTrigger +{ + 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_CREATION_WEBHOOK || ""; + } 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/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/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 f3e923e4f..17bcc15c9 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -719,6 +719,47 @@ paths: type: string required: - 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 + - surname + properties: + id: + type: integer + name: + type: string + surname: + type: string + required: + - role + - user - $ref: '#/components/schemas/PaginationData' '403': $ref: '#/components/responses/NotAuthorized' @@ -1176,8 +1217,6 @@ paths: type: string metal: type: string - x-stoplight: - id: 9afe0zlf0hr2b devices: type: array items: @@ -4138,6 +4177,36 @@ paths: security: - JWT: [] parameters: [] + post: + summary: '' + operationId: post-customers + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + required: + - id + - name + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name /custom_user_fields: get: summary: Get all custom user fields @@ -4237,6 +4306,12 @@ paths: type: integer name: type: string + type: + type: string + required: + - id + - name + - type '403': $ref: '#/components/responses/NotAuthorized' '404': @@ -9665,166 +9740,738 @@ paths: operationId: get-users-me-rank-list security: - JWT: [] -components: - schemas: - AdditionalField: - description: '' - type: object - x-examples: - example-1: - field_id: 12 - name: My field name - value: My field value - properties: - field_id: - type: number - name: - type: string - minLength: 1 - value: - type: string - text: - type: string - is_candidate: - type: boolean - required: - - field_id - - name - - value - Agreement: - type: object - x-examples: - Example 1: - id: 1 - title: Agreement Title - tokens: 10.5 - unitPrice: 165 - expirationDate: '2023-06-06 12:59:16' - startDate: '2023-06-06 12:59:16' - note: Agreement Notes - customer: - id: 1 - company: Customer Company - isTokenBased: true - properties: - title: - type: string - minLength: 1 - tokens: - type: number - unitPrice: - type: number - startDate: - type: string - pattern: '^[0-9]{4}-(0[0-9]|1[0-2])-([0-2][0-9]|3[0-1])$' - expirationDate: - type: string - pattern: '^[0-9]{4}-(0[0-9]|1[0-2])-([0-2][0-9]|3[0-1])$' - note: - type: string - isTokenBased: - type: boolean - default: false - required: - - title - - tokens - - unitPrice - - startDate - - expirationDate - Bug: - title: Bug - type: object - x-examples: {} - properties: - severity: - $ref: '#/components/schemas/BugSeverity' - status: - $ref: '#/components/schemas/BugStatus' - campaign: - allOf: - - $ref: '#/components/schemas/CampaignOptional' - - type: object - properties: - id: - type: integer - title: - type: string - BugSeverity: - title: BugSeverity - type: object - properties: - id: - type: integer - name: - type: string - BugStatus: - title: BugStatus - type: object - properties: - id: - type: integer - name: - type: string - description: - type: string - BugTag: - title: BugTag - type: object - properties: - id: - type: integer - name: + /dossiers: + parameters: [] + post: + summary: '' + operationId: post-dossiers + responses: + '201': + description: Created + content: + application/json: + schema: + type: object + properties: + id: + type: integer + examples: + Example 1: + value: + id: 1 + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + 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 + in: path + required: true + schema: type: string - required: - - id - - name - BugType: - title: BugType - type: object - properties: - id: - type: integer - Campaign: + description: A campaign id + put: + summary: '' + operationId: put-dossiers-campaign + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: {} description: '' - x-examples: {} - allOf: - - $ref: '#/components/schemas/CampaignOptional' - - $ref: '#/components/schemas/CampaignRequired' - CampaignAdditionalField: - title: CampaignAdditionalField - allOf: - - type: object - properties: - name: - type: string - slug: - type: string - error: - type: string - required: - - name - - slug - - error - - oneOf: - - type: object - properties: - type: - type: string - enum: - - select - options: - type: array - items: - type: string - required: - - type - - options - - type: object - properties: - type: - type: string - enum: - - text + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DossierCreationData' + get: + summary: '' + operationId: get-dossiers-campaign + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: integer + title: + type: object + required: + - customer + - tester + 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 + customer: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + project: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + testType: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + deviceList: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + required: + - id + - name + csm: + type: object + required: + - id + - name + properties: + id: + type: integer + name: + type: string + 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 + 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 + 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 + notes: + type: string + x-stoplight: + id: nep7qag1smkcp + required: + - id + - title + - startDate + - endDate + - closeDate + - customer + - project + - testType + - deviceList + - csm + - phase + 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': + 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 + items: + type: object + properties: + id: + type: integer + name: + type: string + required: + - id + - name + required: + - results + operationId: get-customers-customer-projects + description: '' + security: + - JWT: [] + post: + summary: '' + operationId: post-customers-customer-projects + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: number + name: + type: string + required: + - id + - name + security: + - JWT: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + required: + - name + '/users/by-role/{role}': + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + surname: + type: string + 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 + items: + type: object + properties: + id: + type: integer + name: + type: string + 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 + items: + type: object + properties: + id: + type: integer + name: + type: string + 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: [] + /phases: + get: + summary: Your GET endpoint + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + 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 + 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 + name: + type: string + required: + - id + - name + operationId: put-dossiers-campaign-phases + requestBody: + content: + application/json: + schema: + type: object + properties: + phase: + type: integer + required: + - phase + security: + - JWT: [] +components: + schemas: + AdditionalField: + description: '' + type: object + x-examples: + example-1: + field_id: 12 + name: My field name + value: My field value + properties: + field_id: + type: number + name: + type: string + minLength: 1 + value: + type: string + text: + type: string + is_candidate: + type: boolean + required: + - field_id + - name + - value + Agreement: + type: object + x-examples: + Example 1: + id: 1 + title: Agreement Title + tokens: 10.5 + unitPrice: 165 + expirationDate: '2023-06-06 12:59:16' + startDate: '2023-06-06 12:59:16' + note: Agreement Notes + customer: + id: 1 + company: Customer Company + isTokenBased: true + properties: + title: + type: string + minLength: 1 + tokens: + type: number + unitPrice: + type: number + startDate: + type: string + pattern: '^[0-9]{4}-(0[0-9]|1[0-2])-([0-2][0-9]|3[0-1])$' + expirationDate: + type: string + pattern: '^[0-9]{4}-(0[0-9]|1[0-2])-([0-2][0-9]|3[0-1])$' + note: + type: string + isTokenBased: + type: boolean + default: false + required: + - title + - tokens + - unitPrice + - startDate + - expirationDate + Bug: + title: Bug + type: object + x-examples: {} + properties: + severity: + $ref: '#/components/schemas/BugSeverity' + status: + $ref: '#/components/schemas/BugStatus' + campaign: + allOf: + - $ref: '#/components/schemas/CampaignOptional' + - type: object + properties: + id: + type: integer + title: + type: string + BugSeverity: + title: BugSeverity + type: object + properties: + id: + type: integer + name: + type: string + BugStatus: + title: BugStatus + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + BugTag: + title: BugTag + type: object + properties: + id: + type: integer + name: + type: string + required: + - id + - name + BugType: + title: BugType + type: object + properties: + id: + type: integer + Campaign: + description: '' + x-examples: {} + allOf: + - $ref: '#/components/schemas/CampaignOptional' + - $ref: '#/components/schemas/CampaignRequired' + CampaignAdditionalField: + title: CampaignAdditionalField + allOf: + - type: object + properties: + name: + type: string + slug: + type: string + error: + type: string + required: + - name + - slug + - error + - oneOf: + - type: object + properties: + type: + type: string + enum: + - select + options: + type: array + items: + type: string + required: + - type + - options + - type: object + properties: + type: + type: string + enum: + - text regex: type: string required: @@ -10477,6 +11124,127 @@ components: - draft - confirmed - done + CountryCode: + title: CountryCode + type: string + 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 + notes: + type: string + x-stoplight: + id: xmrb08t4qc2fv + required: + - project + - testType + - title + - startDate + - deviceList securitySchemes: JWT: type: http @@ -10775,6 +11543,7 @@ components: schema: type: string examples: {} + requestBodies: {} tags: - name: Authentication - name: Campaign 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/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); } } diff --git a/src/routes/campaigns/_get/fields.spec.ts b/src/routes/campaigns/_get/fields.spec.ts index e71e15554..d418c77aa 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,8 @@ describe("GET /campaigns", () => { type: { name: "CampaignType 1", area: "quality" }, visibility: "admin", resultType: "bug", + phase: { id: 1, name: "Draft" }, + roles: [], }, { id: 3, @@ -129,6 +135,8 @@ describe("GET /campaigns", () => { type: { name: "CampaignType 2", area: "experience" }, visibility: "admin", resultType: "bug", + phase: { id: 1, name: "Draft" }, + roles: [], }, ]) ); 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/filterByRole.spec.ts b/src/routes/campaigns/_get/filterByRole.spec.ts new file mode 100644 index 000000000..79fc7b911 --- /dev/null +++ b/src/routes/campaigns/_get/filterByRole.spec.ts @@ -0,0 +1,152 @@ +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" }, + { ...campaign, id: 5, title: "Fifth 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 }, + { campaign_id: 5, custom_role_id: 2, 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" }), + ]) + ); + }); + 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.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..ddcd6b0e3 100644 --- a/src/routes/campaigns/_get/index.ts +++ b/src/routes/campaigns/_get/index.ts @@ -17,9 +17,16 @@ const ACCEPTABLE_FIELDS = [ "resultType" as const, "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"]; @@ -43,6 +50,11 @@ class RouteItem extends UserRoute<{ type?: number[]; status?: "closed" | "running" | "incoming"; csm?: number; + roles?: { + id: number; + value: number[] | "empty"; + }[]; + phase?: number; } = {}; constructor(configuration: RouteClassConfiguration) { @@ -102,6 +114,23 @@ 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 === "empty" + ? "empty" + : 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); + } } } @@ -152,6 +181,7 @@ class RouteItem extends UserRoute<{ this.addTypeTo(query); this.addVisibilityTo(query); this.addResultTypeTo(query); + this.addPhaseTo(query); if (this.limit) { query.limit(this.limit); @@ -163,7 +193,7 @@ class RouteItem extends UserRoute<{ query.orderBy(this.orderBy, this.order); - return (await query) as { + const results: { id?: number; name?: string; startDate?: string; @@ -181,7 +211,67 @@ class RouteItem extends UserRoute<{ type_area?: 0 | 1; visibility?: 0 | 1 | 2 | 3; 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( @@ -233,8 +323,22 @@ class RouteItem extends UserRoute<{ }, } : {}), + ...(this.fields.includes("phase") && + campaign.phase_id && + campaign.phase_name + ? { + phase: { + id: campaign.phase_id, + name: campaign.phase_name, + }, + } + : {}), visibility: this.getVisibilityName(campaign.visibility), resultType: this.getResultTypeName(campaign.resultType), + + ...(this.fields.includes("roles") && { + roles: campaign.roles, + }), })); } @@ -355,6 +459,34 @@ class RouteItem extends UserRoute<{ .where("wp_appq_evd_campaign.start_date", ">", tryber.fn.now()); } } + + if (this.filterBy.roles) { + this.filterBy.roles.forEach((role) => { + 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) + ); + } + }); + } + + if (this.filterBy.phase) { + query = query.where( + "wp_appq_evd_campaign.phase_id", + this.filterBy.phase + ); + } }); } @@ -498,6 +630,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..1a6bf4ef2 --- /dev/null +++ b/src/routes/campaigns/_get/phases.spec.ts @@ -0,0 +1,100 @@ +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", + }, + }), + ]) + ); + }); + + 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", + }, + }), + ]) + ); + }); +}); 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/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/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/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/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/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/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 new file mode 100644 index 000000000..89842caa1 --- /dev/null +++ b/src/routes/device/device_type/operating_systems/_get/index.spec.ts @@ -0,0 +1,143 @@ +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[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[0]).toHaveProperty("id", 2); + expect(responseFF1.body[0]).toHaveProperty("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[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) + .get("/devices/0/operating_systems?filterBy[model]=Galaxy S10") + .set("authorization", "Bearer tester"); + + expect(response.body).toHaveLength(1); + expect(response.body[0]).toHaveProperty("id", 1); + expect(response.body[0]).toHaveProperty("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[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 de22f7f14..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,84 +1,138 @@ -import * as db from "@src/features/db"; -import { Context } from "openapi-backend"; - -/** 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); - } - 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]; - - let acceptedFilters = ["manufacturer", "model"].filter((f) => - Object.keys(filter).includes(f) - ); - - 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 "); +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +/** 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: "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 = device_type === "all" ? device_type : 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[] } + : {}), + }; } + } - subQuery += ` WHERE ${subWhere}`; - sql += ` WHERE id IN (${subQuery})`; - sql = db.format(sql, subQueryData); + 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; + } - const results = await db.query(sql); + protected async prepare(): Promise { + try { + const results = await this.getOperativeSystems(); - 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 { + return this.setSuccess( + 200, + results.map((row) => ({ id: row.id, name: row.name, - }; - }); + type: this.mapDeviceTypeToFormFactor(row.form_factor), + })) + ); + } catch (error) { + this.setError(404, error as OpenapiError); } + } - 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, - }; + 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 await this.getFallback(); + + const results = await tryber.tables.WpAppqEvdPlatform.do() + .distinct("id") + .select("name") + .select("form_factor") + .whereIn("id", osFromFilters); + + if (!results.length) await this.getFallback(); + return results; + } + + 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) { + 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); + } + } + } + + return (await query).map((row: { platform_id: number }) => row.platform_id); + } + + private async getFallback() { + const query = tryber.tables.WpAppqEvdPlatform.do() + .distinct("id") + .select("name") + .select("form_factor"); + + if (this.deviceType !== "all") query.where("form_factor", this.deviceType); + + const results = await query; + + return results; + } + + 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/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts new file mode 100644 index 000000000..fbf87febb --- /dev/null +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -0,0 +1,711 @@ +import app from "@src/app"; +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, + 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.CampaignPhase.do().insert([ + { id: 1, name: "Test Phase", type_id: 1 }, + ]); + 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", + 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, + }, + ]); + + 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 () => { + await tryber.tables.CampaignPhase.do().delete(); + 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.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 () => { + 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 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") + .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 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") + .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"); + }); + + 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); + }); + + 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", + }); + }); + 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/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts new file mode 100644 index 000000000..241961856 --- /dev/null +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -0,0 +1,552 @@ +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"); +jest.mock("@src/features/webhookTrigger"); + +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.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, + 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(); + await tryber.tables.WpTerms.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 new file mode 100644 index 000000000..e56e61d20 --- /dev/null +++ b/src/routes/dossiers/_post/index.spec.ts @@ -0,0 +1,98 @@ +import app from "@src/app"; +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, + 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..a530d0c7d --- /dev/null +++ b/src/routes/dossiers/_post/index.ts @@ -0,0 +1,707 @@ +/** OPENAPI-CLASS: post-dossiers */ + +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"; +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()) { + 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; + } + 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; + } + 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; + 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() + .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 { + const campaignId = await this.createCampaign(); + await this.linkRolesToCampaign(campaignId); + + await this.generateLinkedData(campaignId); + + const webhook = new WebhookTrigger({ + type: "campaign_created", + data: { + campaignId, + }, + }); + await webhook.trigger(); + + this.setSuccess(201, { + id: campaignId, + }); + } 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(), + close_date: this.getCloseDate(), + 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, + customer_title: this.getBody().title.customer, + os: os.join(","), + form_factor: form_factor.join(","), + }) + .returning("id"); + + 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(), + notes: this.getBody().notes, + }) + .returning("id"); + + 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( + countries.map((country) => ({ + campaign_dossier_data_id: dossierId, + country_code: country, + })) + ); + } + + const languages = this.getBody().languages; + if (languages?.length) { + await tryber.tables.CampaignDossierDataLanguages.do().insert( + languages.map((language) => ({ + campaign_dossier_data_id: dossierId, + language_id: language, + })) + ); + } + + const browsers = this.getBody().browsers; + if (browsers?.length) { + await tryber.tables.CampaignDossierDataBrowsers.do().insert( + browsers.map((browser) => ({ + campaign_dossier_data_id: dossierId, + browser_id: browser, + })) + ); + } + + return campaignId; + } + + private async linkRolesToCampaign(campaignId: number) { + const roles = this.getBody().roles; + if (!roles?.length) 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 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, + }) + .where("id", campaignId); + } + + const previewId = await this.duplicatePage({ + pageId: page_preview_id, + campaignId, + }); + + if (previewId) { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ + page_preview_id: previewId, + }) + .where("id", campaignId); + } + + 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, + }; + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_manual_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + const transId = await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + if (transId) manualTranslations[t] = transId; + } + await this.createPolylangTranslations({ + id: manualId, + translations: manualTranslations, + languages, + }); + 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) { + const previewTranslations: { [key: string]: number } = { + [defaultLanguage]: previewId, + }; + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_preview_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + const transId = await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + if (transId) previewTranslations[t] = transId; + } + await this.createPolylangTranslations({ + id: previewId, + translations: previewTranslations, + languages, + }); + } + } + } + + private async createPolylangTranslations({ + id, + translations, + languages, + }: { + id: number; + translations: { [key: string]: number }; + languages: { [key: string]: any }; + }) { + 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, + }); + + 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({ + 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 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, + }: { + 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?.length) 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(); + } + + 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 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") + .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/_post/triggerWp.spec.ts b/src/routes/dossiers/_post/triggerWp.spec.ts new file mode 100644 index 000000000..022df8269 --- /dev/null +++ b/src/routes/dossiers/_post/triggerWp.spec.ts @@ -0,0 +1,135 @@ +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"); +jest.mock("@src/features/webhookTrigger"); + +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/routes/dossiers/campaignId/_get/index.spec.ts b/src/routes/dossiers/campaignId/_get/index.spec.ts new file mode 100644 index 000000000..f779f37ea --- /dev/null +++ b/src/routes/dossiers/campaignId/_get/index.spec.ts @@ -0,0 +1,533 @@ +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.CampaignPhase.do().insert([ + { + id: 1, + name: "Active", + type_id: 1, + }, + ]); + 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, + }); + + const profile = { + name: "Test", + 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, + 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", + close_date: "2019-08-27T14:15:22Z", + platform_id: 0, + os: "1", + page_manual_id: 0, + page_preview_id: 0, + 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 () => { + 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(); + 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 () => { + 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"); + expect(response.status).toBe(403); + }); + + it("Should answer 200 if admin", async () => { + const response = await request(app) + .get("/dossiers/1") + .set("authorization", "Bearer admin"); + 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); + }); + + 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 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) + .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"); + }); + + 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"); + }); + + 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({ + id: 100, + 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, + notes: "Notes", + }); + await tryber.tables.CampaignDossierDataCountries.do().insert([ + { + campaign_dossier_data_id: 100, + country_code: "IT", + }, + { + campaign_dossier_data_id: 100, + 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: 100, + language_id: 1, + }, + { + campaign_dossier_data_id: 100, + 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: 100, + browser_id: 1, + }, + { + campaign_dossier_data_id: 100, + 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"); + }); + + 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") + .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 new file mode 100644 index 000000000..c3474e466 --- /dev/null +++ b/src/routes/dossiers/campaignId/_get/index.ts @@ -0,0 +1,333 @@ +/** OPENAPI-CLASS: get-dossiers-campaign */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +export default class RouteItem extends UserRoute<{ + 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( + 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", + "project_id", + "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"), + tryber + .ref("id") + + .withSchema("campaign_phase") + .as("phase_id"), + tryber.ref("name").withSchema("campaign_phase").as("phase_name") + ) + .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" + ) + .join( + "campaign_phase", + "campaign_phase.id", + "wp_appq_evd_campaign.phase_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(",")); + + 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); + + const dossierData = await tryber.tables.CampaignDossierData.do() + .select( + tryber.ref("id").withSchema("campaign_dossier_data").as("dossier_id"), + "description", + "link", + "goal", + "out_of_scope", + "target_audience", + "target_size", + "target_devices", + "product_type_id", + "notes", + 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.dossier_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.dossier_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.dossier_id) + : []; + + return { + ...campaign, + devices, + roles, + ...dossierData, + countries: targetCountries, + languages: targetLanguages, + browsers: targetBrowsers, + }; + } + + 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.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; + } + + return true; + } + + private async doesNotHaveAccessToCampaign() { + return !this.hasAccessToCampaign(this.campaignId); + } + + 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.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, + 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) => { + return { + role: { + id: item.role_id, + name: item.role_name, + }, + user: { + id: item.tester_id, + name: item.tester_name, + surname: item.tester_surname, + }, + }; + }), + } + : {}), + ...(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.notes && { + notes: this.campaign.notes, + }), + ...(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); + } + } + + 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.spec.ts b/src/routes/dossiers/campaignId/_put/index.spec.ts new file mode 100644 index 000000000..7a26f08cf --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/index.spec.ts @@ -0,0 +1,135 @@ +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); + }); + + 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 new file mode 100644 index 000000000..07e840032 --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/index.ts @@ -0,0 +1,368 @@ +/** OPENAPI-CLASS: put-dossiers-campaign */ + +import OpenapiError from "@src/features/OpenapiError"; +import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; + +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"]; +}> { + private campaignId: number; + private _campaign: { end_date: string; close_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", "close_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.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; + } + 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 doesNotHaveAccessToCampaign() { + return !this.hasAccessToCampaign(this.campaignId); + } + + 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, + start_date: this.getBody().startDate, + end_date: this.getEndDate(), + close_date: this.getCloseDate(), + pm_id: this.getBody().csm ?? 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, + }); + + 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, + notes: this.getBody().notes, + 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 || !countries.length) 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 || !languages.length) 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 || !browsers.length) return; + + await tryber.tables.CampaignDossierDataBrowsers.do().insert( + browsers.map((browser) => ({ + campaign_dossier_data_id: dossierId, + browser_id: browser, + })) + ); + } + + private async linkRolesToCampaign() { + await this.cleanupCurrentRoles(); + const roles = this.getBody().roles; + if (!roles || !roles.length) 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 || !roles.length) 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); + if (!olpObject || !olpObject.length) continue; + 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() { + if (this.getBody().endDate) return this.getBody().endDate; + + 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") + .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..8f2c68d90 --- /dev/null +++ b/src/routes/dossiers/campaignId/_put/update.spec.ts @@ -0,0 +1,788 @@ +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.CampaignPhase.do().insert({ + id: 1, + name: "Draft", + type_id: 1, + }); + 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, + }, + ]); + + 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" }, + { 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 () => { + 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.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqLang.do().delete(); + await tryber.tables.Browsers.do().delete(); + await tryber.tables.ProductTypes.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", + close_date: "2019-08-25T14: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(); + 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 () => { + 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 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") + .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 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") + .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 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") + .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"); + }); + + 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", + }); + }); + 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", () => { + beforeAll(async () => { + 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.CustomRoles.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/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..f9149b3ab --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.spec.ts @@ -0,0 +1,155 @@ +import { tryber } from "@src/features/database"; +import { StatusChangeHandler } from "."; + +jest.mock("@src/features/webhookTrigger"); +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, 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([ + { 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 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, + 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); + }); + + 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 new file mode 100644 index 000000000..4adae728f --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/StatusChangeHandler/index.ts @@ -0,0 +1,103 @@ +import { tryber } from "@src/features/database"; +import { WebhookTrigger } from "@src/features/webhookTrigger"; + +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.legacySetStatusDetails(); + + 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, close_date: tryber.fn.now() }) + .where("id", this.campaignId); + } else { + await tryber.tables.WpAppqEvdCampaign.do() + .update({ status_id: 1 }) + .where("id", this.campaignId); + } + + 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({ + type: "status_change", + data: { + campaignId: this.campaignId, + oldPhase: this.oldPhase, + newPhase: this.newPhase, + }, + }); + + await webhook.trigger(); + } +} + +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..63d82330d --- /dev/null +++ b/src/routes/dossiers/campaignId/phases/_put/index.spec.ts @@ -0,0 +1,172 @@ +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"); +jest.mock("@src/features/webhookTrigger"); + +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..c0b69a467 --- /dev/null +++ b/src/routes/phases/_get/index.spec.ts @@ -0,0 +1,68 @@ +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 }, + ]); + await tryber.tables.CampaignPhaseType.do().insert([ + { id: 1, name: "Type 1" }, + { id: 2, name: "Type 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", 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 new file mode 100644 index 000000000..1ed82d361 --- /dev/null +++ b/src/routes/phases/_get/index.ts @@ -0,0 +1,49 @@ +/** 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() + .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.map((phase) => ({ + id: phase.id, + name: phase.name, + type: { id: phase.type_id, name: phase.type_name }, + })), + }); + } +} 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/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/routes/users/me/campaigns/_get/filters.spec.ts b/src/routes/users/me/campaigns/_get/filters.spec.ts index 98d586575..323f3c911 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: 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 ae0f0b342..b77961f4c 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: 20, + }, + { + ...campaign, + id: 2, + title: "Small Group campaign", + is_public: 0, + phase_id: 20, + }, + { + ...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..5032688c0 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: 20, + 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..a13702e54 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: 20, }; 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..bd9958be6 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: 20, }; - 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..0747ebb52 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: 20, }); 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..120910061 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: 20, }, { id: 2, @@ -223,6 +225,7 @@ const useBasicData = () => { pm_id: 1, project_id: 1, customer_title: "My campaign", + phase_id: 20, }, ]); 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..d7c11ff0f 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: 20, }); 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: 20, }); await tryber.tables.WpAppqEvdCampaign.do().insert({ @@ -92,6 +95,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 20, }); await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ @@ -148,6 +152,7 @@ const useBasicData = () => { customer_id: 1, pm_id: 1, project_id: 1, + phase_id: 20, }); await tryber.tables.WpAppqCampaignTask.do().insert({ diff --git a/src/schema.ts b/src/schema.ts index 465a9b5b9..f2ad1ddae 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": { @@ -540,6 +541,56 @@ 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; + }; + }; + }; + "/customers/{customer}/projects": { + get: operations["get-customers-customer-projects"]; + post: operations["post-customers-customer-projects"]; + parameters: { + path: { + customer: string; + }; + }; + }; + "/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: {}; + }; + "/phases": { + get: operations["get-phases"]; + }; + "/dossiers/{campaign}/phases": { + put: operations["put-dossiers-campaign-phases"]; + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + }; } export interface components { @@ -847,6 +898,42 @@ export interface components { * @enum {string} */ 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; + notes?: string; + }; }; responses: { /** A user */ @@ -935,6 +1022,7 @@ export interface components { search: string; testerId: string; }; + requestBodies: {}; } export interface operations { @@ -1162,6 +1250,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"]; }; @@ -2181,6 +2284,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: { @@ -2243,8 +2367,9 @@ export interface operations { 200: { content: { "application/json": { - id?: number; - name?: string; + id: number; + name: string; + type: string; }[]; }; }; @@ -3975,6 +4100,281 @@ export interface operations { 404: components["responses"]["NotFound"]; }; }; + "post-dossiers": { + parameters: {}; + responses: { + /** Created */ + 201: { + content: { + "application/json": { + id?: number; + }; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"] & { + duplicate?: { + fields?: number; + useCases?: number; + mailMerges?: number; + pages?: number; + testers?: number; + }; + }; + }; + }; + }; + "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; + /** Format: date-time */ + closeDate: 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; + }; + roles?: { + role?: { + id: number; + name: string; + }; + user?: { + id: number; + name: string; + 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; + }; + phase: { + id: number; + name: string; + }; + notes?: string; + }; + }; + }; + }; + }; + "put-dossiers-campaign": { + parameters: { + path: { + /** A campaign id */ + campaign: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { [key: string]: unknown }; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"]; + }; + }; + }; + "get-customers-customer-projects": { + parameters: { + path: { + customer: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + }[]; + }; + }; + }; + }; + }; + "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: { + 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; + }[]; + }; + }; + }; + }; + }; + "get-phases": { + responses: { + /** OK */ + 200: { + content: { + "application/json": { + results: { + id: number; + name: string; + type: { + 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 c8453c29c..ef5c5f991 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,11 +27,11 @@ dependencies: "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" - -"@appquality/tryber-database@^0.27.0": - version "0.27.0" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.27.0.tgz#bff62fa3256b618d0d83b5af239b9c1ce9962835" - integrity sha512-/CtRBYaI43+ClJhJwGA9B2EXn6UYMQ9HGEiDQK3a0D/jt+895yTYbBrLxIwY0nYePYuo9DYHhHpxIjLNTk+wmw== + +"@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" @@ -2296,6 +2296,11 @@ copyfiles@^2.4.1: untildify "^4.0.0" yargs "^16.1.0" +core-js@^3.6.5: + version "3.36.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578" + integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA== + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" @@ -4708,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"