Quellcode durchsuchen

Add Cypress (E2E testing) to the project

Read the "Integration tests" section of the README.md file for more
information.
Sergiu Miclea vor 2 Jahren
Ursprung
Commit
ca52e146e1
72 geänderte Dateien mit 3390 neuen und 3124 gelöschten Zeilen
  1. 26 2
      .github/workflows/build.yml
  2. 8 3
      .gitignore
  3. 8 0
      README.md
  4. 10 0
      cypress.config.ts
  5. 0 15
      cypress.json
  6. 117 0
      cypress/e2e/dashboard/dashboard.cy.ts
  7. 115 0
      cypress/e2e/endpoints/endpoints-list.cy.ts
  8. 260 0
      cypress/e2e/endpoints/new-endpoint.cy.ts
  9. 81 0
      cypress/e2e/login/login-fails.cy.ts
  10. 49 0
      cypress/e2e/login/login-redirects.cy.ts
  11. 142 0
      cypress/e2e/migrations/migrations-list.cy.ts
  12. 111 0
      cypress/e2e/page header/page-header.cy.ts
  13. 166 0
      cypress/e2e/replicas/replicas-list.cy.ts
  14. 7 0
      cypress/fixtures/auth/fail.json
  15. 112 0
      cypress/fixtures/auth/role-assignments.json
  16. 210 0
      cypress/fixtures/auth/token-scoped.json
  17. 18 0
      cypress/fixtures/endpoints/endpoint.json
  18. 180 0
      cypress/fixtures/endpoints/endpoints.json
  19. 214 0
      cypress/fixtures/endpoints/openstack-connection-schema.json
  20. 60 0
      cypress/fixtures/endpoints/providers.json
  21. 21 0
      cypress/fixtures/endpoints/regions.json
  22. 3 0
      cypress/fixtures/endpoints/secret-ref.json
  23. 6 0
      cypress/fixtures/endpoints/validation-fail.json
  24. 1 0
      cypress/fixtures/endpoints/validation-success.json
  25. 15 0
      cypress/fixtures/licences/appliance-status.json
  26. 1 0
      cypress/fixtures/licences/appliances.json
  27. 8 0
      cypress/fixtures/licences/status.json
  28. 23 0
      cypress/fixtures/projects/projects.json
  29. 398 0
      cypress/fixtures/transfers/migrations.json
  30. 46 0
      cypress/fixtures/transfers/replica-unexecuted.json
  31. 97 0
      cypress/fixtures/transfers/replicas.json
  32. 52 0
      cypress/fixtures/transfers/schedules-disabled.json
  33. 55 0
      cypress/fixtures/transfers/schedules-enabled.json
  34. 13 0
      cypress/fixtures/users/user.json
  35. 48 0
      cypress/fixtures/users/users.json
  36. 112 0
      cypress/support/commands.ts
  37. 5 0
      cypress/support/e2e.ts
  38. 18 0
      cypress/support/routeSelectors.ts
  39. 10 0
      cypress/tsconfig.json
  40. 2 0
      package.json
  41. 0 8
      private/cypress/.eslintrc
  42. 0 69
      private/cypress/config.template.js
  43. 0 1499
      private/cypress/example_spec.js
  44. 0 32
      private/cypress/integration/0 - cleanup/Cleanup.js
  45. 0 41
      private/cypress/integration/1 - login/Invalid Login.js
  46. 0 52
      private/cypress/integration/2 - create endpoints/Create Azure Endpoint.js
  47. 0 53
      private/cypress/integration/2 - create endpoints/Create OCI Endpoint.js
  48. 0 66
      private/cypress/integration/2 - create endpoints/Create Openstack Endpoint.js
  49. 0 51
      private/cypress/integration/2 - create endpoints/Create VmWare Endpoint.js
  50. 0 58
      private/cypress/integration/3 - duplicate endpoints/Duplicate Azure Endpoint.js
  51. 0 58
      private/cypress/integration/3 - duplicate endpoints/Duplicate OCI Endpoint.js
  52. 0 130
      private/cypress/integration/4 - migrations and replicas/Openstack - OCI Migration.js
  53. 0 133
      private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/1 - Create replica.js
  54. 0 77
      private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/2 - Scheduler Operations.js
  55. 0 40
      private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/3 - Delete replica.js
  56. 0 44
      private/cypress/integration/5 - delete endpoints/Delete Azure endpoint.js
  57. 0 44
      private/cypress/integration/5 - delete endpoints/Delete OCI endpoint.js
  58. 0 49
      private/cypress/integration/5 - delete endpoints/Delete Openstack and VmWare endpoints.js
  59. 0 48
      private/cypress/integration/6 - users and projects/1 - Create a project.js
  60. 0 72
      private/cypress/integration/6 - users and projects/2 - Add a new user as a member.js
  61. 0 61
      private/cypress/integration/6 - users and projects/3 - Add existing user as a member.js
  62. 0 51
      private/cypress/integration/6 - users and projects/4 - Create a user.js
  63. 0 62
      private/cypress/integration/6 - users and projects/5 - Edit and delete user.js
  64. 0 61
      private/cypress/integration/6 - users and projects/6 - Edit and delete project.js
  65. 0 204
      private/cypress/support/commands.js
  66. 0 20
      private/cypress/support/index.js
  67. 1 1
      src/components/smart/EndpointsPage/EndpointsPage.tsx
  68. 1 1
      src/components/smart/MigrationsPage/MigrationsPage.tsx
  69. 3 2
      src/components/smart/ReplicasPage/ReplicasPage.tsx
  70. 1 1
      src/sources/UserSource.ts
  71. 1 1
      tsconfig.json
  72. 555 15
      yarn.lock

+ 26 - 2
.github/workflows/build.yml

@@ -18,7 +18,7 @@ jobs:
       - name: Setup Corepack
         run: corepack enable
 
-      - name: Install dependencies
+      - name: Install development dependencies
         env:
           YARN_ENABLE_IMMUTABLE_INSTALLS: false
         run: yarn install
@@ -35,12 +35,36 @@ jobs:
       - name: Run unit tests
         run: npm run test
 
+      - name: Development build
+        env:
+          NODE_OPTIONS: "--openssl-legacy-provider"
+          NODE_ENV: development
+        run: npm run build
+
+      - name: Run integration tests
+        env:
+          NODE_ENV: development
+          # needs an invalid URL to prevent CORS related delays
+          CORIOLIS_URL: http://invalidd.it/
+        run: |
+          touch .not-first-launch
+          npm run start &
+          sleep 5
+          npm run e2e
+
+      - name: Upload failure screenshots
+        uses: actions/upload-artifact@v3
+        if: failure()
+        with:
+          name: cypress-screenshots
+          path: cypress/screenshots
+
       - name: Install production dependencies
         run: |
           rm -rf node_modules
           yarn workspaces focus --all --production
 
-      - name: Build
+      - name: Production build
         env:
           NODE_OPTIONS: "--openssl-legacy-provider"
         run: npm run build

+ 8 - 3
.gitignore

@@ -1,13 +1,18 @@
+# MacOS
 .DS_Store
-.happypack
+
+# Install and build
 dist
 *.log
 node_modules
-private/cypress/config.js
 .env
 .not-first-launch
 
-# yarn v3
+# Cypress
+cypress/screenshots
+cypress/videos
+
+# yarn
 .pnp.*
 .yarn/*
 !.yarn/patches

+ 8 - 0
README.md

@@ -26,6 +26,14 @@ Your server will be running at `http://localhost:3000/` (the port is configurabl
 - unit tests can be run using `npm run test`
 - run `npm run test-release` to check for Typescript, ESLint and prettier errors. This will also run the unit tests and will try to build and start a production version. If eeverything is OK, it will revert to the development installation.
 
+### Integration tests
+
+Integration tests can be executed using `npm run e2e`. All API calls will be mocked, eliminating the need for a running Coriolis instance.
+
+To run the integration tests, you must set the environment variable `NODE_ENV='development'`, then execute `npm run build` and `npm run start`. It is also recommended to set `CORIOLIS_URL` to a non-existent URL (such as <https://invalidd.it/>) to prevent the UI from attempting to connect to a Coriolis instance for CORS checks. Although Cypress is configured to mock API calls, if a valid URL is set, the UI will still attempt to connect to it for CORS checks.
+
+You can also run the integration tests for easier debugging by using `npm run server-dev` and `npm run client-dev`, and by updating the `baseUrl` in `cypress.config.ts` to `<http://localhost:3001>`. The variables `NODE_ENV` and `CORIOLIS_URL`, as described above, are still required. Subsequently, execute the tests using `npx cypress open`. This procedure allows you to update the source code and see the changes reflected in the UI without having to rebuild and restart the server. Additionally, the tests will automatically re-run when you save a test file.
+
 ## Development mode
 
 - set env. variable `NODE_ENV='development'`

+ 10 - 0
cypress.config.ts

@@ -0,0 +1,10 @@
+import { defineConfig } from "cypress";
+
+export default defineConfig({
+  e2e: {
+    baseUrl: "http://localhost:3000",
+  },
+  env: {
+    CORIOLIS_URL: "https://invalidd.it/",
+  },
+});

+ 0 - 15
cypress.json

@@ -1,15 +0,0 @@
-{
-  "fileServerFolder": "private/cypress",
-  "fixturesFolder": false,
-  "integrationFolder": "private/cypress/integration",
-  "pluginsFile": false,
-  "screenshotsFolder": "private/cypress/screenshots",
-  "videosFolder": "private/cypress/videos",
-  "supportFile": "private/cypress/support/index.js",
-  "viewportWidth": 1280,
-  "viewportHeight": 900,
-  "defaultCommandTimeout": 7000,
-  "execTimeout": 5000,
-  "requestTimeout": 10000,
-  "responseTimeout": 60000
-}

+ 117 - 0
cypress/e2e/dashboard/dashboard.cy.ts

@@ -0,0 +1,117 @@
+/// <reference types="cypress" />
+
+import { DateTime } from "luxon";
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Dashboard", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth();
+
+    cy.intercept(routeSelectors.APPLIANCES, {
+      fixture: "licences/appliances.json",
+    }).as("appliances");
+    cy.intercept(routeSelectors.STATUS, {
+      fixture: "licences/status.json",
+    }).as("status");
+    cy.intercept(routeSelectors.APPLIANCE_STATUS, {
+      fixture: "licences/appliance-status.json",
+    }).as("appliance-status");
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth();
+
+    cy.wait(["@appliances", "@status", "@appliance-status"]);
+  };
+
+  it("renders empty dashboard", () => {
+    cy.intercept(routeSelectors.REPLICAS, {
+      body: { replicas: [] },
+    }).as("replicas");
+    cy.intercept(routeSelectors.MIGRATIONS, {
+      body: { migrations: [] },
+    }).as("migrations");
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      body: { endpoints: [] },
+    }).as("endpoints");
+
+    cy.visit("/");
+    waitForAll();
+    cy.wait(["@replicas", "@migrations", "@endpoints"]);
+
+    cy.get("*[class^='DashboardActivity__Message']").should(
+      "contain.text",
+      "There is no recent activity"
+    );
+
+    cy.fixture("licences/appliance-status.json").then(applianceStatus => {
+      cy.get("*[class^='DashboardLicence__TopInfoDateTop']").should(
+        "contain.text",
+        `${DateTime.fromISO(
+          applianceStatus.appliance_licence_status.earliest_licence_expiry_time
+        )
+          .toFormat("LLL |yy")
+          .replace("|", "'")}`
+      );
+
+      cy.get("*[class^='DashboardLicence__ChartHeaderCurrent']").should(
+        "contain.text",
+        `${applianceStatus.appliance_licence_status.current_performed_replicas} Used Replica ${applianceStatus.appliance_licence_status.current_performed_migrations} Used Migrations`
+      );
+    });
+
+    cy.get("button").should("contain.text", "New Replica / Migration");
+    cy.get("button").should("contain.text", "New Endpoint");
+  });
+
+  it("renders dashboard with data", () => {
+    cy.intercept(routeSelectors.REPLICAS, {
+      fixture: "transfers/replicas.json",
+    }).as("replicas");
+    cy.intercept(routeSelectors.MIGRATIONS, {
+      fixture: "transfers/migrations.json",
+    }).as("migrations");
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      fixture: "endpoints/endpoints.json",
+    }).as("endpoints");
+
+    cy.visit("/");
+    waitForAll();
+    cy.wait(["@replicas", "@migrations", "@endpoints"]);
+
+    cy.loadFixtures(
+      [
+        "transfers/replicas.json",
+        "transfers/migrations.json",
+        "endpoints/endpoints.json",
+      ],
+      results => {
+        const [replicasFixture, migrationsFixture, endpointsFixture] = results;
+        cy.get("div[class^='DashboardInfoCount__CountBlock']").should(
+          "contain.text",
+          `${replicasFixture.replicas.length}Replicas${migrationsFixture.migrations.length}Migrations${endpointsFixture.endpoints.length}Endpoints`
+        );
+
+        const checkItem = (type: "migration" | "replica", item: any) => {
+          cy.get("div[class^='NotificationDropdown__ItemDescription']").should(
+            "contain.text",
+            `New ${type} ${item.id.substr(
+              0,
+              7
+            )}... status: ${item.last_execution_status.toLowerCase()}`
+          );
+        };
+
+        migrationsFixture.migrations.forEach((migration: any) => {
+          checkItem("migration", migration);
+        });
+
+        replicasFixture.replicas.forEach((replica: any) => {
+          checkItem("replica", replica);
+        });
+      }
+    );
+  });
+});

+ 115 - 0
cypress/e2e/endpoints/endpoints-list.cy.ts

@@ -0,0 +1,115 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Endpoints list", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth({ filterResources: ["users"] });
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      fixture: "endpoints/endpoints",
+    }).as("endpoints");
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth({ filterResources: ["users"] });
+    cy.wait(["@endpoints"]);
+  };
+
+  it("renders empty list", () => {
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      body: { endpoints: [] },
+    }).as("endpoints-empty");
+
+    cy.visit("/endpoints");
+    cy.wait(["@endpoints-empty"]);
+    cy.waitMockAuth({ filterResources: ["users"] });
+
+    cy.get("div[class^='MainList__EmptyListMessage']").should(
+      "contain.text",
+      "don't have any Cloud Endpoints in this project"
+    );
+    cy.get("button").should("contain.text", "Add Endpoint");
+  });
+
+  it("filters list", () => {
+    cy.visit("/endpoints");
+    waitForAll();
+
+    cy.fixture("endpoints/endpoints").then((endpointsFixture: any) => {
+      const endpoints = endpointsFixture.endpoints;
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Azure")
+        .click();
+      cy.get("div[class^='EndpointListItem__Wrapper']").should(
+        "have.length",
+        endpoints.filter(r => r.type === "azure").length
+      );
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("VMware")
+        .click();
+      cy.get("div[class^='EndpointListItem__Wrapper']").should(
+        "have.length",
+        endpoints.filter(r => r.type === "vmware_vsphere").length
+      );
+
+      cy.get("div[class^='SearchButton__Wrapper']").click();
+      cy.get("input[class*='SearchInput']").type("cor");
+      cy.get("div[class^='EndpointListItem__Wrapper']").should(
+        "have.length",
+        endpoints.filter(
+          e => e.type === "vmware_vsphere" && e.name.includes("cor")
+        ).length
+      );
+      cy.get("div[class^='TextInput__Close']").click();
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("All")
+        .click();
+      cy.get("div[class^='EndpointListItem__Wrapper']").should(
+        "have.length",
+        endpoints.length
+      );
+
+      cy.get("div[class^='SearchButton__Wrapper']").click();
+      cy.get("input[class*='SearchInput']").type("cor");
+      cy.get("div[class^='EndpointListItem__Wrapper']").should(
+        "have.length",
+        endpoints.filter(e => e.name.includes("cor")).length
+      );
+    });
+  });
+
+  it("does bulk actions", () => {
+    cy.visit("/endpoints");
+    waitForAll();
+
+    cy.get("div[class^='SearchButton__Wrapper']").click();
+    cy.get("input[class*='SearchInput']").type("cor");
+    cy.get(
+      "div[class^='MainListFilter__Wrapper'] div[class^='Checkbox__Wrapper']"
+    ).click();
+
+    cy.fixture("endpoints/endpoints").then((endpointsFixture: any) => {
+      const endpoints = endpointsFixture.endpoints;
+      const corEndpoints = endpoints.filter(e => e.name.includes("cor"));
+      cy.get("div[class^='MainListFilter__SelectionText']").should(
+        "contain.text",
+        `${corEndpoints.length} of ${corEndpoints.length}`
+      );
+
+      cy.get("div[class^='ActionDropdown__Wrapper']").click();
+      cy.get("div[class^='ActionDropdown__ListItem']")
+        .contains("Delete")
+        .click();
+
+      cy.get("div[class^='AlertModal__Message']").should(
+        "contain.text",
+        "they are in use by replicas or migrations"
+      );
+    });
+  });
+});

+ 260 - 0
cypress/e2e/endpoints/new-endpoint.cy.ts

@@ -0,0 +1,260 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+const clickOpenstack = () => {
+  cy.intercept(routeSelectors.CONN_SCHEMA_OPENSTACK, {
+    fixture: "endpoints/openstack-connection-schema",
+  }).as("openstack-schema");
+
+  let matchFound = false;
+  return cy
+    .get("div[class^=EndpointLogos__Logo]")
+    .each(logo => {
+      const style = window.getComputedStyle(logo[0]);
+      const matches = style.backgroundImage.match(/\/api\/logos\/(.*?)\//);
+
+      if (matches?.[1] === "openstack") {
+        matchFound = true;
+        cy.wrap(logo).click();
+        cy.wait("@openstack-schema");
+      }
+    })
+    .then(() => {
+      if (!matchFound) {
+        throw new Error("Openstack logo not found");
+      }
+    });
+};
+
+describe("New endpoint", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth({ filterResources: ["users"] });
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      body: { endpoints: [] },
+    }).as("endpoints");
+    cy.intercept(routeSelectors.PROVIDERS, {
+      fixture: "endpoints/providers",
+    }).as("providers");
+    cy.intercept(routeSelectors.REGIONS, {
+      fixture: "endpoints/regions",
+    }).as("regions");
+
+    cy.visit("/endpoints");
+    waitForAll();
+    cy.get("button").contains("Add Endpoint").click();
+    cy.wait(["@providers", "@regions"]);
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth({ filterResources: ["users"] });
+    cy.wait(["@endpoints"]);
+  };
+
+  it("shows all providers", () => {
+    cy.get("div[class^=Modal__Title]").should(
+      "contain.text",
+      "New Cloud Endpoint"
+    );
+
+    cy.fixture("endpoints/providers").then(providersFixture => {
+      const providers = providersFixture.providers;
+      cy.get("div[class^=EndpointLogos__Logo]").should(
+        "have.length",
+        Object.keys(providers).length
+      );
+
+      cy.get("div[class^=EndpointLogos__Logo]").each(logo => {
+        const style = window.getComputedStyle(logo[0]);
+        const matches = style.backgroundImage.match(/\/api\/logos\/(.*?)\//);
+
+        expect(Object.keys(providers)).to.include(matches?.[1]);
+      });
+    });
+  });
+
+  it("validates the form for a new openstack endpoint", () => {
+    clickOpenstack().then(() => {
+      cy.get("input[placeholder='Password']").should(
+        "have.attr",
+        "type",
+        "password"
+      );
+
+      cy.get("div[class^=FieldInput__Wrapper]")
+        .contains("Username")
+        .not("div[class^=FieldInput__HighlightLabel]");
+      cy.get("button").contains("Validate and save").click();
+      cy.get("div[class^=notifications-wrapper]").should(
+        "contain.text",
+        "Please fill all the required fields"
+      );
+      cy.get("div[class^=FieldInput__Wrapper]")
+        .contains("Username")
+        .get("div[class^=FieldInput__HighlightLabel]")
+        .should("exist");
+
+      cy.get("input[placeholder='Username']").type("username");
+      cy.get("button").contains("Validate and save").click();
+      cy.get("div[class^=FieldInput__Wrapper]")
+        .contains("Username")
+        .not("div[class^=FieldInput__HighlightLabel]");
+    });
+  });
+
+  it("toggles simple and advanced mode", () => {
+    clickOpenstack().then(() => {
+      cy.get("div[class^=FieldInput__Wrapper]")
+        .contains("Allow Untrusted")
+        .should("not.exist");
+      cy.get("div[class^=ToggleButtonBar__Item]").contains("Advanced").click();
+      cy.get("div[class^=FieldInput__Wrapper]")
+        .contains("Allow Untrusted")
+        .should("exist");
+    });
+  });
+
+  it("saves the endpoint", () => {
+    clickOpenstack().then(() => {
+      cy.get("input[placeholder='Name']").type("new openstack");
+      cy.get("input[placeholder='Username']").type("username");
+      cy.get("input[placeholder='Password']").type("password");
+      cy.get("input[placeholder='Authentication URL']").type("auth url");
+      cy.get("input[placeholder='Project Name']").type("project name");
+
+      cy.intercept("POST", routeSelectors.SECRETS, req => {
+        expect(req.body).to.have.property("algorithm", "aes");
+        expect(req.body).to.have.property("payload");
+        expect(JSON.parse(req.body.payload)).to.have.property(
+          "auth_url",
+          "auth url"
+        );
+        req.reply({ fixture: "endpoints/secret-ref" });
+      }).as("secrets-post");
+
+      cy.intercept("POST", routeSelectors.ENDPOINTS, req => {
+        expect(req.body).to.have.property("endpoint");
+        expect(req.body.endpoint).to.have.property("name", "new openstack");
+        expect(req.body.endpoint).to.have.property("type", "openstack");
+        expect(req.body.endpoint).to.have.property("connection_info");
+        expect(req.body.endpoint.connection_info).to.have.property(
+          "secret_ref",
+          "http://127.0.0.1:9311/v1/secrets/secret-ref-1"
+        );
+        req.reply({ fixture: "endpoints/endpoint" });
+      }).as("endpoints-post");
+
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1`, {
+        body: { status: "ACTIVE" },
+      }).as("secrets-active");
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1/payload`, {
+        body: { username: "username", password: "password" },
+      }).as("secrets-payload");
+      cy.intercept("POST", `${routeSelectors.ENDPOINTS}/**/actions`, {
+        fixture: "endpoints/validation-fail",
+      }).as("endpoints-validate");
+
+      cy.get("button").contains("Validate and save").click();
+      cy.wait([
+        "@secrets-post",
+        "@endpoints-post",
+        "@secrets-active",
+        "@secrets-payload",
+        "@endpoints-validate",
+      ]);
+    });
+  });
+
+  it("fails validation", () => {
+    clickOpenstack().then(() => {
+      cy.get("input[placeholder='Name']").type("new openstack");
+      cy.get("input[placeholder='Username']").type("username");
+      cy.get("input[placeholder='Password']").type("password");
+      cy.get("input[placeholder='Authentication URL']").type("auth url");
+      cy.get("input[placeholder='Project Name']").type("project name");
+
+      cy.intercept("POST", routeSelectors.SECRETS, {
+        fixture: "endpoints/secret-ref",
+      }).as("secrets-post");
+      cy.intercept("POST", routeSelectors.ENDPOINTS, {
+        fixture: "endpoints/endpoint",
+      }).as("endpoints-post");
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1`, {
+        body: { status: "ACTIVE" },
+      }).as("secrets-active");
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1/payload`, {
+        body: { username: "username", password: "password" },
+      }).as("secrets-payload");
+      cy.intercept("POST", `${routeSelectors.ENDPOINTS}/**/actions`, {
+        fixture: "endpoints/validation-fail",
+      }).as("endpoints-validate");
+
+      cy.get("button").contains("Validate and save").click();
+      cy.wait([
+        "@secrets-post",
+        "@endpoints-post",
+        "@secrets-active",
+        "@secrets-payload",
+        "@endpoints-validate",
+      ]);
+
+      cy.get("div[class^=EndpointModal__StatusMessage]").should(
+        "contain.text",
+        "Validation failed"
+      );
+
+      cy.get("span[class^=EndpointModal__ShowErrorButton]").click();
+
+      cy.fixture("endpoints/validation-fail").then(validationFailFixture => {
+        cy.get("div[class^=EndpointModal__StatusError]").should(
+          "contain.text",
+          validationFailFixture["validate-connection"].message
+        );
+      });
+    });
+  });
+
+  it("validates successfully", () => {
+    clickOpenstack().then(() => {
+      cy.get("input[placeholder='Name']").type("new openstack");
+      cy.get("input[placeholder='Username']").type("username");
+      cy.get("input[placeholder='Password']").type("password");
+      cy.get("input[placeholder='Authentication URL']").type("auth url");
+      cy.get("input[placeholder='Project Name']").type("project name");
+
+      cy.intercept("POST", routeSelectors.SECRETS, {
+        fixture: "endpoints/secret-ref",
+      }).as("secrets-post");
+      cy.intercept("POST", routeSelectors.ENDPOINTS, {
+        fixture: "endpoints/endpoint",
+      }).as("endpoints-post");
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1`, {
+        body: { status: "ACTIVE" },
+      }).as("secrets-active");
+      cy.intercept(`${routeSelectors.SECRETS}/secret-ref-1/payload`, {
+        body: { username: "username", password: "password" },
+      }).as("secrets-payload");
+
+      cy.intercept("POST", `${routeSelectors.ENDPOINTS}/**/actions`, req => {
+        expect(req.body).to.have.property("validate-connection", null);
+        req.reply({ fixture: "endpoints/validation-success" });
+      }).as("endpoints-validate");
+
+      cy.get("button").contains("Validate and save").click();
+      cy.wait([
+        "@secrets-post",
+        "@endpoints-post",
+        "@secrets-active",
+        "@secrets-payload",
+        "@endpoints-validate",
+      ]);
+
+      cy.get("div[class^=EndpointModal__StatusMessage]").should(
+        "contain.text",
+        "Endpoint is Valid"
+      );
+    });
+  });
+});

+ 81 - 0
cypress/e2e/login/login-fails.cy.ts

@@ -0,0 +1,81 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Login fails", () => {
+  beforeEach(() => {
+    cy.intercept(routeSelectors.AUTH_TOKENS, {
+      statusCode: 401,
+      fixture: "auth/fail.json",
+    }).as("token-fail");
+
+    cy.visit("/login");
+  });
+
+  it("token auto login fails", () => {
+    cy.wait("@token-fail")
+      .its("response")
+      .should((response: any) => {
+        console.log(response);
+        expect(response.statusCode).to.equal(401);
+        expect(response.body.error.code).to.equal(401);
+      });
+    cy.get("button").should("contain.text", "Login");
+  });
+
+  it("shows message on empty inputs", () => {
+    cy.wait("@token-fail");
+    cy.get("button").click();
+    cy.get("*[class='notification-title']").should(
+      "contain.text",
+      "Please fill in all fields"
+    );
+  });
+
+  it("shows message on request invalid response", () => {
+    cy.wait("@token-fail");
+    cy.get("input[name='username']").type("wrong-user");
+    cy.get("input[name='password']").type("wrong-pass");
+
+    cy.intercept("POST", routeSelectors.AUTH_TOKENS, {
+      body: "invalid",
+    }).as("auth-post");
+    cy.get("button").click();
+    cy.wait("@auth-post").then(interception => {
+      expect(
+        interception.request.body.auth.identity.password.user.name
+      ).to.equal("wrong-user");
+      expect(
+        interception.request.body.auth.identity.password.user.password
+      ).to.equal("wrong-pass");
+    });
+    cy.get("*[class^='LoginForm__LoginErrorText']").should(
+      "contain.text",
+      "Request failed, there might be a problem with the connection to the server."
+    );
+  });
+
+  it("shows message on invalid credentials", () => {
+    cy.wait("@token-fail");
+    cy.get("input[name='username']").type("wrong-user");
+    cy.get("input[name='password']").type("wrong-pass");
+
+    cy.intercept("POST", routeSelectors.AUTH_TOKENS, {
+      statusCode: 401,
+      fixture: "auth/fail.json",
+    }).as("auth-post");
+    cy.get("button").click();
+    cy.wait("@auth-post").then(interception => {
+      expect(
+        interception.request.body.auth.identity.password.user.name
+      ).to.equal("wrong-user");
+      expect(
+        interception.request.body.auth.identity.password.user.password
+      ).to.equal("wrong-pass");
+    });
+    cy.get("*[class^='LoginForm__LoginErrorText']").should(
+      "contain.text",
+      "Incorrect credentials."
+    );
+  });
+});

+ 49 - 0
cypress/e2e/login/login-redirects.cy.ts

@@ -0,0 +1,49 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Login success", () => {
+  it("redirects to home on correct username and password", () => {
+    // the app tries to login automatically on load
+    // we mock as fail to force the login page
+    cy.intercept(routeSelectors.AUTH_TOKENS, {
+      statusCode: 401,
+      fixture: "auth/fail.json",
+    }).as("token-auto");
+    cy.visit("/login");
+    cy.wait("@token-auto");
+
+    cy.intercept("POST", routeSelectors.AUTH_TOKENS, {
+      fixture: "auth/token-scoped.json",
+    }).as("token");
+    cy.intercept(routeSelectors.USER, {
+      fixture: "users/user.json",
+    }).as("user");
+    cy.intercept(routeSelectors.PROJECTS, {
+      fixture: "projects/projects.json",
+    }).as("projects");
+    cy.intercept(routeSelectors.ROLE_ASSIGNMENTS, {
+      fixture: "auth/role-assignments.json",
+    }).as("roles");
+
+    cy.get("input[name='username']").type("admin");
+    cy.get("input[name='password']").type("admin");
+    cy.get("button").click();
+
+    cy.wait(["@user", "@projects", "@token", "@roles"]);
+
+    cy.url().should("eq", `${Cypress.config().baseUrl}/`);
+  });
+
+  it("redirects to login on 401", () => {
+    cy.intercept(routeSelectors.AUTH_TOKENS, {
+      statusCode: 401,
+      fixture: "auth/fail.json",
+    }).as("token-auto");
+    cy.visit("/endpoints");
+    cy.wait("@token-auto");
+
+    cy.url().should("contain", "/login");
+    cy.get("button").should("contain.text", "Login");
+  });
+});

+ 142 - 0
cypress/e2e/migrations/migrations-list.cy.ts

@@ -0,0 +1,142 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Migrations list", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth();
+
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      fixture: "endpoints/endpoints.json",
+    }).as("endpoints");
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth();
+
+    cy.wait(["@endpoints"]);
+  };
+
+  it("renders empty list", () => {
+    cy.intercept(routeSelectors.MIGRATIONS, {
+      body: { replicas: [] },
+    }).as("migrations");
+
+    cy.visit("/migrations");
+    waitForAll();
+
+    cy.wait(["@migrations"]);
+
+    cy.get("div[class^='MainList__EmptyListMessage']").should(
+      "contain.text",
+      "don't have any Migrations in this project"
+    );
+    cy.get("button").should("contain.text", "Create a Migration");
+  });
+
+  it("filters list", () => {
+    cy.visit("/migrations");
+    waitForAll();
+
+    cy.loadFixtures(["transfers/migrations"], (results: any[]) => {
+      const migrations = results[0].migrations;
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Running")
+        .click();
+      cy.get("div[class^='MainList__NoResults']").should("exist");
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Error")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.filter(r => r.last_execution_status === "ERROR").length
+      );
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Completed")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.filter(r => r.last_execution_status === "COMPLETED").length
+      );
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Canceled")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.filter(r => r.last_execution_status === "CANCELED").length
+      );
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("All")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.length
+      );
+
+      cy.get("div[class^='SearchButton__Wrapper']").click();
+      cy.get("input[class*='SearchInput']").type("ol88-uefi");
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.filter(r => r.instances.find(i => i.includes("ol88-uefi")))
+          .length
+      );
+      cy.get("div[class^='TextInput__Close']").click();
+    });
+  });
+
+  it("does bulk actions", () => {
+    cy.visit("/migrations");
+    waitForAll();
+
+    cy.loadFixtures(["transfers/migrations"], (results: any[]) => {
+      const migrations: any[] = results[0].migrations;
+
+      cy.get("div[class*='TransferListItem__Checkbox']").eq(0).click();
+      cy.get("div[class^='SearchButton__Wrapper']").click();
+      cy.get("input[class*='SearchInput']").type("ol88-uefi");
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        migrations.filter(r => r.instances.find(i => i.includes("ol88-uefi")))
+          .length
+      );
+      cy.get("div[class*='TransferListItem__Checkbox']").eq(0).click();
+      cy.get("div[class*='MainListFilter__SelectionText']").should(
+        "contain.text",
+        "2 of 1"
+      );
+
+      cy.get("div[class^='ActionDropdown__Wrapper']").click();
+      cy.get("div[class^='ActionDropdown__ListItem']")
+        .contains("Recreate Migrations")
+        .click();
+      cy.get("div[class^='AlertModal__Message']").should(
+        "contain.text",
+        "Are you sure you want to recreate"
+      );
+
+      let postCount = 0;
+      cy.intercept("POST", routeSelectors.MIGRATIONS, req => {
+        postCount += 1;
+        if (postCount === 1) {
+          expect(req.body.migration.instances).to.deep.eq([
+            "Datacenter/ol88-bios",
+          ]);
+        } else if (postCount === 2) {
+          expect(req.body.migration.instances).to.deep.eq([
+            "Datacenter/ol88-uefi",
+          ]);
+        }
+      }).as("migrations-recreate");
+
+      cy.get("button").contains("Yes").click();
+      cy.wait(["@migrations-recreate"]);
+    });
+  });
+});

+ 111 - 0
cypress/e2e/page header/page-header.cy.ts

@@ -0,0 +1,111 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Page header", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth();
+
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      fixture: "endpoints/endpoints.json",
+    }).as("endpoints");
+    cy.intercept(routeSelectors.SCHEDULES, {
+      fixture: "transfers/schedules-enabled.json",
+    }).as("schedules");
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth();
+
+    cy.wait(["@endpoints", "@schedules"]);
+  };
+
+  it("switches project", () => {
+    cy.visit("/replicas");
+    waitForAll();
+
+    cy.get("div[class^='Dropdown__Wrapper']").contains("admin").click();
+    cy.setCookie("unscopedToken", "[unscopedToken]");
+
+    cy.intercept("POST", routeSelectors.AUTH_TOKENS).as("login");
+    cy.get("div[class^='Dropdown__ListItem']").contains("admin").click();
+    cy.wait(["@login"]);
+
+    cy.loadFixtures(["projects/projects"], (results: any[]) => {
+      const projects = results[0].projects;
+      cy.getCookie("projectId").should(
+        "have.property",
+        "value",
+        projects.find(p => p.name === "admin").id
+      );
+    });
+  });
+
+  it("redirects to user info", () => {
+    cy.visit("/replicas");
+    waitForAll();
+    cy.get("div[class^='UserDropdown__Wrapper']").click();
+    cy.get("a[class^='UserDropdown__Username']").click();
+    cy.url().should("include", "/users");
+  });
+
+  it("shows about coriolis", () => {
+    cy.visit("/replicas");
+    waitForAll();
+    cy.get("div[class^='UserDropdown__Wrapper']").click();
+
+    cy.intercept(routeSelectors.APPLIANCES, {
+      fixture: "licences/appliances.json",
+    }).as("appliances");
+    cy.intercept(routeSelectors.STATUS, {
+      fixture: "licences/status.json",
+    }).as("status");
+    cy.intercept(routeSelectors.APPLIANCE_STATUS, {
+      fixture: "licences/appliance-status.json",
+    }).as("appliance-status");
+
+    cy.get("div[class^='UserDropdown__ListItem']")
+      .contains("About Coriolis")
+      .click();
+
+    cy.wait(["@appliances", "@status", "@appliance-status"]);
+
+    cy.loadFixtures(["licences/appliances"], (results: any[]) => {
+      const appliances = results[0].appliances;
+      cy.get("div[class^='LicenceModule__Wrapper']").should(
+        "contain.text",
+        `${appliances[0].id}-licencev2`
+      );
+    });
+  });
+
+  it("redirects to help", () => {
+    cy.visit("/replicas", {
+      onBeforeLoad(win) {
+        cy.stub(win, "open").as("winOpen");
+      },
+    });
+    waitForAll();
+    cy.get("div[class^='UserDropdown__Wrapper']").click();
+
+    cy.get("div[class^='UserDropdown__ListItem']").contains("Help").click();
+    cy.get("@winOpen").should(
+      "be.calledWith",
+      "https://cloudbase.it/coriolis-overview/"
+    );
+  });
+
+  it("logs out", () => {
+    cy.visit("/replicas");
+    waitForAll();
+
+    cy.get("div[class^='UserDropdown__Wrapper']").click();
+
+    cy.intercept("DELETE", routeSelectors.AUTH_TOKENS).as("logout");
+    cy.get("div[class^='UserDropdown__ListItem']").contains("Sign Out").click();
+    cy.wait(["@logout"]);
+    cy.url().should("include", "/login");
+  });
+});

+ 166 - 0
cypress/e2e/replicas/replicas-list.cy.ts

@@ -0,0 +1,166 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "../../support/routeSelectors";
+
+describe("Replicas list", () => {
+  beforeEach(() => {
+    cy.setProjectIdCookie();
+
+    cy.mockAuth();
+
+    cy.intercept(routeSelectors.ENDPOINTS, {
+      fixture: "endpoints/endpoints.json",
+    }).as("endpoints");
+  });
+
+  const waitForAll = () => {
+    cy.waitMockAuth();
+
+    cy.wait(["@endpoints"]);
+  };
+
+  it("renders empty list", () => {
+    cy.intercept(routeSelectors.REPLICAS, {
+      body: { replicas: [] },
+    }).as("replicas");
+
+    cy.visit("/replicas");
+
+    waitForAll();
+    cy.wait(["@replicas"]);
+
+    cy.get("div[class^='MainList__EmptyListMessage']").should(
+      "contain.text",
+      "don't have any Replicas in this project"
+    );
+    cy.get("button").should("contain.text", "Create a Replica");
+  });
+
+  it("renders list with scheduled icon", () => {
+    const scheduleAliases: string[] = [];
+    let schedules: any[] = [];
+    cy.loadFixtures(
+      [
+        "transfers/replicas",
+        "transfers/schedules-enabled",
+        "transfers/schedules-disabled",
+      ],
+      (results: any[]) => {
+        const replicas = results[0].replicas;
+        schedules = replicas.map((_, index) =>
+          index % 2 === 0 ? results[1].schedules : results[2].schedules
+        );
+
+        for (const [index, replica] of replicas.entries()) {
+          const scheduleAlias = `schedule-${index}`;
+          cy.intercept(`**/coriolis/**/replicas/${replica.id}/schedules`, {
+            body: {
+              schedules: schedules[index],
+            },
+          }).as(scheduleAlias);
+          scheduleAliases.push(`@${scheduleAlias}`);
+        }
+      }
+    );
+
+    cy.visit("/replicas");
+    waitForAll();
+    cy.wait(scheduleAliases);
+
+    cy.get("div[class^='MainList__EmptyListMessage']").should("not.exist");
+
+    for (const [index, schedule] of schedules.entries()) {
+      const shouldHaveIcon = schedule.find(s => s.enabled);
+      const scheduleImage = cy
+        .get("div[class^='TransferListItem__StatusWrapper']")
+        .eq(index)
+        .get("div[class^='TransferListItem__ScheduleImage']");
+      if (shouldHaveIcon) {
+        scheduleImage.should("exist");
+      } else {
+        scheduleImage.should("not.exist");
+      }
+    }
+  });
+
+  it("filters list", () => {
+    cy.intercept(routeSelectors.SCHEDULES, {
+      fixture: "transfers/schedules-enabled.json",
+    }).as("schedules");
+    cy.visit("/replicas");
+    waitForAll();
+    cy.wait(["@schedules"]);
+
+    cy.loadFixtures(["transfers/replicas"], (results: any[]) => {
+      const replicas = results[0].replicas;
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Error")
+        .click();
+      cy.get("div[class^='MainList__NoResults']").should("exist");
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("Completed")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        replicas.filter(r => r.last_execution_status === "COMPLETED").length
+      );
+
+      cy.get("div[class^='MainListFilter__FilterItem']")
+        .contains("All")
+        .click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        replicas.length
+      );
+
+      cy.get("div[class^='SearchButton__Wrapper']").click();
+      cy.get("input[class*='SearchInput']").type("ol88");
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        replicas.filter(r => r.instances.find(i => i.includes("ol88"))).length
+      );
+
+      cy.get("div[class^='TextInput__Close']").click();
+      cy.get("div[class^='TransferListItem__Wrapper']").should(
+        "have.length",
+        replicas.length
+      );
+    });
+  });
+
+  it("does bulk actions", () => {
+    cy.intercept(routeSelectors.SCHEDULES, {
+      fixture: "transfers/schedules-enabled.json",
+    }).as("schedules");
+    cy.visit("/replicas");
+    waitForAll();
+    cy.wait(["@schedules"]);
+
+    cy.get("div[class*='TransferListItem__Checkbox']").eq(0).click();
+    cy.get("div[class*='MainListFilter__SelectionText']").should(
+      "contain.text",
+      "1 of 2"
+    );
+
+    cy.get("div[class^='ActionDropdown__Wrapper']").click();
+    cy.loadFixtures(["transfers/replicas"], (results: any[]) => {
+      const replicas = results[0].replicas;
+      cy.intercept(`**/coriolis/**/replicas/${replicas[0].id}`, {
+        fixture: "transfers/replica-unexecuted",
+      }).as("replica");
+
+      cy.get("div[class^='ActionDropdown__ListItem']")
+        .contains("Delete Replicas")
+        .click();
+      cy.wait(["@replica"]);
+
+      cy.intercept("DELETE", `**/coriolis/**/replicas/${replicas[0].id}`, {
+        fixture: "transfers/replica-unexecuted",
+      }).as("delete-replica");
+      cy.get("button").contains("Delete Replica").click();
+      cy.wait(["@delete-replica"]);
+    });
+  });
+});

+ 7 - 0
cypress/fixtures/auth/fail.json

@@ -0,0 +1,7 @@
+{
+  "error": {
+    "code": 401,
+    "message": "The request you have made requires authentication.",
+    "title": "Unauthorized"
+  }
+}

+ 112 - 0
cypress/fixtures/auth/role-assignments.json

@@ -0,0 +1,112 @@
+{
+  "role_assignments": [
+    {
+      "links": {
+        "assignment": "http://[redacted]/v3/projects/c7fc329e310e436ea7fa5e6e41c642c7/users/aa80baecc9ae4c02aff33695c5c2f84c/roles/7c27a0c3f8a84493bbf28b90de0c7438"
+      },
+      "scope": {
+        "project": {
+          "id": "c7fc329e310e436ea7fa5e6e41c642c7",
+          "name": "service",
+          "domain": {
+            "id": "default",
+            "name": "Default"
+          }
+        }
+      },
+      "user": {
+        "id": "aa80baecc9ae4c02aff33695c5c2f84c",
+        "name": "coriolis",
+        "domain": {
+          "id": "default",
+          "name": "Default"
+        }
+      },
+      "role": {
+        "id": "7c27a0c3f8a84493bbf28b90de0c7438",
+        "name": "admin"
+      }
+    },
+    {
+      "links": {
+        "assignment": "http://[redacted]/v3/projects/c7fc329e310e436ea7fa5e6e41c642c7/users/e26d485a4f31426a9dd8a3d697eb2f51/roles/7c27a0c3f8a84493bbf28b90de0c7438"
+      },
+      "scope": {
+        "project": {
+          "id": "c7fc329e310e436ea7fa5e6e41c642c7",
+          "name": "service",
+          "domain": {
+            "id": "default",
+            "name": "Default"
+          }
+        }
+      },
+      "user": {
+        "id": "e26d485a4f31426a9dd8a3d697eb2f51",
+        "name": "barbican",
+        "domain": {
+          "id": "default",
+          "name": "Default"
+        }
+      },
+      "role": {
+        "id": "7c27a0c3f8a84493bbf28b90de0c7438",
+        "name": "admin"
+      }
+    },
+    {
+      "links": {
+        "assignment": "http://[redacted]/v3/projects/a379d2b6bd8d4b898733fb1e1b7967f7/users/eff03964a29b4f89b9fefc4c16de5f4f/roles/7c27a0c3f8a84493bbf28b90de0c7438"
+      },
+      "scope": {
+        "project": {
+          "id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+          "name": "admin",
+          "domain": {
+            "id": "default",
+            "name": "Default"
+          }
+        }
+      },
+      "user": {
+        "id": "eff03964a29b4f89b9fefc4c16de5f4f",
+        "name": "admin",
+        "domain": {
+          "id": "default",
+          "name": "Default"
+        }
+      },
+      "role": {
+        "id": "7c27a0c3f8a84493bbf28b90de0c7438",
+        "name": "admin"
+      }
+    },
+    {
+      "links": {
+        "assignment": "http://[redacted]/v3/system/users/eff03964a29b4f89b9fefc4c16de5f4f/roles/7c27a0c3f8a84493bbf28b90de0c7438"
+      },
+      "scope": {
+        "system": {
+          "all": true
+        }
+      },
+      "user": {
+        "id": "eff03964a29b4f89b9fefc4c16de5f4f",
+        "name": "admin",
+        "domain": {
+          "id": "default",
+          "name": "Default"
+        }
+      },
+      "role": {
+        "id": "7c27a0c3f8a84493bbf28b90de0c7438",
+        "name": "admin"
+      }
+    }
+  ],
+  "links": {
+    "next": null,
+    "self": "http://[redacted]/v3/role_assignments?include_names",
+    "previous": null
+  }
+}

+ 210 - 0
cypress/fixtures/auth/token-scoped.json

@@ -0,0 +1,210 @@
+{
+  "token": {
+    "methods": ["password", "token"],
+    "user": {
+      "domain": {
+        "id": "default",
+        "name": "Default"
+      },
+      "id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "name": "admin",
+      "password_expires_at": null
+    },
+    "audit_ids": ["7W-7RfFGSxak3AnVD7njxw", "5i9XBeTtTWWaLg4wNlerKA"],
+    "expires_at": "2023-10-07T19:34:50.000000Z",
+    "issued_at": "2023-10-06T19:34:50.000000Z",
+    "project": {
+      "domain": {
+        "id": "default",
+        "name": "Default"
+      },
+      "id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "name": "admin"
+    },
+    "is_domain": false,
+    "roles": [
+      {
+        "id": "d6f18c23d2bf459e8ca869c616375d3d",
+        "name": "reader"
+      },
+      {
+        "id": "4e01db07623b43c0bcab64e34f3fc99d",
+        "name": "member"
+      },
+      {
+        "id": "7c27a0c3f8a84493bbf28b90de0c7438",
+        "name": "admin"
+      }
+    ],
+    "catalog": [
+      {
+        "endpoints": [
+          {
+            "id": "03ebe1c21bd944e7942efa4325a97b07",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:5000",
+            "region": "RegionOne"
+          },
+          {
+            "id": "24fa9bcb5a244e58b09e3f7670ffdb31",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:5000",
+            "region": "RegionOne"
+          },
+          {
+            "id": "89c76368ac884052abbbff3ade5243c7",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:35357",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "0c4a61c8fddb4b85846ddd90749ff227",
+        "type": "identity",
+        "name": "keystone"
+      },
+      {
+        "endpoints": [
+          {
+            "id": "36c1b90e4e4f4f94b1658306585339d1",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9998/api/v1",
+            "region": "RegionOne"
+          },
+          {
+            "id": "451a587098e444fc8771819e75420a8f",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9998/api/v1",
+            "region": "RegionOne"
+          },
+          {
+            "id": "522fdf7c40d14b91b4c81f6b61d8b86b",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9998/api/v1",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "854156989f86479db9a21f88bd9a3cb3",
+        "type": "coriolis-logger",
+        "name": "coriolis-logger"
+      },
+      {
+        "endpoints": [
+          {
+            "id": "96e04f2c563f430d99d156c9864a9b66",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:37667/v2",
+            "region": "RegionOne"
+          },
+          {
+            "id": "b15f4869ce8a461797d6a9aa4e3bfaf4",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:37667/v2",
+            "region": "RegionOne"
+          },
+          {
+            "id": "de3c07c114194fc787da0ecf4abea57d",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:37667/v2",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "9390ffd8d769465f89fc2f71455c9bee",
+        "type": "coriolis-licensing",
+        "name": "coriolis-licensing"
+      },
+      {
+        "endpoints": [
+          {
+            "id": "1494d21bb15c4da9b5a87da8265c2d9d",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:7667/v1/a379d2b6bd8d4b898733fb1e1b7967f7",
+            "region": "RegionOne"
+          },
+          {
+            "id": "5979003c8dff49a08b15ce3b3e1773f6",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:7667/v1/a379d2b6bd8d4b898733fb1e1b7967f7",
+            "region": "RegionOne"
+          },
+          {
+            "id": "ddf9740f27fe43ac8ff53a66408a08c6",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:7667/v1/a379d2b6bd8d4b898733fb1e1b7967f7",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "95160175eafc48938868d022772f72fa",
+        "type": "migration",
+        "name": "coriolis"
+      },
+      {
+        "endpoints": [
+          {
+            "id": "35ec1d67bb4c44ad9d3225968a22fe91",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9311",
+            "region": "RegionOne"
+          },
+          {
+            "id": "5b136b3f1de040a89b8e18c300307696",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9311",
+            "region": "RegionOne"
+          },
+          {
+            "id": "d19561a127db4480930496ca91c267fb",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9311",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "c5febee4e98f420ea11fa3b684e7105b",
+        "type": "key-manager",
+        "name": "barbican"
+      },
+      {
+        "endpoints": [
+          {
+            "id": "56ec1c4005354452aaafbdd47724bdec",
+            "interface": "admin",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9900/api/v1",
+            "region": "RegionOne"
+          },
+          {
+            "id": "a94370b8b2304e969531fde981ccf242",
+            "interface": "internal",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9900/api/v1",
+            "region": "RegionOne"
+          },
+          {
+            "id": "cd9147782ec44d3b8277291fe696753b",
+            "interface": "public",
+            "region_id": "RegionOne",
+            "url": "http://127.0.0.1:9900/api/v1",
+            "region": "RegionOne"
+          }
+        ],
+        "id": "eda4476c83a34442bfd61f46f0e7af6a",
+        "type": "coriolis-metal-hub",
+        "name": "coriolis-metal-hub"
+      }
+    ]
+  }
+}

+ 18 - 0
cypress/fixtures/endpoints/endpoint.json

@@ -0,0 +1,18 @@
+{
+  "endpoint": {
+    "created_at": "2023-09-15T11:46:10.000000",
+    "updated_at": null,
+    "deleted_at": null,
+    "deleted": "0",
+    "id": "6d6aee40-0204-4ee6-96f3-14da916faeb4",
+    "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+    "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+    "connection_info": {
+      "secret_ref": "http://127.0.0.1:9311/v1/secrets/secret-ref-1"
+    },
+    "type": "openstack",
+    "name": "openstack-aio",
+    "description": null,
+    "mapped_regions": []
+  }
+}

+ 180 - 0
cypress/fixtures/endpoints/endpoints.json

@@ -0,0 +1,180 @@
+{
+  "endpoints": [
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "6c603c28-ed7b-48da-96ec-db4c44b8c273",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/b5a461e3-d55c-4a46-bee0-49bd24e983a2"
+      },
+      "type": "azure",
+      "name": "coriolis-azure",
+      "description": "coriolis-azure",
+      "mapped_regions": ["4a0feabf-b3ab-43c8-8259-496d53466ba7"]
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "4786df73-3e05-4fbd-9e52-37e819508d9a",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/1813b19e-9ea6-47e6-85a0-defb40022b21"
+      },
+      "type": "oracle_vm",
+      "name": "coriolis-ovm",
+      "description": null,
+      "mapped_regions": ["4a0feabf-b3ab-43c8-8259-496d53466ba7"]
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "9ed32585-df97-4c46-852a-748e56e9c4d1",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/2d22111b-c73b-43f8-8ffa-c1e2000e8fe4"
+      },
+      "type": "hyper-v",
+      "name": "Hyper-V",
+      "description": null,
+      "mapped_regions": ["4a0feabf-b3ab-43c8-8259-496d53466ba7"]
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "8ede076e-d941-4b16-8485-47234196b1a7",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/f47c07af-cf43-4419-ba4d-c4b1112e88b6"
+      },
+      "type": "vmware_vsphere",
+      "name": "vmware_vsphere_8.0",
+      "description": "cbsl vsphere8",
+      "mapped_regions": ["4a0feabf-b3ab-43c8-8259-496d53466ba7"]
+    },
+    {
+      "created_at": "2023-09-11T15:42:17.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "26df29ea-df07-4a6f-b659-4f6d81baa7a7",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/f55d91d7-3c63-48a8-9f36-07a07494935f"
+      },
+      "type": "metal",
+      "name": "appliance-metal-hub",
+      "description": null,
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "551f525d-6bf8-4813-9306-3f242f54853c",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/7dd5e7be-c2ef-44ce-9b20-d56fca352cad"
+      },
+      "type": "oci",
+      "name": "oci",
+      "description": null,
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "6d6aee40-0204-4ee6-96f3-14da916faeb4",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/07018644-7e52-4d18-9c16-024cb8ccf3ed"
+      },
+      "type": "openstack",
+      "name": "openstack-aio",
+      "description": null,
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-15T11:46:11.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/da175348-97f7-442a-878a-805654b1423b"
+      },
+      "type": "lxd",
+      "name": "MicroCloud",
+      "description": "",
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-15T11:46:11.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "dc411abe-8113-4266-9664-72f080dd7cb5",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/438b9260-907d-41e6-b5a3-eea048340795"
+      },
+      "type": "olvm",
+      "name": "OLVM",
+      "description": "",
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-29T14:44:12.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "f12b06da-5742-4383-9b2f-6fdae202903e",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/0d17a48d-482b-4b1e-98ba-ad46bbd49785"
+      },
+      "type": "aws",
+      "name": "aws",
+      "description": "Amazon Web Services",
+      "mapped_regions": []
+    },
+    {
+      "created_at": "2023-09-15T11:46:10.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "connection_info": {
+        "secret_ref": "http://127.0.0.1:9311/v1/secrets/62e55963-b9f1-4df7-8234-cb9e342744b1"
+      },
+      "type": "vmware_vsphere",
+      "name": "coriolis-vsphere",
+      "description": "coriolis-vsphere",
+      "mapped_regions": []
+    }
+  ]
+}

+ 214 - 0
cypress/fixtures/endpoints/openstack-connection-schema.json

@@ -0,0 +1,214 @@
+{
+  "schemas": {
+    "connection_info_schema": {
+      "$schema": "http://cloudbase.it/coriolis/schemas/openstack_connection#",
+      "oneOf": [
+        {
+          "type": "object",
+          "properties": {
+            "identity_api_version": {
+              "type": "integer",
+              "title": "Identity API Version",
+              "description": "Version of the Identity API to use.",
+              "minimum": 2,
+              "maximum": 3,
+              "default": 2
+            },
+            "username": {
+              "type": "string",
+              "title": "Username",
+              "description": "The OpenStack user."
+            },
+            "password": {
+              "type": "string",
+              "title": "Password",
+              "description": "The password for the OpenStack user."
+            },
+            "project_name": {
+              "type": "string",
+              "title": "Project Name",
+              "description": "Name of the project/tenant for the OpenStack user."
+            },
+            "user_domain_name": {
+              "type": "string",
+              "title": "User Domain Name",
+              "description": "The name of the user domain."
+            },
+            "user_domain_id": {
+              "type": "string",
+              "title": "User Domain ID",
+              "description": "The ID of the user domain."
+            },
+            "project_domain_name": {
+              "type": "string",
+              "title": "Project Domain Name",
+              "description": "The name of the project domain."
+            },
+            "project_domain_id": {
+              "type": "string",
+              "title": "Project Domain ID",
+              "description": "The ID of the project domain."
+            },
+            "auth_url": {
+              "type": "string",
+              "title": "Authentication URL",
+              "description": "The full URL of the public endpoint of the authentication service (Keystone) for the OpenStack platform. Must include the port number as well as the API version path element. ('/v2.0' for Keystone v2 and '/v3' for Keystone v3)"
+            },
+            "glance_api_version": {
+              "type": "integer",
+              "title": "Glance API Version",
+              "description": "The API used by the OpenStack Image Service.(Glance)",
+              "minimum": 1,
+              "maximum": 2,
+              "default": 2
+            },
+            "allow_untrusted": {
+              "type": "boolean",
+              "title": "Allow Untrusted",
+              "description": "Whether or not to accept HTTPS connections with self-signed or untrusted certificates when interacting with the APIs of both the authentication service (Keystone) as well the other non-Swift services.",
+              "default": false
+            },
+            "allow_untrusted_swift": {
+              "type": "boolean",
+              "title": "Allow Untrusted Swift",
+              "description": "Whether or not to accept HTTPS connections with self-signed or untrusted certificates when interacting with the API of the OpenStack's Object Storage Service. (Swift or RADOS Gateway) Only used when Replicating from a source OpenStack which has Cinder-backup configured to use Swift.",
+              "default": false
+            },
+            "region_name": {
+              "type": "string",
+              "title": "Region Name",
+              "description": "The name of the OpenStack region to use."
+            },
+            "nova_region_name": {
+              "type": "string",
+              "title": "Nova Region Name",
+              "description": "The region name of the Openstack Compute Service (Nova)."
+            },
+            "neutron_region_name": {
+              "type": "string",
+              "title": "Neutron Region Name",
+              "description": "The region name of the OpenStack Networking Service (Neutron)."
+            },
+            "glance_region_name": {
+              "type": "string",
+              "title": "Glance Region Name",
+              "description": "The region name of the OpenStack Image Service (Glance)."
+            },
+            "cinder_region_name": {
+              "type": "string",
+              "title": "Cinder Region Name",
+              "description": "The region name of the OpenStack Block Storage Service (Cinder)."
+            },
+            "swift_region_name": {
+              "type": "string",
+              "title": "Swift Region Name",
+              "description": "The region name of the Openstack Object Storage Service(Swift or RADOS Gateway) Only used when Replicating from a source OpenStack which has Cinder-backup configured to use Swift/RADOS."
+            },
+            "interface_name": {
+              "type": "string",
+              "title": "Interface Name",
+              "description": "The name of the interface to use for all services. If a specific interface is required for different services, please use the option afferent to each service."
+            },
+            "nova_interface_name": {
+              "type": "string",
+              "title": "Nova Interface Name",
+              "description": "The interface name for the Openstack Compute Service (Nova)."
+            },
+            "neutron_interface_name": {
+              "type": "string",
+              "title": "Neutron Interface Name",
+              "description": "The interface name of the OpenStack Networking Service (Neutron)."
+            },
+            "glance_interface_name": {
+              "type": "string",
+              "title": "Glance Interface Name",
+              "description": "The interface name of the OpenStack Image Service (Glance)."
+            },
+            "cinder_interface_name": {
+              "type": "string",
+              "title": "Cinder Interface Name",
+              "description": "The interface name of the OpenStack Block Storage Service (Cinder)."
+            },
+            "swift_interface_name": {
+              "type": "string",
+              "title": "Swift Interface Name",
+              "description": "The interface name of the Openstack Object Storage Service(Swift or RADOS Gateway) Only used when Replicating from a source OpenStack which has Cinder-backup configured to use Swift/RADOS."
+            },
+            "ceph_options": {
+              "type": "object",
+              "title": "Cinder Ceph Options",
+              "descriptions": "If performing Ceph-based Replicas from a source OpenStack, the Ceph configuration file and credentials for a user with read-only access to the Ceph pool used by Cinder backups/snapshots must be provided. Coriolis must be able to connect to the source OpenStack's Ceph RADOS cluster by being able to reach at least one Ceph-monitor host. For the easiest setup possible, simply using the same credentials used by the Cinder service(s) will work.",
+              "properties": {
+                "ceph_conf_file": {
+                  "type": "string",
+                  "title": "Ceph Configuration File",
+                  "description": "Contents of the ceph.conf configuration file containing the list of monitor hosts to connect to. Ideally, this should be the same ceph.conf as used by the Cinder volume/backup service(s). The '[mon] keyring' path options are irrelevant, as the keyring which is passed through the 'Ceph Keyring File' option will be used."
+                },
+                "ceph_username": {
+                  "type": "string",
+                  "title": "Ceph Username",
+                  "description": "Ceph user to use when connecting to the source OpenStack's Ceph cluster. The user must have read-only access to the Ceph pool(s) used by the Cinder-backup and Cinder-volume service(s). Ideally, this should be the same user which the Cinder services themselves are using."
+                },
+                "ceph_keyring_file": {
+                  "type": "string",
+                  "title": "Ceph Keyring File",
+                  "description": "Ceph keyring file with access key(s) for the user given as the 'Ceph Username' for the cluster described in the given 'Ceph Configuration File'. Ideally, this should be the same keyring file as used by the Cinder service(s)."
+                },
+                "ceph_pool_name": {
+                  "type": "string",
+                  "title": "Ceph Pool Name",
+                  "description": "Name of the Ceph pool in which Cinder volume snapshots/backups are stored."
+                },
+                "ceph_cluster_name": {
+                  "type": "string",
+                  "title": "Ceph Cluster Name",
+                  "description": "Name of the Ceph cluster in which Cinder volume snapshots/backups are stored."
+                },
+                "ceph_connection_timeout": {
+                  "type": "integer",
+                  "title": "Ceph Connection Timeout",
+                  "description": "Integer number of seconds to wait on Ceph connections before timing out."
+                }
+              },
+              "additionalProperties": false,
+              "required": [
+                "ceph_conf_file",
+                "ceph_username",
+                "ceph_keyring_file",
+                "ceph_pool_name",
+                "ceph_cluster_name"
+              ]
+            }
+          },
+          "required": [
+            "identity_api_version",
+            "username",
+            "password",
+            "project_name",
+            "auth_url"
+          ],
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "properties": {
+            "secret_ref": {
+              "type": "string",
+              "format": "uri"
+            }
+          },
+          "required": ["secret_ref"],
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "properties": {},
+          "additionalProperties": false
+        },
+        {
+          "type": "null"
+        }
+      ]
+    }
+  }
+}

+ 60 - 0
cypress/fixtures/endpoints/providers.json

@@ -0,0 +1,60 @@
+{
+  "providers": {
+    "openstack": {
+      "types": [
+        4, 8, 16, 32, 64, 128, 256, 512, 4096, 16384, 32768, 65536, 131072,
+        262144, 524288, 1048576
+      ]
+    },
+    "oracle_vm": {
+      "types": [
+        4, 8, 16, 32, 64, 128, 512, 4096, 16384, 32768, 65536, 131072, 262144
+      ]
+    },
+    "opc": {
+      "types": [8, 16, 32, 4096, 65536, 131072]
+    },
+    "vmware_vsphere": {
+      "types": [
+        4, 8, 16, 32, 64, 128, 512, 1024, 4096, 16384, 32768, 65536, 131072,
+        262144
+      ]
+    },
+    "azure": {
+      "types": [
+        4, 8, 16, 32, 64, 128, 256, 512, 4096, 16384, 32768, 65536, 131072,
+        262144
+      ]
+    },
+    "aws": {
+      "types": [
+        4, 8, 16, 32, 64, 128, 256, 512, 4096, 16384, 32768, 65536, 131072,
+        262144
+      ]
+    },
+    "hyper-v": {
+      "types": [8, 16, 32, 4096, 65536]
+    },
+    "metal": {
+      "types": [8, 16, 32, 4096, 65536, 131072]
+    },
+    "oci": {
+      "types": [4, 16, 32, 64, 128, 256, 512, 16384, 32768, 262144, 1048576]
+    },
+    "opca": {
+      "types": [4, 16, 32, 64, 128, 256, 512, 16384, 32768, 262144, 1048576]
+    },
+    "scvmm": {
+      "types": [4, 16, 64, 128]
+    },
+    "olvm": {
+      "types": [4, 16, 64, 128, 512, 16384, 32768, 262144]
+    },
+    "rhev": {
+      "types": [4, 16, 64, 128, 512, 16384, 32768, 262144]
+    },
+    "lxd": {
+      "types": [4, 16, 64, 128, 512, 16384, 32768, 262144]
+    }
+  }
+}

+ 21 - 0
cypress/fixtures/endpoints/regions.json

@@ -0,0 +1,21 @@
+{
+  "regions": [
+    {
+      "created_at": "2023-09-11T15:41:53.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "4a0feabf-b3ab-43c8-8259-496d53466ba7",
+      "name": "Public",
+      "description": "Default Coriolis Region created by coriolis-docker.",
+      "enabled": true,
+      "mapped_services": ["ae01aaa4-2875-431f-8f30-c02b72816733"],
+      "mapped_endpoints": [
+        "6c603c28-ed7b-48da-96ec-db4c44b8c273",
+        "4786df73-3e05-4fbd-9e52-37e819508d9a",
+        "9ed32585-df97-4c46-852a-748e56e9c4d1",
+        "8ede076e-d941-4b16-8485-47234196b1a7"
+      ]
+    }
+  ]
+}

+ 3 - 0
cypress/fixtures/endpoints/secret-ref.json

@@ -0,0 +1,3 @@
+{
+  "secret_ref": "http://127.0.0.1:9311/v1/secrets/secret-ref-1"
+}

+ 6 - 0
cypress/fixtures/endpoints/validation-fail.json

@@ -0,0 +1,6 @@
+{
+  "validate-connection": {
+    "valid": false,
+    "message": "Could not find requested endpoint in Service Catalog."
+  }
+}

+ 1 - 0
cypress/fixtures/endpoints/validation-success.json

@@ -0,0 +1 @@
+{ "validate-connection": { "valid": true, "message": null } }

+ 15 - 0
cypress/fixtures/licences/appliance-status.json

@@ -0,0 +1,15 @@
+{
+  "appliance_licence_status": {
+    "appliance_id": "29b06e8f-2ae3-4267-a1ec-c587e732391e",
+    "earliest_licence_expiry_time": "2024-09-09T11:58:40Z",
+    "latest_licence_expiry_time": "2024-09-09T11:58:40Z",
+    "current_performed_migrations": 5,
+    "current_performed_replicas": 1,
+    "lifetime_performed_migrations": 8,
+    "lifetime_performed_replicas": 2,
+    "current_available_migrations": 100,
+    "current_available_replicas": 100,
+    "lifetime_available_migrations": 100,
+    "lifetime_available_replicas": 100
+  }
+}

+ 1 - 0
cypress/fixtures/licences/appliances.json

@@ -0,0 +1 @@
+{ "appliances": [{ "id": "29b06e8f-2ae3-4267-a1ec-c587e732391e" }] }

+ 8 - 0
cypress/fixtures/licences/status.json

@@ -0,0 +1,8 @@
+{
+  "status": {
+    "hostname": "coriolis",
+    "multi_appliance": false,
+    "supported_licence_versions": ["v2"],
+    "server_local_time": "2023-10-06T19:34:52.004947251Z"
+  }
+}

+ 23 - 0
cypress/fixtures/projects/projects.json

@@ -0,0 +1,23 @@
+{
+  "projects": [
+    {
+      "id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "name": "admin",
+      "domain_id": "default",
+      "description": "Bootstrap project for initializing the cloud.",
+      "enabled": true,
+      "parent_id": "default",
+      "is_domain": false,
+      "tags": [],
+      "options": {},
+      "links": {
+        "self": "http://[redacted]/v3/projects/a379d2b6bd8d4b898733fb1e1b7967f7"
+      }
+    }
+  ],
+  "links": {
+    "next": null,
+    "self": "http://[redacted]/v3/auth/projects",
+    "previous": null
+  }
+}

+ 398 - 0
cypress/fixtures/transfers/migrations.json

@@ -0,0 +1,398 @@
+{
+  "migrations": [
+    {
+      "base_id": "549caaa0-e84e-43b3-b5fc-c28f51ceb6a0",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "4be82ece0d5d7a2265e2d15d19d539f280186b10549fcc0c19f347fa41305103"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "f8f94f7d-07b5-4e78-aee7-2935a8683c10",
+      "notes": "",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "CANCELED",
+      "created_at": "2023-09-23T19:08:53.000000",
+      "updated_at": "2023-09-23T19:11:25.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "549caaa0-e84e-43b3-b5fc-c28f51ceb6a0",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    },
+    {
+      "base_id": "760b955c-f321-45f6-aa2f-3bb8d298d43d",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "ba714c2aca06757c2c19faa0b1bf1409757bb4574174ff12073ef951a195f3f2"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-uefi"],
+      "reservation_id": "91a77449-7ada-4e03-8f4f-438445aaadb3",
+      "notes": null,
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "COMPLETED",
+      "created_at": "2023-09-17T20:13:15.000000",
+      "updated_at": "2023-09-17T20:28:47.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "760b955c-f321-45f6-aa2f-3bb8d298d43d",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    },
+    {
+      "base_id": "97add5e0-cbf2-4f1d-94ff-bb2aa8d05295",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "1e5c8a4627a4121bbde2749e4e6fbb313dba3052e2053c3f3f624e0543724cba"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "e3fbc524-c5af-46bc-a4e5-01697539fb0b",
+      "notes": "",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "ERROR",
+      "created_at": "2023-09-23T19:11:43.000000",
+      "updated_at": "2023-09-23T19:27:13.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "97add5e0-cbf2-4f1d-94ff-bb2aa8d05295",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    },
+    {
+      "base_id": "9aad18e1-4c20-4a98-b16f-7a2f7655d18e",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "1e5c8a4627a4121bbde2749e4e6fbb313dba3052e2053c3f3f624e0543724cba"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "fb44d241-8c97-4687-891f-2a3a21ba85d0",
+      "notes": "",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "COMPLETED",
+      "created_at": "2023-09-23T19:30:33.000000",
+      "updated_at": "2023-09-23T19:45:56.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "9aad18e1-4c20-4a98-b16f-7a2f7655d18e",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    },
+    {
+      "base_id": "d8ad2fe7-57ef-4940-ae80-c392383831b5",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "1e5c8a4627a4121bbde2749e4e6fbb313dba3052e2053c3f3f624e0543724cba"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "28c1a203-22da-49a6-9a49-134069eb5e20",
+      "notes": "",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": {
+        "Datacenter/ol88-bios": {
+          "num_cpu": 4,
+          "memory_mb": 4096,
+          "name": "ol88-bios",
+          "id": "ol88-bios",
+          "firmware_type": "EFI",
+          "os_type": "linux",
+          "nested_virtualization": false,
+          "devices": {
+            "cdroms": [],
+            "floppies": [],
+            "serial_ports": [],
+            "controllers": [],
+            "disks": [
+              {
+                "format": "raw",
+                "id": "root",
+                "size_bytes": 13958643712.0,
+                "path": "/"
+              },
+              {
+                "format": "raw",
+                "id": "/dev/sdb",
+                "size_bytes": 1073741824.0,
+                "path": ""
+              }
+            ],
+            "nics": [
+              {
+                "id": "eth0",
+                "name": "eth0",
+                "mac_address": "00:x:x:x:x:b0",
+                "ip_addresses": ["10.x.x.6", "fd42:x:x:x:x:x:x:c1b0"],
+                "network_name": "tapff00339e",
+                "network_id": "tapff00339e"
+              }
+            ]
+          },
+          "public_ip_address": "10.x.x.6"
+        }
+      },
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "COMPLETED",
+      "created_at": "2023-09-23T19:51:50.000000",
+      "updated_at": "2023-09-23T20:07:12.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "d8ad2fe7-57ef-4940-ae80-c392383831b5",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    },
+    {
+      "base_id": "e25ffdc0-c81a-4003-a2d6-2592bdc19c9d",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "1e5c8a4627a4121bbde2749e4e6fbb313dba3052e2053c3f3f624e0543724cba"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "migration",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "c791029e-8f58-4c34-bc37-abc2427ab1e6",
+      "notes": "",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": {
+        "Datacenter/ol88-bios": {
+          "num_cpu": 4,
+          "memory_mb": 4096,
+          "name": "ol88-bios",
+          "id": "ol88-bios",
+          "firmware_type": "EFI",
+          "os_type": "linux",
+          "nested_virtualization": false,
+          "devices": {
+            "cdroms": [],
+            "floppies": [],
+            "serial_ports": [],
+            "controllers": [],
+            "disks": [
+              {
+                "format": "raw",
+                "id": "root",
+                "size_bytes": 13958643712.0,
+                "path": "/"
+              },
+              {
+                "format": "raw",
+                "id": "/dev/sdb",
+                "size_bytes": 1073741824.0,
+                "path": ""
+              }
+            ],
+            "nics": [
+              {
+                "id": "eth0",
+                "name": "eth0",
+                "mac_address": "00:x:x:x:x:f4",
+                "ip_addresses": ["10.x.x.x", "fd42:x:x:x:x:x:x:79f4"],
+                "network_name": "tapx10",
+                "network_id": "tapx10"
+              }
+            ]
+          },
+          "public_ip_address": "10.x.x.6"
+        }
+      },
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "COMPLETED",
+      "created_at": "2023-09-24T19:31:41.000000",
+      "updated_at": "2023-09-24T19:47:02.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "e25ffdc0-c81a-4003-a2d6-2592bdc19c9d",
+      "replica_id": null,
+      "shutdown_instances": false,
+      "replication_count": 2
+    }
+  ]
+}

+ 46 - 0
cypress/fixtures/transfers/replica-unexecuted.json

@@ -0,0 +1,46 @@
+{
+  "replica": {
+    "base_id": "5395842a-7006-41a3-95ac-e5083561a4d9",
+    "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+    "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+    "destination_environment": {
+      "data_transfer_mechanism": "HTTPS",
+      "windows_virtio_iso_url": "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso",
+      "migr_flavor_name": "m1.small",
+      "migr_image_map": {
+        "linux": "498df770-bfab-446b-83f5-800225062f61"
+      },
+      "migr_network": "ext-net",
+      "network_map": {
+        "VM Network": "e541dabd-b3b4-4c80-a258-a0f66946117c"
+      },
+      "storage_mappings": {}
+    },
+    "type": "replica",
+    "executions": [],
+    "instances": ["Datacenter/dvincze-ol9-bios"],
+    "reservation_id": "3e147d9c-7259-48a6-bf73-db399e972bae",
+    "notes": "dvincze-ol9-bios",
+    "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+    "destination_endpoint_id": "6d6aee40-0204-4ee6-96f3-14da916faeb4",
+    "transfer_result": null,
+    "network_map": {
+      "VM Network": "e541dabd-b3b4-4c80-a258-a0f66946117c"
+    },
+    "storage_mappings": {},
+    "source_environment": {
+      "automatically_enable_cbt": true,
+      "vixdisklib_compatibility_version": "6.7"
+    },
+    "last_execution_status": "UNEXECUTED",
+    "created_at": "2023-10-11T20:11:49.000000",
+    "updated_at": null,
+    "deleted_at": null,
+    "deleted": "0",
+    "origin_minion_pool_id": null,
+    "destination_minion_pool_id": null,
+    "instance_osmorphing_minion_pool_mappings": {},
+    "user_scripts": {},
+    "id": "5395842a-7006-41a3-95ac-e5083561a4d9"
+  }
+}

+ 97 - 0
cypress/fixtures/transfers/replicas.json

@@ -0,0 +1,97 @@
+{
+  "replicas": [
+    {
+      "base_id": "5395842a-7006-41a3-95ac-e5083561a4d9",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "data_transfer_mechanism": "HTTPS",
+        "windows_virtio_iso_url": "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso",
+        "migr_flavor_name": "m1.small",
+        "migr_image_map": {
+          "linux": "498df770-bfab-446b-83f5-800225062f61"
+        },
+        "migr_network": "ext-net",
+        "network_map": {
+          "VM Network": "e541dabd-b3b4-4c80-a258-a0f66946117c"
+        },
+        "storage_mappings": {}
+      },
+      "type": "replica",
+      "executions": [],
+      "instances": ["Datacenter/dvincze-ol9-bios"],
+      "reservation_id": "3e147d9c-7259-48a6-bf73-db399e972bae",
+      "notes": "dvincze-ol9-bios",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "6d6aee40-0204-4ee6-96f3-14da916faeb4",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "e541dabd-b3b4-4c80-a258-a0f66946117c"
+      },
+      "storage_mappings": {},
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "UNEXECUTED",
+      "created_at": "2023-10-11T20:11:49.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "5395842a-7006-41a3-95ac-e5083561a4d9"
+    },
+    {
+      "base_id": "a9868952-85bf-48d7-bde4-bc866745c124",
+      "user_id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "project_id": "a379d2b6bd8d4b898733fb1e1b7967f7",
+      "destination_environment": {
+        "migr_worker_cpu_limit": 2,
+        "migr_worker_memory_limit": 4,
+        "use_network_forwarding": true,
+        "retain_user_credentials": true,
+        "migr_network": "default",
+        "migr_image_map": {
+          "linux": "ba714c2aca06757c2c19faa0b1bf1409757bb4574174ff12073ef951a195f3f2"
+        },
+        "network_map": {
+          "VM Network": "default"
+        },
+        "storage_mappings": {
+          "default": "remote"
+        }
+      },
+      "type": "replica",
+      "executions": [],
+      "instances": ["Datacenter/ol88-bios"],
+      "reservation_id": "09ee12d0-f0d0-4603-949e-6312f17430a8",
+      "notes": "ol88-bios",
+      "origin_endpoint_id": "f8fd5f35-2c54-4786-b32d-8f6fb3b057e7",
+      "destination_endpoint_id": "72ebf901-85c8-4a7a-9842-cb058c38b65d",
+      "transfer_result": null,
+      "network_map": {
+        "VM Network": "default"
+      },
+      "storage_mappings": {
+        "default": "remote"
+      },
+      "source_environment": {
+        "automatically_enable_cbt": true,
+        "vixdisklib_compatibility_version": "6.7"
+      },
+      "last_execution_status": "COMPLETED",
+      "created_at": "2023-09-17T12:28:13.000000",
+      "updated_at": "2023-09-17T12:44:33.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "origin_minion_pool_id": null,
+      "destination_minion_pool_id": null,
+      "instance_osmorphing_minion_pool_mappings": {},
+      "user_scripts": {},
+      "id": "a9868952-85bf-48d7-bde4-bc866745c124"
+    }
+  ]
+}

+ 52 - 0
cypress/fixtures/transfers/schedules-disabled.json

@@ -0,0 +1,52 @@
+{
+  "schedules": [
+    {
+      "created_at": "2023-10-04T10:45:16.000000",
+      "updated_at": "2023-10-11T20:06:58.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "6d2f29ac-f0fa-4cff-96c4-ede485db6c17",
+      "replica_id": "a9868952-85bf-48d7-bde4-bc866745c124",
+      "schedule": {
+        "hour": 21,
+        "minute": 0
+      },
+      "expiration_date": null,
+      "enabled": false,
+      "shutdown_instance": false,
+      "trust_id": "b9e5f80aca804d9d9c36e5dbcb39d39f"
+    },
+    {
+      "created_at": "2023-09-29T15:20:12.000000",
+      "updated_at": "2023-10-11T20:06:56.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "97951429-9d90-4a93-a2c7-aa4970a806ba",
+      "replica_id": "a9868952-85bf-48d7-bde4-bc866745c124",
+      "schedule": {
+        "hour": 21,
+        "minute": 0
+      },
+      "expiration_date": "2026-10-01T04:00:00.000000",
+      "enabled": false,
+      "shutdown_instance": false,
+      "trust_id": "3c9996e315f34453a4d0a4e57063b4bf"
+    },
+    {
+      "created_at": "2023-09-29T20:17:24.000000",
+      "updated_at": "2023-10-11T20:07:34.000000",
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "f7b79bb0-6d85-44e2-9b48-38965650007f",
+      "replica_id": "a9868952-85bf-48d7-bde4-bc866745c124",
+      "schedule": {
+        "hour": 4,
+        "minute": 0
+      },
+      "expiration_date": null,
+      "enabled": false,
+      "shutdown_instance": false,
+      "trust_id": "bd281c9aedd54d09b3b2e6c03fe70d12"
+    }
+  ]
+}

+ 55 - 0
cypress/fixtures/transfers/schedules-enabled.json

@@ -0,0 +1,55 @@
+{
+  "schedules": [
+    {
+      "created_at": "2023-10-11T20:11:51.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "8d3277c9-7b71-4768-8719-f385b17f7bf7",
+      "replica_id": "5395842a-7006-41a3-95ac-e5083561a4d9",
+      "schedule": {
+        "hour": 21,
+        "minute": 0,
+        "month": 1
+      },
+      "expiration_date": null,
+      "enabled": true,
+      "shutdown_instance": false,
+      "trust_id": "81ddb3fe3e2f4a5d97e9a12debbaa5de"
+    },
+    {
+      "created_at": "2023-10-11T20:11:51.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "dc94b905-b4c8-4ed1-9d7b-2003dc17ca41",
+      "replica_id": "5395842a-7006-41a3-95ac-e5083561a4d9",
+      "schedule": {
+        "hour": 21,
+        "minute": 0,
+        "month": 2
+      },
+      "expiration_date": "2023-10-25T21:00:00.000000",
+      "enabled": false,
+      "shutdown_instance": true,
+      "trust_id": "ac764562f76d4947bbab15bcf5e722d2"
+    },
+    {
+      "created_at": "2023-10-11T20:11:51.000000",
+      "updated_at": null,
+      "deleted_at": null,
+      "deleted": "0",
+      "id": "e851e44f-b7a5-40ef-b81a-cd03bc4cf005",
+      "replica_id": "5395842a-7006-41a3-95ac-e5083561a4d9",
+      "schedule": {
+        "hour": 21,
+        "minute": 0,
+        "month": 3
+      },
+      "expiration_date": null,
+      "enabled": true,
+      "shutdown_instance": false,
+      "trust_id": "287b5eeb7d3e4967be0c87f6b6e42a70"
+    }
+  ]
+}

+ 13 - 0
cypress/fixtures/users/user.json

@@ -0,0 +1,13 @@
+{
+  "user": {
+    "id": "eff03964a29b4f89b9fefc4c16de5f4f",
+    "name": "admin",
+    "domain_id": "default",
+    "enabled": true,
+    "password_expires_at": null,
+    "options": {},
+    "links": {
+      "self": "http://[redacted]/v3/users/eff03964a29b4f89b9fefc4c16de5f4f"
+    }
+  }
+}

+ 48 - 0
cypress/fixtures/users/users.json

@@ -0,0 +1,48 @@
+{
+  "users": [
+    {
+      "id": "eff03964a29b4f89b9fefc4c16de5f4f",
+      "name": "admin",
+      "domain_id": "default",
+      "enabled": true,
+      "password_expires_at": null,
+      "options": {},
+      "links": {
+        "self": "http://[redacted]/v3/users/eff03964a29b4f89b9fefc4c16de5f4f"
+      }
+    },
+    {
+      "email": null,
+      "description": null,
+      "id": "e26d485a4f31426a9dd8a3d697eb2f51",
+      "name": "barbican",
+      "domain_id": "default",
+      "enabled": true,
+      "default_project_id": "c7fc329e310e436ea7fa5e6e41c642c7",
+      "password_expires_at": null,
+      "options": {},
+      "links": {
+        "self": "http://[redacted]/v3/users/e26d485a4f31426a9dd8a3d697eb2f51"
+      }
+    },
+    {
+      "email": "coriolis@localhost",
+      "description": null,
+      "id": "aa80baecc9ae4c02aff33695c5c2f84c",
+      "name": "coriolis",
+      "domain_id": "default",
+      "enabled": true,
+      "default_project_id": "c7fc329e310e436ea7fa5e6e41c642c7",
+      "password_expires_at": null,
+      "options": {},
+      "links": {
+        "self": "http://[redacted]/v3/users/aa80baecc9ae4c02aff33695c5c2f84c"
+      }
+    }
+  ],
+  "links": {
+    "next": null,
+    "self": "http://[redacted]/v3/users",
+    "previous": null
+  }
+}

+ 112 - 0
cypress/support/commands.ts

@@ -0,0 +1,112 @@
+/// <reference types="cypress" />
+
+import { routeSelectors } from "./routeSelectors";
+
+Cypress.Commands.add(
+  "loadFixtures",
+  (fixtures: string[], finalCallback: (results: any[]) => void) => {
+    const loadFixtures = (
+      fixtures: string[],
+      callback: (results: any[]) => void,
+      index = 0,
+      results: any[] = []
+    ) => {
+      if (index >= fixtures.length) {
+        callback(results);
+        return;
+      }
+      cy.fixture(fixtures[index]).then(fixture => {
+        results.push(fixture);
+        loadFixtures(fixtures, callback, index + 1, results);
+      });
+    };
+    loadFixtures(fixtures, finalCallback);
+  }
+);
+
+const AUTH_RESOURCES = [
+  "token",
+  "user",
+  "users",
+  "projects",
+  "roles",
+  "replicas",
+  "migrations",
+];
+
+Cypress.Commands.add("mockAuth", (options?: { filterResources?: string[] }) => {
+  const { filterResources = [] } = options || {};
+  const resources = AUTH_RESOURCES.filter(r => !filterResources.includes(r));
+
+  for (const resource of resources) {
+    switch (resource) {
+      case "token":
+        cy.intercept(routeSelectors.AUTH_TOKENS, {
+          fixture: "auth/token-scoped",
+        }).as("token");
+        break;
+      case "user":
+        cy.intercept(routeSelectors.USER, {
+          fixture: "users/user",
+        }).as("user");
+        break;
+      case "users":
+        cy.intercept(routeSelectors.USERS, {
+          fixture: "users/users",
+        }).as("users");
+        break;
+      case "projects":
+        cy.intercept(routeSelectors.PROJECTS, {
+          fixture: "projects/projects",
+        }).as("projects");
+        break;
+      case "roles":
+        cy.intercept(routeSelectors.ROLE_ASSIGNMENTS, {
+          fixture: "auth/role-assignments",
+        }).as("roles");
+        break;
+      case "replicas":
+        cy.intercept(routeSelectors.REPLICAS, {
+          fixture: "transfers/replicas",
+        }).as("replicas");
+        break;
+      case "migrations":
+        cy.intercept(routeSelectors.MIGRATIONS, {
+          fixture: "transfers/migrations",
+        }).as("migrations");
+        break;
+    }
+  }
+});
+
+Cypress.Commands.add(
+  "waitMockAuth",
+  (options?: { filterResources?: string[] }) => {
+    const { filterResources = [] } = options || {};
+    const resources = AUTH_RESOURCES.filter(r => !filterResources.includes(r));
+    for (const resource of resources) {
+      cy.wait(`@${resource}`);
+    }
+  }
+);
+
+Cypress.Commands.add("setProjectIdCookie", () => {
+  cy.setCookie("projectId", "[project-id]");
+});
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/no-namespace
+  namespace Cypress {
+    interface Chainable {
+      loadFixtures(
+        fixtures: string[],
+        finalCallback: (results: any[]) => void
+      ): Chainable<void>;
+      mockAuth(options?: { filterResources?: string[] }): Chainable<void>;
+      waitMockAuth(options?: { filterResources?: string[] }): Chainable<void>;
+      setProjectIdCookie(): Chainable<void>;
+    }
+  }
+}
+
+export {};

+ 5 - 0
cypress/support/e2e.ts

@@ -0,0 +1,5 @@
+import "./commands";
+
+Cypress.on("uncaught:exception", () => {
+  return false;
+});

+ 18 - 0
cypress/support/routeSelectors.ts

@@ -0,0 +1,18 @@
+export const routeSelectors = {
+  APPLIANCE_STATUS: "**/licensing/appliances/**/status",
+  APPLIANCES: "**/licensing/appliances",
+  AUTH_TOKENS: "**/identity/auth/tokens",
+  CONN_SCHEMA_OPENSTACK: "**/coriolis/**/providers/openstack/schemas/16",
+  ENDPOINTS: "**/coriolis/**/endpoints",
+  MIGRATIONS: "**/coriolis/**/migrations",
+  PROJECTS: "**/identity/auth/projects",
+  PROVIDERS: "**/coriolis/**/providers",
+  REGIONS: "**/coriolis/**/regions",
+  REPLICAS: "**/coriolis/**/replicas",
+  ROLE_ASSIGNMENTS: "**/identity/role_assignments*",
+  SCHEDULES: "**/coriolis/**/replicas/**/schedules",
+  SECRETS: "**/barbican/**/secrets",
+  STATUS: "**/licensing/status",
+  USER: "**/identity/users/*",
+  USERS: "**/identity/users",
+};

+ 10 - 0
cypress/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "lib": ["es6", "dom"],
+    "types": ["cypress", "node"],
+    "esModuleInterop": true,
+    "moduleResolution": "node"
+  },
+  "include": ["**/*.ts"]
+}

+ 2 - 0
package.json

@@ -16,6 +16,7 @@
     "eslint": "npx eslint \"src/**\" \"server/**\"",
     "format": "prettier --check src/**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
     "test": "jest",
+    "e2e": "cypress run",
     "test-release": "node ./tests/testRelease",
     "test-coverage": "node ./tests/testCoverage",
     "storybook": "start-storybook"
@@ -43,6 +44,7 @@
     "@typescript-eslint/eslint-plugin": "^5.36.2",
     "@typescript-eslint/parser": "^5.36.2",
     "cross-spawn": "^7.0.3",
+    "cypress": "13.3.1",
     "eslint": "^8.23.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-coriolis-web": "file:./src/utils/eslint-plugin-coriolis-web",

+ 0 - 8
private/cypress/.eslintrc

@@ -1,8 +0,0 @@
-{
-  "env": {
-    "cypress/globals": true
-  },
-  "plugins": [
-    "cypress"
-  ]
-}

+ 0 - 69
private/cypress/config.template.js

@@ -1,69 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-export default {
-  nodeServer: 'http://localhost:3000/',
-  coriolisUrl: '',
-  username: 'cypress',
-  password: 'cypress',
-  endpoints: {
-    azure: {
-      username: '',
-      password: '',
-      subscriptionId: '',
-    },
-    vmware: {
-      username: '',
-      password: '',
-      host: '',
-    },
-    openstack: {
-      userDomainName: '',
-      authUrl: '',
-      projectName: '',
-      projectDomainName: '',
-      password: '',
-      username: '',
-      glanceApiVersion: 2,
-      identityVersion: 3,
-      allowUntrusted: true,
-      allowUntrustedSwift: true,
-    },
-    oci: {
-      privateKeyData: '',
-      region: '',
-      tenancy: '',
-      user: '',
-      privateKeyPassphrase: '',
-    },
-  },
-  wizard: {
-    azure: {
-      resourceGroup: { label: 'Coriolis', value: 'coriolis' },
-    },
-    oci: {
-      compartment: '',
-      migrSubnetId: '',
-      availabilityDomain: '',
-    },
-    instancesSearch: {
-      vmwareSearchText: '',
-      vmwareItemIndex: 0,
-      openstackSearchText: '',
-      openstackItemIndex: 0,
-    },
-  },
-}

+ 0 - 1499
private/cypress/example_spec.js

@@ -1,1499 +0,0 @@
-//
-// **** Kitchen Sink Tests ****
-//
-// This app was developed to demonstrate
-// how to write tests in Cypress utilizing
-// all of the available commands
-//
-// Feel free to modify this spec in your
-// own application as a jumping off point
-
-// Please read our "Introduction to Cypress"
-// https://on.cypress.io/introduction-to-cypress
-
-/* eslint-disable */
-
-describe('Kitchen Sink', function () {
-  it('.should() - assert that <title> is correct', function () {
-    // https://on.cypress.io/visit
-    cy.visit('https://example.cypress.io')
-
-    // Here we've made our first assertion using a '.should()' command.
-    // An assertion is comprised of a chainer, subject, and optional value.
-
-    // https://on.cypress.io/should
-    // https://on.cypress.io/and
-
-    // https://on.cypress.io/title
-    cy.title().should('include', 'Kitchen Sink')
-    //   ↲               ↲            ↲
-    // subject        chainer      value
-  })
-
-  context('Querying', function () {
-    beforeEach(function () {
-      // Visiting our app before each test removes any state build up from
-      // previous tests. Visiting acts as if we closed a tab and opened a fresh one
-      cy.visit('https://example.cypress.io/commands/querying')
-    })
-
-    // Let's query for some DOM elements and make assertions
-    // The most commonly used query is 'cy.get()', you can
-    // think of this like the '$' in jQuery
-
-    it('cy.get() - query DOM elements', function () {
-      // https://on.cypress.io/get
-
-      // Get DOM elements by id
-      cy.get('#query-btn').should('contain', 'Button')
-
-      // Get DOM elements by class
-      cy.get('.query-btn').should('contain', 'Button')
-
-      cy.get('#querying .well>button:first').should('contain', 'Button')
-      //              ↲
-      // Use CSS selectors just like jQuery
-    })
-
-    it('cy.contains() - query DOM elements with matching content', function () {
-      // https://on.cypress.io/contains
-      cy.get('.query-list')
-        .contains('bananas').should('have.class', 'third')
-
-      // we can pass a regexp to `.contains()`
-      cy.get('.query-list')
-        .contains(/^b\w+/).should('have.class', 'third')
-
-      cy.get('.query-list')
-        .contains('apples').should('have.class', 'first')
-
-      // passing a selector to contains will yield the selector containing the text
-      cy.get('#querying')
-        .contains('ul', 'oranges').should('have.class', 'query-list')
-
-      // `.contains()` will favor input[type='submit'],
-      // button, a, and label over deeper elements inside them
-      // this will not yield the <span> inside the button,
-      // but the <button> itself
-      cy.get('.query-button')
-        .contains('Save Form').should('have.class', 'btn')
-    })
-
-    it('.within() - query DOM elements within a specific element', function () {
-      // https://on.cypress.io/within
-      cy.get('.query-form').within(function () {
-        cy.get('input:first').should('have.attr', 'placeholder', 'Email')
-        cy.get('input:last').should('have.attr', 'placeholder', 'Password')
-      })
-    })
-
-    it('cy.root() - query the root DOM element', function () {
-      // https://on.cypress.io/root
-      // By default, root is the document
-      cy.root().should('match', 'html')
-
-      cy.get('.query-ul').within(function () {
-        // In this within, the root is now the ul DOM element
-        cy.root().should('have.class', 'query-ul')
-      })
-    })
-  })
-
-  context('Traversal', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/traversal')
-    })
-
-    // Let's query for some DOM elements and make assertions
-
-    it('.children() - get child DOM elements', function () {
-      // https://on.cypress.io/children
-      cy.get('.traversal-breadcrumb').children('.active')
-        .should('contain', 'Data')
-    })
-
-    it('.closest() - get closest ancestor DOM element', function () {
-      // https://on.cypress.io/closest
-      cy.get('.traversal-badge').closest('ul')
-        .should('have.class', 'list-group')
-    })
-
-    it('.eq() - get a DOM element at a specific index', function () {
-      // https://on.cypress.io/eq
-      cy.get('.traversal-list>li').eq(1).should('contain', 'siamese')
-    })
-
-    it('.filter() - get DOM elements that match the selector', function () {
-      // https://on.cypress.io/filter
-      cy.get('.traversal-nav>li').filter('.active').should('contain', 'About')
-    })
-
-    it('.find() - get descendant DOM elements of the selector', function () {
-      // https://on.cypress.io/find
-      cy.get('.traversal-pagination').find('li').find('a')
-        .should('have.length', 7)
-    })
-
-    it('.first() - get first DOM element', function () {
-      // https://on.cypress.io/first
-      cy.get('.traversal-table td').first().should('contain', '1')
-    })
-
-    it('.last() - get last DOM element', function () {
-      // https://on.cypress.io/last
-      cy.get('.traversal-buttons .btn').last().should('contain', 'Submit')
-    })
-
-    it('.next() - get next sibling DOM element', function () {
-      // https://on.cypress.io/next
-      cy.get('.traversal-ul').contains('apples').next().should('contain', 'oranges')
-    })
-
-    it('.nextAll() - get all next sibling DOM elements', function () {
-      // https://on.cypress.io/nextall
-      cy.get('.traversal-next-all').contains('oranges')
-        .nextAll().should('have.length', 3)
-    })
-
-    it('.nextUntil() - get next sibling DOM elements until next el', function () {
-      // https://on.cypress.io/nextuntil
-      cy.get('#veggies').nextUntil('#nuts').should('have.length', 3)
-    })
-
-    it('.not() - remove DOM elements from set of DOM elements', function () {
-      // https://on.cypress.io/not
-      cy.get('.traversal-disabled .btn').not('[disabled]').should('not.contain', 'Disabled')
-    })
-
-    it('.parent() - get parent DOM element from DOM elements', function () {
-      // https://on.cypress.io/parent
-      cy.get('.traversal-mark').parent().should('contain', 'Morbi leo risus')
-    })
-
-    it('.parents() - get parent DOM elements from DOM elements', function () {
-      // https://on.cypress.io/parents
-      cy.get('.traversal-cite').parents().should('match', 'blockquote')
-    })
-
-    it('.parentsUntil() - get parent DOM elements from DOM elements until el', function () {
-      // https://on.cypress.io/parentsuntil
-      cy.get('.clothes-nav').find('.active').parentsUntil('.clothes-nav')
-        .should('have.length', 2)
-    })
-
-    it('.prev() - get previous sibling DOM element', function () {
-      // https://on.cypress.io/prev
-      cy.get('.birds').find('.active').prev().should('contain', 'Lorikeets')
-    })
-
-    it('.prevAll() - get all previous sibling DOM elements', function () {
-      // https://on.cypress.io/prevAll
-      cy.get('.fruits-list').find('.third').prevAll().should('have.length', 2)
-    })
-
-    it('.prevUntil() - get all previous sibling DOM elements until el', function () {
-      // https://on.cypress.io/prevUntil
-      cy.get('.foods-list').find('#nuts').prevUntil('#veggies')
-    })
-
-    it('.siblings() - get all sibling DOM elements', function () {
-      // https://on.cypress.io/siblings
-      cy.get('.traversal-pills .active').siblings().should('have.length', 2)
-    })
-  })
-
-  context('Actions', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/actions')
-    })
-
-    // Let's perform some actions on DOM elements
-    // https://on.cypress.io/interacting-with-elements
-
-    it('.type() - type into a DOM element', function () {
-      // https://on.cypress.io/type
-      cy.get('.action-email')
-        .type('fake@email.com').should('have.value', 'fake@email.com')
-
-        // .type() with special character sequences
-        .type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
-        .type('{del}{selectall}{backspace}')
-
-        // .type() with key modifiers
-        .type('{alt}{option}') //these are equivalent
-        .type('{ctrl}{control}') //these are equivalent
-        .type('{meta}{command}{cmd}') //these are equivalent
-        .type('{shift}')
-
-        // Delay each keypress by 0.1 sec
-        .type('slow.typing@email.com', { delay: 100 })
-        .should('have.value', 'slow.typing@email.com')
-
-      cy.get('.action-disabled')
-        // Ignore error checking prior to type
-        // like whether the input is visible or disabled
-        .type('disabled error checking', { force: true })
-        .should('have.value', 'disabled error checking')
-    })
-
-    it('.focus() - focus on a DOM element', function () {
-      // https://on.cypress.io/focus
-      cy.get('.action-focus').focus()
-        .should('have.class', 'focus')
-        .prev().should('have.attr', 'style', 'color: orange;')
-    })
-
-    it('.blur() - blur off a DOM element', function () {
-      // https://on.cypress.io/blur
-      cy.get('.action-blur').type('I\'m about to blur').blur()
-        .should('have.class', 'error')
-        .prev().should('have.attr', 'style', 'color: red;')
-    })
-
-    it('.clear() - clears an input or textarea element', function () {
-      // https://on.cypress.io/clear
-      cy.get('.action-clear').type('We are going to clear this text')
-        .should('have.value', 'We are going to clear this text')
-        .clear()
-        .should('have.value', '')
-    })
-
-    it('.submit() - submit a form', function () {
-      // https://on.cypress.io/submit
-      cy.get('.action-form')
-        .find('[type="text"]').type('HALFOFF')
-      cy.get('.action-form').submit()
-        .next().should('contain', 'Your form has been submitted!')
-    })
-
-    it('.click() - click on a DOM element', function () {
-      // https://on.cypress.io/click
-      cy.get('.action-btn').click()
-
-      // You can click on 9 specific positions of an element:
-      //  -----------------------------------
-      // | topLeft        top       topRight |
-      // |                                   |
-      // |                                   |
-      // |                                   |
-      // | left          center        right |
-      // |                                   |
-      // |                                   |
-      // |                                   |
-      // | bottomLeft   bottom   bottomRight |
-      //  -----------------------------------
-
-      // clicking in the center of the element is the default
-      cy.get('#action-canvas').click()
-
-      cy.get('#action-canvas').click('topLeft')
-      cy.get('#action-canvas').click('top')
-      cy.get('#action-canvas').click('topRight')
-      cy.get('#action-canvas').click('left')
-      cy.get('#action-canvas').click('right')
-      cy.get('#action-canvas').click('bottomLeft')
-      cy.get('#action-canvas').click('bottom')
-      cy.get('#action-canvas').click('bottomRight')
-
-      // .click() accepts an x and y coordinate
-      // that controls where the click occurs :)
-
-      cy.get('#action-canvas')
-        .click(80, 75) // click 80px on x coord and 75px on y coord
-        .click(170, 75)
-        .click(80, 165)
-        .click(100, 185)
-        .click(125, 190)
-        .click(150, 185)
-        .click(170, 165)
-
-      // click multiple elements by passing multiple: true
-      cy.get('.action-labels>.label').click({ multiple: true })
-
-      // Ignore error checking prior to clicking
-      // like whether the element is visible, clickable or disabled
-      // this button below is covered by another element.
-      cy.get('.action-opacity>.btn').click({ force: true })
-    })
-
-    it('.dblclick() - double click on a DOM element', function () {
-      // Our app has a listener on 'dblclick' event in our 'scripts.js'
-      // that hides the div and shows an input on double click
-
-      // https://on.cypress.io/dblclick
-      cy.get('.action-div').dblclick().should('not.be.visible')
-      cy.get('.action-input-hidden').should('be.visible')
-    })
-
-    it('cy.check() - check a checkbox or radio element', function () {
-      // By default, .check() will check all
-      // matching checkbox or radio elements in succession, one after another
-
-      // https://on.cypress.io/check
-      cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
-        .check().should('be.checked')
-
-      cy.get('.action-radios [type="radio"]').not('[disabled]')
-        .check().should('be.checked')
-
-      // .check() accepts a value argument
-      // that checks only checkboxes or radios
-      // with matching values
-      cy.get('.action-radios [type="radio"]').check('radio1').should('be.checked')
-
-      // .check() accepts an array of values
-      // that checks only checkboxes or radios
-      // with matching values
-      cy.get('.action-multiple-checkboxes [type="checkbox"]')
-        .check(['checkbox1', 'checkbox2']).should('be.checked')
-
-      // Ignore error checking prior to checking
-      // like whether the element is visible, clickable or disabled
-      // this checkbox below is disabled.
-      cy.get('.action-checkboxes [disabled]')
-        .check({ force: true }).should('be.checked')
-
-      cy.get('.action-radios [type="radio"]')
-        .check('radio3', { force: true }).should('be.checked')
-    })
-
-    it('.uncheck() - uncheck a checkbox element', function () {
-      // By default, .uncheck() will uncheck all matching
-      // checkbox elements in succession, one after another
-
-      // https://on.cypress.io/uncheck
-      cy.get('.action-check [type="checkbox"]')
-        .not('[disabled]')
-        .uncheck().should('not.be.checked')
-
-      // .uncheck() accepts a value argument
-      // that unchecks only checkboxes
-      // with matching values
-      cy.get('.action-check [type="checkbox"]')
-        .check('checkbox1')
-        .uncheck('checkbox1').should('not.be.checked')
-
-      // .uncheck() accepts an array of values
-      // that unchecks only checkboxes or radios
-      // with matching values
-      cy.get('.action-check [type="checkbox"]')
-        .check(['checkbox1', 'checkbox3'])
-        .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
-
-      // Ignore error checking prior to unchecking
-      // like whether the element is visible, clickable or disabled
-      // this checkbox below is disabled.
-      cy.get('.action-check [disabled]')
-        .uncheck({ force: true }).should('not.be.checked')
-    })
-
-    it('.select() - select an option in a <select> element', function () {
-      // https://on.cypress.io/select
-
-      // Select option with matching text content
-      cy.get('.action-select').select('apples')
-
-      // Select option with matching value
-      cy.get('.action-select').select('fr-bananas')
-
-      // Select options with matching text content
-      cy.get('.action-select-multiple')
-        .select(['apples', 'oranges', 'bananas'])
-
-      // Select options with matching values
-      cy.get('.action-select-multiple')
-        .select(['fr-apples', 'fr-oranges', 'fr-bananas'])
-    })
-
-    it('.scrollIntoView() - scroll an element into view', function () {
-      // https://on.cypress.io/scrollintoview
-
-      // normally all of these buttons are hidden, because they're not within
-      // the viewable area of their parent (we need to scroll to see them)
-      cy.get('#scroll-horizontal button')
-        .should('not.be.visible')
-
-      // scroll the button into view, as if the user had scrolled
-      cy.get('#scroll-horizontal button').scrollIntoView()
-        .should('be.visible')
-
-      cy.get('#scroll-vertical button')
-        .should('not.be.visible')
-
-      // Cypress handles the scroll direction needed
-      cy.get('#scroll-vertical button').scrollIntoView()
-        .should('be.visible')
-
-      cy.get('#scroll-both button')
-        .should('not.be.visible')
-
-      // Cypress knows to scroll to the right and down
-      cy.get('#scroll-both button').scrollIntoView()
-        .should('be.visible')
-    })
-
-    it('cy.scrollTo() - scroll the window or element to a position', function () {
-
-      // https://on.cypress.io/scrollTo
-
-      // You can scroll to 9 specific positions of an element:
-      //  -----------------------------------
-      // | topLeft        top       topRight |
-      // |                                   |
-      // |                                   |
-      // |                                   |
-      // | left          center        right |
-      // |                                   |
-      // |                                   |
-      // |                                   |
-      // | bottomLeft   bottom   bottomRight |
-      //  -----------------------------------
-
-      // if you chain .scrollTo() off of cy, we will
-      // scroll the entire window
-      cy.scrollTo('bottom')
-
-      cy.get('#scrollable-horizontal').scrollTo('right')
-
-      // or you can scroll to a specific coordinate:
-      // (x axis, y axis) in pixels
-      cy.get('#scrollable-vertical').scrollTo(250, 250)
-
-      // or you can scroll to a specific percentage
-      // of the (width, height) of the element
-      cy.get('#scrollable-both').scrollTo('75%', '25%')
-
-      // control the easing of the scroll (default is 'swing')
-      cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
-
-      // control the duration of the scroll (in ms)
-      cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
-    })
-
-    it('.trigger() - trigger an event on a DOM element', function () {
-      // To interact with a range input (slider), we need to set its value and
-      // then trigger the appropriate event to signal it has changed
-
-      // Here, we invoke jQuery's val() method to set the value
-      // and trigger the 'change' event
-
-      // Note that some implementations may rely on the 'input' event,
-      // which is fired as a user moves the slider, but is not supported
-      // by some browsers
-
-      // https://on.cypress.io/trigger
-      cy.get('.trigger-input-range')
-        .invoke('val', 25)
-        .trigger('change')
-        .get('input[type=range]').siblings('p')
-        .should('have.text', '25')
-
-      // See our example recipes for more examples of using trigger
-      // https://on.cypress.io/examples
-    })
-  })
-
-  context('Window', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/window')
-    })
-
-    it('cy.window() - get the global window object', function () {
-      // https://on.cypress.io/window
-      cy.window().should('have.property', 'top')
-    })
-
-    it('cy.document() - get the document object', function () {
-      // https://on.cypress.io/document
-      cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
-    })
-
-    it('cy.title() - get the title', function () {
-      // https://on.cypress.io/title
-      cy.title().should('include', 'Kitchen Sink')
-    })
-  })
-
-  context('Viewport', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/viewport')
-    })
-
-    it('cy.viewport() - set the viewport size and dimension', function () {
-
-      cy.get('#navbar').should('be.visible')
-
-      // https://on.cypress.io/viewport
-      cy.viewport(320, 480)
-
-      // the navbar should have collapse since our screen is smaller
-      cy.get('#navbar').should('not.be.visible')
-      cy.get('.navbar-toggle').should('be.visible').click()
-      cy.get('.nav').find('a').should('be.visible')
-
-      // lets see what our app looks like on a super large screen
-      cy.viewport(2999, 2999)
-
-      // cy.viewport() accepts a set of preset sizes
-      // to easily set the screen to a device's width and height
-
-      // We added a cy.wait() between each viewport change so you can see
-      // the change otherwise it's a little too fast to see :)
-
-      cy.viewport('macbook-15')
-      cy.wait(200)
-      cy.viewport('macbook-13')
-      cy.wait(200)
-      cy.viewport('macbook-11')
-      cy.wait(200)
-      cy.viewport('ipad-2')
-      cy.wait(200)
-      cy.viewport('ipad-mini')
-      cy.wait(200)
-      cy.viewport('iphone-6+')
-      cy.wait(200)
-      cy.viewport('iphone-6')
-      cy.wait(200)
-      cy.viewport('iphone-5')
-      cy.wait(200)
-      cy.viewport('iphone-4')
-      cy.wait(200)
-      cy.viewport('iphone-3')
-      cy.wait(200)
-
-      // cy.viewport() accepts an orientation for all presets
-      // the default orientation is 'portrait'
-      cy.viewport('ipad-2', 'portrait')
-      cy.wait(200)
-      cy.viewport('iphone-4', 'landscape')
-      cy.wait(200)
-
-      // The viewport will be reset back to the default dimensions
-      // in between tests (the  default is set in cypress.json)
-    })
-  })
-
-  context('Location', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/location')
-    })
-
-    // We look at the url to make assertions
-    // about the page's state
-
-    it('cy.hash() - get the current URL hash', function () {
-      // https://on.cypress.io/hash
-      cy.hash().should('be.empty')
-    })
-
-    it('cy.location() - get window.location', function () {
-      // https://on.cypress.io/location
-      cy.location().should(function (location) {
-        expect(location.hash).to.be.empty
-        expect(location.href).to.eq('https://example.cypress.io/commands/location')
-        expect(location.host).to.eq('example.cypress.io')
-        expect(location.hostname).to.eq('example.cypress.io')
-        expect(location.origin).to.eq('https://example.cypress.io')
-        expect(location.pathname).to.eq('/commands/location')
-        expect(location.port).to.eq('')
-        expect(location.protocol).to.eq('https:')
-        expect(location.search).to.be.empty
-      })
-    })
-
-    it('cy.url() - get the current URL', function () {
-      // https://on.cypress.io/url
-      cy.url().should('eq', 'https://example.cypress.io/commands/location')
-    })
-  })
-
-  context('Navigation', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io')
-      cy.get('.navbar-nav').contains('Commands').click()
-      cy.get('.dropdown-menu').contains('Navigation').click()
-    })
-
-    it('cy.go() - go back or forward in the browser\'s history', function () {
-      cy.location('pathname').should('include', 'navigation')
-
-      // https://on.cypress.io/go
-      cy.go('back')
-      cy.location('pathname').should('not.include', 'navigation')
-
-      cy.go('forward')
-      cy.location('pathname').should('include', 'navigation')
-
-      // equivalent to clicking back
-      cy.go(-1)
-      cy.location('pathname').should('not.include', 'navigation')
-
-      // equivalent to clicking forward
-      cy.go(1)
-      cy.location('pathname').should('include', 'navigation')
-    })
-
-    it('cy.reload() - reload the page', function () {
-      // https://on.cypress.io/reload
-      cy.reload()
-
-      // reload the page without using the cache
-      cy.reload(true)
-    })
-
-    it('cy.visit() - visit a remote url', function () {
-      // Visit any sub-domain of your current domain
-      // https://on.cypress.io/visit
-
-      // Pass options to the visit
-      cy.visit('https://example.cypress.io/commands/navigation', {
-        timeout: 50000, // increase total time for the visit to resolve
-        onBeforeLoad (contentWindow) {
-          // contentWindow is the remote page's window object
-        },
-        onLoad (contentWindow) {
-          // contentWindow is the remote page's window object
-        },
-      })
-      })
-  })
-
-  context('Assertions', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/assertions')
-    })
-
-    describe('Implicit Assertions', function () {
-
-      it('.should() - make an assertion about the current subject', function () {
-        // https://on.cypress.io/should
-        cy.get('.assertion-table')
-          .find('tbody tr:last').should('have.class', 'success')
-      })
-
-      it('.and() - chain multiple assertions together', function () {
-        // https://on.cypress.io/and
-        cy.get('.assertions-link')
-          .should('have.class', 'active')
-          .and('have.attr', 'href')
-          .and('include', 'cypress.io')
-      })
-    })
-
-    describe('Explicit Assertions', function () {
-      // https://on.cypress.io/assertions
-      it('expect - assert shape of an object', function () {
-        const person = {
-          name: 'Joe',
-          age: 20,
-        }
-        expect(person).to.have.all.keys('name', 'age')
-      })
-
-      it('expect - make an assertion about a specified subject', function () {
-        // We can use Chai's BDD style assertions
-        expect(true).to.be.true
-
-        // Pass a function to should that can have any number
-        // of explicit assertions within it.
-        cy.get('.assertions-p').find('p')
-        .should(function ($p) {
-          // return an array of texts from all of the p's
-          let texts = $p.map(function (i, el) {
-            // https://on.cypress.io/$
-            return Cypress.$(el).text()
-          })
-
-          // jquery map returns jquery object
-          // and .get() convert this to simple array
-          texts = texts.get()
-
-          // array should have length of 3
-          expect(texts).to.have.length(3)
-
-          // set this specific subject
-          expect(texts).to.deep.eq([
-            'Some text from first p',
-            'More text from second p',
-            'And even more text from third p',
-          ])
-        })
-      })
-    })
-  })
-
-  context('Misc', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/misc')
-    })
-
-    it('.end() - end the command chain', function () {
-      // cy.end is useful when you want to end a chain of commands
-      // and force Cypress to re-query from the root element
-
-      // https://on.cypress.io/end
-      cy.get('.misc-table').within(function () {
-        // ends the current chain and yields null
-        cy.contains('Cheryl').click().end()
-
-        // queries the entire table again
-        cy.contains('Charles').click()
-      })
-    })
-
-    it('cy.exec() - execute a system command', function () {
-      // cy.exec allows you to execute a system command.
-      // so you can take actions necessary for your test,
-      // but outside the scope of Cypress.
-
-      // https://on.cypress.io/exec
-      cy.exec('echo Jane Lane')
-        .its('stdout').should('contain', 'Jane Lane')
-
-      // we can use Cypress.platform string to
-      // select appropriate command
-      // https://on.cypress/io/platform
-      cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
-
-      if (Cypress.platform === 'win32') {
-        cy.exec('print cypress.json')
-          .its('stderr').should('be.empty')
-      } else {
-        cy.exec('cat cypress.json')
-          .its('stderr').should('be.empty')
-
-        cy.exec('pwd')
-          .its('code').should('eq', 0)
-      }
-    })
-
-    it('cy.focused() - get the DOM element that has focus', function () {
-      // https://on.cypress.io/focused
-      cy.get('.misc-form').find('#name').click()
-      cy.focused().should('have.id', 'name')
-
-      cy.get('.misc-form').find('#description').click()
-      cy.focused().should('have.id', 'description')
-    })
-
-    it('cy.screenshot() - take a screenshot', function () {
-      // https://on.cypress.io/screenshot
-      cy.screenshot('my-image')
-    })
-
-    it('cy.wrap() - wrap an object', function () {
-      // https://on.cypress.io/wrap
-      cy.wrap({ foo: 'bar' })
-        .should('have.property', 'foo')
-        .and('include', 'bar')
-    })
-  })
-
-  context('Connectors', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/connectors')
-    })
-
-    it('.each() - iterate over an array of elements', function () {
-      // https://on.cypress.io/each
-      cy.get('.connectors-each-ul>li')
-        .each(function ($el, index, $list) {
-          console.log($el, index, $list)
-        })
-    })
-
-    it('.its() - get properties on the current subject', function () {
-      // https://on.cypress.io/its
-      cy.get('.connectors-its-ul>li')
-        // calls the 'length' property yielding that value
-        .its('length')
-        .should('be.gt', 2)
-    })
-
-    it('.invoke() - invoke a function on the current subject', function () {
-      // our div is hidden in our script.js
-      // $('.connectors-div').hide()
-
-      // https://on.cypress.io/invoke
-      cy.get('.connectors-div').should('be.hidden')
-
-        // call the jquery method 'show' on the 'div.container'
-        .invoke('show')
-        .should('be.visible')
-    })
-
-    it('.spread() - spread an array as individual args to callback function', function () {
-      // https://on.cypress.io/spread
-      let arr = ['foo', 'bar', 'baz']
-
-      cy.wrap(arr).spread(function (foo, bar, baz) {
-        expect(foo).to.eq('foo')
-        expect(bar).to.eq('bar')
-        expect(baz).to.eq('baz')
-      })
-    })
-
-    it('.then() - invoke a callback function with the current subject', function () {
-      // https://on.cypress.io/then
-      cy.get('.connectors-list>li').then(function ($lis) {
-        expect($lis).to.have.length(3)
-        expect($lis.eq(0)).to.contain('Walk the dog')
-        expect($lis.eq(1)).to.contain('Feed the cat')
-        expect($lis.eq(2)).to.contain('Write JavaScript')
-      })
-    })
-  })
-
-  context('Aliasing', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/aliasing')
-    })
-
-    // We alias a DOM element for use later
-    // We don't have to traverse to the element
-    // later in our code, we just reference it with @
-
-    it('.as() - alias a route or DOM element for later use', function () {
-      // this is a good use case for an alias,
-      // we don't want to write this long traversal again
-
-      // https://on.cypress.io/as
-      cy.get('.as-table').find('tbody>tr')
-        .first().find('td').first().find('button').as('firstBtn')
-
-      // maybe do some more testing here...
-
-      // when we reference the alias, we place an
-      // @ in front of it's name
-      cy.get('@firstBtn').click()
-
-      cy.get('@firstBtn')
-        .should('have.class', 'btn-success')
-        .and('contain', 'Changed')
-    })
-  })
-
-  context('Waiting', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/waiting')
-    })
-    // BE CAREFUL of adding unnecessary wait times.
-
-    // https://on.cypress.io/wait
-    it('cy.wait() - wait for a specific amount of time', function () {
-      cy.get('.wait-input1').type('Wait 1000ms after typing')
-      cy.wait(1000)
-      cy.get('.wait-input2').type('Wait 1000ms after typing')
-      cy.wait(1000)
-      cy.get('.wait-input3').type('Wait 1000ms after typing')
-      cy.wait(1000)
-    })
-
-    // Waiting for a specific resource to resolve
-    // is covered within the cy.route() test below
-  })
-
-  context('Network Requests', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/network-requests')
-    })
-
-    // Manage AJAX / XHR requests in your app
-
-    it('cy.server() - control behavior of network requests and responses', function () {
-      // https://on.cypress.io/server
-      cy.server().should(function (server) {
-        // the default options on server
-        // you can override any of these options
-        expect(server.delay).to.eq(0)
-        expect(server.method).to.eq('GET')
-        expect(server.status).to.eq(200)
-        expect(server.headers).to.be.null
-        expect(server.response).to.be.null
-        expect(server.onRequest).to.be.undefined
-        expect(server.onResponse).to.be.undefined
-        expect(server.onAbort).to.be.undefined
-
-        // These options control the server behavior
-        // affecting all requests
-
-        // pass false to disable existing route stubs
-        expect(server.enable).to.be.true
-        // forces requests that don't match your routes to 404
-        expect(server.force404).to.be.false
-        // whitelists requests from ever being logged or stubbed
-        expect(server.whitelist).to.be.a('function')
-      })
-
-      cy.server({
-        method: 'POST',
-        delay: 1000,
-        status: 422,
-        response: {},
-      })
-
-      // any route commands will now inherit the above options
-      // from the server. anything we pass specifically
-      // to route will override the defaults though.
-    })
-
-    it('cy.request() - make an XHR request', function () {
-      // https://on.cypress.io/request
-      cy.request('https://jsonplaceholder.typicode.com/comments')
-        .should(function (response) {
-          expect(response.status).to.eq(200)
-          expect(response.body).to.have.length(500)
-          expect(response).to.have.property('headers')
-          expect(response).to.have.property('duration')
-        })
-    })
-
-    it('cy.route() - route responses to matching requests', function () {
-      let message = 'whoa, this comment doesn\'t exist'
-      cy.server()
-
-      // **** GET comments route ****
-
-      // https://on.cypress.io/route
-      cy.route(/comments\/1/).as('getComment')
-
-      // we have code that fetches a comment when
-      // the button is clicked in scripts.js
-      cy.get('.network-btn').click()
-
-      // **** Wait ****
-
-      // Wait for a specific resource to resolve
-      // continuing to the next command
-
-      // https://on.cypress.io/wait
-      cy.wait('@getComment').its('status').should('eq', 200)
-
-      // **** POST comment route ****
-
-      // Specify the route to listen to method 'POST'
-      cy.route('POST', '/comments').as('postComment')
-
-      // we have code that posts a comment when
-      // the button is clicked in scripts.js
-      cy.get('.network-post').click()
-      cy.wait('@postComment')
-
-      // get the route
-      cy.get('@postComment').then(function (xhr) {
-        expect(xhr.requestBody).to.include('email')
-        expect(xhr.requestHeaders).to.have.property('Content-Type')
-        expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()')
-      })
-
-      // **** Stubbed PUT comment route ****
-      cy.route({
-        method: 'PUT',
-        url: /comments\/\d+/,
-        status: 404,
-        response: { error: message },
-        delay: 500,
-      }).as('putComment')
-
-      // we have code that puts a comment when
-      // the button is clicked in scripts.js
-      cy.get('.network-put').click()
-
-      cy.wait('@putComment')
-
-      // our 404 statusCode logic in scripts.js executed
-      cy.get('.network-put-comment').should('contain', message)
-    })
-  })
-
-  context('Files', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/files')
-    })
-    it('cy.fixture() - load a fixture', function () {
-      // Instead of writing a response inline you can
-      // connect a response with a fixture file
-      // located in fixtures folder.
-
-      cy.server()
-
-      // https://on.cypress.io/fixture
-      cy.fixture('example.json').as('comment')
-
-      cy.route(/comments/, '@comment').as('getComment')
-
-      // we have code that gets a comment when
-      // the button is clicked in scripts.js
-      cy.get('.fixture-btn').click()
-
-      cy.wait('@getComment').its('responseBody')
-        .should('have.property', 'name')
-        .and('include', 'Using fixtures to represent data')
-
-      // you can also just write the fixture in the route
-      cy.route(/comments/, 'fixture:example.json').as('getComment')
-
-      // we have code that gets a comment when
-      // the button is clicked in scripts.js
-      cy.get('.fixture-btn').click()
-
-      cy.wait('@getComment').its('responseBody')
-        .should('have.property', 'name')
-        .and('include', 'Using fixtures to represent data')
-
-      // or write fx to represent fixture
-      // by default it assumes it's .json
-      cy.route(/comments/, 'fx:example').as('getComment')
-
-      // we have code that gets a comment when
-      // the button is clicked in scripts.js
-      cy.get('.fixture-btn').click()
-
-      cy.wait('@getComment').its('responseBody')
-        .should('have.property', 'name')
-        .and('include', 'Using fixtures to represent data')
-    })
-
-    it('cy.readFile() - read a files contents', function () {
-      // You can read a file and yield its contents
-      // The filePath is relative to your project's root.
-
-      // https://on.cypress.io/readfile
-      cy.readFile('cypress.json').then(function (json) {
-        expect(json).to.be.an('object')
-      })
-
-    })
-
-    it('cy.writeFile() - write to a file', function () {
-      // You can write to a file with the specified contents
-
-      // Use a response from a request to automatically
-      // generate a fixture file for use later
-      cy.request('https://jsonplaceholder.typicode.com/users')
-        .then(function (response) {
-          // https://on.cypress.io/writefile
-          cy.writeFile('cypress/fixtures/users.json', response.body)
-        })
-      cy.fixture('users').should(function (users) {
-        expect(users[0].name).to.exist
-      })
-
-      // JavaScript arrays and objects are stringified and formatted into text.
-      cy.writeFile('cypress/fixtures/profile.json', {
-        id: 8739,
-        name: 'Jane',
-        email: 'jane@example.com',
-      })
-
-      cy.fixture('profile').should(function (profile) {
-        expect(profile.name).to.eq('Jane')
-      })
-    })
-  })
-
-  context('Local Storage', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/commands/local-storage')
-    })
-    // Although local storage is automatically cleared
-    // to maintain a clean state in between tests
-    // sometimes we need to clear the local storage manually
-
-    it('cy.clearLocalStorage() - clear all data in local storage', function () {
-      // https://on.cypress.io/clearlocalstorage
-      cy.get('.ls-btn').click().should(function () {
-        expect(localStorage.getItem('prop1')).to.eq('red')
-        expect(localStorage.getItem('prop2')).to.eq('blue')
-        expect(localStorage.getItem('prop3')).to.eq('magenta')
-      })
-
-      // clearLocalStorage() yields the localStorage object
-      cy.clearLocalStorage().should(function (ls) {
-        expect(ls.getItem('prop1')).to.be.null
-        expect(ls.getItem('prop2')).to.be.null
-        expect(ls.getItem('prop3')).to.be.null
-      })
-
-      // **** Clear key matching string in Local Storage ****
-      cy.get('.ls-btn').click().should(function () {
-        expect(localStorage.getItem('prop1')).to.eq('red')
-        expect(localStorage.getItem('prop2')).to.eq('blue')
-        expect(localStorage.getItem('prop3')).to.eq('magenta')
-      })
-
-      cy.clearLocalStorage('prop1').should(function (ls) {
-        expect(ls.getItem('prop1')).to.be.null
-        expect(ls.getItem('prop2')).to.eq('blue')
-        expect(ls.getItem('prop3')).to.eq('magenta')
-      })
-
-      // **** Clear key's matching regex in Local Storage ****
-      cy.get('.ls-btn').click().should(function () {
-        expect(localStorage.getItem('prop1')).to.eq('red')
-        expect(localStorage.getItem('prop2')).to.eq('blue')
-        expect(localStorage.getItem('prop3')).to.eq('magenta')
-      })
-
-      cy.clearLocalStorage(/prop1|2/).should(function (ls) {
-        expect(ls.getItem('prop1')).to.be.null
-        expect(ls.getItem('prop2')).to.be.null
-        expect(ls.getItem('prop3')).to.eq('magenta')
-      })
-    })
-  })
-
-  context('Cookies', function () {
-    beforeEach(function () {
-      Cypress.Cookies.debug(true)
-
-      cy.visit('https://example.cypress.io/commands/cookies')
-
-      // clear cookies again after visiting to remove
-      // any 3rd party cookies picked up such as cloudflare
-      cy.clearCookies()
-    })
-
-    it('cy.getCookie() - get a browser cookie', function () {
-      // https://on.cypress.io/getcookie
-      cy.get('#getCookie .set-a-cookie').click()
-
-      // cy.getCookie() yields a cookie object
-      cy.getCookie('token').should('have.property', 'value', '123ABC')
-    })
-
-    it('cy.getCookies() - get browser cookies', function () {
-      // https://on.cypress.io/getcookies
-      cy.getCookies().should('be.empty')
-
-      cy.get('#getCookies .set-a-cookie').click()
-
-      // cy.getCookies() yields an array of cookies
-      cy.getCookies().should('have.length', 1).should(function (cookies) {
-
-        // each cookie has these properties
-        expect(cookies[0]).to.have.property('name', 'token')
-        expect(cookies[0]).to.have.property('value', '123ABC')
-        expect(cookies[0]).to.have.property('httpOnly', false)
-        expect(cookies[0]).to.have.property('secure', false)
-        expect(cookies[0]).to.have.property('domain')
-        expect(cookies[0]).to.have.property('path')
-      })
-    })
-
-    it('cy.setCookie() - set a browser cookie', function () {
-      // https://on.cypress.io/setcookie
-      cy.getCookies().should('be.empty')
-
-      cy.setCookie('foo', 'bar')
-
-      // cy.getCookie() yields a cookie object
-      cy.getCookie('foo').should('have.property', 'value', 'bar')
-    })
-
-    it('cy.clearCookie() - clear a browser cookie', function () {
-      // https://on.cypress.io/clearcookie
-      cy.getCookie('token').should('be.null')
-
-      cy.get('#clearCookie .set-a-cookie').click()
-
-      cy.getCookie('token').should('have.property', 'value', '123ABC')
-
-      // cy.clearCookies() yields null
-      cy.clearCookie('token').should('be.null')
-
-      cy.getCookie('token').should('be.null')
-    })
-
-    it('cy.clearCookies() - clear browser cookies', function () {
-      // https://on.cypress.io/clearcookies
-      cy.getCookies().should('be.empty')
-
-      cy.get('#clearCookies .set-a-cookie').click()
-
-      cy.getCookies().should('have.length', 1)
-
-      // cy.clearCookies() yields null
-      cy.clearCookies()
-
-      cy.getCookies().should('be.empty')
-    })
-  })
-
-  context('Spies, Stubs, and Clock', function () {
-    it('cy.spy() - wrap a method in a spy', function () {
-      // https://on.cypress.io/spy
-      cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
-
-      let obj = {
-        foo () {},
-      }
-
-      let spy = cy.spy(obj, 'foo').as('anyArgs')
-
-      obj.foo()
-
-      expect(spy).to.be.called
-
-    })
-
-    it('cy.stub() - create a stub and/or replace a function with a stub', function () {
-      // https://on.cypress.io/stub
-      cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
-
-      let obj = {
-        foo () {},
-      }
-
-      let stub = cy.stub(obj, 'foo').as('foo')
-
-      obj.foo('foo', 'bar')
-
-      expect(stub).to.be.called
-
-    })
-
-    it('cy.clock() - control time in the browser', function () {
-      // create the date in UTC so its always the same
-      // no matter what local timezone the browser is running in
-      let now = new Date(Date.UTC(2017, 2, 14)).getTime()
-
-      // https://on.cypress.io/clock
-      cy.clock(now)
-      cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
-      cy.get('#clock-div').click()
-        .should('have.text', '1489449600')
-    })
-
-    it('cy.tick() - move time in the browser', function () {
-      // create the date in UTC so its always the same
-      // no matter what local timezone the browser is running in
-      let now = new Date(Date.UTC(2017, 2, 14)).getTime()
-
-      // https://on.cypress.io/tick
-      cy.clock(now)
-      cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
-      cy.get('#tick-div').click()
-        .should('have.text', '1489449600')
-      cy.tick(10000) // 10 seconds passed
-      cy.get('#tick-div').click()
-        .should('have.text', '1489449610')
-    })
-  })
-
-  context('Utilities', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/utilities')
-    })
-
-    it('Cypress._.method() - call a lodash method', function () {
-      // use the _.chain, _.map, _.take, and _.value functions
-      // https://on.cypress.io/_
-      cy.request('https://jsonplaceholder.typicode.com/users')
-        .then(function (response) {
-          let ids = Cypress._.chain(response.body).map('id').take(3).value()
-
-          expect(ids).to.deep.eq([1, 2, 3])
-        })
-    })
-
-    it('Cypress.$(selector) - call a jQuery method', function () {
-      // https://on.cypress.io/$
-      let $li = Cypress.$('.utility-jquery li:first')
-
-      cy.wrap($li)
-        .should('not.have.class', 'active')
-        .click()
-        .should('have.class', 'active')
-    })
-
-    it('Cypress.moment() - format or parse dates using a moment method', function () {
-      // use moment's format function
-      // https://on.cypress.io/cypress-moment
-      let time = Cypress.moment().utc('2014-04-25T19:38:53.196Z').format('h:mm A')
-
-      cy.get('.utility-moment').contains('3:38 PM')
-        .should('have.class', 'badge')
-    })
-
-    it('Cypress.Blob.method() - blob utilities and base64 string conversion', function () {
-      cy.get('.utility-blob').then(function ($div) {
-        // https://on.cypress.io/blob
-        // https://github.com/nolanlawson/blob-util#imgSrcToDataURL
-        // get the dataUrl string for the javascript-logo
-        return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
-          .then(function (dataUrl) {
-            // create an <img> element and set its src to the dataUrl
-            let img = Cypress.$('<img />', { src: dataUrl })
-            // need to explicitly return cy here since we are initially returning
-            // the Cypress.Blob.imgSrcToDataURL promise to our test
-            // append the image
-            $div.append(img)
-
-            cy.get('.utility-blob img').click()
-            .should('have.attr', 'src', dataUrl)
-          })
-      })
-    })
-
-    it('new Cypress.Promise(function) - instantiate a bluebird promise', function () {
-      // https://on.cypress.io/promise
-      let waited = false
-
-      function waitOneSecond () {
-        // return a promise that resolves after 1 second
-        return new Cypress.Promise(function (resolve, reject) {
-          setTimeout(function () {
-            // set waited to true
-            waited = true
-
-            // resolve with 'foo' string
-            resolve('foo')
-          }, 1000)
-        })
-      }
-
-      cy.then(function () {
-        // return a promise to cy.then() that
-        // is awaited until it resolves
-        return waitOneSecond().then(function (str) {
-          expect(str).to.eq('foo')
-          expect(waited).to.be.true
-        })
-      })
-    })
-  })
-
-
-  context('Cypress.config()', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/cypress-api/config')
-    })
-
-    it('Cypress.config() - get and set configuration options', function () {
-      // https://on.cypress.io/config
-      let myConfig = Cypress.config()
-
-      expect(myConfig).to.have.property('animationDistanceThreshold', 5)
-      expect(myConfig).to.have.property('baseUrl', null)
-      expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
-      expect(myConfig).to.have.property('requestTimeout', 5000)
-      expect(myConfig).to.have.property('responseTimeout', 30000)
-      expect(myConfig).to.have.property('viewportHeight', 660)
-      expect(myConfig).to.have.property('viewportWidth', 1000)
-      expect(myConfig).to.have.property('pageLoadTimeout', 60000)
-      expect(myConfig).to.have.property('waitForAnimations', true)
-
-      expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
-
-      // this will change the config for the rest of your tests!
-      Cypress.config('pageLoadTimeout', 20000)
-
-      expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
-
-      Cypress.config('pageLoadTimeout', 60000)
-    })
-  })
-
-  context('Cypress.env()', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/cypress-api/env')
-    })
-
-    // We can set environment variables for highly dynamic values
-
-    // https://on.cypress.io/environment-variables
-    it('Cypress.env() - get environment variables', function () {
-      // https://on.cypress.io/env
-      // set multiple environment variables
-      Cypress.env({
-        host: 'veronica.dev.local',
-        api_server: 'http://localhost:8888/v1/',
-      })
-
-      // get environment variable
-      expect(Cypress.env('host')).to.eq('veronica.dev.local')
-
-      // set environment variable
-      Cypress.env('api_server', 'http://localhost:8888/v2/')
-      expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
-
-      // get all environment variable
-      expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
-      expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
-    })
-  })
-
-  context('Cypress.Cookies', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/cypress-api/cookies')
-    })
-
-    // https://on.cypress.io/cookies
-    it('Cypress.Cookies.debug() - enable or disable debugging', function () {
-      Cypress.Cookies.debug(true)
-
-      // Cypress will now log in the console when
-      // cookies are set or cleared
-      cy.setCookie('fakeCookie', '123ABC')
-      cy.clearCookie('fakeCookie')
-      cy.setCookie('fakeCookie', '123ABC')
-      cy.clearCookie('fakeCookie')
-      cy.setCookie('fakeCookie', '123ABC')
-    })
-
-    it('Cypress.Cookies.preserveOnce() - preserve cookies by key', function () {
-      // normally cookies are reset after each test
-      cy.getCookie('fakeCookie').should('not.be.ok')
-
-      // preserving a cookie will not clear it when
-      // the next test starts
-      cy.setCookie('lastCookie', '789XYZ')
-      Cypress.Cookies.preserveOnce('lastCookie')
-    })
-
-    it('Cypress.Cookies.defaults() - set defaults for all cookies', function () {
-      // now any cookie with the name 'session_id' will
-      // not be cleared before each new test runs
-      Cypress.Cookies.defaults({
-        whitelist: 'session_id',
-      })
-    })
-  })
-
-  context('Cypress.dom', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/cypress-api/dom')
-    })
-
-    // https://on.cypress.io/dom
-    it('Cypress.dom.isHidden() - determine if a DOM element is hidden', function () {
-      let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
-      let visibleP = Cypress.$('.dom-p p.visible').get(0)
-
-      // our first paragraph has css class 'hidden'
-      expect(Cypress.dom.isHidden(hiddenP)).to.be.true
-      expect(Cypress.dom.isHidden(visibleP)).to.be.false
-    })
-  })
-
-  context('Cypress.Server', function () {
-    beforeEach(function () {
-      cy.visit('https://example.cypress.io/cypress-api/server')
-    })
-
-    // Permanently override server options for
-    // all instances of cy.server()
-
-    // https://on.cypress.io/cypress-server
-    it('Cypress.Server.defaults() - change default config of server', function () {
-      Cypress.Server.defaults({
-        delay: 0,
-        force404: false,
-        whitelist (xhr) {
-          // handle custom logic for whitelisting
-        },
-      })
-    })
-  })
-})

+ 0 - 32
private/cypress/integration/0 - cleanup/Cleanup.js

@@ -1,32 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Cleaning up Cypress environment', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Loaded the UI', () => {
-    cy.getById('navigation-item-replicas').should('exist')
-    cy.cleanup()
-  })
-
-  it('Successfully cleaned up the Cypress environment', () => { })
-})

+ 0 - 41
private/cypress/integration/1 - login/Invalid Login.js

@@ -1,41 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Coriolis Login Failed', () => {
-  before(() => {
-    cy.logout()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Displays incorrect password', () => {
-    cy.server()
-    cy.route({ url: '**/identity/**', method: 'POST' }).as('login')
-
-    cy.visit(config.nodeServer)
-    cy.get('input[label="Username"]').type('blabla')
-    cy.get('input[label="Password"]').type('blabla')
-
-    cy.get('button').click()
-    cy.wait('@login')
-
-    cy.get('#app').should('contain', 'Incorrect credentials.')
-  })
-})

+ 0 - 52
private/cypress/integration/2 - create endpoints/Create Azure Endpoint.js

@@ -1,52 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Create Azure Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows new Azure endpoint dialog', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Endpoint').click()
-    cy.getById('cProvider-endpointLogo-azure').click()
-  })
-
-  it('Fills Azure connection info', () => {
-    cy.get('input[placeholder="Name"]').type('e2e-azure-test')
-    cy.getById('endpointField-switch-allow_untrusted').click()
-    cy.get('input[placeholder="Username"]').type(config.endpoints.azure.username)
-    cy.get('input[placeholder="Password"]').type(config.endpoints.azure.password)
-    cy.get('input[placeholder="Subscription ID"]').type(config.endpoints.azure.subscriptionId)
-
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.get('button').contains('Validate and save').click()
-    cy.wait('@validate')
-    cy.getById('endpointStatus').should('contain', 'Endpoint is Valid')
-  })
-
-  it('Added Endpoint to endpoint list', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-azure-test').should('contain', 'e2e-azure-test')
-  })
-})

+ 0 - 53
private/cypress/integration/2 - create endpoints/Create OCI Endpoint.js

@@ -1,53 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Create OCI Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows new OCI endpoint dialog', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Endpoint').click()
-    cy.getById('cProvider-endpointLogo-oci').click()
-  })
-
-  it('Fills OCI connection info', () => {
-    cy.getById('endpointField-textInput-name', 'input').type('e2e-oci-test')
-    cy.getById('endpointField-textArea-private_key_data', 'textarea')
-      .type(config.endpoints.oci.privateKeyData, { delay: 0 })
-    cy.getById('endpointField-textInput-region', 'input').type(config.endpoints.oci.region)
-    cy.getById('endpointField-textInput-tenancy', 'input').type(config.endpoints.oci.tenancy)
-    cy.getById('endpointField-textInput-user', 'input').type(config.endpoints.oci.user)
-    cy.getById('endpointField-textInput-private_key_passphrase', 'input').type(config.endpoints.oci.privateKeyPassphrase)
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.get('button').contains('Validate and save').click()
-    cy.wait('@validate')
-    cy.getById('endpointStatus').should('contain', 'Endpoint is Valid')
-  })
-
-  it('Added Endpoint to endpoint list', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-oci-test').should('contain', 'e2e-oci-test')
-  })
-})

+ 0 - 66
private/cypress/integration/2 - create endpoints/Create Openstack Endpoint.js

@@ -1,66 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Create Openstack Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows new Openstack endpoint dialog', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Endpoint').click()
-    cy.getById('cProvider-endpointLogo-openstack').click()
-  })
-
-  it('Fills Openstack connection info', () => {
-    cy.get('div').contains('Advanced').click()
-    cy.get('input[placeholder="Name"]').type('e2e-openstack-test')
-    cy.get('input[placeholder="Username"]').type(config.endpoints.openstack.username)
-    cy.get('input[placeholder="Password"]').type(config.endpoints.openstack.password)
-    cy.get('input[placeholder="Authentication URL"]').type(config.endpoints.openstack.authUrl)
-    cy.get('input[placeholder="Project Name"]').type(config.endpoints.openstack.projectName)
-    cy.getById('endpointField-dropdown-glance_api_version').first().click()
-    cy.getById('dropdownListItem').contains('2').click()
-    cy.getById('endpointField-dropdown-identity_api_version').first().click()
-    cy.getById('dropdownListItem').contains('3').click()
-    cy.get('input[placeholder="Project Domain Name"]').type(config.endpoints.openstack.projectDomainName)
-    cy.get('input[placeholder="User Domain Name"]').type(config.endpoints.openstack.userDomainName)
-
-    if (config.endpoints.openstack.allowUntrusted) {
-      cy.getById('endpointField-switch-allow_untrusted').click()
-    }
-    if (config.endpoints.openstack.allowUntrustedSwift) {
-      cy.getById('endpointField-switch-allow_untrusted_swift').click()
-    }
-
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.get('button').contains('Validate and save').click()
-    cy.wait('@validate')
-    cy.getById('endpointStatus').should('contain', 'Endpoint is Valid')
-  })
-
-  it('Added Openstack to endpoint list', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-openstack-test').should('contain', 'e2e-openstack-test')
-  })
-})

+ 0 - 51
private/cypress/integration/2 - create endpoints/Create VmWare Endpoint.js

@@ -1,51 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Create VmWare Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows new VmWare endpoint dialog', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Endpoint').click()
-    cy.getById('cProvider-endpointLogo-vmware_vsphere').click()
-  })
-
-  it('Fills VmWare connection info', () => {
-    cy.get('input[placeholder="Name"]').type('e2e-vmware-test')
-    cy.get('input[placeholder="Username"]').type(config.endpoints.vmware.username)
-    cy.get('input[placeholder="Password"]').type(config.endpoints.vmware.password)
-    cy.get('input[placeholder="Host"]').type(config.endpoints.vmware.host)
-
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.get('button').contains('Validate and save').click()
-    cy.wait('@validate')
-    cy.getById('endpointStatus').should('contain', 'Endpoint is Valid')
-  })
-
-  it('Added Endpoint to endpoint list', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-vmware-test').should('contain', 'e2e-vmware-test')
-  })
-})

+ 0 - 58
private/cypress/integration/3 - duplicate endpoints/Duplicate Azure Endpoint.js

@@ -1,58 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Duplicate Azure Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Creates duplicate', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-azure-test').should('contain', 'e2e-azure-test')
-    cy.getById('endpointListItem-checkbox-e2e-azure-test').click()
-    cy.getById('dropdown-dropdownButton').contains('Select an action').click()
-    cy.getById('dropdownListItem').contains('Duplicate').click()
-    cy.server()
-    cy.route({ url: '**/endpoints', method: 'POST' }).as('duplicate')
-    cy.get('button').contains('Duplicate').click()
-    cy.wait('@duplicate')
-  })
-
-  it('Validates duplicated endpoint', () => {
-    cy.get('div[data-test-id="endpointListItem-content-e2e-azure-test (copy)"').click()
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.getById('edContent-validateButton').click()
-    cy.wait('@validate')
-    cy.getById('eValidation-title').should('contain', 'Endpoint is Valid')
-    cy.get('button').contains('Dismiss').click()
-  })
-
-  it('Deletes duplicated endpoint', () => {
-    cy.server()
-    cy.route({ url: '**/replicas/detail', method: 'GET' }).as('replicas')
-    cy.route({ url: '**/migrations/detail', method: 'GET' }).as('migrations')
-    cy.getById('edContent-deleteButton').click()
-    cy.wait(['@replicas', '@migrations'])
-    cy.route({ url: '**/secrets/**', method: 'DELETE' }).as('delete')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@delete')
-  })
-})

+ 0 - 58
private/cypress/integration/3 - duplicate endpoints/Duplicate OCI Endpoint.js

@@ -1,58 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Duplicate OCI Endpoint', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Creates duplicate', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-oci-test').should('contain', 'e2e-oci-test')
-    cy.getById('endpointListItem-checkbox-e2e-oci-test').click()
-    cy.getById('dropdown-dropdownButton').contains('Select an action').click()
-    cy.getById('dropdownListItem').contains('Duplicate').click()
-    cy.server()
-    cy.route({ url: '**/endpoints', method: 'POST' }).as('duplicate')
-    cy.get('button').contains('Duplicate').click()
-    cy.wait('@duplicate')
-  })
-
-  it('Validates duplicated endpoint', () => {
-    cy.get('div[data-test-id="endpointListItem-content-e2e-oci-test (copy)"').click()
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('validate')
-    cy.getById('edContent-validateButton').click()
-    cy.wait('@validate')
-    cy.getById('eValidation-title').should('contain', 'Endpoint is Valid')
-    cy.get('button').contains('Dismiss').click()
-  })
-
-  it('Deletes duplicated endpoint', () => {
-    cy.server()
-    cy.route({ url: '**/replicas/detail', method: 'GET' }).as('replicas')
-    cy.route({ url: '**/migrations/detail', method: 'GET' }).as('migrations')
-    cy.getById('edContent-deleteButton').click()
-    cy.wait(['@replicas', '@migrations'])
-    cy.route({ url: '**/secrets/**', method: 'DELETE' }).as('delete')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@delete')
-  })
-})

+ 0 - 130
private/cypress/integration/4 - migrations and replicas/Openstack - OCI Migration.js

@@ -1,130 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../config'
-
-describe('Create Openstack to OCI Migration', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows Wizard page', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Migration').click()
-    cy.get('#app').should('contain', 'New Migration')
-  })
-
-  it('Chooses Openstack as Source Cloud', () => {
-    cy.server()
-    cy.route({ url: '**/instances**', method: 'GET' }).as('sourceInstances')
-    cy.get('button').contains('Next').click()
-    cy.getById('wEndpointList-dropdown-openstack').first().click()
-    cy.get('div').contains('e2e-openstack-test').click()
-    cy.wait('@sourceInstances')
-  })
-
-  it('Searches and selects instances', () => {
-    cy.get('button').contains('Next').click()
-    // cy.server()
-    // cy.route({ url: '**/instances**', method: 'GET' }).as('search')
-    cy.get('input[placeholder="Search VMs"]').type(config.wizard.instancesSearch.openstackSearchText)
-    // cy.wait('@search')
-    cy.getById('wInstances-instanceItem').contains(config.wizard.instancesSearch.openstackSearchText)
-    cy.getById('wInstances-instanceItem').its('length').should('be.gt', 0)
-    cy.getById('wInstances-instanceItem').eq(config.wizard.instancesSearch.openstackItemIndex).click()
-  })
-
-  it('Chooses OCI as Target Cloud', () => {
-    cy.server()
-    cy.get('button').contains('Next').click()
-    cy.getById('wEndpointList-dropdown-oci').first().click()
-    cy.route({ url: '**/destination-options', method: 'GET' }).as('destOptions')
-    cy.get('div').contains('e2e-oci-test').click()
-    cy.wait('@destOptions')
-  })
-
-  it('Fills OCI migration info', () => {
-    cy.get('button').contains('Next').click()
-    cy.getById('wOptionsField-enumDropdown-compartment').click()
-    cy.getById('dropdownListItem').contains(config.wizard.oci.compartment).click()
-    cy.getById('wOptionsField-enumDropdown-availability_domain').click()
-    cy.server()
-    cy.route({ url: '**/destination-options**', method: 'GET' }).as('destOptions')
-    cy.getById('dropdownListItem').contains(config.wizard.oci.availabilityDomain).click()
-    cy.wait('@destOptions')
-    cy.getById('wOptionsField-enumDropdown-migr_subnet_id').click()
-    cy.getById('dropdownListItem').contains(config.wizard.oci.migrSubnetId).click()
-  })
-
-  it('Selects first available network mapping', () => {
-    cy.server()
-    cy.route({ url: '**/networks**', method: 'GET' }).as('networks')
-    cy.route({ url: '**/instances/**', method: 'GET' }).as('instances')
-    cy.get('button').contains('Next').click()
-    cy.wait(['@networks', '@instances'])
-    cy.get('button').contains('Next').should('be.disabled')
-    cy.getById('networkItem').its('length').should('be.gt', 0)
-    cy.get('div[value="Select ..."]').first().click()
-    cy.getById('dropdownListItem').first().click()
-    cy.get('button').contains('Next').should('not.be.disabled')
-  })
-
-  it('Shows summary page', () => {
-    cy.get('button').contains('Next').click()
-    cy.get('#app').should('contain', 'Summary')
-    cy.get('#app').should('contain', 'e2e-openstack-test')
-    cy.get('#app').should('contain', 'e2e-oci-test')
-    cy.get('#app').should('contain', 'Coriolis Migration')
-    cy.get('#app').should('contain', 'Migration Target Options')
-    cy.getById('wSummary-optionValue-compartment').should('contain', 'ocid1.compartment')
-    cy.getById('wSummary-optionValue-availability_domain').should('contain', config.wizard.oci.availabilityDomain)
-    cy.getById('wSummary-optionValue-migr_subnet_id').should('contain', 'ocid1.subnet')
-  })
-
-  it('Executes migration', () => {
-    cy.server()
-    cy.route({ url: '**/migrations', method: 'POST' }).as('migration')
-    cy.get('button').contains('Finish').click()
-    cy.wait('@migration')
-  })
-
-  it('Shows running migration page', () => {
-    cy.getById('statusPill-RUNNING').should('exist')
-  })
-
-  it('Cancels migration', () => {
-    cy.getById('dcHeader-actionButton').click()
-    cy.get('*[data-test-id="actionDropdown-listItem-Cancel"]', { timeout: 10000 }).click()
-    cy.server()
-    cy.route({ url: '**/actions', method: 'POST' }).as('cancel')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@cancel')
-    cy.get('div[data-test-id="dcHeader-statusPill-ERROR"]', { timeout: 120000 })
-  })
-
-  it('Deletes migration', () => {
-    cy.getById('dcHeader-actionButton').click()
-    cy.getById('actionDropdown-listItem-Delete Migration').click()
-    cy.server()
-    cy.route({ url: '**/migrations/**', method: 'DELETE' }).as('delete')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@delete')
-  })
-})

+ 0 - 133
private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/1 - Create replica.js

@@ -1,133 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../../../config'
-
-describe('Create VmWare to Azure Replica', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows Wizard page', () => {
-    cy.get('div').contains('New').click()
-    cy.getById('newItemDropdown-listItem-Replica').click()
-    cy.get('#app').should('contain', 'New Replica')
-  })
-
-  it('Chooses VmWare as Source Cloud', () => {
-    cy.server()
-    cy.route({ url: '**/instances**', method: 'GET' }).as('sourceInstances')
-    cy.get('button').contains('Next').click()
-    cy.getById('wEndpointList-dropdown-vmware_vsphere').first().click()
-    cy.get('div').contains('e2e-vmware-test').click()
-    cy.wait('@sourceInstances')
-  })
-
-  it('Searches and selects instances', () => {
-    cy.get('button').contains('Next').click()
-    cy.server()
-    cy.route({ url: '**/instances**', method: 'GET' }).as('search')
-    cy.get('input[placeholder="Search VMs"]').type(config.wizard.instancesSearch.vmwareSearchText)
-    cy.wait('@search')
-    cy.getById('wInstances-instanceItem').contains(config.wizard.instancesSearch.vmwareSearchText)
-    cy.getById('wInstances-instanceItem').its('length').should('be.gt', 0)
-    cy.getById('wInstances-instanceItem').eq(config.wizard.instancesSearch.vmwareItemIndex).click()
-  })
-
-  it('Chooses Azure as Target Cloud', () => {
-    cy.get('button').contains('Next').click()
-    cy.getById('wEndpointList-dropdown-azure').first().click()
-    cy.server()
-    cy.route({ url: '**/destination-options**', method: 'GET' }).as('dest-options')
-    cy.get('div').contains('e2e-azure-test').click()
-    cy.wait('@dest-options')
-  })
-
-  it('Fills Azure replica info', () => {
-    cy.get('button').contains('Next').click()
-    cy.getById('acDropdown-wrapper').first().click()
-    cy.server()
-    cy.route({ url: '**/destination-options**', method: 'GET' }).as('dest-options')
-    cy.getById('ad-listItem').contains(config.wizard.azure.resourceGroup).click()
-    cy.wait('@dest-options')
-  })
-
-  it('Selects first available network mapping', () => {
-    cy.server()
-    cy.route({ url: '**/networks**', method: 'GET' }).as('networks')
-    cy.route({ url: '**/instances/**', method: 'GET' }).as('instances')
-    cy.get('button').contains('Next').click()
-    cy.wait(['@networks', '@instances'])
-    cy.get('button').contains('Next').should('be.disabled')
-    cy.getById('networkItem').its('length').should('be.gt', 0)
-    cy.get('div[value="Select ..."]').first().click()
-    cy.getById('dropdownListItem').first().click()
-    cy.get('button').contains('Next').should('not.be.disabled')
-  })
-
-  it('Shows storage screen', () => {
-    cy.get('button').contains('Next').click()
-    cy.getById('wpContent-header').should('contain', 'Storage')
-  })
-
-  it('Shows schedule page', () => {
-    cy.get('button').contains('Next').click()
-    cy.getById('wpContent-header').should('contain', 'Schedule')
-  })
-
-  it('Shows summary page', () => {
-    cy.get('button').contains('Next').click()
-    cy.get('#app').should('contain', 'Summary')
-    cy.get('#app').should('contain', 'e2e-vmware-test')
-    cy.get('#app').should('contain', 'e2e-azure-test')
-    cy.get('#app').should('contain', 'Coriolis Replica')
-    cy.get('#app').should('contain', 'Replica Target Options')
-    cy.getById('wSummary-optionValue-resource_group').should('contain', config.wizard.azure.resourceGroup)
-  })
-
-  it('Executes replica', () => {
-    cy.server()
-    cy.route({ url: '**/replicas', method: 'POST' }).as('replica')
-    cy.get('button').contains('Finish').click()
-    cy.wait('@replica')
-  })
-
-  it('Shows running replica page', () => {
-    cy.getById('statusPill-RUNNING').should('exist')
-  })
-
-  it('Cancels replica execution', () => {
-    cy.server()
-    cy.getById('executions-cancelButton').click()
-    cy.route({ url: '**/actions', method: 'POST' }).as('cancel')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@cancel')
-    cy.get('div[data-test-id="dcHeader-statusPill-ERROR"]', { timeout: 120000 })
-  })
-
-  it('Should show in usage message when trying to delete', () => {
-    cy.getById('dcHeader-backButton').click()
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.getById('endpointListItem-content-e2e-azure-test').click()
-    cy.getById('edContent-deleteButton').click()
-    cy.getById('alertModal').should('contain', 'The endpoint can\'t be deleted because it is in use by replicas or migrations.')
-    cy.getById('aModal-dismissButton').click()
-  })
-})

+ 0 - 77
private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/2 - Scheduler Operations.js

@@ -1,77 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Scheduler Operations', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Creates a schedule', () => {
-    cy.server()
-    cy.route('GET', '**/executions/detail').as('execution')
-    cy.getById('mainListItem-content').first().click()
-    cy.wait('@execution')
-    cy.getById('detailsNavigation-schedule').click()
-    cy.route('POST', '**/schedules').as('schedule')
-    cy.getById('schedule-noScheduleAddButton').click()
-    cy.wait('@schedule')
-    cy.getById('scheduleItem-saveButton').should('not.be.visible')
-  })
-
-  it('Changes the month', () => {
-    cy.getById('scheduleItem-monthDropdown').last().click()
-    cy.getById('dropdownListItem').contains('October').click()
-    cy.getById('scheduleItem-monthDropdown').last().should('contain', 'October')
-    cy.getById('scheduleItem-saveButton').should('be.visible')
-  })
-
-  it('Changes the hour', () => {
-    cy.getById('scheduleItem-hourDropdown').last().click()
-    cy.getById('dropdownListItem').contains('04').click()
-    cy.getById('scheduleItem-hourDropdown').last().should('contain', '04')
-  })
-
-  it('Changes timezone', () => {
-    cy.getById('schedule-timezoneDropdown').click()
-    cy.get('div').contains('UTC').click()
-    let utcTime = 4 + (new Date().getTimezoneOffset() / 60)
-    if (utcTime < 10) {
-      utcTime = `0${utcTime}`
-    }
-    utcTime = utcTime.toString()
-    cy.getById('scheduleItem-hourDropdown').last().should('contain', utcTime)
-  })
-
-  it('Saves the changes', () => {
-    cy.server()
-    cy.route('PUT', '**/schedules/**').as('schedule')
-    cy.getById('scheduleItem-saveButton').should('be.visible').last().click()
-    cy.wait('@schedule')
-    cy.getById('scheduleItem-saveButton').should('not.be.visible')
-  })
-
-  it('Deletes the last schedule', () => {
-    cy.getById('scheduleItem-deleteButton').last().click()
-    cy.server()
-    cy.route('DELETE', '**/schedules/**').as('schedule')
-    cy.get('button').contains('Yes').click()
-    cy.wait('@schedule')
-  })
-})

+ 0 - 40
private/cypress/integration/4 - migrations and replicas/VmWare - Azure Replica/3 - Delete replica.js

@@ -1,40 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Delete replica', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Goes to replica page', () => {
-    cy.server()
-    cy.route('GET', '**/executions/detail').as('execution')
-    cy.getById('mainListItem-content').first().click()
-    cy.wait('@execution')
-  })
-
-  it('Deletes replica', () => {
-    cy.server()
-    cy.getById('rdContent-deleteButton').click()
-    cy.route({ url: '**/replicas/**', method: 'DELETE' }).as('delete')
-    cy.get('button').contains('Yes').click()
-    cy.wait('@delete')
-  })
-})

+ 0 - 44
private/cypress/integration/5 - delete endpoints/Delete Azure endpoint.js

@@ -1,44 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Delete the Azure endpoint created for e2e testing', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Goes to endpoints page', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.get('#app').should('contain', 'Coriolis Endpoints')
-  })
-
-  it('Delete e2e Azure endpoint', () => {
-    cy.getById('endpointListItem-content-e2e-azure-test').should('contain', 'e2e-azure-test')
-    cy.getById('endpointListItem-content-e2e-azure-test').first().click()
-    cy.server()
-    cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
-    cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')
-    cy.get('button').contains('Delete Endpoint').click()
-    cy.wait(['@migrations', '@replicas'])
-    cy.route({ url: '**/endpoints/**', method: 'DELETE' }).as('delete')
-    cy.get('button').contains('Yes').click()
-    cy.wait('@delete')
-  })
-})
-

+ 0 - 44
private/cypress/integration/5 - delete endpoints/Delete OCI endpoint.js

@@ -1,44 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Delete the OCI endpoint created for e2e testing', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Goes to endpoints page', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.get('#app').should('contain', 'Coriolis Endpoints')
-  })
-
-  it('Delete e2e OCI endpoint', () => {
-    cy.getById('endpointListItem-content-e2e-oci-test').should('contain', 'e2e-oci-test')
-    cy.getById('endpointListItem-content-e2e-oci-test').first().click()
-    cy.server()
-    cy.route({ url: '**/migrations/**', method: 'GET' }).as('migrations')
-    cy.route({ url: '**/replicas/**', method: 'GET' }).as('replicas')
-    cy.get('button').contains('Delete Endpoint').click()
-    cy.wait(['@migrations', '@replicas'])
-    cy.route({ url: '**/endpoints/**', method: 'DELETE' }).as('delete')
-    cy.get('button').contains('Yes').click()
-    cy.wait('@delete')
-  })
-})
-

+ 0 - 49
private/cypress/integration/5 - delete endpoints/Delete Openstack and VmWare endpoints.js

@@ -1,49 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Delete the Openstack and VmWare endpoints created for e2e testing', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Goes to endpoints page', () => {
-    cy.getById('navigation-smallMenuItem-endpoints').click()
-    cy.get('#app').should('contain', 'Coriolis Endpoints')
-  })
-
-  it('Selects both endpoints', () => {
-    cy.getById('endpointListItem-checkbox-e2e-openstack-test').should('have.length', 1)
-    cy.getById('endpointListItem-checkbox-e2e-vmware-test').should('have.length', 1)
-    cy.getById('endpointListItem-checkbox-e2e-openstack-test').click()
-    cy.getById('endpointListItem-checkbox-e2e-vmware-test').click()
-    cy.getById('mainListFilter-selectionText').should('contain', '2 of 2')
-  })
-
-  it('Deletes selected endpoints', () => {
-    cy.getById('dropdown-dropdownButton').contains('Select an action').click()
-    cy.getById('dropdownListItem').contains('Delete').click()
-    cy.server()
-    cy.route({ url: '**/endpoints/**', method: 'DELETE' }).as('delete')
-    cy.getById('aModal-yesButton').click()
-    cy.wait(['@delete', '@delete'])
-    cy.getById('endpointListItem-checkbox-e2e-openstack-test').should('have.length', 0)
-    cy.getById('endpointListItem-checkbox-e2e-vmware-test').should('have.length', 0)
-  })
-})

+ 0 - 48
private/cypress/integration/6 - users and projects/1 - Create a project.js

@@ -1,48 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Create a project', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows projects page', () => {
-    cy.getById('navigation-smallMenuItem-projects').click()
-    cy.title().should('eq', 'Projects')
-  })
-
-  it('Shows new project modal', () => {
-    cy.getById('newItemDropdown-button').click()
-    cy.getById('newItemDropdown-listItem-Project').click()
-    cy.getById('modal-title').should('contain', 'New Project')
-  })
-
-  it('Creates project', () => {
-    cy.getById('endpointField-textInput-project_name').last().type('cypress-project')
-    cy.getById('endpointField-textInput-description').last().type('Project created by Cypress')
-    cy.server()
-    cy.route({ url: '**/projects/', method: 'POST' }).as('addProject')
-    cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRole')
-    cy.get('button').contains('New Project').click()
-    cy.wait(['@addProject', '@addRole'])
-    cy.getById('plItem-content').should('contain', 'cypress-project')
-    cy.getById('plItem-content').should('contain', 'Project created by Cypress')
-  })
-})

+ 0 - 72
private/cypress/integration/6 - users and projects/2 - Add a new user as a member.js

@@ -1,72 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Adds a new user as a member to the project', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows projects details page', () => {
-    cy.getById('navigation-smallMenuItem-projects').click()
-    cy.getById('plItem-content').contains('cypress-project').click()
-    cy.title().should('eq', 'Project Details')
-  })
-
-  it('Opens add member modal', () => {
-    cy.get('button').contains('Add Member').click()
-    cy.getById('modal-title').should('contain', 'Add Project Member')
-  })
-
-  it('Creates new user', () => {
-    cy.getById('toggleButtonBar-new').click()
-    cy.getById('endpointField-textInput-username').last().type('cypress-member-user')
-    cy.getById('endpointField-textInput-description').last().type('User created by Cypress in Add Project Member modal')
-    cy.getById('endpointField-dropdown-Primary Project').click()
-    cy.getById('dropdownListItem').contains('cypress-project').click()
-    cy.getById('endpointField-multidropdown-role(s)').click()
-    cy.getById('dropdownListItem').contains('_member_').click()
-    cy.getById('endpointField-textInput-password').last().type('cypress-member-user')
-    cy.getById('endpointField-textInput-confirm_password').last().type('cypress-member-user')
-    cy.server()
-    cy.route({ url: '**/users', method: 'POST' }).as('addUser')
-    cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRole')
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('pmModal-addButton').contains('Add Member').click()
-    cy.wait(['@addUser', '@addRole', '@getRoles'])
-    cy.getById('pdContent-users-cypress-member-user').its('length').should('eq', 1)
-    cy.getById('pdContent-roles-cypress-member-user').should('contain', '_member_')
-  })
-
-  it('Adds admin as its role', () => {
-    cy.getById('pdContent-roles-cypress-member-user').click()
-    cy.getById('dropdownLink-listItem').contains('admin').click()
-    cy.getById('pdContent-roles-cypress-member-user').should('contain', '_member_, admin')
-  })
-
-  it('Removes user from project', () => {
-    cy.getById('pdContent-actions-cypress-member-user').click()
-    cy.getById('dropdownLink-listItem').contains('Remove').click()
-    cy.server()
-    cy.route({ url: '**/roles/**', method: 'DELETE' }).as('deleteRole')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@deleteRole')
-    cy.getById('pdContent-users-cypress-member-user').should('not.exist')
-  })
-})

+ 0 - 61
private/cypress/integration/6 - users and projects/3 - Add existing user as a member.js

@@ -1,61 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Adds existing user as a member to the project', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows projects details page', () => {
-    cy.getById('navigation-smallMenuItem-projects').click()
-    cy.getById('plItem-content').contains('cypress-project').click()
-  })
-
-  it('Opens add member modal', () => {
-    cy.get('button').contains('Add Member').click()
-    cy.getById('modal-title').should('contain', 'Add Project Member')
-  })
-
-  it('Adds existing user', () => {
-    cy.getById('acInput-text').last().type('cy')
-    cy.getById('ad-listItem').contains('cypress-member-user').click()
-    cy.getById('endpointField-multidropdown-role(s)').click()
-    cy.getById('dropdownListItem').contains('_member_').click()
-    cy.getById('dropdownListItem').contains('admin').click()
-    cy.getById('modal-title').click()
-    cy.server()
-    cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRoles')
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('pmModal-addButton').click()
-    cy.wait(['@addRoles', '@getRoles'])
-    cy.getById('pdContent-roles-cypress-member-user').should('contain', '_member_, admin')
-  })
-
-  it('Deletes the user', () => {
-    cy.server()
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('pdContent-users-cypress-member-user').click()
-    cy.wait('@getRoles')
-    cy.get('button').contains('Delete user').click()
-    cy.route({ url: '**/users/**', method: 'DELETE' }).as('deleteUser')
-    cy.getById('aModal-yesButton').click()
-    cy.wait('@deleteUser')
-  })
-})

+ 0 - 51
private/cypress/integration/6 - users and projects/4 - Create a user.js

@@ -1,51 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Create a user', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows users page', () => {
-    cy.getById('navigation-smallMenuItem-users').click()
-    cy.title().should('eq', 'Users')
-  })
-
-  it('Shows new user modal', () => {
-    cy.getById('newItemDropdown-button').click()
-    cy.getById('newItemDropdown-listItem-User').click()
-    cy.getById('modal-title').should('contain', 'New User')
-  })
-
-  it('Creates user', () => {
-    cy.getById('endpointField-textInput-username').last().type('cypress-user')
-    cy.getById('endpointField-textInput-description').last().type('User created by Cypress')
-    cy.getById('endpointField-dropdown-Primary Project').click()
-    cy.getById('dropdownListItem').contains('cypress-project').click()
-    cy.getById('endpointField-textInput-new_password').last().type('cypress-user')
-    cy.getById('endpointField-textInput-confirm_password').last().type('cypress-user')
-    cy.server()
-    cy.route({ url: '**/users', method: 'POST' }).as('addUser')
-    cy.route({ url: '**/roles/**', method: 'PUT' }).as('addRole')
-    cy.get('button').contains('New User').click()
-    cy.wait(['@addUser', '@addRole'])
-    cy.getById('ulItem-name').contains('cypress-user').should('exist')
-  })
-})

+ 0 - 62
private/cypress/integration/6 - users and projects/5 - Edit and delete user.js

@@ -1,62 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Edit created user', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows user details page', () => {
-    cy.getById('navigation-smallMenuItem-users').click()
-    cy.server()
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('ulItem-name').contains('cypress-user').click()
-    cy.wait('@getRoles')
-    cy.title().should('eq', 'User Details')
-    cy.getById('dcHeader-title').should('contain', 'cypress-user')
-  })
-
-  it('Opens user edit modal', () => {
-    cy.getById('dcHeader-actionButton').click()
-    cy.getById('actionDropdown-listItem-Edit user').click()
-    cy.getById('modal-title').should('contain', 'Update User')
-  })
-
-  it('Edits user', () => {
-    cy.getById('endpointField-textInput-username').last().clear()
-    cy.getById('endpointField-textInput-username').last().type('user-cypress')
-    cy.server()
-    cy.route({ url: '**/users/**', method: 'PATCH' }).as('updateUser')
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.get('button').contains('Update User').click()
-    cy.wait(['@updateUser', '@getRoles'])
-    cy.getById('dcHeader-title').should('contain', 'user-cypress')
-  })
-
-  it('Deletes the user', () => {
-    cy.server()
-    cy.get('button').contains('Delete user').click()
-    cy.route({ url: '**/users/**', method: 'DELETE' }).as('deleteUser')
-    cy.route({ url: '**/users', method: 'GET' }).as('getUsers')
-    cy.getById('aModal-yesButton').click()
-    cy.wait(['@deleteUser', '@getUsers'])
-    cy.getById('ulItem-name').contains('user-cypress').should('not.exist')
-  })
-})

+ 0 - 61
private/cypress/integration/6 - users and projects/6 - Edit and delete project.js

@@ -1,61 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-describe('Edit created project', () => {
-  before(() => {
-    cy.login()
-  })
-
-  beforeEach(() => {
-    Cypress.Cookies.preserveOnce('token', 'projectId')
-  })
-
-  it('Shows project details page', () => {
-    cy.getById('navigation-smallMenuItem-projects').click()
-    cy.server()
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('plItem-content').contains('cypress-project').click()
-    cy.wait('@getRoles')
-    cy.title().should('eq', 'Project Details')
-    cy.getById('dcHeader-title').should('contain', 'cypress-project')
-  })
-
-  it('Opens project edit modal', () => {
-    cy.getById('dcHeader-actionButton').click()
-    cy.getById('actionDropdown-listItem-Edit Project').click()
-    cy.getById('modal-title').should('contain', 'Update Project')
-  })
-
-  it('Edits project', () => {
-    cy.getById('endpointField-textInput-project_name').last().clear()
-    cy.get('input[data-test-id="endpointField-textInput-project_name').last().type('project-cypress')
-    cy.server()
-    cy.route({ url: '**/projects/**', method: 'PATCH' }).as('updateProject')
-    cy.get('button').contains('Update Project').click()
-    cy.wait('@updateProject')
-    cy.getById('dcHeader-title').should('contain', 'project-cypress')
-  })
-
-  it('Deletes the project', () => {
-    cy.server()
-    cy.get('button').contains('Delete Project').click()
-    cy.route({ url: '**/projects/**', method: 'DELETE' }).as('deleteProject')
-    cy.route({ url: '**/role_assignments**', method: 'GET' }).as('getRoles')
-    cy.getById('aModal-yesButton').click()
-    cy.wait(['@deleteProject', '@getRoles'])
-    cy.getById('plItem-content').contains('project-cypress').should('not.exist')
-  })
-})

+ 0 - 204
private/cypress/support/commands.js

@@ -1,204 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import config from '../config.js'
-
-const identityUrl = `${config.coriolisUrl}identity/auth/tokens`
-const projectsUrl = `${config.coriolisUrl}identity/auth/projects`
-const coriolisUrl = `${config.coriolisUrl}coriolis`
-
-Cypress.Commands.add('login', () => {
-  let unscopedBody = {
-    auth: {
-      identity: {
-        methods: ['password'],
-        password: {
-          user: {
-            name: config.username,
-            domain: { name: 'default' },
-            password: config.password,
-          },
-        },
-      },
-      scope: 'unscoped',
-    },
-  }
-
-  cy.request({
-    method: 'POST',
-    url: identityUrl,
-    body: unscopedBody,
-  }).then(unscopedResponse => {
-    let unscopedToken = unscopedResponse.headers['x-subject-token']
-
-    // $FlowIssue
-    expect(unscopedToken).to.exist
-
-    cy.request({
-      method: 'GET',
-      url: projectsUrl,
-      headers: { 'X-Auth-Token': unscopedToken },
-    }).then(projectsReponse => {
-      let projects = projectsReponse.body.projects
-      let cypressProject = projects.find(p => p.name === 'cypress')
-      let projectId = cypressProject ? cypressProject.id : projects[0].id
-
-      // $FlowIssue
-      expect(projectId).to.exist
-
-      let scopedBody = {
-        auth: {
-          identity: {
-            methods: ['token'],
-            token: {
-              id: unscopedToken,
-            },
-          },
-          scope: {
-            project: {
-              id: projectId,
-            },
-          },
-        },
-      }
-
-      cy.request({
-        method: 'POST',
-        url: identityUrl,
-        body: scopedBody,
-      }).then(scopedResponse => {
-        let scopedToken = scopedResponse.headers['x-subject-token']
-        // $FlowIssue
-        expect(scopedToken).to.exist
-
-        cy.setCookie('token', scopedToken)
-        cy.setCookie('projectId', projectId)
-        cy.visit(config.nodeServer)
-      })
-    })
-  })
-})
-
-Cypress.Commands.add('logout', () => {
-  let token
-  return cy.getCookies().then(cookies => {
-    let tokenCookie = cookies.find(c => c.name === 'token')
-    if (tokenCookie) {
-      token = tokenCookie.value
-    }
-  }).then(() => {
-    if (!token) {
-      return Promise.resolve()
-    }
-    return cy.request({
-      method: 'DELETE',
-      url: `${config.coriolisUrl}identity/auth/tokens`,
-      headers: { 'X-Subject-Token': token, 'X-Auth-Token': token },
-    })
-  })
-})
-
-Cypress.Commands.add('cleanup', () => {
-  if (config.username !== 'cypress') {
-    return Promise.resolve()
-  }
-
-  let token
-  let projectId
-  return cy.getCookies().then(cookies => {
-    token = cookies.find(c => c.name === 'token').value
-    projectId = cookies.find(c => c.name === 'projectId').value
-  }).then(() => {
-    if (!token || !projectId) {
-      return Promise.resolve()
-    }
-
-    // Delete replicas
-    return cy.request({
-      method: 'GET',
-      url: `${coriolisUrl}/${projectId}/replicas/detail`,
-      headers: { 'X-Auth-Token': token },
-    }).then(response => response.body.replicas)
-      .then(replicas => Promise.all(replicas.map(replica => cy.request({
-        method: 'DELETE',
-        url: `${coriolisUrl}/${projectId}/replicas/${replica.id}`,
-        headers: { 'X-Auth-Token': token },
-      }))))
-  }).then(() => {
-    // Delete migrations
-    return cy.request({
-      method: 'GET',
-      url: `${coriolisUrl}/${projectId}/migrations/detail`,
-      headers: { 'X-Auth-Token': token },
-    }).then(response => response.body.migrations)
-      .then(migrations => Promise.all(migrations.map(migration => cy.request({
-        method: 'DELETE',
-        url: `${coriolisUrl}/${projectId}/migrations/${migration.id}`,
-        headers: { 'X-Auth-Token': token },
-      }))))
-  }).then(() => {
-    // Delete endpoints
-    return cy.request({
-      method: 'GET',
-      url: `${coriolisUrl}/${projectId}/endpoints`,
-      headers: { 'X-Auth-Token': token },
-    }).then(response => response.body.endpoints)
-      .then(endpoints => Promise.all(endpoints.map(endpoint => cy.request({
-        method: 'DELETE',
-        url: `${coriolisUrl}/${projectId}/endpoints/${endpoint.id}`,
-        headers: { 'X-Auth-Token': token },
-      }))))
-  }).then(() => {
-    // Delete users created by Cypress
-    return cy.request({
-      method: 'GET',
-      url: `${config.coriolisUrl}identity/users`,
-      headers: { 'X-Auth-Token': token },
-    }).then(response => response.body.users
-      .filter(user => user.description && /user created by cypress/gi.test(user.description))
-    ).then(users => Promise.all(users.map(user => cy.request({
-      method: 'DELETE',
-      url: `${config.coriolisUrl}identity/users/${user.id}`,
-      headers: { 'X-Auth-Token': token },
-    }))))
-  }).then(() => {
-    // Delete projects created by Cypress
-    return cy.request({
-      method: 'GET',
-      url: `${config.coriolisUrl}identity/projects`,
-      headers: { 'X-Auth-Token': token },
-    }).then(response => response.body.projects
-      .filter(project => project.description && /project created by cypress/gi.test(project.description))
-    ).then(projects => Promise.all(projects.map(project => cy.request({
-      method: 'DELETE',
-      url: `${config.coriolisUrl}identity/projects/${project.id}`,
-      headers: { 'X-Auth-Token': token },
-    }))))
-  })
-})
-
-Cypress.Commands.add('getById', (id, type) => {
-  return cy.get(`${type || ''}[data-test-id="${id}"]`)
-})
-
-Cypress.Commands.add('getServerConfig', () => {
-  return cy.request({
-    url: `${config.nodeServer}config`,
-    method: 'GET',
-  }).then(response => {
-    return response.body
-  })
-})

+ 0 - 20
private/cypress/support/index.js

@@ -1,20 +0,0 @@
-/*
-Copyright (C) 2018  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import './commands'
-
-/* eslint func-names: off */
-afterEach(function () { if (this.currentTest.state === 'failed') { Cypress.runner.stop() } })

+ 1 - 1
src/components/smart/EndpointsPage/EndpointsPage.tsx

@@ -339,7 +339,7 @@ class EndpointsPage extends React.Component<{ history: any }, State> {
                 />
               )}
               emptyListImage={endpointImage}
-              emptyListMessage="You dont have any Cloud Endpoints in this project."
+              emptyListMessage="You don't have any Cloud Endpoints in this project."
               emptyListExtraMessage="A Cloud Endpoint is used for the source or target of a Replica/Migration."
               emptyListButtonLabel="Add Endpoint"
               onEmptyListButtonClick={() => {

+ 1 - 1
src/components/smart/MigrationsPage/MigrationsPage.tsx

@@ -304,7 +304,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
                 />
               )}
               emptyListImage={migrationLargeImage}
-              emptyListMessage="It seems like you dont have any Migrations in this project."
+              emptyListMessage="It seems like you don't have any Migrations in this project."
               emptyListExtraMessage="A Coriolis Migration is a full virtual machine migration between two cloud endpoints."
               emptyListButtonLabel="Create a Migration"
               onEmptyListButtonClick={() => {

+ 3 - 2
src/components/smart/ReplicasPage/ReplicasPage.tsx

@@ -348,7 +348,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     if (!bulkScheduleItem) {
       return false;
     }
-    return Boolean(bulkScheduleItem.schedules.find(s => s.enabled));
+    const result = Boolean(bulkScheduleItem.schedules.find(s => s.enabled));
+    return result;
   }
 
   render() {
@@ -459,7 +460,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
                 />
               )}
               emptyListImage={replicaLargeImage}
-              emptyListMessage="It seems like you dont have any Replicas in this project."
+              emptyListMessage="It seems like you don't have any Replicas in this project."
               emptyListExtraMessage="The Coriolis Replica is obtained by replicating incrementally the virtual machines data from the source cloud endpoint to the target."
               emptyListButtonLabel="Create a Replica"
               onEmptyListButtonClick={() => {

+ 1 - 1
src/sources/UserSource.ts

@@ -155,7 +155,7 @@ class UserSource {
       cookie.remove("projectId");
       return;
     }
-    throw new Error();
+    throw new Error("No unscoped token");
   }
 
   async logout(): Promise<void> {

+ 1 - 1
tsconfig.json

@@ -49,7 +49,7 @@
     },
     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
     // "typeRoots": [],                       /* List of folders to include type definitions from. */
-    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "types": [] /* Type declaration files to be included in compilation. */,
     // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
     "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
     // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

Datei-Diff unterdrückt, da er zu groß ist
+ 555 - 15
yarn.lock


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.