diff --git a/e2e/test/scenarios/admin/databases/actions.cy.spec.js b/e2e/test/scenarios/admin/databases/actions.cy.spec.js index 77d777eb0450d971d4f1ad8dcae8b558c9258259..5790e3c53fb1800d96d315fcf1b77b4986b86817 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 2b110a2c4f8054bf25d20379b3e1d800c4258bad..64936179deec9080d8ac6124ff4a147d228ca927 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 6e2f831dee17148cacead1c9791a9230529f15d5..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..f24424a50c9f715c48b46dd743baef4473949b53 --- /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 0000000000000000000000000000000000000000..eb0662a3aa229b1f097b44639729f5860c1e535e --- /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 3b7cd6a9895e93fcc9f0de170751a9b745eaa5cd..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..b138ec39c073bc9b9e8c57e20d989eb6136b5849 --- /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 1f6bf97dfc5c413358b47a645da8799a0ec697a8..0000000000000000000000000000000000000000 --- 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 4654e998b19495520f54bce6b2a7cfa04f33fcf4..0000000000000000000000000000000000000000 --- 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."); - }); - }); -});