From 1d931d5b9bcb0024bd087c5aaca661a388b2690d Mon Sep 17 00:00:00 2001
From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com>
Date: Wed, 5 Apr 2023 15:15:15 +0200
Subject: [PATCH] [CI] Optimize `admin > databases` E2E tests (#29418)

* Rework repro for 16382

* Merge multiple "connection settings" tests together

* Merge multiple "scheduling settings" tests together

* Merge tiny test with the main body

* Remove redundant tests

* Rework the sample database action sidebar

* Handle database exceptions in one spec

* Move stray database test to exceptions spec

* Move Postgres SSL test to external

* Group together Google service account JSON tests

* Custom caching

* Move remaining pieces of add to external

* Rename spec to `add-new-database`

* Move repro for 20471 to exceptions

* Add segments and metrics to the database we want to delete

* Add `visitDatabase` helper
---
 .../admin/databases/actions.cy.spec.js        |   5 +-
 ...cy.spec.js => add-new-database.cy.spec.js} | 186 +++++++++++-
 .../scenarios/admin/databases/add.cy.spec.js  | 236 ---------------
 .../databases/database-exceptions.cy.spec.js  |  92 ++++++
 .../default-sample-database.cy.spec.js        | 250 ++++++++++++++++
 .../scenarios/admin/databases/edit.cy.spec.js | 272 ------------------
 .../databases/helpers/e2e-database-helpers.js |  18 ++
 .../scenarios/admin/databases/list.cy.spec.js | 125 --------
 .../admin/settings/spinner.cy.spec.js         |  23 --
 9 files changed, 535 insertions(+), 672 deletions(-)
 rename e2e/test/scenarios/admin/databases/{add-external.cy.spec.js => add-new-database.cy.spec.js} (51%)
 delete mode 100644 e2e/test/scenarios/admin/databases/add.cy.spec.js
 create mode 100644 e2e/test/scenarios/admin/databases/database-exceptions.cy.spec.js
 create mode 100644 e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
 delete mode 100644 e2e/test/scenarios/admin/databases/edit.cy.spec.js
 create mode 100644 e2e/test/scenarios/admin/databases/helpers/e2e-database-helpers.js
 delete mode 100644 e2e/test/scenarios/admin/databases/list.cy.spec.js
 delete mode 100644 e2e/test/scenarios/admin/settings/spinner.cy.spec.js

diff --git a/e2e/test/scenarios/admin/databases/actions.cy.spec.js b/e2e/test/scenarios/admin/databases/actions.cy.spec.js
index 77d777eb045..5790e3c53fb 100644
--- a/e2e/test/scenarios/admin/databases/actions.cy.spec.js
+++ b/e2e/test/scenarios/admin/databases/actions.cy.spec.js
@@ -1,6 +1,8 @@
 import { restore } from "e2e/support/helpers";
 import { WRITABLE_DB_ID, WRITABLE_DB_CONFIG } from "e2e/support/cypress_data";
 
+import { visitDatabase } from "./helpers/e2e-database-helpers";
+
 describe(
   "admin > database > external databases > enable actions",
   { tags: ["@external", "@actions"] },
@@ -10,7 +12,7 @@ describe(
         restore(`${dialect}-writable`);
         cy.signInAsAdmin();
 
-        cy.request(`/api/database/${WRITABLE_DB_ID}`).then(({ body }) => {
+        visitDatabase(WRITABLE_DB_ID).then(({ response: { body } }) => {
           expect(body.name).to.include("Writable");
           expect(body.name.toLowerCase()).to.include(dialect);
 
@@ -20,7 +22,6 @@ describe(
           expect(body.settings["database-enable-actions"]).to.eq(true);
         });
 
-        cy.visit(`/admin/databases/${WRITABLE_DB_ID}`);
         cy.get("#model-actions-toggle").should(
           "have.attr",
           "aria-checked",
diff --git a/e2e/test/scenarios/admin/databases/add-external.cy.spec.js b/e2e/test/scenarios/admin/databases/add-new-database.cy.spec.js
similarity index 51%
rename from e2e/test/scenarios/admin/databases/add-external.cy.spec.js
rename to e2e/test/scenarios/admin/databases/add-new-database.cy.spec.js
index 2b110a2c4f8..64936179dee 100644
--- a/e2e/test/scenarios/admin/databases/add-external.cy.spec.js
+++ b/e2e/test/scenarios/admin/databases/add-new-database.cy.spec.js
@@ -1,24 +1,60 @@
-import { restore, typeAndBlurUsingLabel } from "e2e/support/helpers";
+import {
+  restore,
+  popover,
+  typeAndBlurUsingLabel,
+  isEE,
+} from "e2e/support/helpers";
 import {
   QA_MONGO_PORT,
   QA_MYSQL_PORT,
   QA_POSTGRES_PORT,
 } from "e2e/support/cypress_data";
 
-describe(
-  "admin > database > add > external databases",
-  { tags: "@external" },
-  () => {
-    beforeEach(() => {
-      restore();
-      cy.signInAsAdmin();
+describe("admin > database > add", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
 
-      cy.intercept("POST", "/api/database").as("createDatabase");
+    cy.intercept("POST", "/api/database").as("createDatabase");
 
-      cy.visit("/admin/databases/create");
-      cy.findByLabelText("Database type").click();
+    cy.visit("/admin/databases/create");
+    // should display a setup help card
+    cy.findByText("Need help connecting?");
+
+    cy.findByLabelText("Database type").click();
+  });
+
+  it("should add a new database", () => {
+    popover().within(() => {
+      if (isEE) {
+        // EE should ship with Oracle and Vertica as options
+        cy.findByText("Oracle");
+        // cy.findByText("Vertica");
+      }
+      cy.findByText("H2").click();
     });
 
+    typeAndBlurUsingLabel("Display name", "Test");
+    typeAndBlurUsingLabel("Connection String", "invalid");
+
+    // should surface an error if the connection string is invalid
+    cy.button("Save").click();
+    cy.wait("@createDatabase");
+    cy.findByText(": check your connection string");
+    cy.findByText("Implicitly relative file paths are not allowed.");
+
+    // should be able to recover from an error and add database with the correct connection string
+    cy.findByDisplayValue("invalid")
+      .clear()
+      .type(
+        "zip:./target/uberjar/metabase.jar!/sample-database.db;USER=GUEST;PASSWORD=guest",
+        { delay: 0 },
+      );
+    cy.button("Save", { timeout: 10000 }).click();
+    cy.wait("@createDatabase");
+  });
+
+  describe("external databases", { tags: "@external" }, () => {
     it("should add Postgres database and redirect to listing (metabase#12972, metabase#14334, metabase#17450)", () => {
       cy.contains("PostgreSQL").click({ force: true });
 
@@ -50,6 +86,53 @@ describe(
       typeAndBlurUsingLabel("Username", "metabase");
       typeAndBlurUsingLabel("Password", "metasample123");
 
+      const confirmSSLFields = (visible, hidden) => {
+        visible.forEach(field => cy.findByText(field));
+        hidden.forEach(field => cy.findByText(field).should("not.exist"));
+      };
+
+      const ssl = "Use a secure connection (SSL)",
+        sslMode = "SSL Mode",
+        useClientCert = "Authenticate client certificate?",
+        clientPemCert = "SSL Client Certificate (PEM)",
+        clientPkcsCert = "SSL Client Key (PKCS-8/DER)",
+        sslRootCert = "SSL Root Certificate (PEM)";
+
+      // initially, all SSL sub-properties should be hidden
+      confirmSSLFields(
+        [ssl],
+        [sslMode, useClientCert, clientPemCert, clientPkcsCert, sslRootCert],
+      );
+
+      toggleFieldWithDisplayName(ssl);
+      // when ssl is enabled, the mode and "enable client cert" options should be shown
+      confirmSSLFields(
+        [ssl, sslMode, useClientCert],
+        [clientPemCert, clientPkcsCert, sslRootCert],
+      );
+
+      toggleFieldWithDisplayName(useClientCert);
+      // when the "enable client cert" option is enabled, its sub-properties should be shown
+      confirmSSLFields(
+        [ssl, sslMode, useClientCert, clientPemCert, clientPkcsCert],
+        [sslRootCert],
+      );
+
+      selectFieldOption(sslMode, "verify-ca");
+      // when the ssl mode is set to "verify-ca", then the root cert option should be shown
+      confirmSSLFields(
+        [
+          ssl,
+          sslMode,
+          useClientCert,
+          clientPemCert,
+          clientPkcsCert,
+          sslRootCert,
+        ],
+        [],
+      );
+      toggleFieldWithDisplayName(ssl);
+
       cy.button("Save").should("not.be.disabled").click();
 
       cy.wait("@createDatabase").then(({ request }) => {
@@ -123,7 +206,9 @@ describe(
       cy.findByLabelText("Port").should("not.exist");
       cy.findByLabelText("Paste your connection string").type(
         connectionString,
-        { delay: 0 },
+        {
+          delay: 0,
+        },
       );
 
       cy.findByText("Save").should("not.be.disabled").click();
@@ -176,5 +261,78 @@ describe(
         cy.findByText("Done!");
       });
     });
-  },
-);
+  });
+
+  describe("Google service account JSON upload", () => {
+    const serviceAccountJSON = '{"foo": 123}';
+
+    it("should work for BigQuery", () => {
+      cy.visit("/admin/databases/create");
+
+      chooseDatabase("BigQuery");
+      typeAndBlurUsingLabel("Display name", "BQ");
+      selectFieldOption("Datasets", "Only these...");
+      cy.findByPlaceholderText("E.x. public,auth*").type("some-dataset");
+
+      mockUploadServiceAccountJSON(serviceAccountJSON);
+      mockSuccessfulDatabaseSave().then(({ request: { body } }) => {
+        expect(body.details["service-account-json"]).to.equal(
+          serviceAccountJSON,
+        );
+      });
+    });
+
+    it("should work for Google Analytics", () => {
+      cy.visit("/admin/databases/create");
+
+      chooseDatabase("Google Analytics");
+      typeAndBlurUsingLabel("Display name", "GA");
+      typeAndBlurUsingLabel("Google Analytics Account ID", " 9  ");
+
+      mockUploadServiceAccountJSON(serviceAccountJSON);
+      mockSuccessfulDatabaseSave().then(({ request: { body } }) => {
+        expect(body.details["service-account-json"]).to.equal(
+          serviceAccountJSON,
+        );
+      });
+    });
+  });
+});
+
+function toggleFieldWithDisplayName(displayName) {
+  cy.findByLabelText(displayName).click();
+}
+
+function selectFieldOption(fieldName, option) {
+  cy.findByLabelText(fieldName).click();
+  popover().contains(option).click({ force: true });
+}
+
+function chooseDatabase(database) {
+  selectFieldOption("Database type", database);
+}
+
+function mockUploadServiceAccountJSON(fileContents) {
+  // create blob to act as selected file
+  cy.get("input[type=file]")
+    .then(async input => {
+      const blob = await Cypress.Blob.binaryStringToBlob(fileContents);
+      const file = new File([blob], "service-account.json");
+      const dataTransfer = new DataTransfer();
+
+      dataTransfer.items.add(file);
+      input[0].files = dataTransfer.files;
+      return input;
+    })
+    .trigger("change", { force: true })
+    .trigger("blur", { force: true });
+}
+
+function mockSuccessfulDatabaseSave() {
+  cy.intercept("POST", "/api/database", req => {
+    req.reply({ statusCode: 200, body: { id: 42 }, delay: 100 });
+  }).as("createDatabase");
+
+  cy.button("Save").click();
+  return cy.wait("@createDatabase");
+}
diff --git a/e2e/test/scenarios/admin/databases/add.cy.spec.js b/e2e/test/scenarios/admin/databases/add.cy.spec.js
deleted file mode 100644
index 6e2f831dee1..00000000000
--- a/e2e/test/scenarios/admin/databases/add.cy.spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import {
-  restore,
-  popover,
-  describeEE,
-  mockSessionProperty,
-  isEE,
-  typeAndBlurUsingLabel,
-} from "e2e/support/helpers";
-
-describe("scenarios > admin > databases > add", () => {
-  beforeEach(() => {
-    restore();
-    cy.signInAsAdmin();
-  });
-
-  it("should show validation error if you enter invalid db connection info", () => {
-    cy.intercept("POST", "/api/database").as("createDatabase");
-
-    // should display a setup help card
-    cy.visit("/admin/databases/create");
-    cy.findByText("Need help connecting?");
-
-    chooseDatabase("H2");
-    typeAndBlurUsingLabel("Display name", "Test");
-    typeAndBlurUsingLabel("Connection String", "invalid");
-
-    cy.button("Save").click();
-    cy.wait("@createDatabase");
-    cy.findByText(": check your connection string");
-    cy.findByText("Implicitly relative file paths are not allowed.");
-  });
-
-  it("should show error correctly on server error", () => {
-    cy.intercept("POST", "/api/database", req => {
-      req.reply({
-        statusCode: 400,
-        body: "DATABASE CONNECTION ERROR",
-        delay: 1000,
-      });
-    }).as("createDatabase");
-
-    cy.visit("/admin/databases/create");
-
-    typeAndBlurUsingLabel("Display name", "Test");
-    typeAndBlurUsingLabel("Database name", "db");
-    typeAndBlurUsingLabel("Username", "admin");
-
-    cy.button("Save").click();
-
-    cy.wait("@createDatabase");
-    cy.findByText("DATABASE CONNECTION ERROR").should("exist");
-  });
-
-  it("EE should ship with Oracle and Vertica as options", () => {
-    cy.onlyOn(isEE);
-
-    cy.visit("/admin/databases/create");
-    cy.findByLabelText("Database type").click();
-    popover().within(() => {
-      cy.findByText("Oracle");
-      cy.findByText("Vertica");
-    });
-  });
-
-  describe("BigQuery", () => {
-    it("should let you upload the service account json from a file", () => {
-      cy.visit("/admin/databases/create");
-
-      chooseDatabase("BigQuery");
-
-      // enter text
-      typeAndBlurUsingLabel("Display name", "bq db");
-      // typeAndBlurUsingLabel("Dataset ID", "some-dataset");
-      selectFieldOption("Datasets", "Only these...");
-      cy.findByPlaceholderText("E.x. public,auth*").type("some-dataset");
-
-      // create blob to act as selected file
-      cy.get("input[type=file]")
-        .then(async input => {
-          const blob = await Cypress.Blob.binaryStringToBlob('{"foo": 123}');
-          const file = new File([blob], "service-account.json");
-          const dataTransfer = new DataTransfer();
-
-          dataTransfer.items.add(file);
-          input[0].files = dataTransfer.files;
-          return input;
-        })
-        .trigger("change", { force: true })
-        .trigger("blur", { force: true });
-
-      cy.intercept("POST", "/api/database", req => {
-        req.reply({
-          statusCode: 200,
-          body: { id: 123 },
-          delay: 100,
-        });
-      }).as("createDatabase");
-
-      // submit form and check that the file's body is included
-      cy.button("Save").click();
-      cy.wait("@createDatabase").should(xhr => {
-        expect(xhr.request.body.details["service-account-json"]).to.equal(
-          '{"foo": 123}',
-        );
-      });
-    });
-  });
-
-  describe("Google Analytics ", () => {
-    it("should let you upload the service account json from a file", () => {
-      cy.visit("/admin/databases/create");
-      chooseDatabase("Google Analytics");
-
-      typeAndBlurUsingLabel("Display name", "google analytics");
-
-      typeAndBlurUsingLabel("Google Analytics Account ID", " 999  ");
-
-      // create blob to act as selected file
-      cy.get("input[type=file]")
-        .then(async input => {
-          const blob = await Cypress.Blob.binaryStringToBlob('{"foo": 123}');
-          const file = new File([blob], "service-account.json");
-          const dataTransfer = new DataTransfer();
-
-          dataTransfer.items.add(file);
-          input[0].files = dataTransfer.files;
-          return input;
-        })
-        .trigger("change", { force: true })
-        .trigger("blur", { force: true });
-
-      cy.intercept("POST", "/api/database", req => {
-        req.reply({ statusCode: 200, body: { id: 123 }, delay: 100 });
-      }).as("createDatabase");
-
-      // submit form and check that the file's body is included
-      cy.button("Save").click();
-      cy.wait("@createDatabase").should(xhr => {
-        expect(xhr.request.body.details["service-account-json"]).to.equal(
-          '{"foo": 123}',
-        );
-      });
-    });
-  });
-
-  describeEE("caching", () => {
-    beforeEach(() => {
-      mockSessionProperty("enable-query-caching", true);
-
-      cy.intercept("POST", "/api/database", { id: 42 }).as("createDatabase");
-      cy.visit("/admin/databases/create");
-
-      typeAndBlurUsingLabel("Display name", "Test");
-      typeAndBlurUsingLabel("Host", "localhost");
-      typeAndBlurUsingLabel("Database name", "db");
-      typeAndBlurUsingLabel("Username", "admin");
-
-      cy.findByText("Show advanced options").click();
-    });
-
-    it("sets cache ttl to null by default", () => {
-      cy.button("Save").click();
-
-      cy.wait("@createDatabase").then(({ request }) => {
-        expect(request.body.cache_ttl).to.equal(null);
-      });
-    });
-
-    it("allows to set cache ttl", () => {
-      cy.findByText("Use instance default (TTL)").click();
-      popover().findByText("Custom").click();
-      cy.findByDisplayValue("24").clear().type("48").blur();
-
-      cy.button("Save").click();
-
-      cy.wait("@createDatabase").then(({ request }) => {
-        expect(request.body.cache_ttl).to.equal(48);
-      });
-    });
-  });
-
-  it("should show the various Postgres SSL options correctly", () => {
-    const confirmSSLFields = (visible, hidden) => {
-      visible.forEach(field => cy.findByText(field));
-      hidden.forEach(field => cy.findByText(field).should("not.exist"));
-    };
-
-    const ssl = "Use a secure connection (SSL)",
-      sslMode = "SSL Mode",
-      useClientCert = "Authenticate client certificate?",
-      clientPemCert = "SSL Client Certificate (PEM)",
-      clientPkcsCert = "SSL Client Key (PKCS-8/DER)",
-      sslRootCert = "SSL Root Certificate (PEM)";
-
-    cy.visit("/admin/databases/create");
-    chooseDatabase("PostgreSQL");
-    // initially, all SSL sub-properties should be hidden
-    confirmSSLFields(
-      [ssl],
-      [sslMode, useClientCert, clientPemCert, clientPkcsCert, sslRootCert],
-    );
-
-    toggleFieldWithDisplayName(ssl);
-    // when ssl is enabled, the mode and "enable client cert" options should be shown
-    confirmSSLFields(
-      [ssl, sslMode, useClientCert],
-      [clientPemCert, clientPkcsCert, sslRootCert],
-    );
-
-    toggleFieldWithDisplayName(useClientCert);
-    // when the "enable client cert" option is enabled, its sub-properties should be shown
-    confirmSSLFields(
-      [ssl, sslMode, useClientCert, clientPemCert, clientPkcsCert],
-      [sslRootCert],
-    );
-
-    selectFieldOption(sslMode, "verify-ca");
-    // when the ssl mode is set to "verify-ca", then the root cert option should be shown
-    confirmSSLFields(
-      [ssl, sslMode, useClientCert, clientPemCert, clientPkcsCert, sslRootCert],
-      [],
-    );
-  });
-});
-function toggleFieldWithDisplayName(displayName) {
-  cy.findByLabelText(displayName).click();
-}
-
-function selectFieldOption(fieldName, option) {
-  cy.findByLabelText(fieldName).click();
-  popover().contains(option).click({ force: true });
-}
-
-function chooseDatabase(database) {
-  selectFieldOption("Database type", database);
-}
diff --git a/e2e/test/scenarios/admin/databases/database-exceptions.cy.spec.js b/e2e/test/scenarios/admin/databases/database-exceptions.cy.spec.js
new file mode 100644
index 00000000000..f24424a50c9
--- /dev/null
+++ b/e2e/test/scenarios/admin/databases/database-exceptions.cy.spec.js
@@ -0,0 +1,92 @@
+import { restore, typeAndBlurUsingLabel, isEE } from "e2e/support/helpers";
+
+describe("scenarios > admin > databases > exceptions", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+  });
+
+  it("should handle malformed (null) database details (metabase#25715)", () => {
+    cy.intercept("GET", "/api/database/1", req => {
+      req.reply(res => {
+        res.body.details = null;
+      });
+    }).as("loadDatabase");
+
+    cy.visit("/admin/databases/1");
+    cy.wait("@loadDatabase");
+
+    // It is unclear how this issue will be handled,
+    // but at the very least it shouldn't render the blank page.
+    cy.get("nav").should("contain", "Metabase Admin");
+    // The response still contains the database name,
+    // so there's no reason we can't display it.
+    cy.contains(/Sample Database/i);
+    // This seems like a reasonable CTA if the database is beyond repair.
+    cy.button("Remove this database").should("not.be.disabled");
+  });
+
+  it("should show error upon a bad request", () => {
+    cy.intercept("POST", "/api/database", req => {
+      req.reply({
+        statusCode: 400,
+        body: "DATABASE CONNECTION ERROR",
+      });
+    }).as("createDatabase");
+
+    cy.visit("/admin/databases/create");
+
+    typeAndBlurUsingLabel("Display name", "Test");
+    typeAndBlurUsingLabel("Database name", "db");
+    typeAndBlurUsingLabel("Username", "admin");
+
+    cy.button("Save").click();
+    cy.wait("@createDatabase");
+
+    cy.findByText("DATABASE CONNECTION ERROR").should("exist");
+  });
+
+  it("should handle non-existing databases (metabase#11037)", () => {
+    cy.intercept("GET", "/api/database/999").as("loadDatabase");
+    cy.visit("/admin/databases/999");
+    cy.wait("@loadDatabase").then(({ response }) => {
+      expect(response.statusCode).to.eq(404);
+    });
+    cy.findByText("Not found.");
+    cy.findByRole("table").should("not.exist");
+  });
+
+  it("should handle a failure to `GET` the list of all databases (metabase#20471)", () => {
+    const errorMessage = "Lorem ipsum dolor sit amet, consectetur adip";
+
+    cy.intercept(
+      {
+        method: "GET",
+        pathname: "/api/database",
+        query: isEE
+          ? {
+              exclude_uneditable_details: "true",
+            }
+          : null,
+      },
+      req => {
+        req.reply({
+          statusCode: 500,
+          body: { message: errorMessage },
+        });
+      },
+    ).as("failedGet");
+
+    cy.visit("/admin/databases");
+    cy.wait("@failedGet");
+
+    cy.findByRole("heading", { name: "Something's gone wrong" });
+    cy.findByText(
+      "We've run into an error. You can try refreshing the page, or just go back.",
+    );
+
+    cy.findByText(errorMessage).should("not.be.visible");
+    cy.findByText("Show error details").click();
+    cy.findByText(errorMessage).should("be.visible");
+  });
+});
diff --git a/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
new file mode 100644
index 00000000000..eb0662a3aa2
--- /dev/null
+++ b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
@@ -0,0 +1,250 @@
+import { restore, popover, modal, describeEE } from "e2e/support/helpers";
+
+import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
+import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
+
+import { visitDatabase } from "./helpers/e2e-database-helpers";
+
+const { ORDERS_ID, ORDERS } = SAMPLE_DATABASE;
+
+describe("scenarios > admin > databases > sample database", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+    cy.intercept("PUT", "/api/database/*").as("databaseUpdate");
+  });
+
+  it("database settings", () => {
+    visitDatabase(SAMPLE_DB_ID);
+    // should not display a setup help card
+    cy.findByText("Need help connecting?").should("not.exist");
+
+    cy.log(
+      "should not be possible to change database type for the Sample Database (metabase#16382)",
+    );
+    cy.findByLabelText("Database type")
+      .should("have.text", "H2")
+      .and("be.disabled");
+
+    cy.log("should correctly display connection settings");
+    cy.findByLabelText("Display name").should("have.value", "Sample Database");
+    cy.findByLabelText("Connection String")
+      .should("have.attr", "value")
+      .and("contain", "sample-database.db");
+
+    cy.log("should be possible to modify the connection settings");
+    cy.findByText("Show advanced options").click();
+    // `auto_run_queries` toggle should be ON by default
+    cy.findByLabelText("Rerun queries for simple explorations")
+      .should("have.attr", "aria-checked", "true")
+      .click();
+    // Reported failing in v0.36.4
+    cy.log(
+      "should respect the settings for automatic query running (metabase#13187)",
+    );
+    cy.findByLabelText("Rerun queries for simple explorations").should(
+      "have.attr",
+      "aria-checked",
+      "false",
+    );
+
+    cy.log("change the metadata_sync period");
+    cy.findByLabelText("Choose when syncs and scans happen").click();
+    cy.findByText("Hourly").click();
+    popover().within(() => {
+      cy.findByText("Daily").click({ force: true });
+    });
+
+    // "lets you change the cache_field_values period"
+    cy.findByLabelText("Never, I'll do this manually if I need to").should(
+      "have.attr",
+      "aria-selected",
+      "true",
+    );
+
+    cy.findByLabelText("Regularly, on a schedule")
+      .click()
+      .within(() => {
+        cy.findByText("Daily").click();
+      });
+    popover().findByText("Weekly").click();
+
+    cy.button("Save changes").click();
+    cy.wait("@databaseUpdate").then(({ response: { body } }) => {
+      expect(body.details["let-user-control-scheduling"]).to.equal(true);
+      expect(body.schedules.metadata_sync.schedule_type).to.equal("daily");
+      expect(body.schedules.cache_field_values.schedule_type).to.equal(
+        "weekly",
+      );
+    });
+    cy.button("Success");
+
+    // "lets you change the cache_field_values to 'Only when adding a new filter widget'"
+    cy.findByLabelText("Only when adding a new filter widget").click();
+    cy.button("Save changes", { timeout: 10000 }).click();
+    cy.wait("@databaseUpdate").then(({ response: { body } }) => {
+      expect(body.is_full_sync).to.equal(false);
+      expect(body.is_on_demand).to.equal(true);
+    });
+
+    // and back to never
+    cy.findByLabelText("Never, I'll do this manually if I need to").click();
+    cy.button("Save changes", { timeout: 10000 }).click();
+    cy.wait("@databaseUpdate").then(({ response: { body } }) => {
+      expect(body.is_full_sync).to.equal(false);
+      expect(body.is_on_demand).to.equal(false);
+    });
+  });
+
+  it("database actions sidebar", () => {
+    cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/sync_schema`).as(
+      "sync_schema",
+    );
+    cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/rescan_values`).as(
+      "rescan_values",
+    );
+    cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/discard_values`).as(
+      "discard_values",
+    );
+    cy.intercept("GET", `/api/database/${SAMPLE_DB_ID}/usage_info`).as(
+      `usage_info`,
+    );
+    cy.intercept("DELETE", `/api/database/${SAMPLE_DB_ID}`).as("delete");
+    // model
+    cy.request("PUT", "/api/card/1", { dataset: true });
+    // Create a segment through API
+    cy.request("POST", "/api/segment", {
+      name: "Small orders",
+      description: "All orders with a total under $100.",
+      table_id: ORDERS_ID,
+      definition: {
+        "source-table": ORDERS_ID,
+        aggregation: [["count"]],
+        filter: ["<", ["field", ORDERS.TOTAL, null], 100],
+      },
+    });
+    // metric
+    cy.request("POST", "/api/metric", {
+      name: "Revenue",
+      description: "Sum of orders subtotal",
+      table_id: ORDERS_ID,
+      definition: {
+        "source-table": ORDERS_ID,
+        aggregation: [["sum", ["field", ORDERS.SUBTOTAL, null]]],
+      },
+    });
+
+    visitDatabase(SAMPLE_DB_ID);
+
+    // lets you trigger the manual database schema sync
+    cy.button("Sync database schema now").click();
+    cy.wait("@sync_schema");
+    cy.findByText("Sync triggered!");
+
+    // lets you trigger the manual rescan of field values
+    cy.findByText("Re-scan field values now").click();
+    cy.wait("@rescan_values");
+    cy.findByText("Scan triggered!");
+
+    // lets you discard saved field values
+    cy.findByText("Danger Zone")
+      .parent()
+      .as("danger")
+      .within(() => {
+        cy.button("Discard saved field values").click();
+      });
+    modal().within(() => {
+      cy.findByRole("heading").should(
+        "have.text",
+        "Discard saved field values",
+      );
+      cy.findByText("Are you sure you want to do this?");
+      cy.button("Yes").click();
+    });
+    cy.wait("@discard_values");
+
+    // lets you remove the Sample Database
+    cy.get("@danger").within(() => {
+      cy.button("Remove this database").click();
+      cy.wait("@usage_info");
+    });
+
+    modal().within(() => {
+      cy.button("Delete this content and the DB connection")
+        .as("deleteButton")
+        .should("be.disabled");
+      cy.findByLabelText(/Delete [0-9]* saved questions?/)
+        .should("not.be.checked")
+        .click()
+        .should("be.checked");
+      cy.findByLabelText(/Delete [0-9]* models?/)
+        .should("not.be.checked")
+        .click()
+        .should("be.checked");
+      cy.findByLabelText(/Delete [0-9]* metrics?/)
+        .should("not.be.checked")
+        .click()
+        .should("be.checked");
+      cy.findByLabelText(/Delete [0-9]* segments?/)
+        .should("not.be.checked")
+        .click()
+        .should("be.checked");
+      cy.findByText(
+        "This will delete every saved question, model, metric, and segment you’ve made that uses this data, and can’t be undone!",
+      );
+
+      cy.get("@deleteButton").should("be.disabled");
+
+      cy.findByPlaceholderText("Are you completely sure?")
+        .type("Sample Database")
+        .blur();
+
+      cy.intercept("GET", "/api/database").as("fetchDatabases");
+      cy.get("@deleteButton").should("be.enabled").click();
+      cy.wait(["@delete", "@fetchDatabases"]);
+    });
+
+    cy.location("pathname").should("eq", "/admin/databases/"); // FIXME why the trailing slash?
+    cy.intercept("POST", "/api/database/sample_database").as("sample_database");
+    cy.contains("Bring the sample database back", {
+      timeout: 10000,
+    }).click();
+    cy.wait("@sample_database");
+
+    cy.findAllByRole("cell").contains("Sample Database").click();
+    const newSampleDatabaseId = SAMPLE_DB_ID + 1;
+    cy.location("pathname").should(
+      "eq",
+      `/admin/databases/${newSampleDatabaseId}`,
+    );
+  });
+
+  describeEE("custom caching", () => {
+    it("should set custom cache ttl", () => {
+      cy.request("PUT", "api/setting/enable-query-caching", { value: true });
+
+      visitDatabase(SAMPLE_DB_ID).then(({ response: { body } }) => {
+        expect(body.cache_ttl).to.be.null;
+      });
+
+      cy.findByText("Show advanced options").click();
+
+      setCustomCacheTTLValue("48");
+
+      cy.button("Save changes").click();
+      cy.wait("@databaseUpdate").then(({ request, response }) => {
+        expect(request.body.cache_ttl).to.equal(48);
+        expect(response.body.cache_ttl).to.equal(48);
+      });
+
+      function setCustomCacheTTLValue(value) {
+        cy.findAllByTestId("select-button")
+          .contains("Use instance default (TTL)")
+          .click();
+
+        popover().findByText("Custom").click();
+        cy.findByDisplayValue("24").clear().type(value).blur();
+      }
+    });
+  });
+});
diff --git a/e2e/test/scenarios/admin/databases/edit.cy.spec.js b/e2e/test/scenarios/admin/databases/edit.cy.spec.js
deleted file mode 100644
index 3b7cd6a9895..00000000000
--- a/e2e/test/scenarios/admin/databases/edit.cy.spec.js
+++ /dev/null
@@ -1,272 +0,0 @@
-import {
-  restore,
-  popover,
-  modal,
-  describeEE,
-  mockSessionProperty,
-} from "e2e/support/helpers";
-
-import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
-
-describe("scenarios > admin > databases > edit", () => {
-  beforeEach(() => {
-    restore();
-    cy.signInAsAdmin();
-    cy.intercept("PUT", "/api/database/*").as("databaseUpdate");
-  });
-
-  describe("Database type", () => {
-    it("should be disabled for the Sample Dataset (metabase#16382)", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("H2").parentsUntil("a").should("be.disabled");
-    });
-  });
-
-  describe("Connection settings", () => {
-    it("shows the connection settings for sample database correctly", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByLabelText("Display name").should(
-        "have.value",
-        "Sample Database",
-      );
-      cy.findByLabelText("Connection String").should($input =>
-        expect($input[0].value).to.match(/sample-database\.db/),
-      );
-    });
-
-    it("lets you modify the connection settings", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-      cy.findByText("Show advanced options").click();
-      cy.findByLabelText("Choose when syncs and scans happen").click();
-
-      cy.findByText("Save changes").click();
-      cy.wait("@databaseUpdate").then(({ response }) =>
-        expect(response.body.details["let-user-control-scheduling"]).to.equal(
-          true,
-        ),
-      );
-
-      cy.findByText("Success");
-    });
-
-    it("`auto_run_queries` toggle should be ON by default for `SAMPLE_DATABASE`", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-      cy.findByText("Show advanced options").click();
-      cy.findByLabelText("Rerun queries for simple explorations").should(
-        "have.attr",
-        "aria-checked",
-        "true",
-      );
-    });
-
-    it("should respect the settings for automatic query running (metabase#13187)", () => {
-      cy.log("Turn off `auto run queries`");
-      cy.request("PUT", `/api/database/${SAMPLE_DB_ID}`, {
-        auto_run_queries: false,
-      });
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-      cy.log("Reported failing on v0.36.4");
-      cy.findByText("Show advanced options").click();
-      cy.findByLabelText("Rerun queries for simple explorations").should(
-        "have.attr",
-        "aria-checked",
-        "false",
-      );
-    });
-
-    describeEE("caching", () => {
-      beforeEach(() => {
-        mockSessionProperty("enable-query-caching", true);
-      });
-
-      it("allows to manage cache ttl", () => {
-        cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-        cy.findByText("Show advanced options").click();
-        cy.findByText("Use instance default (TTL)").click();
-        popover().findByText("Custom").click();
-        cy.findByDisplayValue("24").clear().type("32").blur();
-
-        cy.button("Save changes").click();
-        cy.wait("@databaseUpdate").then(({ request, response }) => {
-          expect(request.body.cache_ttl).to.equal(32);
-          expect(response.body.cache_ttl).to.equal(32);
-        });
-
-        cy.findByTextEnsureVisible("Custom").click();
-        popover().findByText("Use instance default (TTL)").click();
-
-        // We need to wait until "Success" button state is gone first
-        cy.button("Save changes", { timeout: 10000 }).click();
-        cy.wait("@databaseUpdate").then(({ request }) => {
-          expect(request.body.cache_ttl).to.equal(null);
-        });
-      });
-    });
-  });
-
-  describe("Scheduling settings", () => {
-    beforeEach(() => {
-      // Turn on scheduling without relying on the previous test(s)
-      cy.request("PUT", `/api/database/${SAMPLE_DB_ID}`, {
-        details: {
-          "let-user-control-scheduling": true,
-        },
-        engine: "h2",
-      });
-    });
-
-    it("shows the initial scheduling settings correctly", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-      cy.findByText("Show advanced options").click();
-      cy.findByText("Regularly, on a schedule").should("exist");
-      cy.findByText("Hourly").should("exist");
-      cy.findByLabelText("Regularly, on a schedule").should(
-        "have.attr",
-        "aria-selected",
-        "true",
-      );
-    });
-
-    it("lets you change the metadata_sync period", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-
-      cy.findByText("Show advanced options").click();
-
-      cy.findByText("Hourly").click();
-      popover().within(() => {
-        cy.findByText("Daily").click({ force: true });
-      });
-
-      cy.findByLabelText("Regularly, on a schedule").should(
-        "have.attr",
-        "aria-selected",
-        "true",
-      );
-
-      cy.findByText("Save changes").click();
-      cy.wait("@databaseUpdate").then(({ response }) =>
-        expect(response.body.schedules.metadata_sync.schedule_type).to.equal(
-          "daily",
-        ),
-      );
-    });
-
-    it("lets you change the cache_field_values perid", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Show advanced options").click();
-
-      cy.findByText("Regularly, on a schedule")
-        .parent()
-        .parent()
-        .within(() => {
-          cy.findByText("Daily").click();
-        });
-      popover().within(() => {
-        cy.findByText("Weekly").click({ force: true });
-      });
-
-      cy.findByText("Save changes").click();
-      cy.wait("@databaseUpdate").then(({ response }) => {
-        expect(
-          response.body.schedules.cache_field_values.schedule_type,
-        ).to.equal("weekly");
-      });
-    });
-
-    it("lets you change the cache_field_values to 'Only when adding a new filter widget'", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Show advanced options").click();
-
-      cy.findByText("Only when adding a new filter widget").click();
-      cy.findByText("Save changes").click();
-      cy.wait("@databaseUpdate").then(({ response }) => {
-        expect(response.body.is_full_sync).to.equal(false);
-        expect(response.body.is_on_demand).to.equal(true);
-      });
-    });
-
-    it("lets you change the cache_field_values to Never", () => {
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Show advanced options").click();
-
-      cy.findByText("Never, I'll do this manually if I need to").click();
-      cy.findByText("Save changes").click();
-      cy.wait("@databaseUpdate").then(({ response }) => {
-        expect(response.body.is_full_sync).to.equal(false);
-        expect(response.body.is_on_demand).to.equal(false);
-      });
-    });
-  });
-
-  describe("Actions sidebar", () => {
-    it("lets you trigger the manual database schema sync", () => {
-      cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/sync_schema`).as(
-        "sync_schema",
-      );
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Sync database schema now").click();
-      cy.wait("@sync_schema");
-      cy.findByText("Sync triggered!");
-    });
-
-    it("lets you trigger the manual rescan of field values", () => {
-      cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/rescan_values`).as(
-        "rescan_values",
-      );
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Re-scan field values now").click();
-      cy.wait("@rescan_values");
-      cy.findByText("Scan triggered!");
-    });
-
-    it("lets you discard saved field values", () => {
-      cy.intercept("POST", `/api/database/${SAMPLE_DB_ID}/discard_values`).as(
-        "discard_values",
-      );
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Discard saved field values").click();
-      cy.findByText("Yes").click();
-      cy.wait("@discard_values");
-    });
-
-    it("lets you remove the Sample Database", () => {
-      cy.intercept("DELETE", `/api/database/${SAMPLE_DB_ID}`).as("delete");
-      cy.intercept("GET", `/api/database/${SAMPLE_DB_ID}/usage_info`).as(
-        `usage_info`,
-      );
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.findByText("Remove this database").click();
-      cy.wait("@usage_info");
-
-      modal().within(() => {
-        cy.findByLabelText(/Delete [0-9]* saved questions/).click();
-        cy.findByPlaceholderText("Are you completely sure?").type(
-          "Sample Database",
-        );
-        cy.get(".Button.Button--danger").click();
-      });
-
-      cy.wait("@delete");
-      cy.url().should("match", /\/admin\/databases\/$/);
-    });
-
-    it("should not display a setup help card", () => {
-      cy.intercept("GET", `/api/database/${SAMPLE_DB_ID}`).as("loadDatabase");
-
-      cy.visit(`/admin/databases/${SAMPLE_DB_ID}`);
-      cy.wait("@loadDatabase");
-
-      cy.findByText("Need help connecting?").should("not.exist");
-    });
-  });
-});
diff --git a/e2e/test/scenarios/admin/databases/helpers/e2e-database-helpers.js b/e2e/test/scenarios/admin/databases/helpers/e2e-database-helpers.js
new file mode 100644
index 00000000000..b138ec39c07
--- /dev/null
+++ b/e2e/test/scenarios/admin/databases/helpers/e2e-database-helpers.js
@@ -0,0 +1,18 @@
+/**
+ * Visit a database and immediately wait for the related request.
+ * You can assert on the response of `GET /api/database/:id`.
+ * @param {number} id - Id of the database we're about to visit.
+ *
+ * @example
+ * visitDatabase(3)
+ *
+ * @example
+ * visitDatabase(42).then(({response: { body }})=> {
+ *  expect(body.id).to.equal(42);
+ * })
+ */
+export function visitDatabase(id) {
+  cy.intercept("GET", `/api/database/${id}`).as("loadDatabase");
+  cy.visit(`/admin/databases/${id}`);
+  return cy.wait("@loadDatabase");
+}
diff --git a/e2e/test/scenarios/admin/databases/list.cy.spec.js b/e2e/test/scenarios/admin/databases/list.cy.spec.js
deleted file mode 100644
index 1f6bf97dfc5..00000000000
--- a/e2e/test/scenarios/admin/databases/list.cy.spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { restore, describeEE, isOSS } from "e2e/support/helpers";
-import { SAMPLE_DB_ID } from "e2e/support/cypress_data";
-
-describe("scenarios > admin > databases > list", () => {
-  beforeEach(() => {
-    restore();
-    cy.signInAsAdmin();
-  });
-
-  describe("OSS", { tags: "@OSS" }, () => {
-    it("should not display error messages upon a failed `GET` (metabase#20471)", () => {
-      cy.onlyOn(isOSS);
-
-      const errorMessage = "Lorem ipsum dolor sit amet, consectetur adip";
-
-      cy.intercept(
-        {
-          method: "GET",
-          pathname: "/api/database",
-        },
-        req => {
-          req.reply({
-            statusCode: 500,
-            body: { message: errorMessage },
-          });
-        },
-      ).as("failedGet");
-
-      cy.visit("/admin/databases");
-
-      cy.wait("@failedGet");
-      // Not sure how exactly is this going the be fixed, but we should't show the full error message on the page in any case
-      cy.findByText(errorMessage).should("not.be.visible");
-    });
-  });
-
-  describeEE("EE", () => {
-    it("should not display error messages upon a failed `GET` (metabase#20471)", () => {
-      const errorMessage = "Lorem ipsum dolor sit amet, consectetur adip";
-
-      cy.intercept(
-        {
-          method: "GET",
-          pathname: "/api/database",
-          query: {
-            exclude_uneditable_details: "true",
-          },
-        },
-        req => {
-          req.reply({
-            statusCode: 500,
-            body: { message: errorMessage },
-          });
-        },
-      ).as("failedGet");
-
-      cy.visit("/admin/databases");
-
-      cy.wait("@failedGet");
-      // Not sure how exactly is this going the be fixed, but we should't show the full error message on the page in any case
-      cy.findByText(errorMessage).should("not.be.visible");
-    });
-  });
-
-  it("should let you see databases in list view", () => {
-    cy.visit("/admin/databases");
-    cy.findByText("Sample Database");
-    cy.findByText("H2");
-  });
-
-  it("should not let you see saved questions in the database list", () => {
-    cy.visit("/admin/databases");
-    cy.get("tr").should("have.length", 2);
-  });
-
-  it("should let you view a database's detail view", () => {
-    cy.visit("/admin/databases");
-    cy.contains("Sample Database").click();
-    cy.url().should("match", /\/admin\/databases\/\d+$/);
-  });
-
-  it("should let you add a database", () => {
-    cy.visit("/admin/databases");
-    cy.contains("Add database").click();
-    cy.url().should("match", /\/admin\/databases\/create$/);
-    // *** code here should be more thorough
-  });
-
-  it("should let you access edit page a database", () => {
-    cy.visit("/admin/databases");
-    cy.contains("Sample Database").click();
-    cy.location("pathname").should("eq", `/admin/databases/${SAMPLE_DB_ID}`);
-  });
-
-  it("should let you bring back the sample database", () => {
-    cy.intercept("POST", "/api/database/sample_database").as("sample_database");
-
-    cy.request("DELETE", `/api/database/${SAMPLE_DB_ID}`).as("delete");
-    cy.visit("/admin/databases");
-    cy.contains("Bring the sample database back").click();
-    cy.wait("@sample_database");
-    cy.contains("Sample Database").click();
-    cy.url().should("match", /\/admin\/databases\/\d+$/);
-  });
-
-  it("should handle malformed (null) database details (metabase#25715)", () => {
-    cy.intercept("GET", "/api/database/1", req => {
-      req.reply(res => {
-        res.body.details = null;
-      });
-    }).as("loadDatabase");
-
-    cy.visit("/admin/databases/1");
-    cy.wait("@loadDatabase");
-
-    // It is unclear how this issue will be handled,
-    // but at the very least it shouldn't render the blank page.
-    cy.get("nav").should("contain", "Metabase Admin");
-    // The response still contains the database name,
-    // so there's no reason we can't display it.
-    cy.contains(/Sample Database/i);
-    // This seems like a reasonable CTA if the database is beyond repair.
-    cy.button("Remove this database");
-  });
-});
diff --git a/e2e/test/scenarios/admin/settings/spinner.cy.spec.js b/e2e/test/scenarios/admin/settings/spinner.cy.spec.js
deleted file mode 100644
index 4654e998b19..00000000000
--- a/e2e/test/scenarios/admin/settings/spinner.cy.spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { restore } from "e2e/support/helpers";
-
-describe("scenarios > admin > spinner", () => {
-  beforeEach(() => {
-    restore();
-    cy.signInAsAdmin();
-  });
-
-  describe("API request", () => {
-    it("should return correct DB", () => {
-      cy.visit("/admin/databases/1");
-      cy.findByText("Sample Database");
-      cy.findByText("Add Database").should("not.exist");
-    });
-
-    it("should not spin forever if it returns an error (metabase#11037)", () => {
-      cy.visit("/admin/databases/999");
-      cy.findAllByText("Databases").should("have.length", 2);
-      cy.findByText("Loading...").should("not.exist");
-      cy.findByText("Not found.");
-    });
-  });
-});
-- 
GitLab