diff --git a/.eslintrc.js b/.eslintrc.js index cebd790d..52c34e0d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,10 @@ module.exports = { extends: ['next', 'plugin:storybook/recommended'], rules: { 'import/no-anonymous-default-export': 'off', - 'react/no-unescaped-entities': 'off', + 'react/no-unescaped-entities': 0, 'react/display-name': 'off', }, - ignorePatterns: ['src/**/*.stories.js'], + ignorePatterns: ['src/**/*.stories.js', 'src/**/*.stories.tsx', 'src/tours'], overrides: [ { diff --git a/.github/workflows/ui-workflow.yaml b/.github/workflows/ui-workflow.yaml new file mode 100644 index 00000000..89221dc2 --- /dev/null +++ b/.github/workflows/ui-workflow.yaml @@ -0,0 +1,98 @@ +name: Lagoon UI CI + +on: + pull_request: + branches: + - main + push: + branches: + - main + - cypress-test + +jobs: + format-check: + runs-on: ubuntu-latest + + steps: + - name: Install dependencies + run: yarn + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Formatting check + run: | + yarn add prettier@2.8.7 @trivago/prettier-plugin-sort-imports + yarn format-check + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Linting + run: | + yarn add typescript + yarn lint + + cypress-tests: + runs-on: ubuntu-latest + + needs: [format-check, lint] + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build and start Lagoon-minimal + run: | + cd test + make up + + - name: Start ui + env: + GRAPHQL_API: http://0.0.0.0:33000/graphql + KEYCLOAK_API: http://0.0.0.0:38088/auth + run: | + yarn install + yarn build + yarn start & + + - name: Run RBAC Cypress Tests + uses: cypress-io/github-action@v6 + with: + config-file: ./cypress/cypress.config.ts + auto-cancel-after-failures: 1 + wait-on: 'http://localhost:3000' + command: yarn cypress:runRbac + continue-on-error: true + + - name: Run General Cypress Tests + uses: cypress-io/github-action@v6 + with: + config-file: ./cypress/cypress.config.ts + auto-cancel-after-failures: 1 + wait-on: 'http://localhost:3000' + command: yarn cypress:runGeneral + continue-on-error: true + + - name: Run Organization Cypress Tests + uses: cypress-io/github-action@v6 + with: + config-file: ./cypress/cypress.config.ts + auto-cancel-after-failures: 1 + wait-on: 'http://localhost:3000' + command: yarn cypress:runOrganizations + continue-on-error: true + + - name: Stop Docker containers + run: | + cd test + make down \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 0c54d95b..97441eb7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ build +public *.min.* *.yaml *.yml \ No newline at end of file diff --git a/.storybook/decorators/withLoadingSkeletons.tsx b/.storybook/decorators/withLoadingSkeletons.tsx index f2bfc714..aaff4075 100644 --- a/.storybook/decorators/withLoadingSkeletons.tsx +++ b/.storybook/decorators/withLoadingSkeletons.tsx @@ -2,8 +2,9 @@ import React from 'react'; import ThemedSkeletonWrapper from '../../src/styles/ThemedSkeletonWrapper'; import { darkTheme, lightTheme } from '../../src/styles/theme'; + const withLoadingSkeletons = (Story, context) => { - const initialTheme = (context.globals.theme === '' || context.globals.theme ==="dark") ? 'dark' : "light"; + const initialTheme = context.globals.theme === '' || context.globals.theme === 'dark' ? 'dark' : 'light'; const { //@ts-ignore skeleton: { base, highlight }, diff --git a/.storybook/decorators/withMockedAppContext.tsx b/.storybook/decorators/withMockedAppContext.tsx index 7728ca0a..c2618462 100644 --- a/.storybook/decorators/withMockedAppContext.tsx +++ b/.storybook/decorators/withMockedAppContext.tsx @@ -14,7 +14,7 @@ const withMockedAppContext = initialTheme => (Story, context) => { useEffect(() => { if (!isDocsPage) { - const initialTheme = (context.globals.theme === '' || context.globals.theme ==="dark") ? 'dark' : "light"; + const initialTheme = context.globals.theme === '' || context.globals.theme === 'dark' ? 'dark' : 'light'; const newBg = initialTheme === 'dark' ? '#333333' : '#F8F8F8'; setGlobals({ diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html index 72c52485..6ecfe7f8 100644 --- a/.storybook/manager-head.html +++ b/.storybook/manager-head.html @@ -1,2 +1 @@ - - \ No newline at end of file + diff --git a/.storybook/manager.ts b/.storybook/manager.ts index 02d8ca02..0eb379a0 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -4,7 +4,7 @@ import LagoonTheme from './lagoonTheme'; addons.setConfig({ theme: LagoonTheme, - sidebar:{ + sidebar: { showRoots: false, - } + }, }); diff --git a/.storybook/mocks/api.ts b/.storybook/mocks/api.ts index 07d15835..fc80831d 100644 --- a/.storybook/mocks/api.ts +++ b/.storybook/mocks/api.ts @@ -4,11 +4,11 @@ import { ProblemIdentifier, generateEnvironments, organizationEmails, + organizationEnvironments, organizationGroups, organizationNotifications, organizationOwners, organizationProjects, - organizationEnvironments } from './mocks'; faker.setDefaultRefDate(new Date(`${new Date().getFullYear().toString()}-01-01`)); @@ -126,7 +126,7 @@ export const MockBulkDeployments = (seed: number) => { name: faker.lorem.slug({ min: 1, max: 5 }), created: faker.date.past().toDateString(), started: faker.date.past().toDateString(), - buildStep:faker.word.words(), + buildStep: faker.word.words(), completed: faker.date.past().toDateString(), buildLog: faker.word.words(), bulkId: faker.string.uuid(), diff --git a/.storybook/mocks/mocks.ts b/.storybook/mocks/mocks.ts index 9966a8cf..1a8a6c2f 100644 --- a/.storybook/mocks/mocks.ts +++ b/.storybook/mocks/mocks.ts @@ -10,6 +10,7 @@ interface Tasks { created: string; service: string; status: string; + id: string; }[]; environmentSlug: string; projectSlug: string; @@ -27,8 +28,9 @@ interface Task { status: string; files: TaskFile[]; logs: string; - taskName?: string; + taskName: string; name?: string; + id: string; }; } faker.setDefaultRefDate(new Date(`${new Date().getFullYear().toString()}-01-01`)); @@ -45,6 +47,7 @@ export function createTasks(): Tasks { created: faker.date.anytime().toDateString(), service: 'cli', status: faker.helpers.arrayElement(['Pending', 'In progress', 'Completed']), + id: faker.string.uuid(), }; }); @@ -66,7 +69,8 @@ export function createTask(): Task { `::group::${eventName}\n` + `::${status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)]}:: Job '${jobName}'\n` + `::step-start::${stepName}\n` + - `::${status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)] + `::${ + status[Math.floor(faker.number.int({ min: 0, max: 1 }) * status.length)] }:: Job '${jobName}' step '${stepName}'\n` + `::step-end::${stepName}::${duration}\n` + `${generateLogMessage()}\n` + @@ -88,6 +92,7 @@ export function createTask(): Task { files, logs: log, taskName: faker.lorem.slug(2), + id: faker.string.uuid(), }, }; } @@ -311,7 +316,7 @@ export const getDeployment = (seed: number) => { status: deployStatus(), created, started, - buildStep:faker.word.words(3), + buildStep: faker.word.words(3), completed, environment: generateEnvironments({ seed }), remoteId: faker.number.int(), diff --git a/.storybook/styles/globalStyles.scss b/.storybook/styles/globalStyles.scss index fcff5649..b239c0d6 100644 --- a/.storybook/styles/globalStyles.scss +++ b/.storybook/styles/globalStyles.scss @@ -1,154 +1,153 @@ * { - box-sizing: border-box; + box-sizing: border-box; } html, body { - scroll-behavior: smooth; - font-family: 'source-sans-pro', sans-serif; - font-size: 16px; - height: 100%; - line-height: 1.25rem; - overflow-x: hidden; + scroll-behavior: smooth; + font-family: 'source-sans-pro', sans-serif; + font-size: 16px; + height: 100%; + line-height: 1.25rem; + overflow-x: hidden; } .content-wrapper { - flex: 1 0 auto; - width: 100%; + flex: 1 0 auto; + width: 100%; } #__next { - display: flex; - flex-direction: column; - min-height: 100vh; + display: flex; + flex-direction: column; + min-height: 100vh; } .lagoon-wrapper { - background: rgb(250, 250, 252); - display: flex; - flex-direction: column; - min-height: 100vh; + background: rgb(250, 250, 252); + display: flex; + flex-direction: column; + min-height: 100vh; } a { - text-decoration: none; - - &.hover-state { - position: relative; - transition: all 0.2s ease-in-out; - - &::before, - &::after { - content: ''; - position: absolute; - bottom: 0; - width: 0; - height: 1px; - transition: all 0.2s ease-in-out; - transition-duration: 0.75s; - opacity: 0; - } - - &::after { - left: 0; - background-color: #497ffa; - } - - &:hover { - - &::before, - &::after { - width: 100%; - opacity: 1; - } - } + text-decoration: none; + + &.hover-state { + position: relative; + transition: all 0.2s ease-in-out; + + &::before, + &::after { + content: ''; + position: absolute; + bottom: 0; + width: 0; + height: 1px; + transition: all 0.2s ease-in-out; + transition-duration: 0.75s; + opacity: 0; } + + &::after { + left: 0; + background-color: #497ffa; + } + + &:hover { + &::before, + &::after { + width: 100%; + opacity: 1; + } + } + } } mark { - color: #000; + color: #000; } .bulk-label a:link, .bulk-label a:visited, .bulk-label a:hover, .bulk-label a:active { - color: #fff; + color: #fff; } p { - margin: 0 0 1.25rem; + margin: 0 0 1.25rem; } p a { - text-decoration: none; - transition: background 0.3s ease-out; + text-decoration: none; + transition: background 0.3s ease-out; } strong { - font-weight: normal; + font-weight: normal; } em { - font-style: normal; + font-style: normal; } h2 { - font-size: 36px; - line-height: 42px; - font-weight: normal; - margin: 0 0 38px; + font-size: 36px; + line-height: 42px; + font-weight: normal; + margin: 0 0 38px; } h3 { - font-size: 30px; - line-height: 42px; - font-weight: normal; - margin: 0 0 36px; + font-size: 30px; + line-height: 42px; + font-weight: normal; + margin: 0 0 36px; } h4 { - font-size: 25px; - line-height: 38px; - font-weight: normal; - margin: 4px 0 0; + font-size: 25px; + line-height: 38px; + font-weight: normal; + margin: 4px 0 0; } ul { - list-style: none; - margin: 0 0 1.25rem; - padding-left: 0; - - li { - background-size: 8px; - margin-bottom: 1.25rem; - padding-left: 20px; - - a { - text-decoration: none; - } + list-style: none; + margin: 0 0 1.25rem; + padding-left: 0; + + li { + background-size: 8px; + margin-bottom: 1.25rem; + padding-left: 20px; + + a { + text-decoration: none; } + } } ol { - margin: 0 0 1.25rem; - padding-left: 20px; + margin: 0 0 1.25rem; + padding-left: 20px; - li { - margin-bottom: 1.25rem; + li { + margin-bottom: 1.25rem; - a { - text-decoration: none; - } + a { + text-decoration: none; } + } } .field { - line-height: 25px; + line-height: 25px; - a { - color: #497ffa; - } + a { + color: #497ffa; + } } button, @@ -156,128 +155,126 @@ input, optgroup, select, textarea { - font-family: 'source-sans-pro', sans-serif; - line-height: 1.25rem; + font-family: 'source-sans-pro', sans-serif; + line-height: 1.25rem; } button { - &.hover-state { - all: unset; - display: inline-block; - margin-right: 1rem; - position: relative; - transition: all 0.2s ease-in-out; - cursor: pointer; - - &::before, - &::after { - content: ''; - position: absolute; - bottom: 0; - width: 0; - height: 1px; - transition: all 0.2s ease-in-out; - transition-duration: 0.75s; - opacity: 0; - } - - &::after { - left: 0; - background-color: #497ffa; - ; - } - - &:hover { - - &::before, - &::after { - width: 100%; - opacity: 1; - } - } + &.hover-state { + all: unset; + display: inline-block; + margin-right: 1rem; + position: relative; + transition: all 0.2s ease-in-out; + cursor: pointer; + + &::before, + &::after { + content: ''; + position: absolute; + bottom: 0; + width: 0; + height: 1px; + transition: all 0.2s ease-in-out; + transition-duration: 0.75s; + opacity: 0; } + + &::after { + left: 0; + background-color: #497ffa; + } + + &:hover { + &::before, + &::after { + width: 100%; + opacity: 1; + } + } + } } label { - font-family: 'source-code-pro', sans-serif; - font-size: 13px; - text-transform: uppercase; + font-family: 'source-code-pro', sans-serif; + font-size: 13px; + text-transform: uppercase; } .field-wrapper { - display: flex; - margin-bottom: 18px; + display: flex; + margin-bottom: 18px; - @media (min-width: 600px) { - margin-bottom: 30px; - } + @media (min-width: 600px) { + margin-bottom: 30px; + } - &::before { - @media (min-width: 600px) { - margin-top: 8px; - background-position: center; - background-repeat: no-repeat; - background-size: 21px; - border-right: 1px solid #ebecf0; - content: ''; - display: flex; - height: 60px; - padding-right: 28px; - width: 25px; - } + &::before { + @media (min-width: 600px) { + margin-top: 8px; + background-position: center; + background-repeat: no-repeat; + background-size: 21px; + border-right: 1px solid #ebecf0; + content: ''; + display: flex; + height: 60px; + padding-right: 28px; + width: 25px; } + } - &>div { - @media (min-width: 600px) { - margin-top: 8px; - } + & > div { + @media (min-width: 600px) { + margin-top: 8px; } + } } main { - margin: 0 !important; - padding: 62px !important; + margin: 0 !important; + padding: 62px !important; } .modal__overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 100; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; } .modal__content { - position: absolute; - top: 50%; - left: 50%; - right: auto; - bottom: auto; - margin-right: -50%; - transform: translate(-50%, -50%); - border: 1px solid #ebecf0; - overflow: auto; - -webkit-overflow-scrolling: touch; - border-radius: 4px; - outline: none; - padding: 20px; - max-width: 90vw; - - @media (min-width: 960px) { - max-width: 60vw; - } - - @media (min-width: 1400px) { - max-width: 40vw; - } + position: absolute; + top: 50%; + left: 50%; + right: auto; + bottom: auto; + margin-right: -50%; + transform: translate(-50%, -50%); + border: 1px solid #ebecf0; + overflow: auto; + -webkit-overflow-scrolling: touch; + border-radius: 4px; + outline: none; + padding: 20px; + max-width: 90vw; + + @media (min-width: 960px) { + max-width: 60vw; + } + + @media (min-width: 1400px) { + max-width: 40vw; + } } #nprogress .bar { - background-color: #497ffa !important; - position: fixed !important; - top: 55px !important; - z-index: 9999 !important; - width: 100% !important; - height: 5px !important; -} \ No newline at end of file + background-color: #497ffa !important; + position: fixed !important; + top: 55px !important; + z-index: 9999 !important; + width: 100% !important; + height: 5px !important; +} diff --git a/README.md b/README.md index 3a0d0655..b4f9f429 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,53 @@ It runs during the build step but can also be ran during development by `yarn li Linter and TS configs are both located in the root of the project as `.eslintrc.js` and `tsconfig.json` +## Testing + +Lagoon UI uses cypress for e2e tests. + +A couple of environment variables are required: + +- email - keycloak user +- password - keycloak password +- keycloak - Keycloak url (used for cypress sessions) +- api - GraphQL api endpoint +- url - running UI instance url +- user_guest - user with guest role +- user_reporter - user with reporter role +- user_developer - user with developer role +- user_maintainer - user with maintainer role +- user_owner - user with owner role +- user_orguser - Organization user +- user_orgviewer - Organization viewer +- user_orgowner - Organization owner +- user_platformowner - Platform owner + +These environment variables can either be inlined or saved in `Cypress.config.ts` file: + +```ts +import { defineConfig } from 'cypress' + +export default defineConfig({ + env: { + foo: 'bar', + CYPRESS_CY_EMAIL: ... + ... + }, +}) +``` + +To open cypress in a browser: + +```sh +npx cypress open +``` + +To run cypress tests in headless mode: + +```sh +npx cypress run +``` + ## Styling Lagoon-UI uses styled-components and it's recommended to use separete files for styling for each component. diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000..aecc0dd2 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/cypress.config.ts b/cypress/cypress.config.ts new file mode 100644 index 00000000..9954393f --- /dev/null +++ b/cypress/cypress.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + requestTimeout: 15000, + e2e: { + env: { + api: 'http://0.0.0.0:33000/graphql', + keycloak: 'http://0.0.0.0:38088', + url: 'http://0.0.0.0:3000', + user_guest: 'guest@example.com', + user_reporter: 'reporter@example.com', + user_developer: 'developer@example.com', + user_maintainer: 'maintainer@example.com', + user_owner: 'owner@example.com', + // orgs + user_orguser: 'orguser@example.com', + user_orgviewer: 'orgviewer@example.com', + user_orgowner: 'orgowner@example.com', + // top level user for all default tests + user_platformowner: 'platformowner@example.com', + }, + }, +}); diff --git a/cypress/e2e/general/backups.cy.ts b/cypress/e2e/general/backups.cy.ts new file mode 100644 index 00000000..aff5fca0 --- /dev/null +++ b/cypress/e2e/general/backups.cy.ts @@ -0,0 +1,38 @@ +import BackupsAction from 'cypress/support/actions/backups/BackupsAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const backups = new BackupsAction(); + +describe('Backups page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Retrieves a backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/backups`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addRestore'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + backups.doRetrieveBackup(); + }); + + it('Changes shown results', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.waitForNetworkIdle('@idle', 500); + backups.doResultsLimitedchangeCheck(10); + cy.waitForNetworkIdle('@idle', 500); + backups.doResultsLimitedchangeCheck(25); + cy.waitForNetworkIdle('@idle', 500); + backups.doResultsLimitedchangeCheck(50); + cy.waitForNetworkIdle('@idle', 500); + backups.doResultsLimitedchangeCheck(100); + cy.waitForNetworkIdle('@idle', 500); + backups.doResultsLimitedchangeCheck('all'); + }); +}); diff --git a/cypress/e2e/general/deployment.cy.ts b/cypress/e2e/general/deployment.cy.ts new file mode 100644 index 00000000..522e429b --- /dev/null +++ b/cypress/e2e/general/deployment.cy.ts @@ -0,0 +1,40 @@ +import deploymentAction from 'cypress/support/actions/deployment/DeploymentAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const deployment = new deploymentAction(); + +describe('Deployment page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Cancels a deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doCancelDeployment(); + }); + + it('Toggles logviewer parsed/raw', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doToggleLogViewer(); + }); + + it('Checks accordion headings toggling content', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.waitForNetworkIdle('@idle', 500); + deployment.navigateToRunningDeployment(); + deployment.doLogViewerCheck(); + }); +}); diff --git a/cypress/e2e/general/deployments.cy.ts b/cypress/e2e/general/deployments.cy.ts new file mode 100644 index 00000000..2b958df6 --- /dev/null +++ b/cypress/e2e/general/deployments.cy.ts @@ -0,0 +1,47 @@ +import DeploymentsAction from 'cypress/support/actions/deployments/DeploymentsAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const deployments = new DeploymentsAction(); + +describe('Deployments page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Does a deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doDeployment(); + }); + it('Cancels a deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployments.doCancelDeployment(); + }); + + it('Changes shown results', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doResultsLimitedchangeCheck(10); + cy.waitForNetworkIdle('@idle', 500); + deployments.doResultsLimitedchangeCheck(25); + cy.waitForNetworkIdle('@idle', 500); + deployments.doResultsLimitedchangeCheck(50); + cy.waitForNetworkIdle('@idle', 500); + deployments.doResultsLimitedchangeCheck(100); + cy.waitForNetworkIdle('@idle', 500); + deployments.doResultsLimitedchangeCheck('all'); + }); +}); diff --git a/cypress/e2e/general/environment.cy.ts b/cypress/e2e/general/environment.cy.ts new file mode 100644 index 00000000..31e9de8a --- /dev/null +++ b/cypress/e2e/general/environment.cy.ts @@ -0,0 +1,30 @@ +import EnvOverviewAction from 'cypress/support/actions/envOverview/EnvOverviewAction'; +import { aliasMutation } from 'cypress/utils/aliasQuery'; + +const environmentOverview = new EnvOverviewAction(); + +describe('Environment page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + }); + + it('Checks environment details', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + environmentOverview.doEnvTypeCheck(); + environmentOverview.doDeployTypeCheck(); + + environmentOverview.doSourceCheck(); + + environmentOverview.doRoutesCheck(); + }); + it('Deletes the environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironment('main'); + }); +}); diff --git a/cypress/e2e/general/login.cy.ts b/cypress/e2e/general/login.cy.ts new file mode 100644 index 00000000..4fe72c72 --- /dev/null +++ b/cypress/e2e/general/login.cy.ts @@ -0,0 +1,45 @@ +import { registerIdleHandler } from 'cypress/utils/aliasQuery'; + +describe('Initial login and theme spec', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Logins and gets redirected to /projects with correct user displayed', () => { + cy.visit(Cypress.env('url')); + + cy.location('pathname').should('equal', '/projects'); + cy.getBySel('headerMenu').should('contain', Cypress.env('user_owner')); + }); + + it('Switches themes', () => { + cy.visit(Cypress.env('url')); + + cy.waitForNetworkIdle('@idle', 500); + + cy.getBySel('themeToggler').then($toggler => { + if (localStorage.getItem('theme') === 'dark') { + cy.wrap($toggler).click(); + + cy.window().its('localStorage.theme').should('eq', 'light'); + // revert to dark + cy.getBySel('themeToggler').click(); + } else { + cy.wrap($toggler).click(); + + cy.window().its('localStorage.theme').should('eq', 'dark'); + } + }); + }); + + it('Logs out', () => { + cy.visit(Cypress.env('url')); + cy.getBySel('headerMenu').click(); + cy.getBySel('logout').click(); + + cy.origin(Cypress.env('keycloak'), () => { + cy.contains('Sign in to your account'); + }); + }); +}); diff --git a/cypress/e2e/general/navigation.cy.ts b/cypress/e2e/general/navigation.cy.ts new file mode 100644 index 00000000..68fed588 --- /dev/null +++ b/cypress/e2e/general/navigation.cy.ts @@ -0,0 +1,57 @@ +import NavigationRepository from 'cypress/support/repositories/navigation/NavigationRepository'; +import { registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const navigation = new NavigationRepository(); + +describe('Navigation tests', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Checks navigation to settings, organizations and projects pages', () => { + cy.visit(Cypress.env('url')); + + context('Navigates from /projects to /settings', () => { + cy.getBySel('headerMenu').click(); + + navigation.getLinkElement('settings').click(); + + cy.location('pathname').should('equal', '/settings'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('Navigates from /settings to /organizations', () => { + cy.getBySel('headerMenu').click(); + + navigation.getLinkElement('organizations').click(); + + cy.location('pathname').should('equal', '/organizations'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('Navigates from /organizations to /projects', () => { + cy.getBySel('headerMenu').click(); + + navigation.getLinkElement('projects').click(); + + cy.location('pathname').should('equal', '/projects'); + }); + + cy.waitForNetworkIdle('@idle', 500); + context('Navigates from /projects to /account', () => { + cy.getBySel('headerMenu').click(); + + navigation.getLinkElement('account').click(); + + const redirect = `${Cypress.env('keycloak')}/auth/realms/lagoon/account/`; + cy.origin(redirect, { args: { redirect } }, ({ redirect }) => { + cy.location().should(loc => { + expect(loc.toString()).to.eq(redirect); + }); + }); + }); + }); +}); diff --git a/cypress/e2e/general/project.cy.ts b/cypress/e2e/general/project.cy.ts new file mode 100644 index 00000000..31c5ed20 --- /dev/null +++ b/cypress/e2e/general/project.cy.ts @@ -0,0 +1,36 @@ +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; + +const project = new ProjectAction(); + +describe('Project page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + }); + + it('Navigates from /projects to a project', () => { + cy.visit(Cypress.env('url')); + + project.doNavigateToFirst(); + }); + + it('Checks sidebar values/actions', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doClipboardCheck(); + + project.doSidebarPopulatedCheck(); + + project.doExternalLinkCheck(); + }); + + it('Checks environment routes', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + project.doEnvRouteCheck(); + }); + + it('Creates a dummy environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doCreateDummyEnv(); + }); +}); diff --git a/cypress/e2e/general/projects.cy.ts b/cypress/e2e/general/projects.cy.ts new file mode 100644 index 00000000..36ba3f5e --- /dev/null +++ b/cypress/e2e/general/projects.cy.ts @@ -0,0 +1,39 @@ +import ProjectAction from '../../support/actions/projects/ProjectsAction'; + +const projects = new ProjectAction(); + +describe('Projects page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + }); + + it('Visits projects page', () => { + cy.visit(Cypress.env('url')); + + projects.doPageCheck(); + }); + + it('Checks project length and counter value', () => { + cy.visit(Cypress.env('url')); + + projects.doProjectLengthCheck(); + }); + + it('Does an empty search', () => { + cy.visit(Cypress.env('url')); + + projects.doEmptySearch(); + }); + + it('Searches for projects', () => { + cy.visit(Cypress.env('url')); + + projects.doSearch(); + }); + + it('Displays no projects (stubbed)', () => { + cy.visit(Cypress.env('url')); + + projects.doEmptyProjectCheck(); + }); +}); diff --git a/cypress/e2e/general/settings.cy.ts b/cypress/e2e/general/settings.cy.ts new file mode 100644 index 00000000..b175f82e --- /dev/null +++ b/cypress/e2e/general/settings.cy.ts @@ -0,0 +1,27 @@ +import { testData } from 'cypress/fixtures/variables'; +import SettingAction from 'cypress/support/actions/settings/SettingsAction'; + +const settings = new SettingAction(); + +describe('Settings page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + }); + + it('Checks initial SSH keys', () => { + cy.visit(`${Cypress.env('url')}/settings`); + settings.doEmptySshCheck(); + }); + + it('Adds SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.addSshKey(testData.ssh.name, testData.ssh.value); + }); + + it('Deletes SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.deleteSshKey(testData.ssh.name); + }); +}); diff --git a/cypress/e2e/general/sidebarNav.cy.ts b/cypress/e2e/general/sidebarNav.cy.ts new file mode 100644 index 00000000..08c0a5b5 --- /dev/null +++ b/cypress/e2e/general/sidebarNav.cy.ts @@ -0,0 +1,69 @@ +import { registerIdleHandler } from 'cypress/utils/aliasQuery'; + +describe('Environment sidebar navigation', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Overview/Deployments/Backups/Tasks/Vars/Problems/Facts/Insights', () => { + const suffix = '/projects/lagoon-demo/lagoon-demo-main'; + cy.visit(`${Cypress.env('url')}${suffix}`); + + context('From /Overview to /Deployments', () => { + cy.waitForNetworkIdle('@idle', 500); + + cy.getBySel('deployments-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/deployments`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /deployments to /backups', () => { + cy.getBySel('backups-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/backups`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /backups to /tasks', () => { + cy.getBySel('tasks-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/tasks`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /tasks to /variables', () => { + cy.getBySel('envvars-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/environment-variables`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /variables to /problems', () => { + cy.getBySel('problems-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/problems`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /problems to /facts', () => { + cy.getBySel('facts-tab').click(); + + cy.location('pathname').should('equal', `${suffix}/facts`); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /facts to /overview', () => { + cy.getBySel('overview-tab').first().click(); + + cy.location('pathname').should('equal', `${suffix}`); + }); + }); +}); diff --git a/cypress/e2e/general/task.cy.ts b/cypress/e2e/general/task.cy.ts new file mode 100644 index 00000000..a1489e45 --- /dev/null +++ b/cypress/e2e/general/task.cy.ts @@ -0,0 +1,24 @@ +import TaskAction from 'cypress/support/actions/task/TaskAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const task = new TaskAction(); + +describe('Task page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Cancels a running task ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + task.doNavToRunningTask(); + + task.doCancelTask(); + }); +}); diff --git a/cypress/e2e/general/tasks.cy.ts b/cypress/e2e/general/tasks.cy.ts new file mode 100644 index 00000000..cf177d33 --- /dev/null +++ b/cypress/e2e/general/tasks.cy.ts @@ -0,0 +1,91 @@ +import TasksAction from 'cypress/support/actions/tasks/TasksAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const tasks = new TasksAction(); + +describe('Tasks page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + registerIdleHandler('idle'); + }); + + it('Cancels a running task ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCancelTask(); + }); + + it('Starts cache clear task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCacheClearTask(); + }); + + it('Starts drush cron', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDrushCronTask(); + }); + it('Generates db backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbBackupTask(); + }); + it('Generates db/files backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbAndFilesBackupTask(); + }); + + it('Generates login link', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doLoginLinkTask(); + }); + it('Runs maintainer task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doMaintainerTask(); + }); + + it('Runs developer task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDeveloperTask(); + }); + + it('Changes shown tasks results', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doResultsLimitedchangeCheck(10); + cy.waitForNetworkIdle('@idle', 500); + tasks.doResultsLimitedchangeCheck(25); + cy.waitForNetworkIdle('@idle', 500); + tasks.doResultsLimitedchangeCheck(50); + cy.waitForNetworkIdle('@idle', 500); + tasks.doResultsLimitedchangeCheck(100); + cy.waitForNetworkIdle('@idle', 500); + tasks.doResultsLimitedchangeCheck('all'); + }); +}); diff --git a/cypress/e2e/general/variables.cy.ts b/cypress/e2e/general/variables.cy.ts new file mode 100644 index 00000000..8714ac05 --- /dev/null +++ b/cypress/e2e/general/variables.cy.ts @@ -0,0 +1,64 @@ +import { testData } from 'cypress/fixtures/variables'; +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; +import VariablesAction from 'cypress/support/actions/variables/VariablesAction'; +import { registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectAction(); + +const environment = new VariablesAction(); + +describe('Project variables page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_owner'), Cypress.env('user_owner')); + + cy.wait(500); + registerIdleHandler('idle'); + cy.log('Full user navigation from /projects page'); + + cy.visit(Cypress.env('url')); + + project.doNavigateToFirst(); + + environment.doEnvNavigation(); + }); + + it('Checks for no variables set', () => { + cy.contains('No Project variables set').should('exist'); + }); + it('Adds or updates a variable', () => { + const { name, value } = testData.variables[0]; + + environment.doAddVariable(name, value); + + cy.intercept('POST', Cypress.env('api')).as('addRequest'); + + cy.wait('@addRequest'); + + cy.log('check if variable was created'); + cy.get('.data-table > .data-row').should('contain', name); + }); + + it('Toggles Hide/Show values', () => { + environment.doHideShowToggle(); + + cy.log('show all values'); + + environment.doValueToggle(); + + cy.log('disable show/edit buttons'); + + environment.doHideShowToggle(); + }); + + it('Deletes a variable', () => { + const { name } = testData.variables[0]; + + environment.doDeleteVariable(name); + + cy.intercept('POST', Cypress.env('api')).as('deleteRequest'); + + cy.wait('@deleteRequest'); + + cy.contains('No Project variables set').should('exist'); + }); +}); diff --git a/cypress/e2e/organizations/groups.cy.ts b/cypress/e2e/organizations/groups.cy.ts new file mode 100644 index 00000000..4e014bf5 --- /dev/null +++ b/cypress/e2e/organizations/groups.cy.ts @@ -0,0 +1,37 @@ +import { testData } from 'cypress/fixtures/variables'; +import GroupAction from 'cypress/support/actions/organizations/GroupsAction'; +import { aliasMutation, aliasQuery, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const group = new GroupAction(); + +describe('Organization Groups page', () => { + beforeEach(() => { + // register interceptors/idle handler + cy.intercept('POST', Cypress.env('api'), req => { + aliasQuery(req, 'getOrganization'); + aliasMutation(req, 'addUserToGroup'); + aliasMutation(req, 'addGroupToOrganization'); + }); + registerIdleHandler('groupQuery'); + + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/groups`); + }); + + it('Adds a group', () => { + group.doAddGroup(testData.organizations.groups.newGroupName, testData.organizations.groups.newGroupName2); + }); + + it('Searches groups', () => { + group.doGroupSearch(testData.organizations.groups.newGroupName, testData.organizations.groups.newGroupName2); + }); + + it('Adds a member to a group', () => { + group.doAddMemberToGroup(testData.organizations.users.email); + }); + + it('Deletes groups', () => { + group.doDeleteGroup(testData.organizations.groups.newGroupName); + group.doDeleteGroup(testData.organizations.groups.newGroupName2); + }); +}); diff --git a/cypress/e2e/organizations/manage.cy.ts b/cypress/e2e/organizations/manage.cy.ts new file mode 100644 index 00000000..4c0c6332 --- /dev/null +++ b/cypress/e2e/organizations/manage.cy.ts @@ -0,0 +1,26 @@ +import { testData } from 'cypress/fixtures/variables'; +import ManageAction from 'cypress/support/actions/organizations/ManageAction'; +import { aliasMutation } from 'cypress/utils/aliasQuery'; + +const manage = new ManageAction(); + +describe('Org Manage page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/manage`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'AddUserToOrganization'); + }); + }); + + it('Adds a org viewer', () => { + manage.doAddOrgViewer(testData.organizations.manage.user); + }); + it('Should upgrade org viewer to owner', () => { + manage.doEditOrgViewer(testData.organizations.manage.user); + }); + it('Deletes user', () => { + manage.doDeleteUser(testData.organizations.manage.user); + }); +}); diff --git a/cypress/e2e/organizations/navigation.cy.ts b/cypress/e2e/organizations/navigation.cy.ts new file mode 100644 index 00000000..9b773698 --- /dev/null +++ b/cypress/e2e/organizations/navigation.cy.ts @@ -0,0 +1,55 @@ +import { registerIdleHandler } from 'cypress/utils/aliasQuery'; + +describe('Org sidebar navigation', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + registerIdleHandler('idle'); + }); + + it('Traverses sidebar nav from Groups -> Users -> Projects -> Notifications -> Manage', () => { + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + + context('From /org/id to /groups', () => { + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.groups').click(); + + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/groups'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /groups to /users', () => { + cy.get('.users').click(); + + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/users'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /users to /projects', () => { + cy.get('.projects').click(); + + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/projects'); + }); + + cy.waitForNetworkIdle('@idle', 500); + context('From /projects to /notifications', () => { + cy.get('.notifications').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/notifications'); + }); + cy.waitForNetworkIdle('@idle', 500); + + context('From /notifications to /manage', () => { + cy.get('.manage').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/manage'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + context('From /manage to /overview', () => { + cy.get('.overview').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization'); + }); + }); +}); diff --git a/cypress/e2e/organizations/notifications.cy.ts b/cypress/e2e/organizations/notifications.cy.ts new file mode 100644 index 00000000..bd331f20 --- /dev/null +++ b/cypress/e2e/organizations/notifications.cy.ts @@ -0,0 +1,58 @@ +import { testData } from 'cypress/fixtures/variables'; +import NotificationsAction from 'cypress/support/actions/organizations/NotificationsAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const notifications = new NotificationsAction(); + +describe('Org Notifications page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/notifications`); + + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addNotificationSlack'); + aliasMutation(req, 'UpdateNotificationSlack'); + aliasMutation(req, 'addNotificationRocketChat'); + aliasMutation(req, 'addNotificationMicrosoftTeams'); + aliasMutation(req, 'addNotificationEmail'); + aliasMutation(req, 'addNotificationWebhook'); + }); + }); + + it('Adds Slack notification', () => { + const slackData = testData.organizations.notifications.slack; + + notifications.doAddNotification('slack', slackData); + }); + it('Adds Rocketchat notification', () => { + const rocketData = testData.organizations.notifications.rocketChat; + notifications.doAddNotification('rocketChat', rocketData); + }); + it('Adds Teams notification', () => { + const teamsData = testData.organizations.notifications.teams; + notifications.doAddNotification('teams', teamsData); + }); + it('Adds Email notification', () => { + const emailData = testData.organizations.notifications.email; + notifications.doAddNotification('email', emailData); + }); + it('Adds Webhook notification', () => { + const webhookData = testData.organizations.notifications.webhook; + notifications.doAddNotification('webhook', webhookData); + }); + + it('Edits notification', () => { + cy.waitForNetworkIdle('@idle', 500); + notifications.doEditNotification(); + }); + + it('Deletes notifications', () => { + notifications.doDeleteNotification('webhook'); + notifications.doDeleteNotification('email'); + notifications.doDeleteNotification('teams'); + notifications.doDeleteNotification('rocketChat'); + notifications.doDeleteNotification('slack'); + }); +}); diff --git a/cypress/e2e/organizations/overview.cy.ts b/cypress/e2e/organizations/overview.cy.ts new file mode 100644 index 00000000..a02daa23 --- /dev/null +++ b/cypress/e2e/organizations/overview.cy.ts @@ -0,0 +1,34 @@ +import { testData } from 'cypress/fixtures/variables'; +import OverviewAction from 'cypress/support/actions/organizations/OverviewAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const overview = new OverviewAction(); + +describe('Organization overview page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'updateOrganizationFriendlyName'); + }); + }); + + it('Checks navigation links', () => { + overview.doNavLinkCheck(); + }); + + it('Checks quota fields', () => { + // groups, projects, notifications, envs + overview.doQuotaFieldCheck(); + }); + + it.only('Changes org friendly name/description', () => { + registerIdleHandler('idle'); + + overview.changeOrgFriendlyname(testData.organizations.overview.friendlyName); + + cy.waitForNetworkIdle('@idle', 500); + overview.changeOrgDescription(testData.organizations.overview.description); + }); +}); diff --git a/cypress/e2e/organizations/projects.cy.ts b/cypress/e2e/organizations/projects.cy.ts new file mode 100644 index 00000000..71bb9ef8 --- /dev/null +++ b/cypress/e2e/organizations/projects.cy.ts @@ -0,0 +1,27 @@ +import { testData } from 'cypress/fixtures/variables'; +import ProjectsActions from 'cypress/support/actions/organizations/ProjectsActions'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectsActions(); + +describe('Org Projects page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/projects`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addProjectToOrganization'); + aliasMutation(req, 'deleteProject'); + }); + registerIdleHandler('projectsQuery'); + }); + + it('Adds a project', () => { + cy.waitForNetworkIdle('@projectsQuery', 1000); + project.doAddProject(testData.organizations.project); + }); + + it('Deletes a project', () => { + project.doDeleteProject(testData.organizations.project.projectName); + }); +}); diff --git a/cypress/e2e/organizations/users.cy.ts b/cypress/e2e/organizations/users.cy.ts new file mode 100644 index 00000000..e3879b34 --- /dev/null +++ b/cypress/e2e/organizations/users.cy.ts @@ -0,0 +1,41 @@ +import { testData } from 'cypress/fixtures/variables'; +import GroupAction from 'cypress/support/actions/organizations/GroupsAction'; +import UsersActions from 'cypress/support/actions/organizations/UsersAction'; +import { aliasMutation, aliasQuery, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const users = new UsersActions(); +const group = new GroupAction(); + +describe('Org Users page', () => { + beforeEach(() => { + cy.login(Cypress.env('user_platformowner'), Cypress.env('user_platformowner')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/users`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasQuery(req, 'getOrganization'); + aliasMutation(req, 'addUserToGroup'); + aliasMutation(req, 'removeUserFromGroup'); + aliasMutation(req, 'addGroupToOrganization'); + }); + }); + + it('Creates a group', () => { + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/groups`); + group.doAddGroup(testData.organizations.groups.newGroupName, testData.organizations.groups.newGroupName2); + }); + + it('Adds a user to the group', () => { + users.doAddUser(testData.organizations.users.email); + }); + + it('Deletes user', () => { + users.doDeleteUser(testData.organizations.users.email); + }); + + after(() => { + registerIdleHandler('groupQuery'); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/groups`); + group.doDeleteGroup(testData.organizations.groups.newGroupName); + group.doDeleteGroup(testData.organizations.groups.newGroupName2); + }); +}); diff --git a/cypress/e2e/rbac/developer.cy.ts b/cypress/e2e/rbac/developer.cy.ts new file mode 100644 index 00000000..61780ea5 --- /dev/null +++ b/cypress/e2e/rbac/developer.cy.ts @@ -0,0 +1,272 @@ +import { testData } from 'cypress/fixtures/variables'; +import BackupsAction from 'cypress/support/actions/backups/BackupsAction'; +import deploymentAction from 'cypress/support/actions/deployment/DeploymentAction'; +import DeploymentsAction from 'cypress/support/actions/deployments/DeploymentsAction'; +import EnvOverviewAction from 'cypress/support/actions/envOverview/EnvOverviewAction'; +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; +import SettingAction from 'cypress/support/actions/settings/SettingsAction'; +import TaskAction from 'cypress/support/actions/task/TaskAction'; +import TasksAction from 'cypress/support/actions/tasks/TasksAction'; +import VariablesAction from 'cypress/support/actions/variables/VariablesAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectAction(); + +const settings = new SettingAction(); + +const variable = new VariablesAction(); + +const environmentOverview = new EnvOverviewAction(); + +const deployments = new DeploymentsAction(); + +const deployment = new deploymentAction(); + +const backups = new BackupsAction(); + +const tasks = new TasksAction(); + +const task = new TaskAction(); + +describe('DEVELOPER permission test suites', () => { + beforeEach(() => { + cy.login(Cypress.env('user_developer'), Cypress.env('user_developer')); + }); + + context('Settings', () => { + it('Adds SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.addSshKey(testData.ssh.name, testData.ssh.value); + }); + + it('Deletes SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.deleteSshKey(testData.ssh.name); + }); + }); + + context('Project overview', () => { + it('Checks environment routes', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doEnvRouteCheck(); + }); + + it('Creates environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doCreateDummyEnv(); + }); + }); + + context('Variables', () => { + it('Checks for no variables set', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.contains('No Project variables set').should('exist'); + }); + + it('Fails to add a variable - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addEnvVariable'); + }); + + const { name, value } = testData.variables[0]; + + variable.doAddVariable(name, value); + + cy.wait('@gqladdEnvVariableMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "project:add" on "env_var": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + }); + }); + + context('Environment overview', () => { + it('Checks environment details', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + environmentOverview.doEnvTypeCheck(); + environmentOverview.doDeployTypeCheck(); + + environmentOverview.doSourceCheck(); + + environmentOverview.doRoutesCheck(); + }); + it('Fails to delete PROD - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('main'); + }); + + it('Deletes stating environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironment('staging'); + }); + }); + + context('Deployments', () => { + it('Fails to do a PROD deployment - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doFailedDeployment(); + }); + + it('Does a staging deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doDeployment(); + }); + + it('Fails to cancel any deployment - no permission to CANCEL for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doFailedCancelDeployment(); + }); + }); + + context('Deployment', () => { + it('Fails to cancel deployment - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doFailedCancelDeployment(); + }); + }); + + context('Backups', () => { + it('Checks backup retrieve btns', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/backups`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + backups.doCheckAllRetrieveButtons(); + }); + }); + + context('Tasks', () => { + it('Fails to cancel a running task - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedTaskCancellation(); + }); + + it('Runs cache clear', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCacheClearTask(); + }); + + it('Runs drush cron', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDrushCronTask(); + }); + + it('Generates DB backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbBackupTask(); + }); + + it('Does a developer task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDeveloperTask(); + }); + + it('Generates DB/Files backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbAndFilesBackupTask(); + }); + + it('Fails to generate login link - no permission for DEVELOPER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushUserLogin'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedLoginLinkTask(); + }); + }); + + context('Task Page', () => { + it('Fails to cancel a running task - no permission for DEVELOPER ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + task.doNavToRunningTask(); + + task.doFailedCancelTask(); + }); + }); +}); diff --git a/cypress/e2e/rbac/guest.cy.ts b/cypress/e2e/rbac/guest.cy.ts new file mode 100644 index 00000000..38c447be --- /dev/null +++ b/cypress/e2e/rbac/guest.cy.ts @@ -0,0 +1,269 @@ +import { testData } from 'cypress/fixtures/variables'; +import BackupsAction from 'cypress/support/actions/backups/BackupsAction'; +import deploymentAction from 'cypress/support/actions/deployment/DeploymentAction'; +import DeploymentsAction from 'cypress/support/actions/deployments/DeploymentsAction'; +import EnvOverviewAction from 'cypress/support/actions/envOverview/EnvOverviewAction'; +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; +import SettingAction from 'cypress/support/actions/settings/SettingsAction'; +import TaskAction from 'cypress/support/actions/task/TaskAction'; +import TasksAction from 'cypress/support/actions/tasks/TasksAction'; +import VariablesAction from 'cypress/support/actions/variables/VariablesAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectAction(); + +const settings = new SettingAction(); + +const variable = new VariablesAction(); + +const environmentOverview = new EnvOverviewAction(); + +const deployments = new DeploymentsAction(); + +const deployment = new deploymentAction(); + +const backups = new BackupsAction(); + +const tasks = new TasksAction(); + +const task = new TaskAction(); + +describe('GUEST permission test suites', () => { + beforeEach(() => { + cy.login(Cypress.env('user_guest'), Cypress.env('user_guest')); + }); + + context('Settings', () => { + it('Adds SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.addSshKey(testData.ssh.name, testData.ssh.value); + }); + + it('Deletes SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.deleteSshKey(testData.ssh.name); + }); + }); + + context('Project overview', () => { + it('Checks environment routes', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + project.doEnvRouteCheck(); + }); + + it('Gets environment creation permission error', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doCreateEnvWithPermissionError(); + }); + }); + + context('Variables', () => { + it('Checks for no variables set', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.contains('No Project variables set').should('exist'); + }); + + it('Fails to add a variable - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addEnvVariable'); + }); + + const { name, value } = testData.variables[0]; + + variable.doAddVariable(name, value); + + cy.wait('@gqladdEnvVariableMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "project:add" on "env_var": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + }); + }); + + context('Environment overview', () => { + it('Checks environment details', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + environmentOverview.doEnvTypeCheck(); + environmentOverview.doDeployTypeCheck(); + + environmentOverview.doSourceCheck(); + + environmentOverview.doRoutesCheck(); + }); + it('Fails to delete PROD - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('main'); + }); + it('Fails to delete any env - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('staging'); + }); + }); + + context('Deployments', () => { + it('Fails to do a PROD deployment - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doFailedDeployment(); + }); + + it('Fails to do a any deployment - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doFailedDeployment(); + }); + + it('Fails to do cancel a deployment - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doFailedCancelDeployment(); + }); + }); + + context('Deployment', () => { + it('Fails to cancel deployment - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doFailedCancelDeployment(); + }); + }); + + context('Backups', () => { + it('Fails to view backups - no permission to view for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/backups`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + const errMessage = + 'Error: GraphQL error: Unauthorized: You don\'t have permission to "view" on "backup": {"project":18}'; + + cy.get('main').should('exist').find('p').should('exist').and('have.text', errMessage); + }); + }); + + context('Tasks', () => { + it('Fails to cancel a running task - no permission for GUEST', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedTaskCancellation(); + }); + + it('Runs cache clear ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCacheClearTask(); + }); + + it('Runs drush cron ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDrushCronTask(); + }); + + it('Fails to run DB backup task - no permission for GUEST ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushSqlDump'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedDbBackupTask(); + }); + + it('Fails to run DB/Files backup task - no permission for GUEST ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushArchiveDump'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedDbAndFilesBackupTask(); + }); + + it('Fails to generate login link - no permission for GUEST ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushUserLogin'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedLoginLinkTask(); + }); + }); + + context('Task Page', () => { + it('Fails to cancel a running task - no permission for GUEST ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + task.doNavToRunningTask(); + + task.doFailedCancelTask(); + }); + }); +}); diff --git a/cypress/e2e/rbac/maintainer.cy.ts b/cypress/e2e/rbac/maintainer.cy.ts new file mode 100644 index 00000000..9a7a8e49 --- /dev/null +++ b/cypress/e2e/rbac/maintainer.cy.ts @@ -0,0 +1,303 @@ +import { testData } from 'cypress/fixtures/variables'; +import deploymentAction from 'cypress/support/actions/deployment/DeploymentAction'; +import DeploymentsAction from 'cypress/support/actions/deployments/DeploymentsAction'; +import EnvOverviewAction from 'cypress/support/actions/envOverview/EnvOverviewAction'; +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; +import SettingAction from 'cypress/support/actions/settings/SettingsAction'; +import TaskAction from 'cypress/support/actions/task/TaskAction'; +import TasksAction from 'cypress/support/actions/tasks/TasksAction'; +import VariablesAction from 'cypress/support/actions/variables/VariablesAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectAction(); + +const settings = new SettingAction(); + +const variable = new VariablesAction(); + +const environmentOverview = new EnvOverviewAction(); + +const deployments = new DeploymentsAction(); + +const deployment = new deploymentAction(); + +const tasks = new TasksAction(); + +const task = new TaskAction(); + +describe('MAINTAINER permission test suites', () => { + beforeEach(() => { + cy.login(Cypress.env('user_maintainer'), Cypress.env('user_maintainer')); + }); + + context('Settings', () => { + it('Adds SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.addSshKey(testData.ssh.name, testData.ssh.value); + }); + + it('Deletes SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.deleteSshKey(testData.ssh.name); + }); + }); + + context('Project overview', () => { + it('Checks environment routes', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + project.doEnvRouteCheck(); + }); + + it('Creates environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doCreateDummyEnv(); + }); + }); + + context('Variables', () => { + it('Checks for no variables set', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.contains('No Project variables set').should('exist'); + }); + it('Adds or updates a variable', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addEnvVariable'); + }); + + const { name, value } = testData.variables[0]; + + variable.doAddVariable(name, value); + + cy.wait('@gqladdEnvVariableMutation'); + + cy.log('check if variable was created'); + cy.get('.data-table > .data-row').should('contain', name); + }); + + it('Toggles Hide/Show values', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + variable.doHideShowToggle(); + + cy.log('show all values'); + + variable.doValueToggle(); + + cy.log('disable show/edit buttons'); + + variable.doHideShowToggle(); + }); + + it('Deletes a variable', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + registerIdleHandler('idle'); + + const { name } = testData.variables[0]; + + variable.doDeleteVariable(name); + + cy.intercept('POST', Cypress.env('api')).as('deleteRequest'); + + cy.wait('@deleteRequest'); + + cy.contains('No Project variables set').should('exist'); + }); + }); + context('Environment overview', () => { + it('Checks environment details', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + environmentOverview.doEnvTypeCheck(); + environmentOverview.doDeployTypeCheck(); + + environmentOverview.doSourceCheck(); + + environmentOverview.doRoutesCheck(); + }); + it('Fails to delete PROD - no permission for maintainer', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('main'); + }); + + it('Deletes stating environment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironment('staging'); + }); + }); + + context('Deployments', () => { + it('Does a PROD deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doDeployment(); + }); + + it('Does a staging deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doDeployment(); + }); + + it('Cancels a staging deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doCancelDeployment(); + }); + + it('Cancels a PROD deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doCancelDeployment(); + }); + }); + + context('Deployment', () => { + it('Cancels a deployment', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doCancelDeployment(); + }); + }); + + context('Tasks', () => { + it('Cancels a running task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCancelTask(); + }); + + it('Runs cache clear', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCacheClearTask(); + }); + + it('Runs drush cron', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDrushCronTask(); + }); + + it('Generates DB backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbBackupTask(); + }); + + it('Does a developer task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDeveloperTask(); + }); + + it('Does a maintainer task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDeveloperTask(); + }); + + it('Generates DB/Files backup', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDbAndFilesBackupTask(); + }); + + it('Generates a login link', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushUserLogin'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + tasks.doLoginLinkTask(); + }); + }); + context('Task Page', () => { + it('Cancels a running task', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + task.doNavToRunningTask(); + + task.doCancelTask(); + }); + }); +}); diff --git a/cypress/e2e/rbac/organizations/orgViewer.cy.ts b/cypress/e2e/rbac/organizations/orgViewer.cy.ts new file mode 100644 index 00000000..704efcab --- /dev/null +++ b/cypress/e2e/rbac/organizations/orgViewer.cy.ts @@ -0,0 +1,131 @@ +import { testData } from 'cypress/fixtures/variables'; +import GroupAction from 'cypress/support/actions/organizations/GroupsAction'; +import NotificationsAction from 'cypress/support/actions/organizations/NotificationsAction'; +import OverviewAction from 'cypress/support/actions/organizations/OverviewAction'; +import ProjectsActions from 'cypress/support/actions/organizations/ProjectsActions'; +import { aliasMutation, aliasQuery, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const overview = new OverviewAction(); +const group = new GroupAction(); +const project = new ProjectsActions(); +const notifications = new NotificationsAction(); + +describe(`Organizations ORGVIEWER journey`, () => { + beforeEach(() => { + // register interceptors/idle handler + cy.intercept('POST', Cypress.env('api'), req => { + aliasQuery(req, 'getOrganization'); + aliasMutation(req, 'updateOrganizationFriendlyName'); + aliasMutation(req, 'addUserToGroup'); + aliasMutation(req, 'addGroupToOrganization'); + aliasMutation(req, 'addProjectToGroup'); + aliasMutation(req, 'addNotificationToProject'); + }); + + registerIdleHandler('idle'); + + cy.login(Cypress.env('user_orgviewer'), Cypress.env('user_orgviewer')); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + }); + + it('Fails to change org name and desc - no permission for ORGVIEWER', () => { + overview.doFailedChangeOrgFriendlyname(testData.organizations.overview.friendlyName); + overview.closeModal(); + overview.doFailedChangeOrgDescription(testData.organizations.overview.description); + overview.closeModal(); + }); + + it('Navigates to groups and fails to create a one - no permission for ORGVIEWER', () => { + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.groups').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/groups'); + + group.doFailedAddGroup( + testData.organizations.groups.newGroupName + '-viewer', + testData.organizations.groups.newGroupName2 + ); + }); + + it('Navigates to projects and fails to create a new one - no permission for ORGVIEWER', () => { + registerIdleHandler('projectsQuery'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addProjectToOrganization'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.projects').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/projects'); + cy.waitForNetworkIdle('@projectsQuery', 1000); + + project.doFailedaddProject(testData.organizations.project); + }); + + it('Navigates to notifications and fails to create any - no permission for ORVIEWER', () => { + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addNotificationSlack'); + aliasMutation(req, 'UpdateNotificationSlack'); + aliasMutation(req, 'addNotificationRocketChat'); + aliasMutation(req, 'addNotificationMicrosoftTeams'); + aliasMutation(req, 'addNotificationEmail'); + aliasMutation(req, 'addNotificationWebhook'); + }); + + registerIdleHandler('notificationsQuery'); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.notifications').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/notifications'); + cy.waitForNetworkIdle('@notificationsQuery', 1000); + + const { slack: slackData, email: emailData, webhook: webhookData } = testData.organizations.notifications; + + notifications.doFailedAddNotification('slack', slackData); + notifications.closeModal(); + + notifications.doFailedAddNotification('email', emailData); + notifications.closeModal(); + notifications.doFailedAddNotification('webhook', webhookData); + notifications.closeModal(); + }); + + it('Navigates to a project, fails to add a group or notifications - no permission for ORGVIEWER', () => { + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization/projects/lagoon-demo-org`); + + cy.getBySel('addGroupToProject').click(); + + cy.get('.react-select__indicator').click({ force: true }); + cy.get('#react-select-2-option-0').click(); + + cy.log('Fail to add notifications'); + cy.getBySel('addGroupToProjectConfirm').click(); + + cy.wait('@gqladdProjectToGroupMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + const errorMessage = `Unauthorized: You don't have permission to "addGroup" on "organization": {"organization":1}`; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + + // close modal + cy.get('.modal__overlay').click({ force: true }); + + cy.getBySel('addNotificationToProject').click(); + + cy.get('[class$=control]').click({ force: true }); + cy.get('#react-select-3-option-0').click(); + + cy.getBySel('addNotificationToProjectConfirm').click(); + + cy.wait('@gqladdNotificationToProjectMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + const errorMessage = `Unauthorized: You don't have permission to "addNotification" on "organization": {"organization":1}`; + + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + }); +}); diff --git a/cypress/e2e/rbac/organizations/platformAndOrgOwnerJourney.cy.ts b/cypress/e2e/rbac/organizations/platformAndOrgOwnerJourney.cy.ts new file mode 100644 index 00000000..b416feef --- /dev/null +++ b/cypress/e2e/rbac/organizations/platformAndOrgOwnerJourney.cy.ts @@ -0,0 +1,162 @@ +import { testData } from 'cypress/fixtures/variables'; +import GroupAction from 'cypress/support/actions/organizations/GroupsAction'; +import NotificationsAction from 'cypress/support/actions/organizations/NotificationsAction'; +import OverviewAction from 'cypress/support/actions/organizations/OverviewAction'; +import ProjectsActions from 'cypress/support/actions/organizations/ProjectsActions'; +import { aliasMutation, aliasQuery, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const overview = new OverviewAction(); +const group = new GroupAction(); +const project = new ProjectsActions(); +const notifications = new NotificationsAction(); + +const orgownerAndPlatformOwner = [Cypress.env('user_platformowner'), Cypress.env('user_orgowner')]; + +orgownerAndPlatformOwner.forEach(owner => { + const desc = { + [Cypress.env('user_platformowner')]: 'Platform owner', + [Cypress.env('user_orgowner')]: 'Org owner', + }; + + describe(`Organizations ${desc[owner]} journey`, () => { + beforeEach(() => { + // register interceptors/idle handler + cy.intercept('POST', Cypress.env('api'), req => { + aliasQuery(req, 'getOrganization'); + aliasMutation(req, 'updateOrganizationFriendlyName'); + aliasMutation(req, 'addUserToGroup'); + aliasMutation(req, 'addGroupToOrganization'); + }); + + registerIdleHandler('idle'); + + cy.login(owner, owner); + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + }); + + if (owner === Cypress.env('user_orgowner')) { + it('Fails to change org name and desc - no permission for ORGOWNER', () => { + overview.doFailedChangeOrgFriendlyname(testData.organizations.overview.friendlyName); + overview.closeModal(); + overview.doFailedChangeOrgDescription(testData.organizations.overview.description); + overview.closeModal(); + }); + } else { + it('Changes org name and desc', () => { + overview.changeOrgFriendlyname(testData.organizations.overview.friendlyName); + overview.changeOrgDescription(testData.organizations.overview.description); + }); + } + + it('Navigates to groups and creates', () => { + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.groups').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/groups'); + + group.doAddGroup(testData.organizations.groups.newGroupName, testData.organizations.groups.newGroupName2); + registerIdleHandler('groupQuery'); + group.doAddMemberToGroup(testData.organizations.users.email); + }); + + it('Navigates to projects and creates a new one', () => { + registerIdleHandler('projectsQuery'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addProjectToOrganization'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + cy.get('.projects').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/projects'); + cy.waitForNetworkIdle('@projectsQuery', 1000); + + project.doAddProject(testData.organizations.project); + }); + + it('Navigates to notifications and creates a couple', () => { + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addNotificationSlack'); + aliasMutation(req, 'UpdateNotificationSlack'); + aliasMutation(req, 'addNotificationRocketChat'); + aliasMutation(req, 'addNotificationMicrosoftTeams'); + aliasMutation(req, 'addNotificationEmail'); + aliasMutation(req, 'addNotificationWebhook'); + }); + + registerIdleHandler('notificationsQuery'); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.notifications').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/notifications'); + cy.waitForNetworkIdle('@notificationsQuery', 1000); + + const { slack: slackData, email: emailData, webhook: webhookData } = testData.organizations.notifications; + + notifications.doAddNotification('slack', slackData); + notifications.doAddNotification('email', emailData); + notifications.doAddNotification('webhook', webhookData); + }); + + it('Navigates to a project, adds a group and notifications', () => { + cy.visit( + `${Cypress.env('url')}/organizations/lagoon-demo-organization/projects/${ + testData.organizations.project.projectName + }` + ); + + cy.getBySel('addGroupToProject').click(); + + cy.get('.react-select__indicator').click({ force: true }); + cy.get('#react-select-2-option-0').click(); + + cy.getBySel('addGroupToProjectConfirm').click(); + + cy.log('add notifications'); + + cy.getBySel('addNotificationToProject').click(); + + cy.get('[class$=control]').click({ force: true }); + cy.get('#react-select-3-option-0').click(); + + cy.getBySel('addNotificationToProjectConfirm').click(); + }); + + // cleanup + after(() => { + registerIdleHandler('projectsQuery'); + registerIdleHandler('groupQuery'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'removeNotification'); + aliasMutation(req, 'deleteGroup'); + aliasMutation(req, 'deleteProject'); + }); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.groups').click(); + + group.doDeleteGroup(testData.organizations.groups.newGroupName); + cy.wait('@gqldeleteGroupMutation'); + + group.doDeleteGroup(testData.organizations.groups.newGroupName2); + cy.wait('@gqldeleteGroupMutation'); + + cy.waitForNetworkIdle('@idle', 500); + cy.get('.projects').click(); + + cy.waitForNetworkIdle('@projectsQuery', 1000); + + project.doDeleteProject(testData.organizations.project.projectName); + + cy.get('.notifications').click(); + + cy.waitForNetworkIdle('@idle', 500); + + notifications.doDeleteNotification('webhook'); + cy.wait('@gqlremoveNotificationMutation'); // wait for a delete mutation instead + notifications.doDeleteNotification('email'); + cy.wait('@gqlremoveNotificationMutation'); + notifications.doDeleteNotification('slack'); + }); + }); +}); diff --git a/cypress/e2e/rbac/reporter.cy.ts b/cypress/e2e/rbac/reporter.cy.ts new file mode 100644 index 00000000..75d3f7b0 --- /dev/null +++ b/cypress/e2e/rbac/reporter.cy.ts @@ -0,0 +1,266 @@ +import { testData } from 'cypress/fixtures/variables'; +import deploymentAction from 'cypress/support/actions/deployment/DeploymentAction'; +import DeploymentsAction from 'cypress/support/actions/deployments/DeploymentsAction'; +import EnvOverviewAction from 'cypress/support/actions/envOverview/EnvOverviewAction'; +import ProjectAction from 'cypress/support/actions/project/ProjectAction'; +import SettingAction from 'cypress/support/actions/settings/SettingsAction'; +import TaskAction from 'cypress/support/actions/task/TaskAction'; +import TasksAction from 'cypress/support/actions/tasks/TasksAction'; +import VariablesAction from 'cypress/support/actions/variables/VariablesAction'; +import { aliasMutation, registerIdleHandler } from 'cypress/utils/aliasQuery'; + +const project = new ProjectAction(); + +const settings = new SettingAction(); + +const variable = new VariablesAction(); + +const environmentOverview = new EnvOverviewAction(); + +const deployments = new DeploymentsAction(); + +const deployment = new deploymentAction(); + +const tasks = new TasksAction(); + +const task = new TaskAction(); + +describe('REPORTER permission test suites', () => { + beforeEach(() => { + cy.login(Cypress.env('user_reporter'), Cypress.env('user_reporter')); + }); + + context('Settings', () => { + it('Adds SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.addSshKey(testData.ssh.name, testData.ssh.value); + }); + + it('Deletes SSH key', () => { + cy.visit(`${Cypress.env('url')}/settings`); + + settings.deleteSshKey(testData.ssh.name); + }); + }); + + context('Project overview', () => { + it('Checks environment routes', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + project.doEnvRouteCheck(); + }); + + it('Gets environment creation permission error', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo`); + + project.doCreateEnvWithPermissionError(); + }); + }); + + context('Variables', () => { + it('Checks for no variables set', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.contains('No Project variables set').should('exist'); + }); + + it('Fails to add a variable - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/project-variables`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'addEnvVariable'); + }); + + const { name, value } = testData.variables[0]; + + variable.doAddVariable(name, value); + + cy.wait('@gqladdEnvVariableMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "project:add" on "env_var": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + }); + }); + context('Environment overview', () => { + it('Checks environment details', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + environmentOverview.doEnvTypeCheck(); + environmentOverview.doDeployTypeCheck(); + + environmentOverview.doSourceCheck(); + + environmentOverview.doRoutesCheck(); + }); + it('Fails to delete environment - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('main'); + }); + + it('Fails to delete any env - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deleteEnvironment'); + }); + + environmentOverview.doDeleteEnvironmentError('staging'); + }); + }); + + context('Deployments', () => { + it('Fails to do a PROD deployment - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doFailedDeployment(); + }); + + it('Fails to do a any deployment - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'deployEnvironmentLatest'); + }); + + deployments.doFailedDeployment(); + }); + + it('Fails to do cancel a deployment - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-staging/deployments`); + + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + deployments.doFailedCancelDeployment(); + }); + }); + + context('Deployment', () => { + it('Fails to cancel deployment - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/deployments`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelDeployment'); + }); + cy.waitForNetworkIdle('@idle', 500); + + deployment.navigateToRunningDeployment(); + deployment.doFailedCancelDeployment(); + }); + }); + + context('Backups', () => { + it('Fails to view backups - no permission to view for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/backups`); + registerIdleHandler('idle'); + + cy.waitForNetworkIdle('@idle', 500); + + const errMessage = + 'Error: GraphQL error: Unauthorized: You don\'t have permission to "view" on "backup": {"project":18}'; + + cy.get('main').should('exist').find('p').should('exist').and('have.text', errMessage); + }); + }); + + context('Tasks', () => { + it('Fails to cancel a running task - no permission for REPORTER', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedTaskCancellation(); + }); + + it('Runs cache clear ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doCacheClearTask(); + }); + + it('Runs drush cron ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doDrushCronTask(); + }); + + it('Fails to run DB backup task - no permission for REPORTER ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushSqlDump'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedDbBackupTask(); + }); + + it('Fails to run DB/Files backup task - no permission for REPORTER ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushArchiveDump'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedDbAndFilesBackupTask(); + }); + + it('Fails to generate login link - no permission for REPORTER ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + registerIdleHandler('idle'); + + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'taskDrushUserLogin'); + }); + cy.waitForNetworkIdle('@idle', 500); + + tasks.doFailedLoginLinkTask(); + }); + }); + context('Task Page', () => { + it('Fails to cancel a running task - no permission for REPORTER ', () => { + cy.visit(`${Cypress.env('url')}/projects/lagoon-demo/lagoon-demo-main/tasks`); + + registerIdleHandler('idle'); + cy.intercept('POST', Cypress.env('api'), req => { + aliasMutation(req, 'cancelTask'); + }); + + cy.waitForNetworkIdle('@idle', 500); + + task.doNavToRunningTask(); + + task.doFailedCancelTask(); + }); + }); +}); diff --git a/cypress/fixtures/variables.ts b/cypress/fixtures/variables.ts new file mode 100644 index 00000000..4b23e092 --- /dev/null +++ b/cypress/fixtures/variables.ts @@ -0,0 +1,59 @@ +export const testData = { + variables: [ + { + name: 'Test variable', + value: '123456789', + }, + ], + ssh: { + name: 'Test SSH', + value: + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqdSQ0y7tT+42qEdPlWniU5IGpBC8zKLq7DozcXSPAIzXVz853wFFOVCOcJSCGw/sF/7DQCgFWEV90uUBTdx0HPG6/i0n6DD92q4wK0tRBvYfBPernQ/iXXQxqO/Gg4b0O76z6PId+/35LoO5qdlfgbcAtn4b/ry9WF8hSar4az2qxgcpRVg4TpvFtvBX/ChxcmFzJRk0yWr4B+qEdLjaqJcobCgqcJoWYoIioUWEttX9Muz36Mst59ibqIDygI1kOGqQ7nf3AAVcMPoy7UdvkGD4lsi/Ibbi/8yRdlCzGoHBmTFV/R71XBg+tgN79ztmsxwap0uH1f/WKZRP4HzAd', + }, + organizations: { + overview: { + friendlyName: 'Friendly test org', + description: 'Test org description', + }, + groups: { + newGroupName: 'cypress-group1', + newGroupName2: 'cypress-group2', + }, + users: { + email: 'orguser@example.com', + role: 'developer', + }, + project: { + projectName: 'cy-drupal-test', + gitUrl: 'git@github.com:amazeeio/lagoon-demo.git', + prodEnv: 'main', + }, + notifications: { + slack: { + name: 'cy-slack-notification-1', + webhook: 'cy-slack-webhook-1', + channel: 'cy-slack-channel-1', + }, + rocketChat: { + name: 'cy-rocketChat-notification-1', + webhook: 'cy-rocketChat-webhook-1', + channel: 'cy-rocketChat-channel-1', + }, + email: { + name: 'cy-email-notification-1', + email: 'cy-email-1@example.com', + }, + teams: { + name: 'cy-msTeams-notification-1', + webhook: 'cy-msTeams-webhook-1', + }, + webhook: { + name: 'cy-webhook-notification-1', + webhook: 'cy-webhook-1', + }, + }, + manage: { + user: 'orguser@example.com', + }, + }, +}; diff --git a/cypress/global.d.ts b/cypress/global.d.ts new file mode 100644 index 00000000..0d3138dc --- /dev/null +++ b/cypress/global.d.ts @@ -0,0 +1,13 @@ +declare namespace Cypress { + interface Chainable { + getBySel(dataTestAttribute: string, args?: any): Chainable>; + + login(username: string, password: string): void; + + gqlQuery(operationName: string, query: string, variables?: Record): void; + + gqlMutation(operationName: string, query: string, variables?: Record): void; + + cleanup(): void; + } +} diff --git a/cypress/support/actions/backups/BackupsAction.ts b/cypress/support/actions/backups/BackupsAction.ts new file mode 100644 index 00000000..d592eda1 --- /dev/null +++ b/cypress/support/actions/backups/BackupsAction.ts @@ -0,0 +1,49 @@ +import BackupsRepository from 'cypress/support/repositories/backups/BackupsRepository'; + +const backups = new BackupsRepository(); + +export default class BackupsAction { + doRetrieveBackup() { + backups.getRetrieveButton().first().click(); + cy.wait('@gqladdRestoreMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + }); + } + + doCheckAllRetrieveButtons() { + backups + .getBackups() + .children() + .getBySel('backup-row') + .should('have.length', 4) + .each(($row, idx) => { + if (idx < 3) { + cy.wrap($row) + .getBySel('backup-download') + .should('exist') + .find('button') + .contains(/Retrieving ...|Retrieve/); + } + }); + } + doResultsLimitedchangeCheck(val: string | number) { + const vals = { + 10: 0, + 25: 1, + 50: 2, + 100: 3, + all: 4, + }; + cy.getBySel('select-results').find('div').eq(6).click({ force: true }); + + cy.get(`[id^="react-select-"][id$=-option-${vals[val]}]`).click(); + + if (val !== 'all') { + backups.getResultsLimited().invoke('text').should('be.eq', `Number of results displayed is limited to ${val}`); + } else { + cy.location().should(loc => { + expect(loc.search).to.eq('?limit=-1'); + }); + } + } +} diff --git a/cypress/support/actions/deployment/DeploymentAction.ts b/cypress/support/actions/deployment/DeploymentAction.ts new file mode 100644 index 00000000..e58627f9 --- /dev/null +++ b/cypress/support/actions/deployment/DeploymentAction.ts @@ -0,0 +1,59 @@ +import DeploymentRepository from 'cypress/support/repositories/deployment/DeploymentRepository'; + +const deployment = new DeploymentRepository(); + +export default class deploymentAction { + doCancelDeployment() { + deployment.getCancelDeploymentBtn().click(); + + cy.wait('@gqlcancelDeploymentMutation'); + + deployment.getCancelDeploymentBtn().should('have.text', 'Cancellation requested'); + } + + doToggleLogViewer() { + deployment.getToggler().click(); + + deployment.getLogViewer().should($viewer => { + expect($viewer).to.have.length(1); + + // only text node + expect($viewer.contents().length).to.equal(1); + expect($viewer.contents().first().get(0).nodeType).to.equal(Node.TEXT_NODE); + }); + + // revert back to parsed + deployment.getToggler().click(); + + deployment.getLogViewer().should($viewer => { + expect($viewer).to.have.length(1); + + expect($viewer.find('.processed-logs')).to.exist; + }); + } + + doFailedCancelDeployment() { + deployment.getCancelDeploymentBtn().click(); + + cy.wait('@gqlcancelDeploymentMutation'); + + deployment + .getErrorNotification() + .should('exist') + .should('include.text', 'There was a problem cancelling deployment.'); + } + doLogViewerCheck() { + deployment.getAccordionHeadings().then($headings => { + for (let i = 0; i < $headings.length - 1; i++) { + cy.wrap($headings.eq(i)).click(); + } + + for (let i = 0; i < $headings.length - 1; i++) { + cy.wrap($headings.eq(i)).next().next().getBySel('section-details').getBySel('log-text').should('exist'); + } + }); + } + navigateToRunningDeployment() { + cy.getBySel('deployment-row').getBySel('running').first().click(); + } +} diff --git a/cypress/support/actions/deployments/DeploymentsAction.ts b/cypress/support/actions/deployments/DeploymentsAction.ts new file mode 100644 index 00000000..b290a3f7 --- /dev/null +++ b/cypress/support/actions/deployments/DeploymentsAction.ts @@ -0,0 +1,63 @@ +import DeploymentsRepository from 'cypress/support/repositories/deployments/DeploymentsRepository'; + +const deployments = new DeploymentsRepository(); + +export default class DeploymentsAction { + doCancelDeployment() { + deployments.getCancelBtn().first().click(); + + cy.wait('@gqlcancelDeploymentMutation'); + + deployments.getCancelBtn().first().should('have.text', 'Cancelled'); + } + doFailedCancelDeployment() { + deployments.getCancelBtn().first().click(); + + cy.wait('@gqlcancelDeploymentMutation'); + + deployments + .getErrorNotification() + .should('exist') + .should('include.text', 'There was a problem cancelling deployment.'); + } + + doDeployment() { + deployments.getDeployBtn().click(); + + cy.wait('@gqldeployEnvironmentLatestMutation'); + + deployments.getDeployQueued().should('exist'); + } + + doFailedDeployment() { + deployments.getDeployBtn().click(); + + cy.wait('@gqldeployEnvironmentLatestMutation'); + + deployments.getErrorNotification().should('exist').should('include.text', 'There was a problem deploying.'); + } + + doResultsLimitedchangeCheck(val: string | number) { + const vals = { + 10: 0, + 25: 1, + 50: 2, + 100: 3, + all: 4, + }; + cy.getBySel('select-results').find('div').eq(6).click({ force: true }); + + cy.get(`[id^="react-select-"][id$=-option-${vals[val]}]`).click(); + + if (val !== 'all') { + deployments + .getResultsLimited() + .invoke('text') + .should('be.eq', `Number of results displayed is limited to ${val}`); + } else { + cy.location().should(loc => { + expect(loc.search).to.eq('?limit=-1'); + }); + } + } +} diff --git a/cypress/support/actions/envOverview/EnvOverviewAction.ts b/cypress/support/actions/envOverview/EnvOverviewAction.ts new file mode 100644 index 00000000..89be4862 --- /dev/null +++ b/cypress/support/actions/envOverview/EnvOverviewAction.ts @@ -0,0 +1,63 @@ +import EnvOverviewRepository from 'cypress/support/repositories/envOverview/EnvOverviewRepository'; + +const environment = new EnvOverviewRepository(); + +export default class EnvOverviewAction { + doEnvTypeCheck() { + environment.getEnvType().should('have.text', 'production'); + } + + doDeployTypeCheck() { + environment.getDeployType().should('have.text', 'branch'); + } + + doSourceCheck() { + environment.getSource().should($anchorTag => { + expect($anchorTag).to.have.attr('target', '_blank'); + expect($anchorTag).to.have.attr('href', 'https:////git@example.com/lagoon-demo/tree/main'); + }); + } + + doRoutesCheck() { + environment.getRoutes().each(($element, index) => { + cy.wrap($element) + .find('a') + .should('have.attr', 'href') + .and( + 'eq', + index === 0 ? 'https://lagoondemo.example.org' : 'https://nginx.main.lagoon-demo.ui-kubernetes.lagoon.sh' + ); + }); + } + + doDeleteEnvironment(branch: string) { + environment.getDeleteButton().click(); + cy.getBySel('confirm-input').type(branch); + environment.getDeleteButtonConfirm().click(); + + cy.wait('@gqldeleteEnvironmentMutation'); + + environment.getDeleteInfo().invoke('text').should('eq', 'Delete queued'); + } + + doDeleteEnvironmentError(branch: string) { + environment.getDeleteButton().click(); + cy.getBySel('confirm-input').type(branch); + environment.getDeleteButtonConfirm().click(); + + const errorMessage = `Unauthorized: You don\'t have permission to "delete:${ + branch === 'main' ? 'production' : 'development' + }" on "environment": {"project":18}`; + + cy.wait('@gqldeleteEnvironmentMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + + const UiError = 'GraphQL error: ' + errorMessage; + + environment.getDeleteInfo().invoke('text').should('eq', UiError); + } +} diff --git a/cypress/support/actions/organizations/GroupsAction.ts b/cypress/support/actions/organizations/GroupsAction.ts new file mode 100644 index 00000000..9cb99b84 --- /dev/null +++ b/cypress/support/actions/organizations/GroupsAction.ts @@ -0,0 +1,90 @@ +import GroupsRepository from 'cypress/support/repositories/organizations/GroupsRepository'; + +const groupRepo = new GroupsRepository(); + +export default class GroupAction { + doAddGroup(newGroup1: string, newGroup2: string) { + groupRepo.getAddGroupBtn('addNewGroup').click(); + groupRepo.getGroupNameInput().type(newGroup1); + groupRepo.getAddGroupSubmitBtn().click(); + + cy.wait(['@gqladdGroupToOrganizationMutation', '@gqlgetOrganizationQuery']); + cy.getBySel('table-row').first().should('contain', newGroup1); + + cy.log('Add another'); + cy.reload(); + + groupRepo.getAddGroupBtn('addNewGroup').click(); + groupRepo.getGroupNameInput().type(newGroup2); + groupRepo.getAddGroupSubmitBtn().click(); + + cy.wait(['@gqladdGroupToOrganizationMutation', '@gqlgetOrganizationQuery']); + + cy.getBySel('table-row').eq(1).should('contain', newGroup2); + } + + doFailedAddGroup(newGroup1: string, newGroup2: string) { + groupRepo.getAddGroupBtn('addNewGroup').click(); + groupRepo.getGroupNameInput().type(newGroup1); + groupRepo.getAddGroupSubmitBtn().click(); + + cy.wait('@gqladdGroupToOrganizationMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = `Unauthorized: You don't have permission to "addGroup" on "organization": {"organization":1}`; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + doGroupSearch(group1: string, group2: string) { + cy.log('First group'); + groupRepo.getSearchBar().type(group1); + cy.getBySel('table-row').eq(0).should('contain', group1); + + cy.log('No search results'); + groupRepo.getSearchBar().clear().type('does not exist'); + cy.contains('No groups found').should('be.visible'); + + cy.log('Second group'); + groupRepo.getSearchBar().clear().type(group2); + cy.getBySel('table-row').eq(0).should('contain', group2); + } + doAddMemberToGroup(userEmail: string) { + groupRepo.getAddUserBtn('adduser').first().click(); + cy.getBySel('orgUser-email-input').type(userEmail); + + cy.get('.react-select__indicator').click({ force: true }); + cy.get('#react-select-2-option-2').click(); + cy.getBySel('addUserToGroup').click(); + + cy.wait(['@gqladdUserToGroupMutation', '@gqlgetOrganizationQuery']); + cy.waitForNetworkIdle('@groupQuery', 500); + + cy.getBySel('memberCount') + .first() + .invoke('text') + .then(text => { + const trimmedText = text.trim(); + expect(trimmedText).to.equal('Members: 1'); + }); + } + + doDeleteGroup(groupName: string) { + groupRepo.getDeleteGroupBtn('deleteGroup').first().click(); + cy.getBySel('confirm').click(); + + cy.intercept('POST', Cypress.env('api')).as('deleteGroup'); + cy.wait('@deleteGroup'); + cy.waitForNetworkIdle('@groupQuery', 500); + + cy.getBySel('label-text').each($span => { + const text = $span.text(); + if (text.includes('0')) { + cy.contains('No groups found').should('exist'); + } else { + cy.getBySel('table-row').eq(0).should('not.contain', groupName); + } + }); + } +} diff --git a/cypress/support/actions/organizations/ManageAction.ts b/cypress/support/actions/organizations/ManageAction.ts new file mode 100644 index 00000000..6a8bb8c0 --- /dev/null +++ b/cypress/support/actions/organizations/ManageAction.ts @@ -0,0 +1,36 @@ +import ManageRepository from 'cypress/support/repositories/organizations/ManageRepository'; + +const manageRepo = new ManageRepository(); + +export default class ManageAction { + doAddOrgViewer(viewerUser: string) { + manageRepo.getAddUserBtn().click(); + manageRepo.getUserEmailField().type(viewerUser); + manageRepo.getSubmitBtn().click(); + + cy.wait('@gqlAddUserToOrganizationMutation'); + + manageRepo.getUserRows().should($element => { + const elementText = $element.text(); + expect(elementText).to.include(viewerUser); + }); + } + + doEditOrgViewer(user: string) { + manageRepo.getUserRows().contains(user).parents('.tableRow').find('.link').click(); + + manageRepo.getUserIsOwnerCheckbox().check(); + + manageRepo.getUpdateBtn().click(); + + cy.wait('@gqlAddUserToOrganizationMutation'); + + manageRepo.getUserRows().contains(user).parents('.tableRow').find(':contains("ORG OWNER")').should('exist'); + } + + doDeleteUser(user: string) { + manageRepo.getUserRows().contains(user).parents('.tableRow').find("[aria-label='delete']").click(); + + manageRepo.getDeleteConfirmBtn().click(); + } +} diff --git a/cypress/support/actions/organizations/NotificationsAction.ts b/cypress/support/actions/organizations/NotificationsAction.ts new file mode 100644 index 00000000..5813985f --- /dev/null +++ b/cypress/support/actions/organizations/NotificationsAction.ts @@ -0,0 +1,120 @@ +import NotificationsRepository from 'cypress/support/repositories/organizations/NotificationsRepository'; + +const notificationRepo = new NotificationsRepository(); + +const notifMap = { + slack: ['name', 'webhook', 'channel'], + rocketChat: ['name', 'webhook', 'channel'], + email: ['name', 'email'], + teams: ['name', 'webhook'], + webhook: ['name', 'webhook'], +}; + +type NotificationData = { + name: string; + webhook?: string; + channel?: string; + email?: string; +}; + +const getMutationName = (notification: keyof typeof notifMap) => { + const mutations = { + slack: 'Slack', + rocketChat: 'RocketChat', + email: 'Email', + teams: 'MicrosoftTeams', + webhook: 'Webhook', + } as const; + + return mutations[notification]; +}; +export default class NotificationsAction { + doAddNotification(notifType: keyof typeof notifMap, notificationData: NotificationData) { + const fieldsToFill = notifMap[notifType]; + + const optionIndexMap = { + slack: 0, + rocketChat: 1, + email: 2, + teams: 3, + webhook: 4, + }; + + notificationRepo.getAddNotification().click({ force: true }); + cy.get('.react-select__indicator').click({ force: true }); + + // Select the option based on the mapping + cy.get(`[id^="react-select-"][id$=-option-${optionIndexMap[notifType]}]`).click(); + + // const data = testData.organizations.notifications[notifType]; + + fieldsToFill.forEach(field => { + cy.get(`.input${field.charAt(0).toUpperCase() + field.slice(1)}`).type(notificationData[field]); + }); + + cy.getBySel('addNotifBtn').click(); + + cy.wait(`@gqladdNotification${getMutationName(notifType)}Mutation`); + + // notification name + cy.getBySel('notification-row').should('include.text', notificationData.name); + } + + doFailedAddNotification(notifType: keyof typeof notifMap, notificationData: NotificationData) { + const fieldsToFill = notifMap[notifType]; + + const optionIndexMap = { + slack: 0, + rocketChat: 1, + email: 2, + teams: 3, + webhook: 4, + }; + + notificationRepo.getAddNotification().click(); + cy.get('.react-select__indicator').click({ force: true }); + + // Select the option based on the mapping + cy.get(`[id^="react-select-"][id$=-option-${optionIndexMap[notifType]}]`).click(); + + // const data = testData.organizations.notifications[notifType]; + + fieldsToFill.forEach(field => { + cy.get(`.input${field.charAt(0).toUpperCase() + field.slice(1)}`).type(notificationData[field]); + }); + + cy.getBySel('addNotifBtn').click(); + + cy.wait(`@gqladdNotification${getMutationName(notifType)}Mutation`).then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = `Unauthorized: You don't have permission to "addNotification" on "organization": {"organization":1}`; + + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + + doEditNotification() { + notificationRepo.getLast('link').first().click(); + + cy.getBySel('notification-name').first().type('-edited', { force: true }); + cy.getBySel('input-webhook').first().type('-edited', { force: true }); + + cy.getBySel('continueEdit').first().click({ force: true }); + + cy.wait(`@gqlUpdateNotificationSlackMutation`); + + cy.getBySel('notification-row').should('include.text', '-edited'); + } + doDeleteNotification(notification: keyof typeof notifMap) { + notificationRepo.getLast('btn-red').first().click(); + cy.getBySel('confirmDelete').click(); + + cy.getBySel('notification-row').should('not.have.text', notification); + } + closeModal() { + cy.getBySel('cancel').click(); + } +} diff --git a/cypress/support/actions/organizations/OverviewAction.ts b/cypress/support/actions/organizations/OverviewAction.ts new file mode 100644 index 00000000..cef31db3 --- /dev/null +++ b/cypress/support/actions/organizations/OverviewAction.ts @@ -0,0 +1,99 @@ +import OverviewRepository from 'cypress/support/repositories/organizations/OverviewRepository'; + +const overviewRepo = new OverviewRepository(); + +export default class OverviewAction { + doNavLinkCheck() { + overviewRepo.getLinkElement('group-link').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/groups'); + + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + + overviewRepo.getLinkElement('project-link').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/projects'); + + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + + overviewRepo.getLinkElement('notification-link').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/notifications'); + + cy.visit(`${Cypress.env('url')}/organizations/lagoon-demo-organization`); + overviewRepo.getLinkElement('manage-link').click(); + cy.location('pathname').should('equal', '/organizations/lagoon-demo-organization/manage'); + } + + doQuotaFieldCheck() { + overviewRepo.getFieldElement('group').should('exist').should('not.be.empty'); + + overviewRepo.getFieldElement('project').should('exist').should('not.be.empty'); + + overviewRepo.getFieldElement('notification').should('exist').should('not.be.empty'); + + overviewRepo.getFieldElement('environment').should('exist').should('not.be.empty'); + } + + changeOrgFriendlyname(friendlyName: string) { + overviewRepo.getNameEditButton('edit-name').click(); + overviewRepo.getEditField().type(friendlyName); + overviewRepo.getSubmitButton().click(); + + cy.wait('@gqlupdateOrganizationFriendlyNameMutation'); + + overviewRepo + .getfriendlyName() + .invoke('text') + .then(text => { + const trimmedText = text.trim(); + + // Assert that the trimmed text is equal to the expected value + expect(trimmedText).to.equal(friendlyName); + }); + } + doFailedChangeOrgFriendlyname(friendlyName: string) { + overviewRepo.getNameEditButton('edit-name').click(); + overviewRepo.getEditField().type(friendlyName); + overviewRepo.getSubmitButton().click(); + + cy.wait('@gqlupdateOrganizationFriendlyNameMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "updateOrganization" on "organization": 1'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + + changeOrgDescription(description: string) { + overviewRepo.getDescEditButton('edit-description').click(); + overviewRepo.getEditField().type(description); + overviewRepo.getSubmitButton().click(); + + cy.wait('@gqlupdateOrganizationFriendlyNameMutation'); + + overviewRepo + .getDescription() + .invoke('text') + .then(text => { + const trimmedText = text.trim(); + expect(trimmedText).to.equal(description); + }); + } + doFailedChangeOrgDescription(description: string) { + overviewRepo.getDescEditButton('edit-description').click(); + overviewRepo.getEditField().type(description); + overviewRepo.getSubmitButton().click(); + + cy.wait('@gqlupdateOrganizationFriendlyNameMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "updateOrganization" on "organization": 1'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + closeModal() { + overviewRepo.getCancelButton().click(); + } +} diff --git a/cypress/support/actions/organizations/ProjectsActions.ts b/cypress/support/actions/organizations/ProjectsActions.ts new file mode 100644 index 00000000..98bbc50b --- /dev/null +++ b/cypress/support/actions/organizations/ProjectsActions.ts @@ -0,0 +1,55 @@ +import ProjectsRepository from 'cypress/support/repositories/organizations/ProjectsRepository'; + +const projects = new ProjectsRepository(); + +type ProjectData = { + projectName: string; + gitUrl: string; + prodEnv: string; +}; +export default class ProjectsActions { + doAddProject(projectData: ProjectData) { + projects.getAddBtn().click({ force: true }); + projects.getName().type(projectData.projectName); + projects.getGit().type(projectData.gitUrl); + + projects.getEnv().type(projectData.prodEnv); + + projects.selectTarget(); + + projects.getAddConfirm().click(); + + cy.wait('@gqladdProjectToOrganizationMutation'); + + projects.getProjectRows().contains(projectData.projectName).should('exist'); + } + doFailedaddProject(projectData: ProjectData) { + projects.getAddBtn().click({ force: true }); + projects.getName().type(projectData.projectName); + projects.getGit().type(projectData.gitUrl); + + projects.getEnv().type(projectData.prodEnv); + + projects.selectTarget(); + + projects.getAddConfirm().click(); + + cy.wait('@gqladdProjectToOrganizationMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = `Unauthorized: You don't have permission to "addProject" on "organization": {"organization":1}`; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + doDeleteProject(projectName: string) { + projects.getDeleteBtn().eq(1).click(); + + projects.getDeleteConfirm().click(); + + cy.wait('@gqldeleteProjectMutation'); + + projects.getProjectRows().contains(projectName).should('not.exist'); + } +} diff --git a/cypress/support/actions/organizations/UsersAction.ts b/cypress/support/actions/organizations/UsersAction.ts new file mode 100644 index 00000000..cecf701b --- /dev/null +++ b/cypress/support/actions/organizations/UsersAction.ts @@ -0,0 +1,26 @@ +import UsersRepository from 'cypress/support/repositories/organizations/UsersRepository'; + +const userRepo = new UsersRepository(); +export default class UsersActions { + doAddUser(email: string) { + userRepo.getAddUserBtn().click(); + userRepo.getAddUserEmail().type(email); + userRepo.getAddUserGroup(); + userRepo.getAddUserRole(); + + userRepo.getAddUserConfirm().click(); + + cy.wait('@gqladdUserToGroupMutation'); + + userRepo.getRows().should($element => { + const elementText = $element.text(); + expect(elementText).to.include(email); + }); + } + + doDeleteUser(email: string) { + userRepo.getRows().contains(email).parents('.tableRow').find("[aria-label='delete']").click(); + + userRepo.getConfirmDeleteBtn().click(); + } +} diff --git a/cypress/support/actions/project/ProjectAction.ts b/cypress/support/actions/project/ProjectAction.ts new file mode 100644 index 00000000..d8c79e9f --- /dev/null +++ b/cypress/support/actions/project/ProjectAction.ts @@ -0,0 +1,64 @@ +import { default as Project } from '../../repositories/project/ProjectRepository'; +import ProjectRepository from '../../repositories/projects/ProjectsRepository'; + +const projectRepo = new ProjectRepository(); + +const project = new Project(); + +export default class ProjectAction { + doNavigateToFirst() { + projectRepo.getProject().first().click(); + } + + doClipboardCheck() { + project.getCopyButton().realClick(); + + cy.window().then(win => { + win.navigator.clipboard.readText().then(() => { + project.getGitUrl().invoke('text').should('eq', '//git@example.com/lagoon-demo'); + }); + }); + } + + doSidebarPopulatedCheck() { + project.getGitUrl().should('not.be.empty'); + project.getBranchesField().should('not.be.empty'); + project.getCreatedField().should('not.be.empty'); + project.getDevEnvsField().should('not.be.empty'); + project.getPullRequestsField().should('not.be.empty'); + } + + doExternalLinkCheck() { + cy.getBySel('gitLink').contains('lagoon-demo'); + } + + doEnvRouteCheck() { + project.getEnvRoutes().each($element => { + cy.wrap($element).find('a').should('have.attr', 'href').and('not.be.empty'); + }); + } + + doCreateDummyEnv() { + project.getEnvBtn().click(); + + project.getBranchNameInput().focus().type('123123'); + + project.getSubmitBtn().click(); + + project.getEnvNames().contains('123123').should('exist'); + } + + doCreateEnvWithPermissionError() { + project.getEnvBtn().click(); + project.getBranchNameInput().focus().type('123123'); + project.getSubmitBtn().click(); + + project + .getErrorModal() + .should('exist') + .should( + 'include.text', + 'GraphQL error: Unauthorized: You don\'t have permission to "deploy:development" on "environment": {"project":18}' + ); + } +} diff --git a/cypress/support/actions/projects/ProjectsAction.ts b/cypress/support/actions/projects/ProjectsAction.ts new file mode 100644 index 00000000..ca39c9e5 --- /dev/null +++ b/cypress/support/actions/projects/ProjectsAction.ts @@ -0,0 +1,54 @@ +import ProjectRepository from '../../repositories/projects/ProjectsRepository'; + +const projectRepo = new ProjectRepository(); + +export default class ProjectAction { + doPageCheck() { + projectRepo.getPageTitle().should('have.text', 'Projects'); + } + + doSearch() { + projectRepo.getSearchBar().type('lagoon-demo'); + projectRepo.getProjects().its('length').should('equal', 1); + } + + doEmptySearch() { + projectRepo.getSearchBar().type('This does not exist'); + projectRepo + .getNotMatched() + .invoke('text') + .should('match', /No projects matching .*/); + } + + doEmptyProjectCheck() { + cy.intercept('POST', Cypress.env('api'), req => { + req.reply({ + statusCode: 200, + body: { + data: { + allProjects: [], + }, + }, + }); + }).as('allProjects'); + + cy.wait('@allProjects'); + + projectRepo.getNoProjectsLabel().should('exist'); + } + + doProjectLengthCheck() { + projectRepo + .getLengthCounter() + .invoke('text') + .then(text => { + const numberMatch = text.match(/\d+/); + if (numberMatch) { + let numberOfProjects = parseInt(numberMatch[0], 10); + projectRepo.getProjects().its('length').should('equal', numberOfProjects); + } else { + throw new Error('Failed to extract the number of projects from the element.'); + } + }); + } +} diff --git a/cypress/support/actions/settings/SettingsAction.ts b/cypress/support/actions/settings/SettingsAction.ts new file mode 100644 index 00000000..30802456 --- /dev/null +++ b/cypress/support/actions/settings/SettingsAction.ts @@ -0,0 +1,24 @@ +import SettingsRepository from 'cypress/support/repositories/settings/SettingsRepository'; + +const settings = new SettingsRepository(); + +export default class SettingAction { + doEmptySshCheck() { + // no ssh keys at first + cy.contains('No SSH keys'); + } + + addSshKey(name: string, value: string) { + settings.getNameInput().type(name); + settings.getValueInput().type(value); + settings.getSubmitBtn().should('not.be.disabled').click(); + + cy.contains(name); + } + + deleteSshKey(name: string) { + settings.getDeleteBtn().click(); + + cy.contains(name).should('not.exist'); + } +} diff --git a/cypress/support/actions/task/TaskAction.ts b/cypress/support/actions/task/TaskAction.ts new file mode 100644 index 00000000..b2b97d3c --- /dev/null +++ b/cypress/support/actions/task/TaskAction.ts @@ -0,0 +1,30 @@ +import TaskRepository from 'cypress/support/repositories/task/TaskRepository'; + +const task = new TaskRepository(); + +export default class TaskAction { + doNavToRunningTask() { + cy.getBySel('select-results').find('div').eq(6).click({ force: true }); + + cy.waitForNetworkIdle('@idle', 500); + + cy.get(`[id^="react-select-"][id$=-option-4]`).click(); + + cy.getBySel('task-row').getBySel('pending').click(); + } + doCancelTask() { + task.getCancelBtn().first().click(); + + cy.wait('@gqlcancelTaskMutation'); + + task.getCancelBtn().first().should('have.text', 'Cancelled'); + } + + doFailedCancelTask() { + task.getCancelBtn().first().click(); + + cy.wait('@gqlcancelTaskMutation'); + + task.getErrorNotification().should('exist').should('include.text', 'There was a problem cancelling a task.'); + } +} diff --git a/cypress/support/actions/tasks/TasksAction.ts b/cypress/support/actions/tasks/TasksAction.ts new file mode 100644 index 00000000..78dee92c --- /dev/null +++ b/cypress/support/actions/tasks/TasksAction.ts @@ -0,0 +1,138 @@ +import TasksRepository from 'cypress/support/repositories/tasks/TasksRepository'; + +const tasks = new TasksRepository(); + +export default class TasksAction { + doCancelTask() { + tasks.getCancelBtn().first().click(); + + cy.wait('@gqlcancelTaskMutation'); + + tasks.getCancelBtn().first().should('have.text', 'Cancelled'); + } + + doCacheClearTask() { + tasks.getTaskSelector(0).click(); + tasks.getRunTaskBtn().click(); + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doDrushCronTask() { + tasks.getTaskSelector(1).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doDbBackupTask() { + tasks.getTaskSelector(4).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doFailedDbBackupTask() { + tasks.getTaskSelector(4).click(); + tasks.getRunTaskBtn().click(); + + cy.wait('@gqltaskDrushSqlDumpMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = + 'Unauthorized: You don\'t have permission to "drushSqlDump:production" on "task": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + + doDbAndFilesBackupTask() { + tasks.getTaskSelector(5).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doFailedDbAndFilesBackupTask() { + tasks.getTaskSelector(5).click(); + tasks.getRunTaskBtn().click(); + + cy.wait('@gqltaskDrushArchiveDumpMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = + 'Unauthorized: You don\'t have permission to "drushArchiveDump:production" on "task": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + + doLoginLinkTask() { + tasks.getTaskSelector(6).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doFailedLoginLinkTask() { + tasks.getTaskSelector(6).click(); + tasks.getRunTaskBtn().click(); + + cy.wait('@gqltaskDrushUserLoginMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = + 'Unauthorized: You don\'t have permission to "drushUserLogin:production" on "task": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + } + + doDeveloperTask(taskNum?: number) { + // this task is only visible to users with roles "developer" and "maintainer" + tasks.getTaskSelector(taskNum || 7).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + doMaintainerTask(taskNum?: number) { + // this task is only visible to users with role "maintainer" + tasks.getTaskSelector(taskNum || 8).click(); + tasks.getRunTaskBtn().click(); + + tasks.getTaskConfirmed().should('contain', 'Task added'); + } + + doFailedTaskCancellation() { + tasks.getCancelBtn().click(); + + cy.wait('@gqlcancelTaskMutation').then(interception => { + expect(interception.response?.statusCode).to.eq(200); + + const errorMessage = 'Unauthorized: You don\'t have permission to "cancel:production" on "task": {"project":18}'; + expect(interception.response?.body).to.have.property('errors'); + + cy.wrap(interception.response?.body.errors[0]).should('deep.include', { message: errorMessage }); + }); + + tasks.getErrorNotification().should('exist').should('include.text', 'There was a problem cancelling a task.'); + } + + doResultsLimitedchangeCheck(val: string | number) { + const vals = { + 10: 0, + 25: 1, + 50: 2, + 100: 3, + all: 4, + }; + cy.getBySel('select-results').find('div').eq(6).click({ force: true }); + + cy.get(`[id^="react-select-"][id$=-option-${vals[val]}]`).click(); + + if (val !== 'all') { + tasks.getResultsLimited().invoke('text').should('be.eq', `Number of results displayed is limited to ${val}`); + } else { + cy.location().should(loc => { + expect(loc.search).to.eq('?limit=-1'); + }); + } + } +} diff --git a/cypress/support/actions/variables/VariablesAction.ts b/cypress/support/actions/variables/VariablesAction.ts new file mode 100644 index 00000000..54ff2969 --- /dev/null +++ b/cypress/support/actions/variables/VariablesAction.ts @@ -0,0 +1,39 @@ +import VariablesRepository from 'cypress/support/repositories/variables/VariablesRepository'; + +const environment = new VariablesRepository(); + +export default class VariablesAction { + doEnvNavigation() { + environment.getVariablesLink().click(); + } + + doHideShowToggle() { + environment.getToggleShowButton().click(); + } + + doValueToggle() { + environment.getEnvDataRows().getBySel('showhide-toggle').click({ multiple: true }); + } + + doAddVariable(name: string, value: string) { + environment.getAddButton().click(); + + cy.get('.react-select__indicator').click({ force: true }); + cy.get('#react-select-2-option-1').click(); + + cy.getBySel('varName').focus().type(name); + cy.getBySel('varValue').focus().type(value); + + cy.getBySel('add-variable').click(); + } + + doDeleteVariable(name: string) { + environment.getDeleteBtn(name); + cy.waitForNetworkIdle('@idle', 500); + environment.getDeleteBtn(name); + + cy.log('enter the name and confirm'); + cy.getBySel('variable-input').type(name); + cy.getBySel('delete-button').click(); + } +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 00000000..09dd3db7 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,105 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// + +// stubbing clipboard for localhost on http. +Cypress.on('window:before:load', win => { + if (!win.navigator.clipboard) { + // @ts-ignore + win.navigator.clipboard = { + copyText: null, + }; + } + + // @ts-ignore + win.navigator.clipboard.writeText = text => (this.copyText = text); + // @ts-ignore + win.navigator.clipboard.readText = () => Promise.resolve(this.copyText); +}); + +Cypress.Commands.add('getBySel', (selector: string) => { + return cy.get(`[data-cy=${selector}]`); +}); + +Cypress.Commands.add('login', (username: string, password: string) => { + cy.session([username, password], () => { + cy.visit(Cypress.env('url')); + cy.origin(Cypress.env('keycloak'), { args: { username, password } }, ({ username, password }) => { + cy.get('#username').type(username); + cy.get('#password').type(password); + cy.get('#kc-login').click(); + }); + }); +}); + +Cypress.Commands.add('gqlQuery', (operationName, query, variables) => { + const gqlEndpoint = Cypress.env('graphqlEndpoint'); + + if (!gqlEndpoint) { + throw new Error('GraphQL endpoint is not defined'); + } + + const requestBody = { + operationName, + query, + ...(variables ? { variables } : {}), + }; + + // Send a POST request to the gql endpoint + return cy.request({ + method: 'POST', + url: gqlEndpoint, + body: requestBody, + headers: { + 'Content-Type': 'application/json', + }, + }); +}); + +Cypress.Commands.add('gqlMutation', (operationName, mutation, variables) => { + const gqlEndpoint = Cypress.env('graphqlEndpoint'); + + if (!gqlEndpoint) { + throw new Error('GraphQL endpoint is not defined'); + } + + const requestBody = { + operationName, + query: mutation, + ...(variables ? { variables } : {}), + }; + + // Send a POST request to the gql endpoint + return cy.request({ + method: 'POST', + url: gqlEndpoint, + body: requestBody, + headers: { + 'Content-Type': 'application/json', + }, + }); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 00000000..71076793 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,4 @@ +import 'cypress-network-idle'; +import 'cypress-real-events'; + +import './commands'; diff --git a/cypress/support/repositories/backups/BackupsRepository.ts b/cypress/support/repositories/backups/BackupsRepository.ts new file mode 100644 index 00000000..e4dc5dce --- /dev/null +++ b/cypress/support/repositories/backups/BackupsRepository.ts @@ -0,0 +1,11 @@ +export default class BackupsRepository { + getRetrieveButton() { + return cy.getBySel('retrieve'); + } + getBackups() { + return cy.getBySel('backups'); + } + getResultsLimited() { + return cy.getBySel('resultsLimited'); + } +} diff --git a/cypress/support/repositories/deployment/DeploymentRepository.ts b/cypress/support/repositories/deployment/DeploymentRepository.ts new file mode 100644 index 00000000..6029bbca --- /dev/null +++ b/cypress/support/repositories/deployment/DeploymentRepository.ts @@ -0,0 +1,25 @@ +export default class DeploymentRepository { + getCancelDeploymentBtn() { + return cy.getBySel('cancelDeployment'); + } + getToggler() { + return cy.getBySel('logviewer_toggle'); + } + getLogViewer() { + return cy.get('.log-viewer'); + } + getAccordionHeadings() { + return cy.get('.accordion-heading'); + } + + getRunningDeployment() { + return cy.getBySel('deploy'); + } + getCompletedDeployment() { + return cy.getBySel('deploy_result'); + } + + getErrorNotification() { + return cy.get('.ant-notification-notice'); + } +} diff --git a/cypress/support/repositories/deployments/DeploymentsRepository.ts b/cypress/support/repositories/deployments/DeploymentsRepository.ts new file mode 100644 index 00000000..46556d69 --- /dev/null +++ b/cypress/support/repositories/deployments/DeploymentsRepository.ts @@ -0,0 +1,23 @@ +export default class DeploymentsRepository { + getDeployBtn() { + return cy.getBySel('deploy'); + } + getDeployQueued() { + return cy.getBySel('deploy_result'); + } + getDeployments() { + return cy.getBySel('deploy-table'); + } + getCancelBtn() { + return this.getDeployments().getBySel('deployment-row').first().getBySel('cancel-button'); + } + getResultsLimited() { + return cy.getBySel('resultsLimited'); + } + getResultsSelector() { + return cy.getBySel('result_selector'); + } + getErrorNotification() { + return cy.get('.ant-notification-notice'); + } +} diff --git a/cypress/support/repositories/envOverview/EnvOverviewRepository.ts b/cypress/support/repositories/envOverview/EnvOverviewRepository.ts new file mode 100644 index 00000000..777ca3fe --- /dev/null +++ b/cypress/support/repositories/envOverview/EnvOverviewRepository.ts @@ -0,0 +1,24 @@ +export default class EnvOverviewRepository { + getEnvType() { + return cy.getBySel('envType'); + } + getDeployType() { + return cy.getBySel('deployType'); + } + getSource() { + return cy.getBySel('source'); + } + getRoutes() { + return cy.getBySel('routes'); + } + getDeleteButton() { + return cy.getBySel('delete'); + } + + getDeleteButtonConfirm() { + return cy.getBySel('deleteConfirm'); + } + getDeleteInfo() { + return cy.getBySel('env-details').get('div').last(); + } +} diff --git a/cypress/support/repositories/navigation/NavigationRepository.ts b/cypress/support/repositories/navigation/NavigationRepository.ts new file mode 100644 index 00000000..5c815fcd --- /dev/null +++ b/cypress/support/repositories/navigation/NavigationRepository.ts @@ -0,0 +1,5 @@ +export default class NavigationRepository { + getLinkElement(selector: string) { + return cy.getBySel(selector); + } +} diff --git a/cypress/support/repositories/organizations/GroupsRepository.ts b/cypress/support/repositories/organizations/GroupsRepository.ts new file mode 100644 index 00000000..b3d1c41e --- /dev/null +++ b/cypress/support/repositories/organizations/GroupsRepository.ts @@ -0,0 +1,29 @@ +export default class GroupsRepository { + getElement(selector: string) { + return cy.getBySel(selector); + } + getAddGroupBtn(selector: string) { + return this.getElement(selector); + } + getAddUserBtn(selector: string) { + return this.getElement(selector); + } + getDeleteGroupBtn(selector: string) { + return this.getElement(selector); + } + getSystemGroupCheckbox(selector: string) { + return this.getElement(selector); + } + getSorterDropdown(selector: string) { + return this.getElement(selector); + } + getSearchBar() { + return this.getElement('search-bar'); + } + getGroupNameInput() { + return cy.getBySel('groupName-input'); + } + getAddGroupSubmitBtn() { + return this.getElement('createGroup'); + } +} diff --git a/cypress/support/repositories/organizations/ManageRepository.ts b/cypress/support/repositories/organizations/ManageRepository.ts new file mode 100644 index 00000000..5d05690a --- /dev/null +++ b/cypress/support/repositories/organizations/ManageRepository.ts @@ -0,0 +1,24 @@ +export default class ManageRepository { + getAddUserBtn() { + return cy.getBySel('addUserbtn'); + } + + getUserEmailField() { + return cy.getBySel('manageEmail'); + } + getUserIsOwnerCheckbox() { + return cy.getBySel('userIsOwner'); + } + getSubmitBtn() { + return cy.getBySel('addUserConfirm'); + } + getUpdateBtn() { + return cy.getBySel('updateUser'); + } + getUserRows() { + return cy.getBySel('table-row'); + } + getDeleteConfirmBtn() { + return cy.getBySel('deleteConfirm'); + } +} diff --git a/cypress/support/repositories/organizations/NotificationsRepository.ts b/cypress/support/repositories/organizations/NotificationsRepository.ts new file mode 100644 index 00000000..8721d3df --- /dev/null +++ b/cypress/support/repositories/organizations/NotificationsRepository.ts @@ -0,0 +1,9 @@ +export default class NotificationsRepository { + getAddNotification() { + return cy.getBySel('addNotification'); + } + + getLast(identifier: string) { + return cy.getBySel('notification-row').find(`.${identifier}`); + } +} diff --git a/cypress/support/repositories/organizations/OverviewRepository.ts b/cypress/support/repositories/organizations/OverviewRepository.ts new file mode 100644 index 00000000..aa494f70 --- /dev/null +++ b/cypress/support/repositories/organizations/OverviewRepository.ts @@ -0,0 +1,36 @@ +export default class OverviewRepository { + getElement(selector: string) { + return cy.getBySel(selector); + } + getLinkElement(selector: string) { + return this.getElement(selector); + } + + getFieldElement(selector: string) { + return this.getElement(selector); + } + + getNameEditButton(selector: string) { + return this.getElement(selector); + } + + getfriendlyName() { + return this.getElement('friendlyName'); + } + getDescription() { + return this.getElement('description'); + } + getEditField() { + return this.getElement('input-orgName'); + } + getSubmitButton() { + return this.getElement('submit-btn').first(); + } + getCancelButton() { + return this.getElement('cancel'); + } + + getDescEditButton(selector: string) { + return this.getElement(selector); + } +} diff --git a/cypress/support/repositories/organizations/ProjectsRepository.ts b/cypress/support/repositories/organizations/ProjectsRepository.ts new file mode 100644 index 00000000..65c6ed9f --- /dev/null +++ b/cypress/support/repositories/organizations/ProjectsRepository.ts @@ -0,0 +1,32 @@ +export default class ProjectsRepository { + getAddBtn() { + return cy.getBySel('addNewProject'); + } + + getAddConfirm() { + return cy.getBySel('addProjectConfirm'); + } + + getName() { + return cy.getBySel('project-name'); + } + getGit() { + return cy.getBySel('input-git'); + } + getEnv() { + return cy.getBySel('input-env'); + } + selectTarget() { + cy.get('div[class$=-control]').click({ force: true }); + cy.get(`[id^="react-select-"][id$=-option-0]`).click(); + } + getProjectRows() { + return cy.getBySel('table-row'); + } + getDeleteBtn() { + return cy.get("[aria-label='delete']"); + } + getDeleteConfirm() { + return cy.getBySel('deleteConfirm'); + } +} diff --git a/cypress/support/repositories/organizations/UsersRepository.ts b/cypress/support/repositories/organizations/UsersRepository.ts new file mode 100644 index 00000000..caa0b346 --- /dev/null +++ b/cypress/support/repositories/organizations/UsersRepository.ts @@ -0,0 +1,27 @@ +export default class UsersRepository { + getAddUserBtn() { + return cy.getBySel('addUser'); + } + getAddUserEmail() { + return cy.getBySel('addUserEmail'); + } + getAddUserGroup() { + cy.get('.react-select__indicator').first().click({ force: true }); + cy.get('#react-select-2-option-0').click(); + } + getAddUserRole() { + cy.get('.react-select__indicator').eq(1).click({ force: true }); + cy.get('#react-select-3-option-0').click(); + } + + getAddUserConfirm() { + return cy.getBySel('addUserConfirm'); + } + getRows() { + return cy.getBySel('table-row'); + } + + getConfirmDeleteBtn() { + return cy.getBySel('confirmDeletion'); + } +} diff --git a/cypress/support/repositories/project/ProjectRepository.ts b/cypress/support/repositories/project/ProjectRepository.ts new file mode 100644 index 00000000..b2312821 --- /dev/null +++ b/cypress/support/repositories/project/ProjectRepository.ts @@ -0,0 +1,43 @@ +export default class ProjectRepository { + getGitUrl() { + return cy.getBySel('gitLink'); + } + getCopyButton() { + return cy.getBySel('copyButton'); + } + getCreatedField() { + return cy.getBySel('created'); + } + getBranchesField() { + return cy.getBySel('branches'); + } + getPullRequestsField() { + return cy.getBySel('pullRequests'); + } + getDevEnvsField() { + return cy.getBySel('devEnvs'); + } + getEnvBtn() { + return cy.getBySel('createEnvironment'); + } + getBranchNameInput() { + return cy.getBySel('branchName'); + } + getSubmitBtn() { + return cy.getBySel('create-env'); + } + getErrorNotification() { + return cy.get('.ant-notification-notice'); + } + + getErrorModal() { + return cy.get('.ReactModal__Content'); + } + + getEnvRoutes() { + return cy.getBySel('route-label'); + } + getEnvNames() { + return cy.getBySel('environment-name'); + } +} diff --git a/cypress/support/repositories/projects/ProjectsRepository.ts b/cypress/support/repositories/projects/ProjectsRepository.ts new file mode 100644 index 00000000..221b98f6 --- /dev/null +++ b/cypress/support/repositories/projects/ProjectsRepository.ts @@ -0,0 +1,26 @@ +export default class ProjectRepository { + getPageTitle() { + return cy.getBySel('projectsTitle'); + } + getSearchBar() { + return cy.getBySel('searchBar'); + } + + getLengthCounter() { + return cy.getBySel('projectsLength'); + } + + getProjects() { + return cy.getBySel('projects'); + } + + getNotMatched() { + return cy.getBySel('noMatch'); + } + getNoProjectsLabel() { + return cy.getBySel('noProjects'); + } + getProject() { + return cy.getBySel('project'); + } +} diff --git a/cypress/support/repositories/settings/SettingsRepository.ts b/cypress/support/repositories/settings/SettingsRepository.ts new file mode 100644 index 00000000..046cd8c2 --- /dev/null +++ b/cypress/support/repositories/settings/SettingsRepository.ts @@ -0,0 +1,16 @@ +export default class SettingsRepository { + getNameInput() { + return cy.getBySel('sshKeyName'); + } + + getValueInput() { + return cy.getBySel('sshKey'); + } + + getSubmitBtn() { + return cy.getBySel('sshKey').parent().next(); + } + getDeleteBtn() { + return cy.getBySel('deleteKey').first().get('button').first(); + } +} diff --git a/cypress/support/repositories/task/TaskRepository.ts b/cypress/support/repositories/task/TaskRepository.ts new file mode 100644 index 00000000..dbc4a240 --- /dev/null +++ b/cypress/support/repositories/task/TaskRepository.ts @@ -0,0 +1,9 @@ +export default class TaskRepository { + getCancelBtn() { + return cy.getBySel('cancel-task').first(); + } + + getErrorNotification() { + return cy.get('.ant-notification-notice'); + } +} diff --git a/cypress/support/repositories/tasks/TasksRepository.ts b/cypress/support/repositories/tasks/TasksRepository.ts new file mode 100644 index 00000000..1a2ab2a8 --- /dev/null +++ b/cypress/support/repositories/tasks/TasksRepository.ts @@ -0,0 +1,31 @@ +export default class TasksRepository { + getTasks() { + return cy.getBySel('tasks-table'); + } + getCancelBtn() { + return cy.getBySel('cancel-task').first(); + } + getTaskSelector(taskNumber: number) { + cy.getBySel('select-task').click(); + + return cy.get(`[id^="react-select-"][id$=-option-${taskNumber}]`); + } + + getRunTaskBtn() { + return cy.getBySel('task-btn').contains('Run task'); + } + + getTaskConfirmed() { + return cy.getBySel('task-form').find('div'); + } + + getResultsLimited() { + return cy.getBySel('resultsLimited'); + } + getResultsSelector() { + return cy.getBySel('result_selector'); + } + getErrorNotification() { + return cy.get('.ant-notification-notice'); + } +} diff --git a/cypress/support/repositories/variables/VariablesRepository.ts b/cypress/support/repositories/variables/VariablesRepository.ts new file mode 100644 index 00000000..cfad46c1 --- /dev/null +++ b/cypress/support/repositories/variables/VariablesRepository.ts @@ -0,0 +1,27 @@ +export default class VariablesRepository { + getVariablesLink() { + return cy.getBySel('variablesLink'); + } + + getToggleShowButton() { + return cy.getBySel('hideShowValues'); + } + getAddButton() { + return cy.getBySel('addVariable'); + } + + getEnvDataRows() { + return cy.getBySel('environment-row'); + } + getVariableToDelete() { + return cy.getBySel('environment-row'); + } + getDeleteBtn(name: string) { + this.getVariableToDelete() + .contains(name) + .parent() + .within(() => { + cy.getBySel('varDelete').click(); + }); + } +} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000..a3c61708 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "include": ["./**/*.ts", "../cypress.d.ts"], + "exclude": [], + "compilerOptions": { + "types": ["cypress", "node", "cypress-real-events"], + "lib": ["es2015", "dom"], + "isolatedModules": false, + "allowJs": true, + "noEmit": true + } +} diff --git a/cypress/utils/aliasQuery.ts b/cypress/utils/aliasQuery.ts new file mode 100644 index 00000000..1580c3b3 --- /dev/null +++ b/cypress/utils/aliasQuery.ts @@ -0,0 +1,26 @@ +import { CyHttpMessages } from 'cypress/types/net-stubbing'; + +const hasOperationName = (req: CyHttpMessages.IncomingHttpRequest, operationName: string): boolean => { + const { body } = req; + return Object.prototype.hasOwnProperty.call(body, 'operationName') && body.operationName === operationName; +}; + +export const aliasQuery = (req: CyHttpMessages.IncomingHttpRequest, operationName: string): void => { + if (hasOperationName(req, operationName)) { + req.alias = `gql${operationName}Query`; + } +}; + +export const aliasMutation = (req: CyHttpMessages.IncomingHttpRequest, operationName: string): void => { + if (hasOperationName(req, operationName)) { + req.alias = `gql${operationName}Mutation`; + } +}; + +export const registerIdleHandler = (alias: string) => { + cy.waitForNetworkIdlePrepare({ + method: 'POST', + pattern: '*', + alias, + }); +}; diff --git a/docker-compose.yml b/docker-compose.yml index 55ce4d41..6100b46d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,8 @@ x-environment: &default-environment LAGOON_ROUTE: &default-url http://lagoon-ui.docker.amazee.io # Uncomment if you like to have the system behave like in production LAGOON_ENVIRONMENT_TYPE: production - GRAPHQL_API: "${GRAPHQL_API:-http://localhost:3000/graphql}" - KEYCLOAK_API: "${KEYCLOAK_API:-http://localhost:8088/auth}" + GRAPHQL_API: "${GRAPHQL_API:-http://0.0.0.0:3000/graphql}" + KEYCLOAK_API: "${KEYCLOAK_API:-http://0.0.0.0:8088/auth}" LAGOON_UI_TOURS_ENABLED: enabled services: diff --git a/package.json b/package.json index e3466c56..710572a2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,12 @@ "build-storybook": "storybook build", "serve-storybook": "storybook-server -s ./src", "format": "yarn prettier --write .", - "format-check": "yarn prettier --c ." + "format-check": "yarn prettier --c .", + "cypress:open": "cypress open --config-file cypress/cypress.config.ts", + "cypress:runAll": "cypress run --config-file cypress/cypress.config.ts", + "cypress:runGeneral": "cypress run --config-file cypress/cypress.config.ts --spec 'cypress/e2e/general/**/*.cy.ts'", + "cypress:runOrganizations": "cypress run --config-file cypress/cypress.config.ts --spec 'cypress/e2e/organizations/**/*.cy.ts'", + "cypress:runRbac": "cypress run --config-file cypress/cypress.config.ts --spec 'cypress/e2e/rbac/**/*.cy.ts'" }, "dependencies": { "@ant-design/cssinjs": "^1.17.0", @@ -38,6 +43,7 @@ "colors": "^1.4.0", "core-js": "^3.30.1", "crypto-js": "^4.1.1", + "cypress-real-events": "^1.10.3", "d3": "^7.8.4", "date-fns": "^2.9.0", "dotenv-extended": "^2.2.0", @@ -107,6 +113,8 @@ "babel-loader": "^9.1.2", "babel-plugin-macros": "^3.1.0", "chromatic": "^6.19.5", + "cypress": "13.6.1", + "cypress-network-idle": "^1.14.2", "eslint-plugin-storybook": "^0.6.12", "faker": "^6.6.6", "msw": "^1.2.1", diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index 87e0f31b..0fbd34f2 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -8,121 +8,121 @@ * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' -const activeClientIds = new Set() +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'; +const activeClientIds = new Set(); self.addEventListener('install', function () { - self.skipWaiting() -}) + self.skipWaiting(); +}); self.addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) + event.waitUntil(self.clients.claim()); +}); self.addEventListener('message', async function (event) { - const clientId = event.source.id + const clientId = event.source.id; if (!clientId || !self.clients) { - return + return; } - const client = await self.clients.get(clientId) + const client = await self.clients.get(clientId); if (!client) { - return + return; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', - }) - break + }); + break; } case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', payload: INTEGRITY_CHECKSUM, - }) - break + }); + break; } case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) + activeClientIds.add(clientId); sendToClient(client, { type: 'MOCKING_ENABLED', payload: true, - }) - break + }); + break; } case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break + activeClientIds.delete(clientId); + break; } case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) + activeClientIds.delete(clientId); - const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) + const remainingClients = allClients.filter(client => { + return client.id !== clientId; + }); // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister() + self.registration.unregister(); } - break + break; } } -}) +}); self.addEventListener('fetch', function (event) { - const { request } = event - const accept = request.headers.get('accept') || '' + const { request } = event; + const accept = request.headers.get('accept') || ''; // Bypass server-sent events. if (accept.includes('text/event-stream')) { - return + return; } // Bypass navigation requests. if (request.mode === 'navigate') { - return + return; } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { - return + return; } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been deleted (still remains active until the next reload). if (activeClientIds.size === 0) { - return + return; } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2) + const requestId = Math.random().toString(16).slice(2); event.respondWith( - handleRequest(event, requestId).catch((error) => { + handleRequest(event, requestId).catch(error => { if (error.name === 'NetworkError') { console.warn( '[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, - request.url, - ) - return + request.url + ); + return; } // At this point, any exception indicates an issue with the original request/response. @@ -131,22 +131,22 @@ self.addEventListener('fetch', function (event) { [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, request.method, request.url, - `${error.name}: ${error.message}`, - ) - }), - ) -}) + `${error.name}: ${error.message}` + ); + }) + ); +}); async function handleRequest(event, requestId) { - const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - ;(async function () { - const clonedResponse = response.clone() + (async function () { + const clonedResponse = response.clone(); sendToClient(client, { type: 'RESPONSE', payload: { @@ -155,16 +155,15 @@ async function handleRequest(event, requestId) { ok: clonedResponse.ok, status: clonedResponse.status, statusText: clonedResponse.statusText, - body: - clonedResponse.body === null ? null : await clonedResponse.text(), + body: clonedResponse.body === null ? null : await clonedResponse.text(), headers: Object.fromEntries(clonedResponse.headers.entries()), redirected: clonedResponse.redirected, }, - }) - })() + }); + })(); } - return response + return response; } // Resolve the main client for the given event. @@ -172,49 +171,49 @@ async function handleRequest(event, requestId) { // that registered the worker. It's with the latter the worker should // communicate with during the response resolving phase. async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) + const client = await self.clients.get(event.clientId); if (client?.frameType === 'top-level') { - return client + return client; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); return allClients - .filter((client) => { + .filter(client => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible' + return client.visibilityState === 'visible'; }) - .find((client) => { + .find(client => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) + return activeClientIds.has(client.id); + }); } async function getResponse(event, client, requestId) { - const { request } = event - const clonedRequest = request.clone() + const { request } = event; + const clonedRequest = request.clone(); function passthrough() { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()) + const headers = Object.fromEntries(clonedRequest.headers.entries()); // Remove MSW-specific request headers so the bypassed requests // comply with the server's CORS preflight check. // Operate with the headers as an object because request "Headers" // are immutable. - delete headers['x-msw-bypass'] + delete headers['x-msw-bypass']; - return fetch(clonedRequest, { headers }) + return fetch(clonedRequest, { headers }); } // Bypass mocking when the client is not active. if (!client) { - return passthrough() + return passthrough(); } // Bypass initial page load requests (i.e. static assets). @@ -222,13 +221,13 @@ async function getResponse(event, client, requestId) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough() + return passthrough(); } // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". if (request.headers.get('x-msw-bypass') === 'true') { - return passthrough() + return passthrough(); } // Notify the client that a request has been intercepted. @@ -251,53 +250,53 @@ async function getResponse(event, client, requestId) { bodyUsed: request.bodyUsed, keepalive: request.keepalive, }, - }) + }); switch (clientMessage.type) { case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) + return respondWithMock(clientMessage.data); } case 'MOCK_NOT_FOUND': { - return passthrough() + return passthrough(); } case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data - const networkError = new Error(message) - networkError.name = name + const { name, message } = clientMessage.data; + const networkError = new Error(message); + networkError.name = name; // Rejecting a "respondWith" promise emulates a network error. - throw networkError + throw networkError; } } - return passthrough() + return passthrough(); } function sendToClient(client, message) { return new Promise((resolve, reject) => { - const channel = new MessageChannel() + const channel = new MessageChannel(); - channel.port1.onmessage = (event) => { + channel.port1.onmessage = event => { if (event.data && event.data.error) { - return reject(event.data.error) + return reject(event.data.error); } - resolve(event.data) - } + resolve(event.data); + }; - client.postMessage(message, [channel.port2]) - }) + client.postMessage(message, [channel.port2]); + }); } function sleep(timeMs) { - return new Promise((resolve) => { - setTimeout(resolve, timeMs) - }) + return new Promise(resolve => { + setTimeout(resolve, timeMs); + }); } async function respondWithMock(response) { - await sleep(response.delay) - return new Response(response.body, response) + await sleep(response.delay); + return new Response(response.body, response); } diff --git a/server.js b/server.js index fdb71b38..ac728069 100644 --- a/server.js +++ b/server.js @@ -43,12 +43,11 @@ app server.get('/projects/:projectSlug/project-variables', (req, res) => { app.render(req, res, '/project-variables', { projectName: req.params.projectSlug }); }); - + server.get('/projects/:projectSlug/deploy-targets', (req, res) => { app.render(req, res, '/deploy-targets', { projectName: req.params.projectSlug }); }); - server.get('/projects/:projectSlug/:environmentSlug', (req, res) => { app.render(req, res, '/environment', { openshiftProjectName: req.params.environmentSlug, @@ -107,7 +106,7 @@ app server.get('/projects/:projectSlug/:environmentSlug/environment-variables', (req, res) => { app.render(req, res, '/environment-variables', { - openshiftProjectName: req.params.environmentSlug + openshiftProjectName: req.params.environmentSlug, }); }); @@ -131,58 +130,58 @@ app server.get('/organizations/:organizationSlug', (req, res) => { app.render(req, res, '/organizations/organization', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); server.get('/organizations/:organizationSlug/groups', (req, res) => { app.render(req, res, '/organizations/groups', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); server.get('/organizations/:organizationSlug/groups/:groupSlug', (req, res) => { app.render(req, res, '/organizations/group', { organizationSlug: req.params.organizationSlug, - groupName: req.params.groupSlug + groupName: req.params.groupSlug, }); }); server.get('/organizations/:organizationSlug/users', (req, res) => { app.render(req, res, '/organizations/users', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); server.get('/organizations/:organizationSlug/users/:userSlug', (req, res) => { app.render(req, res, '/organizations/user', { organizationSlug: req.params.organizationSlug, - userSlug: req.params.userSlug + userSlug: req.params.userSlug, }); }); server.get('/organizations/:organizationSlug/projects', (req, res) => { app.render(req, res, '/organizations/projects', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); server.get('/organizations/:organizationSlug/projects/:projectGroupSlug', (req, res) => { app.render(req, res, '/organizations/project', { organizationSlug: req.params.organizationSlug, - projectName: req.params.projectGroupSlug + projectName: req.params.projectGroupSlug, }); }); server.get('/organizations/:organizationSlug/notifications', (req, res) => { app.render(req, res, '/organizations/notifications', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); server.get('/organizations/:organizationSlug/manage', (req, res) => { app.render(req, res, '/organizations/manage', { - organizationSlug: req.params.organizationSlug + organizationSlug: req.params.organizationSlug, }); }); // organizations end diff --git a/src/components/Accordion/StyledAccordion.tsx b/src/components/Accordion/StyledAccordion.tsx index 4b979a60..b208af58 100644 --- a/src/components/Accordion/StyledAccordion.tsx +++ b/src/components/Accordion/StyledAccordion.tsx @@ -13,8 +13,7 @@ export const StyledAccordion = styled.div` justify-content: space-between; padding: 20px 12px; border: 1px solid ${color.lightestGrey}; - background: ${props => - props.theme.colorScheme === 'dark' ? `${props.theme.backgrounds.primary}` : "#fff" }; + background: ${props => (props.theme.colorScheme === 'dark' ? `${props.theme.backgrounds.primary}` : '#fff')}; cursor: pointer; word-break: break-word; @@ -24,7 +23,7 @@ export const StyledAccordion = styled.div` > div { padding: 0 6px; - color: ${props => props.theme.texts.primary} + color: ${props => props.theme.texts.primary}; } &.cols-6 { diff --git a/src/components/Accordion/index.stories.tsx b/src/components/Accordion/index.stories.tsx index eb633fcf..d72e8679 100644 --- a/src/components/Accordion/index.stories.tsx +++ b/src/components/Accordion/index.stories.tsx @@ -1,6 +1,7 @@ +import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; -import {expect} from "@storybook/jest"; + import Accordion from './index'; /** @@ -41,25 +42,25 @@ export const Minified: Story = { export const WithChildren: Story = { args: { ...Default.args, - defaultValue:false, + defaultValue: false, children: (
  • List item
  • Another list item
- ) + ), }, - play:async ({canvasElement})=>{ + play: async ({ canvasElement }) => { // toggle functionality - const canvas = within(canvasElement) - const element = await Promise.resolve(canvas.getByTestId("storybook-accordion")); + const canvas = within(canvasElement); + const element = await Promise.resolve(canvas.getByTestId('storybook-accordion')); await Promise.resolve(userEvent.click(element)); - expect(await Promise.resolve(canvas.getAllByRole("listitem").length)).toBe(2); + expect(await Promise.resolve(canvas.getAllByRole('listitem').length)).toBe(2); // toggle visibility await Promise.resolve(userEvent.click(element)); - expect(await Promise.resolve(canvas.queryByRole("list"))).toBeNull(); - } + expect(await Promise.resolve(canvas.queryByRole('list'))).toBeNull(); + }, }; export default meta; diff --git a/src/components/ActiveStandbyConfirm/StyledActiveStandbyConfirm.tsx b/src/components/ActiveStandbyConfirm/StyledActiveStandbyConfirm.tsx index fae004c8..9af7c91f 100644 --- a/src/components/ActiveStandbyConfirm/StyledActiveStandbyConfirm.tsx +++ b/src/components/ActiveStandbyConfirm/StyledActiveStandbyConfirm.tsx @@ -27,4 +27,4 @@ export const StyledActiveStandbyConfirmModal = styled.div` display: flex; align-items: center; } -`; \ No newline at end of file +`; diff --git a/src/components/ActiveStandbyConfirm/index.tsx b/src/components/ActiveStandbyConfirm/index.tsx index 0fb452bf..e65fe8ef 100644 --- a/src/components/ActiveStandbyConfirm/index.tsx +++ b/src/components/ActiveStandbyConfirm/index.tsx @@ -32,7 +32,7 @@ export const ActiveStandbyConfirm: FC = ({
- +

This will replace the current active environment diff --git a/src/components/AddTask/components/CustomTaskConfirm.tsx b/src/components/AddTask/components/CustomTaskConfirm.tsx index 16e865d8..b1a609e3 100644 --- a/src/components/AddTask/components/CustomTaskConfirm.tsx +++ b/src/components/AddTask/components/CustomTaskConfirm.tsx @@ -33,7 +33,7 @@ export const CustomTaskConfirm: FC = ({ Confirm Task - + {taskText}

diff --git a/src/components/AddTask/components/DrushArchiveDump.js b/src/components/AddTask/components/DrushArchiveDump.js index 53bc6e32..5058c403 100644 --- a/src/components/AddTask/components/DrushArchiveDump.js +++ b/src/components/AddTask/components/DrushArchiveDump.js @@ -57,7 +57,9 @@ const DrushArchiveDump = ({ pageEnvironment, onCompleted, onError, onNewTask }) required />
- + ); }} diff --git a/src/components/AddTask/components/DrushCacheClear.js b/src/components/AddTask/components/DrushCacheClear.js index baa83e75..d82d767f 100644 --- a/src/components/AddTask/components/DrushCacheClear.js +++ b/src/components/AddTask/components/DrushCacheClear.js @@ -57,7 +57,9 @@ const DrushCacheClear = ({ pageEnvironment, onCompleted, onError, onNewTask }) = required /> - + ); }} diff --git a/src/components/AddTask/components/DrushCron.js b/src/components/AddTask/components/DrushCron.js index 9e266ee2..205b2113 100644 --- a/src/components/AddTask/components/DrushCron.js +++ b/src/components/AddTask/components/DrushCron.js @@ -58,7 +58,9 @@ const DrushCron = ({ pageEnvironment, onCompleted, onError, onNewTask }) => ( required /> - + ); }} diff --git a/src/components/AddTask/components/DrushRsyncFiles.js b/src/components/AddTask/components/DrushRsyncFiles.js index 049c6782..caa3a1cc 100644 --- a/src/components/AddTask/components/DrushRsyncFiles.js +++ b/src/components/AddTask/components/DrushRsyncFiles.js @@ -78,6 +78,7 @@ const DrushRsyncFiles = ({ /> ); diff --git a/src/components/AddTask/components/DrushSqlDump.js b/src/components/AddTask/components/DrushSqlDump.js index 258b1765..2988ed3b 100644 --- a/src/components/AddTask/components/DrushSqlDump.js +++ b/src/components/AddTask/components/DrushSqlDump.js @@ -57,7 +57,9 @@ const DrushSqlDump = ({ pageEnvironment, onCompleted, onError, onNewTask }) => ( required /> - + ); }} diff --git a/src/components/AddTask/components/DrushSqlSync.js b/src/components/AddTask/components/DrushSqlSync.js index 353cb187..748dbf7b 100644 --- a/src/components/AddTask/components/DrushSqlSync.js +++ b/src/components/AddTask/components/DrushSqlSync.js @@ -78,6 +78,7 @@ const DrushSqlSync = ({ /> ); diff --git a/src/components/AddTask/components/DrushUserLogin.js b/src/components/AddTask/components/DrushUserLogin.js index 2a3f52fc..9a3e4340 100644 --- a/src/components/AddTask/components/DrushUserLogin.js +++ b/src/components/AddTask/components/DrushUserLogin.js @@ -34,8 +34,8 @@ const DrushUserLogin = ({ pageEnvironment, onCompleted, onError, onNewTask }) => > {(taskDrushUserLogin, { loading, data }) => { if (data) { - onNewTask(); - } + onNewTask(); + } return (
@@ -57,7 +57,9 @@ const DrushUserLogin = ({ pageEnvironment, onCompleted, onError, onNewTask }) => required />
- +
); }} diff --git a/src/components/AddTask/components/InvokeRegisteredTask.js b/src/components/AddTask/components/InvokeRegisteredTask.js index 2c0539aa..f51ab4a6 100644 --- a/src/components/AddTask/components/InvokeRegisteredTask.js +++ b/src/components/AddTask/components/InvokeRegisteredTask.js @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, { useEffect } from 'react'; import { Mutation } from 'react-apollo'; import ReactSelect from 'react-select'; @@ -52,7 +52,7 @@ const InvokeRegisteredTask = ({ defaultArgValues[item.name] = item.defaultValue; } }); - setAdvancedTaskArguments(defaultArgValues) + setAdvancedTaskArguments(defaultArgValues); }, []); let taskArgumentsExist = false; @@ -134,7 +134,7 @@ const InvokeRegisteredTask = ({ { setAdvancedTaskArguments({ ...advancedTaskArguments, @@ -164,10 +164,11 @@ const InvokeRegisteredTask = ({ /> )) || ( )} diff --git a/src/components/AddTask/components/Styles.tsx b/src/components/AddTask/components/Styles.tsx index e24a548f..c8b25a30 100644 --- a/src/components/AddTask/components/Styles.tsx +++ b/src/components/AddTask/components/Styles.tsx @@ -36,19 +36,19 @@ export const SelectWrapper = styled.div` color: white; padding: 10px; } - + .btn--disabled { margin-right: 0; } - + .loader { display: inline-block; width: 50px; height: 15px; } - + .loader:after { - content: " "; + content: ' '; display: block; width: 20px; height: 20px; @@ -58,7 +58,7 @@ export const SelectWrapper = styled.div` border-color: ${color.blue} transparent ${color.blue} transparent; animation: loader 1.2s linear infinite; } - + @keyframes loader { 0% { transform: rotate(0deg); @@ -94,19 +94,19 @@ export const StyledRegisteredTasks = styled.div` color: white; padding: 10px; } - + .btn--disabled { margin-right: 0; } - + .loader { display: inline-block; width: 50px; height: 15px; } - + .loader:after { - content: " "; + content: ' '; display: block; width: 20px; height: 20px; @@ -116,7 +116,7 @@ export const StyledRegisteredTasks = styled.div` border-color: ${color.blue} transparent ${color.blue} transparent; animation: loader 1.2s linear infinite; } - + @keyframes loader { 0% { transform: rotate(0deg); diff --git a/src/components/AddTask/index.js b/src/components/AddTask/index.js index 74df16e8..d453c513 100644 --- a/src/components/AddTask/index.js +++ b/src/components/AddTask/index.js @@ -52,7 +52,7 @@ const AddTask = ({ -
+
{selectedTask && ( -
+
props.theme.backgrounds.primary}; + .react-select__control, + .react-select__menu, + .react-select__option, + .react-select__single-value { + background: ${props => props.theme.backgrounds.primary}; color: ${props => props.theme.texts.primary}; } -`; \ No newline at end of file +`; diff --git a/src/components/AddVariable/index.js b/src/components/AddVariable/index.js index d60bb851..c8bdabf2 100644 --- a/src/components/AddVariable/index.js +++ b/src/components/AddVariable/index.js @@ -1,13 +1,15 @@ -import React, {useEffect, useState} from "react"; -import Modal from "components/Modal"; -import ButtonBootstrap from "react-bootstrap/Button"; -import Button from 'components/Button' -import ReactSelect from "react-select"; -import { Mutation } from "react-apollo"; -import withLogic from "components/AddVariable/logic"; -import addOrUpdateEnvVariableMutation from "../../lib/mutation/AddOrUpdateEnvVariableByName"; -import { NewVariable, NewVariableModal } from "./StyledAddVariable"; +import React, { useEffect, useState } from 'react'; +import { Mutation } from 'react-apollo'; +import ButtonBootstrap from 'react-bootstrap/Button'; +import ReactSelect from 'react-select'; + import { Popconfirm } from 'antd'; +import withLogic from 'components/AddVariable/logic'; +import Button from 'components/Button'; +import Modal from 'components/Modal'; + +import addOrUpdateEnvVariableMutation from '../../lib/mutation/AddOrUpdateEnvVariableByName'; +import { NewVariable, NewVariableModal } from './StyledAddVariable'; import {LoadingOutlined} from "@ant-design/icons"; /** @@ -15,13 +17,13 @@ import {LoadingOutlined} from "@ant-design/icons"; */ const scopeOptions = [ - { value: "BUILD", label: "BUILD" }, - { value: "RUNTIME", label: "RUNTIME" }, - { value: "GLOBAL", label: "GLOBAL" }, - { value: "CONTAINER_REGISTRY", label: "CONTAINER_REGISTRY" }, + { value: 'BUILD', label: 'BUILD' }, + { value: 'RUNTIME', label: 'RUNTIME' }, + { value: 'GLOBAL', label: 'GLOBAL' }, + { value: 'CONTAINER_REGISTRY', label: 'CONTAINER_REGISTRY' }, { - value: "INTERNAL_CONTAINER_REGISTRY", - label: "INTERNAL_CONTAINER_REGISTRY", + value: 'INTERNAL_CONTAINER_REGISTRY', + label: 'INTERNAL_CONTAINER_REGISTRY', }, ]; @@ -69,8 +71,8 @@ export const AddVariable = ({ setUpdateScope(varScope); }, [varName, varValue]); const handleError = () => { - setProjectErrorAlert ? setProjectErrorAlert(true) : setEnvironmentErrorAlert(true) - } + setProjectErrorAlert ? setProjectErrorAlert(true) : setEnvironmentErrorAlert(true); + }; const handlePermissionCheck = () => { let waitForGQL = setTimeout(() => { @@ -86,11 +88,11 @@ export const AddVariable = ({ { icon ? - : !loading ? - + Add : @@ -99,7 +101,7 @@ export const AddVariable = ({ isOpen={open} onRequestClose={closeModal} contentLabel={`Confirm`} - variant={"large"} + variant={'large'} >
@@ -117,9 +119,15 @@ export const AddVariable = ({ aria-label="Scope" placeholder="Select a variable scope" name="results" - value={varScope ? scopeOptions.find((o) => o.value === updateScope.toUpperCase()) : scopeOptions.find((o) => o.value === inputScope)} - onChange={varScope ? (selectedOption) => setUpdateScope(selectedOption.value) - : (selectedOption) => setInputScope(selectedOption.value) + value={ + varScope + ? scopeOptions.find(o => o.value === updateScope.toUpperCase()) + : scopeOptions.find(o => o.value === inputScope) + } + onChange={ + varScope + ? selectedOption => setUpdateScope(selectedOption.value) + : selectedOption => setInputScope(selectedOption.value) } options={scopeOptions} required @@ -129,6 +137,7 @@ export const AddVariable = ({
-
+
cancel - + console.error(e)}> {(addOrUpdateEnvVariableByName, { called, error, data, loading }) => { - let updateVar = varValues.map((varName) => { + let updateVar = varValues.map(varName => { return varName.name; }); updateVar = updateVar.includes(inputName); @@ -168,10 +178,14 @@ export const AddVariable = ({ refresh().then(closeModal).then(handleError); } - if (action === "add" && inputValue !== '' || action === "edit" && updateValue !== '' || action === "edit" && inputValue !== '') { - if (action === "edit" && called ) { + if ( + (action === 'add' && inputValue !== '') || + (action === 'edit' && updateValue !== '') || + (action === 'edit' && inputValue !== '') + ) { + if (action === 'edit' && called) { return
Updating variable
; - } else if (action === "add" && called) { + } else if (action === 'add' && called) { return
Adding variable
; } } @@ -193,7 +207,10 @@ export const AddVariable = ({ }, 1000); }; - if (action === "add" && inputValue === '' || action === "edit" && updateValue === '' && inputValue === '') { + if ( + (action === 'add' && inputValue === '') || + (action === 'edit' && updateValue === '' && inputValue === '') + ) { return ( - {varTarget === "Environment" + {varTarget === 'Environment' ? updateVar || varName - ? "Update environment variable" - : "Add environment variable" + ? 'Update environment variable' + : 'Add environment variable' : updateVar || varName - ? "Update project variable" - : "Add project variable"} + ? 'Update project variable' + : 'Add project variable'} ); } else { return ( - {varTarget === "Environment" + {varTarget === 'Environment' ? updateVar || varName - ? "Update environment variable" - : "Add environment variable" + ? 'Update environment variable' + : 'Add environment variable' : updateVar || varName - ? "Update project variable" - : "Add project variable"} + ? 'Update project variable' + : 'Add project variable'} - ) + ); } - }}
diff --git a/src/components/AddVariable/logic.js b/src/components/AddVariable/logic.js index bb0560d7..c75a7050 100644 --- a/src/components/AddVariable/logic.js +++ b/src/components/AddVariable/logic.js @@ -1,25 +1,37 @@ -import compose from "recompose/compose"; -import withState from "recompose/withState"; -import withHandlers from "recompose/withHandlers"; +import compose from 'recompose/compose'; +import withHandlers from 'recompose/withHandlers'; +import withState from 'recompose/withState'; -const withInputValue = withState("inputValue", "setInputValue", ""); -const withInputName = withState("inputName", "setInputName", ""); -const withInputScope = withState("inputScope", "setInputScope", ""); -const withInputClear = withState("clear", "setClear", ""); +const withInputValue = withState('inputValue', 'setInputValue', ''); +const withInputName = withState('inputName', 'setInputName', ''); +const withInputScope = withState('inputScope', 'setInputScope', ''); +const withInputClear = withState('clear', 'setClear', ''); const withInputHandlers = withHandlers({ - setInputValue: ({ setInputValue }) => (event) => - setInputValue(event.target.value), - setInputName: ({ setInputName }) => (event) => - setInputName(event.target.value), - setClear: ({ setInputValue, setInputName, setInputScope }) => () => + setInputValue: + ({ setInputValue }) => + event => + setInputValue(event.target.value), + setInputName: + ({ setInputName }) => + event => + setInputName(event.target.value), + setClear: + ({ setInputValue, setInputName, setInputScope }) => + () => [setInputValue(''), setInputName(''), setInputScope('')], }); -const withModalState = withState("open", "setOpen", false); +const withModalState = withState('open', 'setOpen', false); const withModalHandlers = withHandlers({ - openModal: ({ setOpen }) => () => setOpen(true), - closeModal: ({ setOpen }) => () => setOpen(false), + openModal: + ({ setOpen }) => + () => + setOpen(true), + closeModal: + ({ setOpen }) => + () => + setOpen(false), }); export default compose( diff --git a/src/components/Alert/StyledAlert.tsx b/src/components/Alert/StyledAlert.tsx index e629dc4f..53d8e148 100644 --- a/src/components/Alert/StyledAlert.tsx +++ b/src/components/Alert/StyledAlert.tsx @@ -1,5 +1,5 @@ +import { color } from 'lib/variables'; import styled, { css } from 'styled-components'; -import { color } from "lib/variables"; const sharedStyles = css` position: relative; @@ -32,14 +32,13 @@ export const StyledAlert = styled.div` cursor: pointer; transition: 0.3s; cursor: pointer; - + &.closebtn:hover { color: black; } } - `; export const StyledAlertContent = styled.div` - ${sharedStyles} -`; \ No newline at end of file + ${sharedStyles} +`; diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js index b21aa671..c9a4abad 100644 --- a/src/components/Alert/index.js +++ b/src/components/Alert/index.js @@ -1,29 +1,27 @@ -import React from "react"; -import {StyledAlert, StyledAlertContent} from './StyledAlert'; +import React from 'react'; -export const Alert = ({ - type, header, message, closeAlert -}) => { - const createClassName = () => { - let className = `${type ? `${type} alert-element` : 'alert-element'}`; - return className; - }; +import { StyledAlert, StyledAlertContent } from './StyledAlert'; - const AlertElement = - - closeAlert()}> - × +export const Alert = ({ type, header, message, closeAlert }) => { + const createClassName = () => { + let className = `${type ? `${type} alert-element` : 'alert-element'}`; + return className; + }; + + const AlertElement = ( + + closeAlert()}> + × + + + + {header} {message} - - - {header} {message} - - - + + + ); - - - return <>{AlertElement}; -} + return <>{AlertElement}; +}; -export default Alert; \ No newline at end of file +export default Alert; diff --git a/src/components/Backups/index.stories.tsx b/src/components/Backups/index.stories.tsx index a2b15e6c..42a882d1 100644 --- a/src/components/Backups/index.stories.tsx +++ b/src/components/Backups/index.stories.tsx @@ -1,11 +1,12 @@ import React from 'react'; + import { faker } from '@faker-js/faker'; import { Meta } from '@storybook/react'; import withButtonOverrides from '../../../.storybook/decorators/withButtonOverrides'; import Backups, { BackupsProps } from './index'; -const meta:Meta = { +const meta: Meta = { component: Backups, title: 'Components/Backups', decorators: [withButtonOverrides('.download', 'click', 'Backups button click')], @@ -14,18 +15,18 @@ const meta:Meta = { faker.seed(123); const backupsData = [ { - id:faker.string.uuid(), + id: faker.string.uuid(), source: 'mariadb', created: '2019-11-18T08:00:00', backupId: '40', restore: { status: 'completed', restoreLocation: 'https://example.com/backup', - restoreSize: 300 + restoreSize: 300, }, }, { - id:faker.string.uuid(), + id: faker.string.uuid(), source: 'mariadb', created: '2019-11-19T08:00:00', backupId: '41', @@ -34,7 +35,7 @@ const backupsData = [ }, }, { - id:faker.string.uuid(), + id: faker.string.uuid(), source: 'mariadb', created: '2019-11-19T09:00:00', backupId: '42', @@ -42,13 +43,10 @@ const backupsData = [ status: 'pending', }, }, -] satisfies BackupsProps["backups"]; - - +] satisfies BackupsProps['backups']; export const Default = () => ; export const NoBackups = () => ; - -export default meta; \ No newline at end of file +export default meta; diff --git a/src/components/Backups/index.tsx b/src/components/Backups/index.tsx index 14e20c71..79cb5d2e 100644 --- a/src/components/Backups/index.tsx +++ b/src/components/Backups/index.tsx @@ -20,7 +20,7 @@ export interface BackupsProps { } const Backups: FC = ({ backups }) => ( -
+
@@ -30,12 +30,12 @@ const Backups: FC = ({ backups }) => ( {!backups.length &&
No Backups
} {backups.map(backup => ( -
+
{backup.source}
{moment.utc(backup.created).local().format('DD MMM YYYY, HH:mm:ss (Z)')}
{backup.backupId}
-
+
diff --git a/src/components/Breadcrumbs/StyledBreadCrumb.tsx b/src/components/Breadcrumbs/StyledBreadCrumb.tsx index eb3e3189..fbad1654 100644 --- a/src/components/Breadcrumbs/StyledBreadCrumb.tsx +++ b/src/components/Breadcrumbs/StyledBreadCrumb.tsx @@ -48,7 +48,7 @@ export const BreadCrumbLink = styled.a` export const StyledBreadcrumbsWrapper = styled.div` padding: 1vw 0.75vw; - min-height:100px; + min-height: 100px; background-color: ${props => props.theme.backgrounds.breadCrumbs}; border-bottom: ${props => props.theme.colorScheme === 'dark' ? `2px solid ${props.theme.borders.box}` : `1px solid ${color.midGrey}`}; diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx index 99ae8b9a..cd2e3218 100644 --- a/src/components/Breadcrumbs/index.tsx +++ b/src/components/Breadcrumbs/index.tsx @@ -5,7 +5,7 @@ import { StyledBreadcrumbsWrapper } from './StyledBreadCrumb'; /** * Displays the Project and, optionally, the Environment breadcrumbs. */ -const Breadcrumbs = ({ children }: { children: JSX.Element[] | ReactNode}) => ( +const Breadcrumbs = ({ children }: { children: JSX.Element[] | ReactNode }) => (
{children}
diff --git a/src/components/BulkDeployments/StyledBulkDeployments.tsx b/src/components/BulkDeployments/StyledBulkDeployments.tsx index 35f719e7..e69853dd 100644 --- a/src/components/BulkDeployments/StyledBulkDeployments.tsx +++ b/src/components/BulkDeployments/StyledBulkDeployments.tsx @@ -87,9 +87,9 @@ export const BulkDeploymentsDataTable = styled.div` .priority { width: 8%; } - .buildstep{ - display:flex; - flex-direction:column; + .buildstep { + display: flex; + flex-direction: column; } .status { @media ${bp.xs_smallOnly} { diff --git a/src/components/BulkDeployments/index.stories.tsx b/src/components/BulkDeployments/index.stories.tsx index 52012f25..08613a26 100644 --- a/src/components/BulkDeployments/index.stories.tsx +++ b/src/components/BulkDeployments/index.stories.tsx @@ -12,16 +12,13 @@ export default { title: 'Components/BulkDeployments', tags: ['autodocs'], decorators: [withButtonOverrides('button', 'click', 'Deployment button click')], - argTypes:{ + argTypes: { deployments: { description: 'Deployments array', }, - } - + }, }; - - const data = [ { ...getDeployment(123), @@ -45,7 +42,6 @@ const data = [ }, ]; - type Story = StoryObj; export const Complete: Story = { diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 268de7cb..090a7097 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -12,6 +12,7 @@ interface ButtonProps { children?: ReactNode; variant?: string; icon?: string; + testId?: string; } const Button: FC = ({ @@ -22,6 +23,7 @@ const Button: FC = ({ children, variant, icon, + testId, }) => { const createClassName = () => { let className = `${variant ? `btn-${variant}` : 'btn'} ${disabled ? 'btn--disabled' : ''}`; @@ -40,9 +42,11 @@ const Button: FC = ({ } }; + const buttonTestProps = testId ? { ['data-cy']: testId } : {}; + const ButtonElement = href ? ( - {icon && } {children} + {icon && } {children} ) : ( = ({ className={createClassName()} onClick={onClick} disabled={loading || disabled} + {...buttonTestProps} > {icon && (typeof icon === 'string' ? : icon)} diff --git a/src/components/CancelDeployment/index.js b/src/components/CancelDeployment/index.js index 833754bf..a1dca598 100644 --- a/src/components/CancelDeployment/index.js +++ b/src/components/CancelDeployment/index.js @@ -26,7 +26,7 @@ export const CancelDeploymentButton = ({ action, success, loading, error, before return ( <> {contextHolder} - {error && openNotificationWithIcon(error.message)} @@ -36,6 +36,7 @@ export const CancelDeploymentButton = ({ action, success, loading, error, before const CancelDeployment = ({ deployment, beforeText, afterText }) => ( console.error(e)} mutation={CANCEL_DEPLOYMENT_MUTATION} variables={{ deploymentId: deployment.id, diff --git a/src/components/CancelTask/index.tsx b/src/components/CancelTask/index.tsx index 383a8a10..52c464e5 100644 --- a/src/components/CancelTask/index.tsx +++ b/src/components/CancelTask/index.tsx @@ -60,7 +60,7 @@ export const CancelTaskButton: FC = ({ return ( <> {contextHolder} - diff --git a/src/components/DeleteConfirm/index.js b/src/components/DeleteConfirm/index.js index dc33bc14..1eec085f 100644 --- a/src/components/DeleteConfirm/index.js +++ b/src/components/DeleteConfirm/index.js @@ -21,7 +21,7 @@ export const DeleteConfirm = ({ }) => { return ( - @@ -33,11 +33,11 @@ export const DeleteConfirm = ({

Type the name of the {deleteType} to confirm.

- + -
diff --git a/src/components/DeleteConfirm/index.stories.tsx b/src/components/DeleteConfirm/index.stories.tsx index e74e787c..18a5d104 100644 --- a/src/components/DeleteConfirm/index.stories.tsx +++ b/src/components/DeleteConfirm/index.stories.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; +import { DeleteOutlined } from '@ant-design/icons'; import { action } from '@storybook/addon-actions'; import { Meta } from '@storybook/react'; import DeleteConfirm, { DeleteConfirm as DeleteConfirmBaseComponent } from './index'; - interface Props { onDeleteFunction: () => void; setInputValueFunction: () => void; @@ -42,6 +42,7 @@ export const WithConfirmationBlocked = ({ closeModalFunction, }: Props) => ( } deleteType="environment" deleteName="Forty-two" onDelete={onDeleteFunction} @@ -53,4 +54,4 @@ export const WithConfirmationBlocked = ({ /> ); -export default meta; \ No newline at end of file +export default meta; diff --git a/src/components/DeleteVariable/StyledDeleteVariable.tsx b/src/components/DeleteVariable/StyledDeleteVariable.tsx index f9be0c4b..f193e9db 100644 --- a/src/components/DeleteVariable/StyledDeleteVariable.tsx +++ b/src/components/DeleteVariable/StyledDeleteVariable.tsx @@ -1,5 +1,5 @@ -import styled from "styled-components"; -import { color } from "lib/variables"; +import { color } from 'lib/variables'; +import styled from 'styled-components'; export const DeleteVariableModal = styled.div` input { diff --git a/src/components/DeleteVariable/index.js b/src/components/DeleteVariable/index.js index 04cb8639..e5e594a1 100644 --- a/src/components/DeleteVariable/index.js +++ b/src/components/DeleteVariable/index.js @@ -1,11 +1,13 @@ -import React, {useEffect, useState} from "react"; -import Modal from "components/Modal"; -import ButtonBootstrap from "react-bootstrap/Button"; -import Button from 'components/Button' -import { Mutation } from "react-apollo"; -import withLogic from "components/AddVariable/logic"; -import {DeleteVariableModal, DeleteVariableButton} from "./StyledDeleteVariable"; -import DeleteEnvVariableMutation from "../../lib/mutation/deleteEnvVariableByName"; +import React, { useEffect, useState } from 'react'; +import { Mutation } from 'react-apollo'; +import ButtonBootstrap from 'react-bootstrap/Button'; + +import withLogic from 'components/AddVariable/logic'; +import Button from 'components/Button'; +import Modal from 'components/Modal'; + +import DeleteEnvVariableMutation from '../../lib/mutation/deleteEnvVariableByName'; +import { DeleteVariableButton, DeleteVariableModal } from './StyledDeleteVariable'; /** * Deletes a Variable. @@ -26,34 +28,27 @@ export const DeleteVariable = ({ closeModal, }) => { return ( - - - { - icon ? - - : - - } - - + + + {icon ? ( + + ) : ( + + )} + +

- This will delete the {deleteType}{' '} - {deleteName} and cannot be undone. Make sure this is something you - really want to do! + This will delete the {deleteType} {deleteName} and cannot be undone. + Make sure this is something you really want to do!

Type the name of the {deleteType} to confirm.

- + @@ -61,12 +56,7 @@ export const DeleteVariable = ({ {(deleteEnvVariableByName, { loading, error, data }) => { if (error) { console.error(error); - return ( -
- Unauthorized: You don't have permission to - delete this variable. -
- ); + return
Unauthorized: You don't have permission to delete this variable.
; } if (data) { @@ -86,7 +76,12 @@ export const DeleteVariable = ({ }; return ( - + {loading ? 'Deleting...' : 'Delete'} ); diff --git a/src/components/DeleteVariable/logic.js b/src/components/DeleteVariable/logic.js index 9ed4801e..909df960 100644 --- a/src/components/DeleteVariable/logic.js +++ b/src/components/DeleteVariable/logic.js @@ -5,23 +5,25 @@ import withState from 'recompose/withState'; const withInputValue = withState('inputValue', 'setInputValue', ''); const withInputHandlers = withHandlers({ setInputValue: - ({ setInputValue }) => - event => - setInputValue(event.target.value), - setClear: ({ setInputValue }) => () => + ({ setInputValue }) => + event => + setInputValue(event.target.value), + setClear: + ({ setInputValue }) => + () => [setInputValue('')], }); const withModalState = withState('open', 'setOpen', false); const withModalHandlers = withHandlers({ openModal: - ({ setOpen }) => - () => - setOpen(true), + ({ setOpen }) => + () => + setOpen(true), closeModal: - ({ setOpen }) => - () => - setOpen(false), + ({ setOpen }) => + () => + setOpen(false), }); export default compose(withInputValue, withInputHandlers, withModalState, withModalHandlers); diff --git a/src/components/DeployLatest/StyledDeployLatest.js b/src/components/DeployLatest/StyledDeployLatest.js index 8e474e6d..b72d185d 100644 --- a/src/components/DeployLatest/StyledDeployLatest.js +++ b/src/components/DeployLatest/StyledDeployLatest.js @@ -41,7 +41,7 @@ export const NewDeployment = styled.div` height: 15px; } .loader:after { - content: " "; + content: ' '; display: block; width: 20px; height: 20px; diff --git a/src/components/DeployLatest/index.js b/src/components/DeployLatest/index.js index 2b1efe68..4391aebc 100644 --- a/src/components/DeployLatest/index.js +++ b/src/components/DeployLatest/index.js @@ -61,6 +61,7 @@ const DeployLatest = ({ pageEnvironment: environment, onDeploy, ...rest }) => { `Start a new deployment from environment ${environment.project.name}-${environment.deployBaseRef}.`}
console.error(e)} mutation={DEPLOY_ENVIRONMENT_LATEST_MUTATION} variables={{ environmentId: environment.id, @@ -74,10 +75,14 @@ const DeployLatest = ({ pageEnvironment: environment, onDeploy, ...rest }) => { return ( {contextHolder} - - {success &&
Deployment queued.
} + {success && ( +
+ Deployment queued. +
+ )} {error && openNotificationWithIcon(error.message)}
); diff --git a/src/components/DeployLatest/index.stories.tsx b/src/components/DeployLatest/index.stories.tsx index fca82312..b07855b6 100644 --- a/src/components/DeployLatest/index.stories.tsx +++ b/src/components/DeployLatest/index.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; -import { generateEnvironments,seed } from '../../../.storybook/mocks/mocks'; -import DeployLatest from './index'; import withButtonOverrides from '../../../.storybook/decorators/withButtonOverrides'; +import { generateEnvironments, seed } from '../../../.storybook/mocks/mocks'; +import DeployLatest from './index'; + const meta: Meta = { component: DeployLatest, title: 'Components/Deploy Latest', @@ -16,7 +17,6 @@ seed(); const environment = generateEnvironments({ name: 'master' }); - /** * Default */ diff --git a/src/components/DeployTargets/DeployTargetsSkeleton.tsx b/src/components/DeployTargets/DeployTargetsSkeleton.tsx index 1990ce61..8e6d562c 100644 --- a/src/components/DeployTargets/DeployTargetsSkeleton.tsx +++ b/src/components/DeployTargets/DeployTargetsSkeleton.tsx @@ -1,20 +1,21 @@ -import React from "react"; -import Skeleton from "react-loading-skeleton"; -import { DeployTargetWrapper } from "./StyledDeployTargets"; +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { DeployTargetWrapper } from './StyledDeployTargets'; const DeployTargetSkeleton = () => { - const numberOfDeployTargetFields = 4 + const numberOfDeployTargetFields = 4; const skeletonItem = (
- +
- +
- +
); @@ -26,11 +27,7 @@ const DeployTargetSkeleton = () => {
-
- {[...Array(numberOfDeployTargetFields)].map( - () => skeletonItem - )} -
+
{[...Array(numberOfDeployTargetFields)].map(() => skeletonItem)}
); }; diff --git a/src/components/DeployTargets/StyledDeployTargets.tsx b/src/components/DeployTargets/StyledDeployTargets.tsx index 902374ee..0ec99087 100644 --- a/src/components/DeployTargets/StyledDeployTargets.tsx +++ b/src/components/DeployTargets/StyledDeployTargets.tsx @@ -1,5 +1,5 @@ -import styled from "styled-components"; -import { bp, color } from "lib/variables"; +import { bp, color } from 'lib/variables'; +import styled from 'styled-components'; export const DeployTargetWrapper = styled.div` width: 50%; diff --git a/src/components/DeployTargets/index.js b/src/components/DeployTargets/index.js index 00a43b61..f25b6681 100644 --- a/src/components/DeployTargets/index.js +++ b/src/components/DeployTargets/index.js @@ -1,5 +1,6 @@ -import React from "react"; -import { DeployTargetWrapper } from "./StyledDeployTargets"; +import React from 'react'; + +import { DeployTargetWrapper } from './StyledDeployTargets'; const DeployTargets = ({ project }) => { return ( @@ -10,7 +11,7 @@ const DeployTargets = ({ project }) => {
- {project.deployTargetConfigs.map((depTarget) => ( + {project.deployTargetConfigs.map(depTarget => (
{depTarget.deployTarget.friendlyName != null diff --git a/src/components/Deployment/index.js b/src/components/Deployment/index.js index 01ff023b..85d504de 100644 --- a/src/components/Deployment/index.js +++ b/src/components/Deployment/index.js @@ -71,7 +71,9 @@ const Deployment = ({ deployment, checkedParseState, changeState }) => (
- +
diff --git a/src/components/Deployment/index.stories.tsx b/src/components/Deployment/index.stories.tsx index da58f59f..f60c80a1 100644 --- a/src/components/Deployment/index.stories.tsx +++ b/src/components/Deployment/index.stories.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import withButtonOverrides from '../../../.storybook/decorators/withButtonOverrides'; import { getDeployment } from '../../../.storybook/mocks/mocks'; import Deployment from './index'; -import withButtonOverrides from '../../../.storybook/decorators/withButtonOverrides'; export default { component: Deployment, @@ -11,10 +11,8 @@ export default { decorators: [withButtonOverrides('.btn:not(div.field .btn)', 'click', 'Cancel deployment click')], }; - const data = getDeployment(123); - export const Complete = () => ( = ({ deployments, environmentSlug, proje
-
+
{!deployments.length &&
No Deployments
} {deployments.map(deployment => ( -
+
= ({ deployments, environmentSlug, proje
{moment.utc(deployment.created).local().format('DD MMM YYYY, HH:mm:ss (Z)')}
-
+
{deployment.status.charAt(0).toUpperCase() + deployment.status.slice(1)} {!['complete', 'cancelled', 'failed'].includes(deployment.status) && deployment.buildStep && ( @@ -75,7 +75,7 @@ const Deployments: FC = ({ deployments, environmentSlug, proje
-
+
{['new', 'pending', 'queued', 'running'].includes(deployment.status) && ( )} diff --git a/src/components/DeploymentsByFilter/StyledDeploymentsByFilter.js b/src/components/DeploymentsByFilter/StyledDeploymentsByFilter.js index 5d206627..cb71813a 100644 --- a/src/components/DeploymentsByFilter/StyledDeploymentsByFilter.js +++ b/src/components/DeploymentsByFilter/StyledDeploymentsByFilter.js @@ -156,9 +156,9 @@ export const DeploymentsDataTable = styled.div` border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } - .buildstep{ - display:flex; - flex-direction:column; + .buildstep { + display: flex; + flex-direction: column; } .status { @media ${bp.smallOnly} { diff --git a/src/components/DeploymentsByFilter/index.stories.tsx b/src/components/DeploymentsByFilter/index.stories.tsx index 483bbb5a..81924f1c 100644 --- a/src/components/DeploymentsByFilter/index.stories.tsx +++ b/src/components/DeploymentsByFilter/index.stories.tsx @@ -1,10 +1,11 @@ -import { MockAllDeployments } from "../../../.storybook/mocks/api"; -import { StoryObj } from '@storybook/react'; import React from 'react'; +import { StoryObj } from '@storybook/react'; + +import withButtonOverrides from '../../../.storybook/decorators/withButtonOverrides'; +import { MockAllDeployments } from '../../../.storybook/mocks/api'; +import DeploymentsByFilterSkeleton from './DeploymentsByFilterSkeleton'; import DeploymentsByFilter from './index'; -import DeploymentsByFilterSkeleton from "./DeploymentsByFilterSkeleton"; -import withButtonOverrides from "../../../.storybook/decorators/withButtonOverrides"; export default { component: DeploymentsByFilter, @@ -15,12 +16,12 @@ export default { type Story = StoryObj; export const Default: Story = { - args:{ - deployments: MockAllDeployments(123) - } -} + args: { + deployments: MockAllDeployments(123), + }, +}; export const Loading: Story = { - render: () => -} + render: () => , +}; export const NoDeployments = () => ; diff --git a/src/components/Environment/EnvironmentSkeleton.tsx b/src/components/Environment/EnvironmentSkeleton.tsx index d39f974b..b76433f3 100644 --- a/src/components/Environment/EnvironmentSkeleton.tsx +++ b/src/components/Environment/EnvironmentSkeleton.tsx @@ -6,54 +6,54 @@ import { StyledEnvironmentDetails } from './StyledEnvironment'; const EnvironmentSkeleton = () => { return ( -
-
- -
- -
+
+
+ +
+
-
-
- -
- -
+
+
+
+ +
+
-
-
- -
- -
+
+
+
+ +
+
-
-
- -
- -
+
+
+
+ +
+
-
-
- -
- -
+
+
+
+ +
+
-
-
- -
- -
+
+
+
+ +
+
+
); }; diff --git a/src/components/Environment/index.js b/src/components/Environment/index.js index 70fe6f8a..766046f3 100644 --- a/src/components/Environment/index.js +++ b/src/components/Environment/index.js @@ -33,11 +33,11 @@ const Environment = ({ environment }) => { : ''; return ( - +
-
+
{environment.environmentType} {environment.project.productionEnvironment && environment.project.standbyProductionEnvironment && @@ -55,7 +55,9 @@ const Environment = ({ environment }) => {
-
{environment.deployType}
+
+ {environment.deployType} +
@@ -75,7 +77,7 @@ const Environment = ({ environment }) => {
@@ -131,7 +133,7 @@ const Environment = ({ environment }) => { )}
-
+
{environment.routes ? environment.routes.split(',').map(route => (
@@ -175,7 +177,7 @@ const Environment = ({ environment }) => { }} )} - + console.error(e)}> {(deleteEnvironment, { loading, called, error, data }) => { if (error) { return
{error.message}
; diff --git a/src/components/Environment/index.stories.tsx b/src/components/Environment/index.stories.tsx index 19637a0a..de32098e 100644 --- a/src/components/Environment/index.stories.tsx +++ b/src/components/Environment/index.stories.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { generateEnvironments, generateProjectInfo, seed } from '../../../.storybook/mocks/mocks'; - import Environment from './index'; export default { diff --git a/src/components/EnvironmentVariables/EnvironmentVariablesSkeleton.tsx b/src/components/EnvironmentVariables/EnvironmentVariablesSkeleton.tsx index 2998202f..d3446623 100644 --- a/src/components/EnvironmentVariables/EnvironmentVariablesSkeleton.tsx +++ b/src/components/EnvironmentVariables/EnvironmentVariablesSkeleton.tsx @@ -1,10 +1,8 @@ -import React from "react"; -import Skeleton from "react-loading-skeleton"; -import { - StyledEnvironmentVariableDetails, - StyledProjectVariableTable, -} from "./StyledEnvironmentVariables"; -import Button from "react-bootstrap/Button"; +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Skeleton from 'react-loading-skeleton'; + +import { StyledEnvironmentVariableDetails, StyledProjectVariableTable } from './StyledEnvironmentVariables'; const EnvironmentVariablesSkeleton = () => { const numberOfVariableFields = 3; @@ -12,10 +10,10 @@ const EnvironmentVariablesSkeleton = () => { const skeletonItem = (
- +
- +
); @@ -23,10 +21,10 @@ const EnvironmentVariablesSkeleton = () => { const projectSkeletonItem = (
- +
- +
); @@ -49,13 +47,9 @@ const EnvironmentVariablesSkeleton = () => {
-
- {[...Array(numberOfVariableFields)].map( - () => skeletonItem - )} -
+
{[...Array(numberOfVariableFields)].map(() => skeletonItem)}
-
+
@@ -72,11 +66,7 @@ const EnvironmentVariablesSkeleton = () => {
-
- {[...Array(numberOfVariableFields)].map( - () => projectSkeletonItem - )} -
+
{[...Array(numberOfVariableFields)].map(() => projectSkeletonItem)}
); diff --git a/src/components/EnvironmentVariables/StyledEnvironmentVariables.tsx b/src/components/EnvironmentVariables/StyledEnvironmentVariables.tsx index 42950f53..32334bc9 100644 --- a/src/components/EnvironmentVariables/StyledEnvironmentVariables.tsx +++ b/src/components/EnvironmentVariables/StyledEnvironmentVariables.tsx @@ -1,5 +1,5 @@ -import styled from "styled-components"; -import { bp, color } from "lib/variables"; +import { bp, color } from 'lib/variables'; +import styled from 'styled-components'; export const VariableActions = styled.div` display: flex; @@ -72,7 +72,7 @@ export const StyledEnvironmentVariableDetails = styled.div` width: 100%; &::before { - background-image: url("/static/images/git-lab.svg"); + background-image: url('/static/images/git-lab.svg'); background-size: 19px 17px; } @@ -113,7 +113,7 @@ export const StyledEnvironmentVariableDetails = styled.div` .header-buttons { display: flex; margin: 0 4px; - + .add-variable { width: 54px; height: 38px; @@ -127,14 +127,14 @@ export const StyledEnvironmentVariableDetails = styled.div` display: inline-block; width: 36px; height: 36px; - + &.value-btn { width: 90px; height: 17px; } } .loader:after { - content: " "; + content: ' '; display: block; width: 24px; height: 24px; @@ -236,15 +236,15 @@ export const StyledVariableTable = styled.div` } .data-table { - background-color: ${(props) => props.theme.backgrounds.table}; - border: 1px solid ${(props) => props.theme.borders.tableRow}; + background-color: ${props => props.theme.backgrounds.table}; + border: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 3px; box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); } .data-none { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 3px; line-height: 1.5rem; padding: 8px 0 7px 0; @@ -252,8 +252,8 @@ export const StyledVariableTable = styled.div` } .values-present.data-row { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 0; line-height: 1.5rem; padding: 8px 0 7px 0; @@ -291,12 +291,12 @@ export const StyledVariableTable = styled.div` width: 32.5%; } & .varUpdate { - display: flex; - padding: 0; + display: flex; + padding: 0; - button { - background-color: #fff; - } + button { + background-color: #fff; + } } & .varActions { width: 20%; @@ -311,8 +311,8 @@ export const StyledVariableTable = styled.div` } .data-row { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 0; line-height: 1.5rem; align-items: center; @@ -438,7 +438,7 @@ export const StyledProjectVariableTable = styled.div` padding-left: 20px; @media ${bp.tabletDown} { - width: 30% + width: 30%; } } &.scope { @@ -459,15 +459,15 @@ export const StyledProjectVariableTable = styled.div` } .data-table { - background-color: ${(props) => props.theme.backgrounds.table}; - border: 1px solid ${(props) => props.theme.borders.tableRow}; + background-color: ${props => props.theme.backgrounds.table}; + border: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 3px; box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); } .data-none { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 3px; line-height: 1.5rem; padding: 8px 0 7px 0; @@ -475,8 +475,8 @@ export const StyledProjectVariableTable = styled.div` } .values-present.data-row { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 0; line-height: 1.5rem; padding: 8px 0 7px 0; @@ -520,8 +520,8 @@ export const StyledProjectVariableTable = styled.div` } .data-row { - border: 1px solid ${(props) => props.theme.borders.tableRow}; - border-bottom: 1px solid ${(props) => props.theme.borders.tableRow}; + border: 1px solid ${props => props.theme.borders.tableRow}; + border-bottom: 1px solid ${props => props.theme.borders.tableRow}; border-radius: 0; line-height: 1.5rem; align-items: center; diff --git a/src/components/EnvironmentVariables/index.js b/src/components/EnvironmentVariables/index.js index 2b9c4719..6559f886 100644 --- a/src/components/EnvironmentVariables/index.js +++ b/src/components/EnvironmentVariables/index.js @@ -1,38 +1,41 @@ -import React, { Fragment, useState } from "react"; -import "bootstrap/dist/css/bootstrap.min.css"; -import EnvironmentProjectByProjectNameWithEnvVarsValueQuery from "../../lib/query/EnvironmentAndProjectByOpenshiftProjectNameWithEnvVarsValue"; -import EnvironmentByProjectNameWithEnvVarsValueQuery from "../../lib/query/EnvironmentByOpenshiftProjectNameWithEnvVarsValue"; -import { useLazyQuery } from "@apollo/react-hooks"; -import DeleteVariable from "components/DeleteVariable"; -import AddVariable from "../AddVariable"; -import ViewVariableValue from "../ViewVariableValue"; -import Button from "react-bootstrap/Button"; -import Collapse from "react-bootstrap/Collapse"; +import React, { Fragment, useState } from 'react'; +import Button from 'react-bootstrap/Button'; +import Collapse from 'react-bootstrap/Collapse'; + +import Image from 'next/image'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { useLazyQuery } from '@apollo/react-hooks'; +import { Tag } from 'antd'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import Alert from 'components/Alert'; +import Btn from 'components/Button'; import withLogic from 'components/DeleteConfirm/logic'; +import DeleteVariable from 'components/DeleteVariable'; +import ProjectVariablesLink from 'components/link/ProjectVariables'; + +import EnvironmentProjectByProjectNameWithEnvVarsValueQuery from '../../lib/query/EnvironmentAndProjectByOpenshiftProjectNameWithEnvVarsValue'; +import EnvironmentByProjectNameWithEnvVarsValueQuery from '../../lib/query/EnvironmentByOpenshiftProjectNameWithEnvVarsValue'; +import hide from '../../static/images/hide.svg'; +import show from '../../static/images/show.svg'; +import AddVariable from '../AddVariable'; +import { DeleteVariableButton } from '../DeleteVariable/StyledDeleteVariable'; +import ViewVariableValue from '../ViewVariableValue'; import { StyledEnvironmentVariableDetails, StyledProjectVariableTable, StyledVariableTable, VariableActions, -} from "./StyledEnvironmentVariables"; -import Image from "next/image"; -import show from "../../static/images/show.svg"; -import hide from "../../static/images/hide.svg"; -import ProjectVariablesLink from "components/link/ProjectVariables"; -import Alert from 'components/Alert' -import {Tag} from "antd"; -import Btn from 'components/Button' -import {DeleteVariableButton} from "../DeleteVariable/StyledDeleteVariable"; -import {LoadingOutlined} from "@ant-design/icons"; +} from './StyledEnvironmentVariables'; /** * Displays the environment variable information. */ -const hashValue = (value) => { - let hashedVal = "●"; +const hashValue = value => { + let hashedVal = '●'; for (let l = 0; l < value.length; l++) { - hashedVal += "●"; + hashedVal += '●'; } return hashedVal; }; @@ -47,9 +50,9 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { const [prjValueState, setPrjValueState] = useState(initProjectValueState); const [openEnvVars, setOpenEnvVars] = useState(false); const [openPrjVars, setOpenPrjVars] = useState(false); - const [updateVarValue, setUpdateVarValue ] = useState(''); - const [updateVarName, setUpdateVarName ] = useState(''); - const [updateVarScope, setUpdateVarScope ] = useState(''); + const [updateVarValue, setUpdateVarValue] = useState(''); + const [updateVarName, setUpdateVarName] = useState(''); + const [updateVarScope, setUpdateVarScope] = useState(''); const [environmentErrorAlert, setEnvironmentErrorAlert] = useState(false); const [projectErrorAlert, setProjectErrorAlert] = useState(false); const [action, setAction] = useState(''); @@ -62,34 +65,34 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { setProjectErrorAlert(false); }; - const [ - getEnvVarValues, - { loading: envLoading, error: envError, data: envValues }, - ] = useLazyQuery(EnvironmentByProjectNameWithEnvVarsValueQuery, { - variables: { openshiftProjectName: environment.openshiftProjectName }, - onError: () => { - setOpenEnvVars(false); - setValueState(initValueState); - setEnvironmentErrorAlert(true); + const [getEnvVarValues, { loading: envLoading, error: envError, data: envValues }] = useLazyQuery( + EnvironmentByProjectNameWithEnvVarsValueQuery, + { + variables: { openshiftProjectName: environment.openshiftProjectName }, + onError: () => { + setOpenEnvVars(false); + setValueState(initValueState); + setEnvironmentErrorAlert(true); + }, } - }); + ); if (envValues) { displayVars = envValues.environmentVars.envVariables; - } + }; if (envError) console.error(envError); - const [ - getPrjEnvVarValues, - { loading: prjLoading, error: prjError, data: prjEnvValues }, - ] = useLazyQuery(EnvironmentProjectByProjectNameWithEnvVarsValueQuery, { - variables: { openshiftProjectName: environment.openshiftProjectName }, - onError: () => { - setOpenPrjVars(!openPrjVars); - setProjectErrorAlert(true); + const [getPrjEnvVarValues, { loading: prjLoading, error: prjError, data: prjEnvValues }] = useLazyQuery( + EnvironmentProjectByProjectNameWithEnvVarsValueQuery, + { + variables: { openshiftProjectName: environment.openshiftProjectName }, + onError: () => { + setOpenPrjVars(!openPrjVars); + setProjectErrorAlert(true); + }, } - }); + ); if (prjEnvValues) { displayProjectVars = prjEnvValues.environmentVars.project.envVariables; @@ -97,32 +100,24 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { if (prjError) console.error(prjError); - const valuesShow = (index) => { - setValueState((valueState) => - valueState.map((el, i) => (i === index ? true : el)) - ); + const valuesShow = index => { + setValueState(valueState => valueState.map((el, i) => (i === index ? true : el))); }; - const valuesHide = (index) => { - setValueState((valueState) => - valueState.map((el, i) => (i === index ? false : el)) - ); + const valuesHide = index => { + setValueState(valueState => valueState.map((el, i) => (i === index ? false : el))); }; - const prjValuesShow = (index) => { - setPrjValueState((prjValueState) => - prjValueState.map((el, i) => (i === index ? true : el)) - ); + const prjValuesShow = index => { + setPrjValueState(prjValueState => prjValueState.map((el, i) => (i === index ? true : el))); }; - const prjValuesHide = (index) => { - setPrjValueState((prjValueState) => - prjValueState.map((el, i) => (i === index ? false : el)) - ); + const prjValuesHide = index => { + setPrjValueState(prjValueState => prjValueState.map((el, i) => (i === index ? false : el))); }; const showVarValue = () => { getEnvVarValues(); setOpenEnvVars(!openEnvVars); setValueState(initValueState); - setAction("view") + setAction('view'); }; const showPrjVarValue = () => { @@ -135,13 +130,13 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { setUpdateVarValue(rowValue); setUpdateVarName(rowName); setUpdateVarScope(rowScope); - } + }; const permissionCheck = (action) => { getEnvVarValues(); setOpenEnvVars(false); setAction(action); - } + }; const renderEnvValue = (envVar, index) => { if (envVar.value.length >= 0 && !valueState[index]) { @@ -153,7 +148,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { } else { return envVar.value } - } + }; const renderPrjValue = (projEnvVar, index) => { if (projEnvVar.value.length >= 0 && !prjValueState[index]) { @@ -165,7 +160,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { } else { return projEnvVar.value } - } + }; const renderEnvValues = (envVar, index) => { if (envLoading) { @@ -184,6 +179,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { @@ -196,7 +192,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { ) } - } + }; const renderPrjValues = (projEnvVar, index) => { if (prjLoading) { @@ -215,6 +211,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { @@ -227,7 +224,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { ) } - } + }; return ( @@ -237,15 +234,15 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => { type="error" closeAlert={closeEnvironmentError} header="Unauthorized:" - message={`You don't have permission to ${action} environment ${action === "view" ? " variable values" : "variables"}. Contact your administrator to obtain the relevant permissions.`} + message={`You don't have permission to ${action} environment ${action === 'view' ? ' variable values' : 'variables'}. Contact your administrator to obtain the relevant permissions.`} /> )}
: { )}
@@ -287,7 +285,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => {
@@ -302,15 +300,11 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => {
-
+
{displayVars.map((envVar, index) => { return ( -
+
{envVar.name}
{envVar.scope}
{renderEnvValues(envVar, index)} @@ -340,12 +334,12 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => {
)}
@@ -421,7 +415,7 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => {
@@ -436,15 +430,11 @@ const EnvironmentVariables = ({ environment, onVariableAdded }) => {
-
+
{displayProjectVars.map((projEnvVar, index) => { return ( -
+
{projEnvVar.name}
{projEnvVar.scope}
{renderPrjValues(projEnvVar, index)} diff --git a/src/components/Environments/index.js b/src/components/Environments/index.js index 7df4853a..16ad5816 100644 --- a/src/components/Environments/index.js +++ b/src/components/Environments/index.js @@ -5,8 +5,8 @@ import EnvironmentLink from 'components/link/Environment'; import { makeSafe } from 'lib/util'; import * as R from 'ramda'; +import NewEnvironment from '../NewEnvironment'; import { StyledEnvironments } from './StyledEnvironments'; -import NewEnvironment from "../NewEnvironment"; const bgImages = { branch: { @@ -61,7 +61,7 @@ const Environments = ({ environments = [], project, refresh, environmentCount })
)} -

{environment.name}

+

{environment.name}

{environment.openshift.friendlyName != null && ( )} @@ -71,8 +71,8 @@ const Environments = ({ environments = [], project, refresh, environmentCount }) )} {environment.routes && environment.routes !== 'undefined' ? ( -
-