diff --git a/.github/workflows/.deploy.yaml b/.github/workflows/.deploy.yaml index 149bab13e..fe754ff01 100644 --- a/.github/workflows/.deploy.yaml +++ b/.github/workflows/.deploy.yaml @@ -1,4 +1,5 @@ -name: Deploy +name: Deploy PR +run-name: Deploy PR-${{ github.event.inputs.pr-number }} env: ACRONYM: chefs @@ -84,7 +85,7 @@ jobs: pr_number: ${{ github.event.inputs.pr-number }} deploy: - name: Deploys to selected environment + name: Deploy environment: name: pr url: ${{ needs.set-vars.outputs.URL }} @@ -123,3 +124,10 @@ jobs: message: | Release ${{ github.sha }} deployed at number: ${{ github.event.inputs.pr-number }} + + scan: + name: Scan + needs: [deploy, set-vars] + uses: ./.github/workflows/reusable-owasp-zap.yaml + with: + url: ${{ needs.set-vars.outputs.URL }} diff --git a/.github/workflows/on_push.yaml b/.github/workflows/on_push.yaml index ecddda514..fe412b8cf 100644 --- a/.github/workflows/on_push.yaml +++ b/.github/workflows/on_push.yaml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build & Push uses: ./.github/actions/build-push-container with: @@ -38,10 +38,12 @@ jobs: url: https://${{ env.ACRONYM }}-dev.apps.silver.devops.gov.bc.ca/app runs-on: ubuntu-latest needs: build + outputs: + url: https://${{ env.ACRONYM }}-dev.apps.silver.devops.gov.bc.ca/app timeout-minutes: 12 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Deploy to Dev uses: ./.github/actions/deploy-to-environment with: @@ -57,6 +59,13 @@ jobs: route_path: /app route_prefix: ${{ vars.ROUTE_PREFIX }} + scan-dev: + name: Scan Dev + needs: deploy-dev + uses: ./.github/workflows/reusable-owasp-zap.yaml + with: + url: ${{ needs.deploy-dev.outputs.url }} + deploy-test: name: Deploy to Test environment: @@ -69,7 +78,7 @@ jobs: timeout-minutes: 12 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Deploy to Test uses: ./.github/actions/deploy-to-environment with: @@ -98,7 +107,7 @@ jobs: timeout-minutes: 12 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Deploy to Prod uses: ./.github/actions/deploy-to-environment with: diff --git a/.github/workflows/owasp-zap-scan.yaml b/.github/workflows/owasp-zap-scan.yaml deleted file mode 100644 index dda4f7015..000000000 --- a/.github/workflows/owasp-zap-scan.yaml +++ /dev/null @@ -1,106 +0,0 @@ -name: owasp-zap-scan - -on: - workflow_dispatch: - inputs: - pr-number: - description: "Pull Request Number:" - type: string - required: true - ZAP_SCAN_TYPE: - description: Zap scan type - type: choice - options: - - base - - full - default: full - required: true - ZAP_DURATION: - description: Zap duration - required: true - type: string - default: 2 - ZAP_MAX_DURATION: - description: Zap max duration - required: true - type: string - default: 10 - ZAP_GCP_PUBLISH: - description: Creates a pre-signed URL with the results - required: true - type: boolean - default: false - ZAP_GCP_PROJECT: - required: false - type: string - ZAP_GCP_BUCKET: - required: false - type: string -jobs: - owasp-zap-scan: - name: OWASP ZAP Scan - runs-on: ubuntu-latest - steps: - - name: Get ref - id: vars - run: | - REF=refs/pull/$PR_NUMBER/head - echo REF:$REF - echo "ref=$REF" >> $GITHUB_OUTPUT - - name: Checkout repository - uses: actions/checkout@v3 - with: - ref: ${{ steps.vars.outputs.ref }} - - name: Set up Cloud SDK - if: ${{ env.ZAP_GCP_PUBLISH == 'true' }} - uses: google-github-actions/setup-gcloud@v1 - with: - version: ">= 416.0.0" - project_id: ${{ env.ZAP_GCP_PROJECT }} - service_account_key: ${{ secrets.GCP_SA_KEY }} - export_default_credentials: true - - name: ZAP Base Scan - if: ${{ env.ZAP_SCAN_TYPE == 'base' }} - uses: zaproxy/action-baseline@v0.7.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - docker_name: "owasp/zap2docker-stable" - target: https://chefs-dev.apps.silver.devops.gov.bc.ca/pr-${{ github.event.inputs.pr-number }} - cmd_options: "-a -d -T ${{ env.ZAP_MAX_DURATION }} -m ${{ env.ZAP_DURATION }}" - issue_title: OWAP Baseline - - name: ZAP Full Scan - if: ${{ env.ZAP_SCAN_TYPE == 'full' }} - uses: zaproxy/action-full-scan@v0.4.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - docker_name: "owasp/zap2docker-stable" - target: https://chefs-dev.apps.silver.devops.gov.bc.ca/pr-${{ github.event.inputs.pr-number }} - cmd_options: "-a -d -T ${{ env.ZAP_MAX_DURATION }} -m ${{ env.ZAP_DURATION }}" - - name: Create Artifact Directory - if: ${{ env.ZAP_GCP_PUBLISH == 'true' }} - run: | - mkdir -p public/zap - - name: Publish Reports to Github - uses: actions/download-artifact@v3 - with: - name: zap_scan - path: public/zap - - name: Rename Markdown - if: ${{ env.ZAP_GCP_PUBLISH == 'true' }} - run: | - mv public/zap/report_md.md public/zap/README.md - - name: ZAP Results - uses: JamesIves/github-pages-deploy-action@v4.4.1 - with: - branch: zap-scan - folder: public/zap - - name: GCP Publish Results URL - if: ${{ env.ZAP_GCP_PUBLISH == 'true' }} - run: | - echo "$GCP_SA_KEY" > gcp-sa-key.json - gsutil mb gs://${{ env.ZAP_GCP_BUCKET }} || echo "Bucket already exists..." - gsutil cp public/zap/report_html.html gs://${{ env.ZAP_GCP_BUCKET }} - echo "URL expires in 10 minutes..." - gsutil signurl -d 10m gcp-sa-key.json gs://${{ env.ZAP_GCP_BUCKET }}/report_html.html - env: - GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} diff --git a/.github/workflows/reusable-owasp-zap.yaml b/.github/workflows/reusable-owasp-zap.yaml new file mode 100644 index 000000000..266bbb0f3 --- /dev/null +++ b/.github/workflows/reusable-owasp-zap.yaml @@ -0,0 +1,31 @@ +# +# Reusable workflow to run the OWASP ZAP (Open Worldwide Application Security +# Project - Zed Attack Proxy) Scan against a deployed application. +# +name: OWASP ZAP Scan +on: + workflow_call: + inputs: + url: + required: true + type: string + +jobs: + owasp-zap: + name: OWASP ZAP Scan + runs-on: ubuntu-latest + + steps: + - name: Run Scan + uses: zaproxy/action-full-scan@v0.10.0 + with: + # Do not create GitHub Issues + allow_issue_writing: false + + artifact_name: OWASP ZAP Scan + + # -a: include the alpha passive scan rules as well + # -d: show debug messages + cmd_options: "-a -d" + + target: ${{ inputs.url }} diff --git a/app/app.js b/app/app.js index 007d4c69a..7a787470c 100644 --- a/app/app.js +++ b/app/app.js @@ -32,6 +32,8 @@ app.use(express.urlencoded({ extended: true })); // See https://express-rate-limit.github.io/ERR_ERL_UNEXPECTED_X_FORWARDED_FOR app.set('trust proxy', 1); +app.set('x-powered-by', false); + // Skip if running tests if (process.env.NODE_ENV !== 'test') { // Initialize connections and exit if unsuccessful diff --git a/app/frontend/src/components/forms/submission/StatusPanel.vue b/app/frontend/src/components/forms/submission/StatusPanel.vue index c1bbca44d..bc5b1aa9b 100644 --- a/app/frontend/src/components/forms/submission/StatusPanel.vue +++ b/app/frontend/src/components/forms/submission/StatusPanel.vue @@ -47,7 +47,6 @@ export default { valid: false, showSendConfirmEmail: false, showStatusContent: false, - selectedUsers: [], // array to hold multiple users for REVISING status }; }, computed: { @@ -236,77 +235,59 @@ export default { throw new Error(this.$t('trans.statusPanel.status')); } - const baseStatusBody = { + const statusBody = { code: this.statusToSet, + submissionUserEmail: this.submissionUserEmail, revisionNotificationEmailContent: this.emailComment, }; - - if (this.showAssignee && this.assignee) { - baseStatusBody.assignedToUserId = this.assignee.userId; - baseStatusBody.assignmentNotificationEmail = this.assignee.email; + if (this.showAssignee) { + if (this.assignee) { + statusBody.assignedToUserId = this.assignee.userId; + statusBody.assignmentNotificationEmail = this.assignee.email; + } + } + const statusResponse = await formService.updateSubmissionStatus( + this.submissionId, + statusBody + ); + if (!statusResponse.data) { + throw new Error( + this.$t('trans.statusPanel.updtSubmissionsStatusErr') + ); } - if (this.statusToSet === 'REVISING') { - // Handle multiple emails for REVISING - for (const user of this.selectedUsers) { - const statusBody = { - ...baseStatusBody, - submissionUserEmail: user.email, - }; - const statusResponse = await formService.updateSubmissionStatus( - this.submissionId, - statusBody - ); - - if (!statusResponse.data) { - throw new Error( - this.$t('trans.statusPanel.updtSubmissionsStatusErr') - ); - } - - if (this.emailComment) { - const formattedComment = `Email to ${user.email}: ${this.emailComment}`; - await this.sendEmailWithComment( - formattedComment, - statusResponse.data[0].submissionStatusId - ); - } + if (this.emailComment) { + let formattedComment; + if (this.statusToSet === 'ASSIGNED') { + formattedComment = `Email to ${this.assignee.email}: ${this.emailComment}`; + } else if ( + this.statusToSet === 'REVISING' || + this.statusToSet === 'COMPLETED' + ) { + formattedComment = `Email to ${this.submissionUserEmail}: ${this.emailComment}`; } - } else { - // Handle single email for other statuses - const statusBody = { - ...baseStatusBody, - submissionUserEmail: this.submissionUserEmail, + + const submissionStatusId = + statusResponse.data[0].submissionStatusId; + const user = await rbacService.getCurrentUser(); + const noteBody = { + submissionId: this.submissionId, + submissionStatusId: submissionStatusId, + note: formattedComment, + userId: user.data.id, }; - const statusResponse = await formService.updateSubmissionStatus( + const response = await formService.addNote( this.submissionId, - statusBody + noteBody ); - - if (!statusResponse.data) { + if (!response.data) { throw new Error( - this.$t('trans.statusPanel.updtSubmissionsStatusErr') - ); - } - - if (this.emailComment) { - let formattedComment; - if (this.statusToSet === 'ASSIGNED') { - formattedComment = `Email to ${this.assignee.email}: ${this.emailComment}`; - } else if (this.statusToSet === 'COMPLETED') { - formattedComment = `Email to ${this.submissionUserEmail}: ${this.emailComment}`; - } - - await this.sendEmailWithComment( - formattedComment, - statusResponse.data[0].submissionStatusId + this.$t('trans.statusPanel.addNoteNoReponserErr') ); } + // Update the parent if the note was updated + this.$emit('note-updated'); } - - // Update the parent if the note was updated - this.$emit('note-updated'); - this.resetForm(); this.getStatus(); } @@ -319,24 +300,6 @@ export default { }); } }, - - async sendEmailWithComment(comment, submissionStatusId) { - const user = await rbacService.getCurrentUser(); - const noteBody = { - submissionId: this.submissionId, - submissionStatusId: submissionStatusId, - note: comment, - userId: user.data.id, - }; - const response = await formService.addNote(this.submissionId, noteBody); - if (!response.data) { - throw new Error(this.$t('trans.statusPanel.addNoteNoReponserErr')); - } - }, - - updateSubmissionUserEmail(selectedUsers) { - this.selectedUsers = selectedUsers; - }, }, }; @@ -484,41 +447,15 @@ export default {
- - - - - - - + data-test="showRecipientEmail" + />
diff --git a/app/src/forms/form/exportService.js b/app/src/forms/form/exportService.js index c08d3eaaf..29268cdb5 100644 --- a/app/src/forms/form/exportService.js +++ b/app/src/forms/form/exportService.js @@ -114,14 +114,7 @@ const service = { if (fields) { return formSchemaheaders.filter((header) => { - // In the 'fields' sent from the caller we'll have something like - // 'datagrid.input', but in the actual submission data in the - // 'formSchemaheaders' we'll have things like 'datagrid.0.input', - // 'datagrid.1.input', etc. Remove the '.0' array index to get - // 'datagrid.input' and then do the comparison. - const flattenedHeader = header.replace(/\.\d+\./gi, '.'); - - if (Array.isArray(fields) && fields.includes(flattenedHeader)) { + if (Array.isArray(fields) && fields.includes(header)) { return header; } }); diff --git a/app/tests/fixtures/submission/kitchen_sink_submission_data_export_datagrid_fields_selection.json b/app/tests/fixtures/submission/kitchen_sink_submission_data_export_datagrid_fields_selection.json index 9203e2fa4..f27b77016 100644 --- a/app/tests/fixtures/submission/kitchen_sink_submission_data_export_datagrid_fields_selection.json +++ b/app/tests/fixtures/submission/kitchen_sink_submission_data_export_datagrid_fields_selection.json @@ -10,10 +10,13 @@ "email", "forWhichBcLakeRegionAreYouCompletingTheseQuestions", "didYouFishAnyBcLakesThisYear", - "oneRowPerLake.lakeName", - "oneRowPerLake.closestTown", - "oneRowPerLake.numberOfDays", - "oneRowPerLake.dataGrid.fishType", - "oneRowPerLake.dataGrid.numberCaught", - "oneRowPerLake.dataGrid.numberKept" + "oneRowPerLake.0.lakeName", + "oneRowPerLake.0.closestTown", + "oneRowPerLake.0.numberOfDays", + "oneRowPerLake.0.dataGrid.0.fishType", + "oneRowPerLake.0.dataGrid.1.fishType", + "oneRowPerLake.0.dataGrid.0.numberCaught", + "oneRowPerLake.0.dataGrid.1.numberCaught", + "oneRowPerLake.0.dataGrid.0.numberKept", + "oneRowPerLake.0.dataGrid.1.numberKept" ] diff --git a/app/tests/unit/forms/form/exportService.spec.js b/app/tests/unit/forms/form/exportService.spec.js index 4d32d8775..329355431 100644 --- a/app/tests/unit/forms/form/exportService.spec.js +++ b/app/tests/unit/forms/form/exportService.spec.js @@ -126,7 +126,8 @@ describe('export', () => { 'form.username', 'form.email', 'dataGrid', - 'dataGrid.simpletextfield', + 'dataGrid.0.simpletextfield', + 'dataGrid.1.simpletextfield', ], template: 'singleRowCSVExport', }; @@ -515,7 +516,7 @@ describe('_buildCsvHeaders', () => { // get result columns if we need to filter out the columns const result = await exportService._buildCsvHeaders(form, submissionsExport, 1, fields, true); - expect(result).toHaveLength(29); + expect(result).toHaveLength(20); expect(result).toEqual( expect.arrayContaining([ 'form.confirmationId', diff --git a/components/package-lock.json b/components/package-lock.json index 5788a680b..2b0b32e0d 100644 --- a/components/package-lock.json +++ b/components/package-lock.json @@ -3509,9 +3509,9 @@ "dev": true }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" diff --git a/components/src/components/Map/Component.ts b/components/src/components/Map/Component.ts index c55cbc7f5..c32056163 100644 --- a/components/src/components/Map/Component.ts +++ b/components/src/components/Map/Component.ts @@ -15,6 +15,7 @@ export default class Component extends (FieldComponent as any) { label: 'Map', key: 'map', input: true, + defaultvalue: { features: [] }, ...extend, }); } @@ -73,29 +74,32 @@ export default class Component extends (FieldComponent as any) { const { numPoints, defaultZoom, readOnlyMap, center, defaultValue } = this.component; - console.log(defaultValue); - let parsedCenter; - if (center) { - parsedCenter = JSON.parse(center).latlng; + + + const { readOnly: viewMode } = this.options; + + let initialCenter; + if (center && center.features && center.features[0]) { + initialCenter = center.features[0].coordinates; } this.mapService = new MapService({ mapContainer, drawOptions, - center: center ? parsedCenter : DEFAULT_CENTER, + center: center ? initialCenter : DEFAULT_CENTER, form, numPoints, defaultZoom, readOnlyMap, defaultValue, onDrawnItemsChange: this.saveDrawnItems.bind(this), + viewMode, }); // Load existing data if available - if (this.dataValue) { + if (this.dataValue && this.dataValue.features) { try { - const parsedValue = JSON.parse(this.dataValue); - this.mapService.loadDrawnItems(parsedValue); + this.mapService.loadDrawnItems(this.dataValue.features); } catch (error) { console.error('Failed to parse dataValue:', error); } @@ -103,11 +107,11 @@ export default class Component extends (FieldComponent as any) { } saveDrawnItems(drawnItems: L.Layer[]) { - const value = drawnItems.map((layer: any) => { + const features = drawnItems.map((layer: any) => { if (layer instanceof L.Marker) { return { type: 'marker', - latlng: layer.getLatLng(), + coordinates: layer.getLatLng(), }; } else if (layer instanceof L.Rectangle) { return { @@ -117,38 +121,34 @@ export default class Component extends (FieldComponent as any) { } else if (layer instanceof L.Circle) { return { type: 'circle', - latlng: layer.getLatLng(), + coordinates: layer.getLatLng(), radius: layer.getRadius(), }; } else if (layer instanceof L.Polygon) { return { type: 'polygon', - latlngs: layer.getLatLngs(), + coordinates: layer.getLatLngs(), }; } else if (layer instanceof L.Polyline) { return { type: 'polyline', - latlngs: layer.getLatLngs(), + coordinates: layer.getLatLngs(), }; } }); - // Convert to JSON string - const jsonValue = - this.component.numPoints === 1 - ? JSON.stringify(value[0]) - : JSON.stringify(value); - this.setValue(jsonValue); + this.setValue({ features }); } setValue(value) { super.setValue(value); // Additional logic to render the saved data on the map if necessary - if (this.mapService && value) { + if (this.mapService && value && value.features) { try { - const parsedValue = JSON.parse(value); - this.mapService.loadDrawnItems(parsedValue); + //const parsedValue = JSON.parse(value); + + this.mapService.loadDrawnItems(value.features); } catch (error) { console.error('Failed to parse value:', error); } diff --git a/components/src/components/Map/services/MapService.ts b/components/src/components/Map/services/MapService.ts index a4174dc09..06b110b23 100644 --- a/components/src/components/Map/services/MapService.ts +++ b/components/src/components/Map/services/MapService.ts @@ -9,13 +9,26 @@ const DEFAULT_LAYER_ATTRIBUTION = const DEFAULT_MAP_ZOOM = 5; const DECIMALS_LATLNG = 5; // the number of decimals of latitude and longitude to be displayed in the marker popup const COMPONENT_EDIT_CLASS = 'component-edit-tabs'; -const FORM_REVIEW_CLASS = 'review-form'; + +interface MapServiceOptions { + mapContainer: HTMLElement; + center: [number, number]; // Ensure center is a tuple with exactly two elements + drawOptions: any; + form: HTMLCollectionOf; + numPoints: number; + defaultZoom?: number; + readOnlyMap?: boolean; + onDrawnItemsChange: (items: any) => void; // Support both single and multiple items + viewMode?: boolean; +} + class MapService { options; map; drawnItems; constructor(options) { this.options = options; + if (options.mapContainer) { const { map, drawnItems } = this.initializeMap(options); this.map = map; @@ -43,9 +56,19 @@ class MapService { }); } } - initializeMap(options) { - let { mapContainer, center, drawOptions, form, defaultZoom, readOnlyMap } = - options; + + initializeMap(options: MapServiceOptions) { + let { + mapContainer, + center, + drawOptions, + form, + defaultZoom, + readOnlyMap, + viewMode, + } = options; + + if (drawOptions.rectangle) { drawOptions.rectangle.showArea = false; } @@ -60,18 +83,17 @@ class MapService { let drawnItems = new L.FeatureGroup(); map.addLayer(drawnItems); // Add Drawing Controllers - const formReviewNode = document.getElementsByClassName(FORM_REVIEW_CLASS); - if ( - !readOnlyMap || - !(formReviewNode && this.hasChildNode(formReviewNode[0], mapContainer)) - ) { - let drawControl = new L.Control.Draw({ - draw: drawOptions, - edit: { - featureGroup: drawnItems, - }, - }); - map.addControl(drawControl); + + if (!readOnlyMap) { + if (!viewMode) { + let drawControl = new L.Control.Draw({ + draw: drawOptions, + edit: { + featureGroup: drawnItems, + }, + }); + map.addControl(drawControl); + } } // Checking to see if the map should be interactable const componentEditNode = @@ -131,15 +153,15 @@ class MapService { items.forEach((item) => { let layer; if (item.type === 'marker') { - layer = L.marker(item.latlng); + layer = L.marker(item.coordinates); } else if (item.type === 'rectangle') { layer = L.rectangle(item.bounds); } else if (item.type === 'circle') { - layer = L.circle(item.latlng, { radius: item.radius }); + layer = L.circle(item.coordinates, { radius: item.radius }); } else if (item.type === 'polygon') { - layer = L.polygon(item.latlngs); + layer = L.polygon(item.coordinates); } else if (item.type === 'polyline') { - layer = L.polyline(item.latlngs); + layer = L.polyline(item.coordinates); } if (layer) { drawnItems.addLayer(layer); diff --git a/openshift/redash/redis.deploy.yaml b/openshift/redash/redis.deploy.yaml index 4a1b36f26..4d1493540 100644 --- a/openshift/redash/redis.deploy.yaml +++ b/openshift/redash/redis.deploy.yaml @@ -37,7 +37,7 @@ objects: secretKeyRef: key: REDIS_PASSWORD name: ${NAME}-redis - image: redis:7.0.11-alpine + image: redis:7.2.5-alpine3.20 imagePullPolicy: Always name: ${NAME}-redis ports: diff --git a/tests/functional/cypress/e2e/form-apikey-cdogs.cy.js b/tests/functional/cypress/e2e/form-apikey-cdogs.cy.js new file mode 100644 index 000000000..ca4ced762 --- /dev/null +++ b/tests/functional/cypress/e2e/form-apikey-cdogs.cy.js @@ -0,0 +1,126 @@ +import 'cypress-keycloak-commands'; +import 'cypress-drag-drop'; +import { formsettings } from '../support/login.js'; + +const depEnv = Cypress.env('depEnv'); + + +Cypress.Commands.add('waitForLoad', () => { + const loaderTimeout = 60000; + + cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist'); +}); + + + +describe('Form Designer', () => { + + beforeEach(()=>{ + + + + cy.on('uncaught:exception', (err, runnable) => { + // Form.io throws an uncaught exception for missing projectid + // Cypress catches it as undefined: undefined so we can't get the text + console.log(err); + return false; + }); + }); + it('Visits the form settings page', () => { + + + cy.viewport(1000, 1100); + cy.waitForLoad(); + + formsettings(); + + + }); + it('checks Apikey Settings', () => { + cy.viewport(1000, 1100); + cy.waitForLoad(); + + cy.get('button').contains('BC Government').click(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-key="simplebcaddress"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -550, { force: true }) + //.trigger('mousemove', coords.y, +100, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + cy.get('button').contains('Save').click(); + + + + }); + cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); + // Form saving + let savedButton = cy.get('[data-cy=saveButton]'); + expect(savedButton).to.not.be.null; + savedButton.trigger('click'); + cy.waitForLoad(); + + // Verify Api key functionality + cy.get('.mdi-cog').click(); + cy.get(':nth-child(2) > .v-expansion-panel > .v-expansion-panel-title > .v-expansion-panel-title__overlay').click(); + + cy.get('[data-test="canGenerateAPIKey"]').click(); + cy.get('[data-test="continue-btn-continue"]').click(); + cy.get('[data-test="continue-btn-cancel"]').should('be.enabled'); + cy.get('[data-test="canAllowCopyAPIKey"]').click(); + //Verify checkbox checked for access submitted files + cy.contains('Allow this API key to access submitted files').click(); + cy.get('input[aria-label="Allow this API key to access submitted files"]').should('be.checked'); + //Delete Apikey + cy.get('[data-test="canDeleteApiKey"]') + + + }) + it('checks Cdogs Upload', () => { + cy.viewport(1000, 1100); + cy.waitForLoad(); + cy.get(':nth-child(3) > .v-expansion-panel > .v-expansion-panel-title > .v-expansion-panel-title__overlay').click(); + let fileUploadInputField = cy.get('input[type=file]'); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('add1.png'); + + // Checking file type functionality + cy.get('div').contains('The template must use one of the following extentions: .txt, .docx, .html, .odt, .pptx, .xlsx').should('be.visible'); + cy.get('.mdi-close-circle').click(); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('SamplePPTx.pptx'); + cy.get('div').contains('The template must use one of the following extentions: .txt, .docx, .html, .odt, .pptx, .xlsx').should('not.exist'); + + cy.waitForLoad(); + cy.waitForLoad(); + cy.get('button[title="Upload"]').click(); + + cy.get('.mdi-minus-circle').click(); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('file_example_XLSX_50.xlsx'); + cy.waitForLoad(); + cy.get('button[title="Upload"]').click(); + cy.get('.mdi-minus-circle').click(); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('Testing_files.txt'); + cy.get('button[title="Upload"]').click(); + cy.get('.mdi-minus-circle').click(); + cy.get('input[type=file]').should('not.to.be.null'); + fileUploadInputField.attachFile('test.docx'); + cy.contains('div','test.docx (11.9 kB)').should('be.visible'); + cy.get('button[title="Upload"]').click(); + cy.contains('span','test.docx').should('be.visible'); + cy.contains('div','test.docx (11.9 kB)').should('not.exist'); + + // Verify cdogs template uplaod success message + cy.get('.v-alert__content').contains('div','Template uploaded successfully.').should('be.visible'); + //Delete form after test run + + cy.get('[data-test="canRemoveForm"]').click(); + cy.get('[data-test="continue-btn-continue"]').click(); + + }) + +}) + diff --git a/tests/functional/cypress/e2e/form-manage-form.cy.js b/tests/functional/cypress/e2e/form-manage-form.cy.js new file mode 100644 index 000000000..03da3a0c2 --- /dev/null +++ b/tests/functional/cypress/e2e/form-manage-form.cy.js @@ -0,0 +1,212 @@ +import 'cypress-drag-drop'; +import { formsettings } from '../support/login.js'; + +const depEnv = Cypress.env('depEnv'); + + +Cypress.Commands.add('waitForLoad', () => { + const loaderTimeout = 60000; + + cy.get('.nprogress-busy', { timeout: loaderTimeout }).should('not.exist'); +}); + + + +describe('Form Designer', () => { + + beforeEach(()=>{ + + + + cy.on('uncaught:exception', (err, runnable) => { + // Form.io throws an uncaught exception for missing projectid + // Cypress catches it as undefined: undefined so we can't get the text + console.log(err); + return false; + }); + }); + it('Visits the form settings page', () => { + + + cy.viewport(1000, 1100); + cy.waitForLoad(); + + formsettings(); + + + }); +// Update manage form settings + it('Checks manage form settings', () => { + cy.viewport(1000, 1100); + cy.waitForLoad(); + + cy.get('button').contains('BC Government').click(); + cy.get('div.formio-builder-form').then($el => { + const coords = $el[0].getBoundingClientRect(); + cy.get('[data-key="simplebcaddress"]') + .trigger('mousedown', { which: 1}, { force: true }) + .trigger('mousemove', coords.x, -550, { force: true }) + .trigger('mouseup', { force: true }); + cy.waitForLoad(); + //cy.get('input[name="data[label]"]').type('s'); + cy.get('button').contains('Save').click(); + //cy.get('.btn-success').click(); + + + }); + cy.intercept('GET', `/${depEnv}/api/v1/forms/*`).as('getForm'); + // Form saving + + cy.get('[data-cy=saveButton]').click(); + cy.waitForLoad(); + + + // Go to My forms + cy.wait('@getForm').then(()=>{ + let userFormsLinks = cy.get('[data-cy=userFormsLinks]'); + expect(userFormsLinks).to.not.be.null; + userFormsLinks.trigger('click'); + }); + // Filter the newly created form + cy.location('search').then(search => { + //let pathName = fullUrl.pathname + let arr = search.split('='); + let arrayValues = arr[1].split('&'); + cy.log(arrayValues[0]); + //cy.log(arrayValues[1]); + //cy.log(arrayValues[2]); + cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`); + cy.waitForLoad(); + }) + cy.get(':nth-child(1) > .v-expansion-panel > .v-expansion-panel-title > .v-expansion-panel-title__overlay').click(); + cy.get('[lang="en"] > .v-btn > .v-btn__content > .mdi-pencil').click(); + cy.get('[data-test="text-description"]').clear(); + cy.get('[data-test="text-description"]').type('test description edit'); + cy.get('[data-test="canSaveAndEditDraftsCheckbox"]').click(); + //Verify form schedule settings is not present + cy.get(':nth-child(5) > .v-card > .v-card-text').should('not.exist'); + //cy.get('span').contains('UPDATE').click(); + cy.get('.mb-5 > .v-btn--elevated').click(); + + //Publish the form + + cy.get('[data-cy="formPublishedSwitch"] > .v-input__control > .v-selection-control > .v-label > span').click(); + cy.get('span').contains('Publish Version 1'); + + cy.contains('Continue').should('be.visible'); + cy.contains('Continue').trigger('click'); + // Update Form settings after publish + + cy.get('[lang="en"] > .v-btn > .v-btn__content > .mdi-pencil').click(); + //Enable submission schedule and event subscription + + cy.contains('Form Submissions Schedule').click(); + cy.contains('Allow event subscription').click(); + cy.get(':nth-child(5) > .v-card > .v-card-text').should('be.visible'); + cy.get('input[placeholder="yyyy-mm-dd"]').click(); + // Select date for open submission + cy.get('input[placeholder="yyyy-mm-dd"]').type('2026-06-17'); + + //Checking the schedule of closing date settings + cy.contains('Schedule a closing date').click(); + cy.get('[data-test="closeSubmissionDateTime"]').should('be.visible'); + cy.get('[data-test="closeSubmissionDateTime"]').click(); + cy.get('[data-test="closeSubmissionDateTime"]').type('2026-09-17'); + cy.contains('Allow late submissions').click(); + cy.get('[data-test="afterCloseDateFor"]').should('be.visible'); + cy.get('[data-test="afterCloseDateFor"]').click(); + cy.get('[data-test="afterCloseDateFor"]').type('5'); + + cy.get('.pl-3 > :nth-child(2) > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').click(); + cy.contains('weeks').click(); + //Set Up submission period + cy.contains('Set up submission period').click(); + cy.get('[data-test="closeSubmissionDateTime"]').should('not.exist'); + cy.get('[data-test="afterCloseDateFor"]').should('not.exist'); + cy.waitForLoad(); + cy.get('input[type="number"]').then($el => { + + const rem=$el[0]; + rem.click(); + cy.get(':nth-child(4) > .v-input > .v-input__control > .v-field').click(); + cy.contains('div','This field is required.').should('be.visible'); + cy.get(rem).type('5'); + + }); + cy.get(':nth-child(4) > .v-input > .v-input__control > .v-field').click(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.waitForLoad(); + cy.contains('days').click(); + //Repeat period + cy.contains('Repeat period').click(); + cy.get('input[type="number"]').then($el => { + + const rem=$el[1]; + cy.get(rem).type('6'); + + }); + + cy.get(':nth-child(4) > :nth-child(2) > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').click(); + cy.contains('quarters').click(); + cy.get('input[type="date"]').then($el => { + + const rem=$el[1]; + rem.click(); + //checking validation message + + cy.get(rem).type('2026-12-17'); + + }); + + //Clsing date for submission + cy.contains('Set custom closing message').click(); + cy.get('textarea').type('closed for some reasons') + cy.contains('SEND Reminder email').click(); + //verification of Summary + cy.contains('span','This form will be open for submissions from').should('be.visible'); + cy.get('b').then($el => { + + const rem=$el[0]; + cy.get(rem).contains('2026-06-17').should('be.visible'); + }); + cy.contains('SEND Reminder email').click(); + cy.contains('b','2026-06-21').should('be.visible'); + cy.get('[data-test="canEditForm"]').click(); + + + + }) + it('Checks Event Subscription settings', () => { + cy.viewport(1000, 1100); + cy.waitForLoad(); + cy.get(':nth-child(2) > .v-expansion-panel > .v-expansion-panel-title > .v-expansion-panel-title__overlay').click(); + cy.get('input[placeholder="https://endpoint.gov.bc.ca/api/v1/"]').click(); + + cy.get('input[type="text"]').then($el => { + + const rem=$el[10]; + cy.get(rem).type('7'); + + }); + + cy.get('input[type="password"]').type('hi'); + + cy.get('div').contains('Please enter a valid endpoint starting with https://').should('be.visible'); + cy.get('input[placeholder="https://endpoint.gov.bc.ca/api/v1/"]').type('https://endpoint.gov.bc.ca/'); + + cy.get('.v-col > .v-btn > .v-btn__content > span').click(); + // Verify form settings updation success message + cy.get('.v-alert__content').contains('div','Your form settings have been updated successfully.').should('be.visible'); + + //Delete form after test run + cy.get('.mdi-delete').click(); + cy.get('[data-test="continue-btn-continue"]').click(); + + + + }) + + + +}) diff --git a/tests/functional/cypress/e2e/form-simple-form-publish.cy.js b/tests/functional/cypress/e2e/form-simple-form-publish.cy.js index af58f7e53..0eb43f789 100644 --- a/tests/functional/cypress/e2e/form-simple-form-publish.cy.js +++ b/tests/functional/cypress/e2e/form-simple-form-publish.cy.js @@ -76,8 +76,6 @@ describe('Form Designer', () => { let arr = search.split('='); let arrayValues = arr[1].split('&'); cy.log(arrayValues[0]); - //cy.log(arrayValues[1]); - //cy.log(arrayValues[2]); cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`); cy.waitForLoad(); diff --git a/tests/functional/cypress/e2e/form-team-management.cy.js b/tests/functional/cypress/e2e/form-team-management.cy.js index ce520187e..93e2be68c 100644 --- a/tests/functional/cypress/e2e/form-team-management.cy.js +++ b/tests/functional/cypress/e2e/form-team-management.cy.js @@ -98,42 +98,30 @@ describe('Form Designer', () => { cy.get(':nth-child(5) > .v-chip__content').click(); cy.get('.v-btn--elevated > .v-btn__content > span').click(); // Verify member is added with proper roles - cy.get('#input-90').should('be.checked'); - cy.get('#input-91').should('be.checked'); - cy.get('#input-93').should('be.checked'); + cy.get('[data-test="ApproverRoleCheckbox"]').should('be.visible'); + cy.get('[data-test="ReviewerRoleCheckbox"]').should('be.visible'); + cy.get('[data-test="TeamManagerRoleCheckbox"]').should('be.visible'); + cy.get('[data-test="ApproverRoleCheckbox"]').click({multiple:true,force:true}); //Manage column views + cy.get('.mdi-view-column').click(); - cy.get('#input-121').should('be.checked'); - cy.get('#input-122').should('be.checked'); - cy.get('#input-123').should('be.checked'); - cy.get('#input-124').should('be.checked'); - cy.get('#input-121').click(); + + cy.get('table').contains('td','Reviewer').should('be.visible'); + cy.get('table').contains('td','Approver').should('be.visible'); + + + cy.get('[data-test="filter-table"] > .v-table__wrapper > table > tbody > :nth-child(1) > :nth-child(2)').click(); cy.waitForLoad(); - cy.get('#input-121').should('not.be.checked'); + //Column view management cy.get('.search').click(); cy.get('.search').type('Designer'); - cy.get('[data-test="filter-table"] > .v-table__wrapper > table > tbody > .v-data-table__tr > :nth-child(2)').click(); + cy.get('table').contains('td','Designer').should('be.visible'); cy.get('[data-test="save-btn"] > .v-btn__content').click(); cy.waitForLoad(); - //Verify the roles on dashboard - if(depEnv=="app") - { - cy.get('#input-137').should('not.exist'); - cy.get('#input-149').should('not.be.checked'); - } - else - { - cy.get('#input-150').should('not.be.checked'); - cy.get('#input-153').should('not.be.checked'); - cy.get('#input-154').should('not.be.checked'); - - } - - - + //Remove a user from Roles cy.get('tbody > :nth-child(1) > [style="width: 1rem;"] > .v-btn').click(); cy.waitForLoad(); diff --git a/tests/functional/cypress/fixtures/SamplePPTx.pptx b/tests/functional/cypress/fixtures/SamplePPTx.pptx new file mode 100644 index 000000000..e45ab2e6f Binary files /dev/null and b/tests/functional/cypress/fixtures/SamplePPTx.pptx differ diff --git a/tests/functional/cypress/fixtures/Testing_files.txt b/tests/functional/cypress/fixtures/Testing_files.txt new file mode 100644 index 000000000..fb7fb4e1f --- /dev/null +++ b/tests/functional/cypress/fixtures/Testing_files.txt @@ -0,0 +1 @@ +Testing cdogs.txt \ No newline at end of file diff --git a/tests/functional/cypress/fixtures/file_example_XLSX_50.xlsx b/tests/functional/cypress/fixtures/file_example_XLSX_50.xlsx new file mode 100644 index 000000000..ca420719d Binary files /dev/null and b/tests/functional/cypress/fixtures/file_example_XLSX_50.xlsx differ diff --git a/tests/functional/cypress/fixtures/test.docx b/tests/functional/cypress/fixtures/test.docx new file mode 100644 index 000000000..78440df31 Binary files /dev/null and b/tests/functional/cypress/fixtures/test.docx differ