diff --git a/e2e/support/helpers/e2e-qa-databases-helpers.js b/e2e/support/helpers/e2e-qa-databases-helpers.js index 2f96be93fe0e49376ca5fccc4f7ed2e24422af6a..c77b405273e83dad1470e15b7df727322e0ad53b 100644 --- a/e2e/support/helpers/e2e-qa-databases-helpers.js +++ b/e2e/support/helpers/e2e-qa-databases-helpers.js @@ -185,6 +185,29 @@ export function getTableId({ databaseId = WRITABLE_DB_ID, name }) { }); } +export const createModelFromTableName = ({ + tableName, + modelName = "Test Action Model", + idAlias = 'modelId' +}) => { + getTableId({ name: tableName }).then(tableId => { + cy.createQuestion( + { + database: WRITABLE_DB_ID, + name: modelName, + query: { + "source-table": tableId, + }, + dataset: true, + }, + { + wrapId: true, + idAlias, + }, + ); + }); +}; + export function waitForSyncToFinish({ iteration = 0, dbId = 2, tableName = '' }) { // 100 x 100ms should be plenty of time for the sync to finish. if (iteration === 100) { diff --git a/e2e/test/scenarios/dashboard/actions-on-dashboards.cy.spec.js b/e2e/test/scenarios/dashboard/actions-on-dashboards.cy.spec.js index 11e97d18e6fd6ac6b8a50528b790adc51bec338a..843ac1ce54666661b2c039d6da9938a35990cee2 100644 --- a/e2e/test/scenarios/dashboard/actions-on-dashboards.cy.spec.js +++ b/e2e/test/scenarios/dashboard/actions-on-dashboards.cy.spec.js @@ -2,7 +2,7 @@ import { restore, queryWritableDB, resetTestTable, - getTableId, + createModelFromTableName, fillActionQuery, resyncDatabase, visitDashboard, @@ -55,6 +55,7 @@ const MODEL_NAME = "Test Action Model"; restore(`${dialect}-writable`); cy.signInAsAdmin(); resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE }); + createModelFromTableName({ tableName: TEST_TABLE, modelName: MODEL_NAME }); }); it("adds a custom query action to a dashboard and runs it", () => { @@ -68,8 +69,6 @@ const MODEL_NAME = "Test Action Model"; expect(result.rows[0].score).to.equal(0); }); - createModelFromTable(TEST_TABLE); - cy.get("@modelId").then(id => { cy.visit(`/model/${id}/detail`); cy.wait(["@getModel", "@getModelActions", "@getCardAssociations"]); @@ -124,7 +123,6 @@ const MODEL_NAME = "Test Action Model"; }); it("adds an implicit create action to a dashboard and runs it", () => { - createModelFromTable(TEST_TABLE); cy.get("@modelId").then(id => { createImplicitAction({ kind: "create", @@ -160,8 +158,6 @@ const MODEL_NAME = "Test Action Model"; it("adds an implicit update action to a dashboard and runs it", () => { const actionName = "Update"; - createModelFromTable(TEST_TABLE); - cy.get("@modelId").then(id => { createImplicitAction({ kind: "update", @@ -216,8 +212,6 @@ const MODEL_NAME = "Test Action Model"; expect(result.rows[0].id).to.equal(3); }); - createModelFromTable(TEST_TABLE); - cy.get("@modelId").then(id => { createImplicitAction({ kind: "delete", @@ -252,11 +246,11 @@ const MODEL_NAME = "Test Action Model"; resetTestTable({ type: dialect, table: TEST_COLUMNS_TABLE }); restore(`${dialect}-writable`); cy.signInAsAdmin(); - resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_TABLE }); + resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: TEST_COLUMNS_TABLE }); + createModelFromTableName({ tableName: TEST_COLUMNS_TABLE, modelName: MODEL_NAME }); }); it("can update various data types via implicit actions", () => { - createModelFromTable(TEST_COLUMNS_TABLE); cy.get("@modelId").then(id => { createImplicitAction({ kind: "update", @@ -353,7 +347,6 @@ const MODEL_NAME = "Test Action Model"; }); it("can insert various data types via implicit actions", () => { - createModelFromTable(TEST_COLUMNS_TABLE); cy.get("@modelId").then(id => { createImplicitAction({ kind: "create", @@ -439,7 +432,6 @@ const MODEL_NAME = "Test Action Model"; }); it("does not show json, enum, or binary columns for implicit actions", () => { - createModelFromTable(TEST_COLUMNS_TABLE); cy.get("@modelId").then(id => { createImplicitAction({ kind: "create", @@ -468,7 +460,6 @@ const MODEL_NAME = "Test Action Model"; }); it("properly loads and updates date and time fields for implicit update actions", () => { - createModelFromTable(TEST_COLUMNS_TABLE); cy.get("@modelId").then(id => { createImplicitAction({ kind: "update", @@ -581,25 +572,6 @@ const MODEL_NAME = "Test Action Model"; }); }); -const createModelFromTable = tableName => { - getTableId({ name: tableName }).then(tableId => { - cy.createQuestion( - { - database: WRITABLE_DB_ID, - name: MODEL_NAME, - query: { - "source-table": tableId, - }, - dataset: true, - }, - { - wrapId: true, - idAlias: "modelId", - }, - ); - }); -}; - function createDashboardWithActionButton({ actionName, modelName = MODEL_NAME, diff --git a/e2e/test/scenarios/models/model-actions.cy.spec.js b/e2e/test/scenarios/models/model-actions.cy.spec.js index 07868197f5ba6cc4115c45b3de992bcd5dc5e23d..f1911d575bfa4ae20c05699bca21d39c32f08740 100644 --- a/e2e/test/scenarios/models/model-actions.cy.spec.js +++ b/e2e/test/scenarios/models/model-actions.cy.spec.js @@ -1,3 +1,4 @@ +import { assocIn } from "icepick"; import { setActionsEnabledForDB, modal, @@ -7,13 +8,19 @@ import { createAction, navigationSidebar, openNavigationSidebar, + resetTestTable, + resyncDatabase, + createModelFromTableName, + queryWritableDB, } from "e2e/support/helpers"; -import { SAMPLE_DB_ID, USER_GROUPS } from "e2e/support/cypress_data"; + +import { SAMPLE_DB_ID, USER_GROUPS, WRITABLE_DB_ID } from "e2e/support/cypress_data"; import { createMockActionParameter } from "metabase-types/api/mocks"; const PG_DB_ID = 2; const PG_ORDERS_TABLE_ID = 9; +const WRITABLE_TEST_TABLE = "scoreboard_actions"; const SAMPLE_ORDERS_MODEL = { name: "Order", @@ -68,6 +75,12 @@ const SAMPLE_QUERY_ACTION = { }, }; +const SAMPLE_WRITABLE_QUERY_ACTION = assocIn( + SAMPLE_QUERY_ACTION, + ["dataset_query", "native", "query"], + `UPDATE ${WRITABLE_TEST_TABLE} SET score = 22 WHERE id = {{ ${TEST_TEMPLATE_TAG.name} }}`, +); + describe( "scenarios > models > actions", { tags: ["@external", "@actions"] }, @@ -193,105 +206,6 @@ describe( cy.findByText(QUERY).should("be.visible"); }); - it("should allow to execute actions from the model page", () => { - cy.get("@modelId").then(modelId => { - createAction({ - ...SAMPLE_QUERY_ACTION, - model_id: modelId, - }); - cy.visit(`/model/${modelId}/detail/actions`); - cy.wait("@getModel"); - }); - - runActionFor(SAMPLE_QUERY_ACTION.name); - - modal().within(() => { - cy.findByLabelText(TEST_PARAMETER.name).type("1"); - cy.button(SAMPLE_QUERY_ACTION.name).click(); - }); - - cy.findByText(`${SAMPLE_QUERY_ACTION.name} ran successfully`).should( - "be.visible", - ); - }); - - it("should allow to make actions public and execute them", () => { - const IMPLICIT_ACTION_NAME = "Update order"; - - cy.get("@modelId").then(modelId => { - createAction({ - ...SAMPLE_QUERY_ACTION, - model_id: modelId, - }); - createAction({ - type: "implicit", - kind: "row/update", - name: IMPLICIT_ACTION_NAME, - model_id: modelId, - }); - cy.visit(`/model/${modelId}/detail/actions`); - cy.wait("@getModel"); - }); - - enableSharingFor(SAMPLE_QUERY_ACTION.name, { - publicUrlAlias: "queryActionPublicUrl", - }); - enableSharingFor(IMPLICIT_ACTION_NAME, { - publicUrlAlias: "implicitActionPublicUrl", - }); - - cy.signOut(); - - cy.get("@queryActionPublicUrl").then(url => { - cy.visit(url); - cy.findByLabelText(TEST_PARAMETER.name).type("1"); - cy.button(SAMPLE_QUERY_ACTION.name).click(); - cy.findByText(`${SAMPLE_QUERY_ACTION.name} ran successfully`).should( - "be.visible", - ); - cy.findByRole("form").should("not.exist"); - cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist"); - }); - - cy.get("@implicitActionPublicUrl").then(url => { - cy.visit(url); - - // Order 1 has quantity 2 by default, so we're not actually mutating data - cy.findByLabelText("Id").type("1"); - cy.findByLabelText(/quantity/i).type("2"); - - cy.button(IMPLICIT_ACTION_NAME).click(); - cy.findByText(`${IMPLICIT_ACTION_NAME} ran successfully`).should( - "be.visible", - ); - cy.findByRole("form").should("not.exist"); - cy.button(IMPLICIT_ACTION_NAME).should("not.exist"); - }); - - cy.signInAsAdmin(); - cy.get("@modelId").then(modelId => { - cy.visit(`/model/${modelId}/detail/actions`); - cy.wait("@getModel"); - }); - - disableSharingFor(SAMPLE_QUERY_ACTION.name); - disableSharingFor(IMPLICIT_ACTION_NAME); - - cy.get("@queryActionPublicUrl").then(url => { - cy.visit(url); - cy.findByRole("form").should("not.exist"); - cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist"); - cy.findByText("Not found").should("be.visible"); - }); - - cy.get("@implicitActionPublicUrl").then(url => { - cy.visit(url); - cy.findByRole("form").should("not.exist"); - cy.button(IMPLICIT_ACTION_NAME).should("not.exist"); - cy.findByText("Not found").should("be.visible"); - }); - }); - it("should respect permissions", () => { // Enabling actions for sample database as well // to test database picker behavior in the action editor @@ -381,6 +295,163 @@ describe( }, ); +['postgres', 'mysql'].forEach((dialect) => { + describe(`Write actions on model detail page (${dialect})`, () => { + + beforeEach(() => { + cy.intercept("GET", "/api/card/*").as("getModel"); + + resetTestTable({ type: dialect, table: WRITABLE_TEST_TABLE }); + restore(`${dialect}-writable`); + cy.signInAsAdmin(); + resyncDatabase({ dbId: WRITABLE_DB_ID, tableName: WRITABLE_TEST_TABLE }); + + createModelFromTableName({ tableName: WRITABLE_TEST_TABLE, idAlias: "writableModelId" }); + }); + + it("should allow action execution from the model detail page", () => { + queryWritableDB( + `SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`, + dialect, + ).then(result => { + const row = result.rows[0]; + expect(row.score).to.equal(0); + }); + + cy.get("@writableModelId").then(modelId => { + createAction({ + ...SAMPLE_WRITABLE_QUERY_ACTION, + model_id: modelId, + }); + cy.visit(`/model/${modelId}/detail/actions`); + cy.wait("@getModel"); + }); + + runActionFor(SAMPLE_QUERY_ACTION.name); + + modal().within(() => { + cy.findByLabelText(TEST_PARAMETER.name).type("1"); + cy.button(SAMPLE_QUERY_ACTION.name).click(); + }); + + cy.findByText(`${SAMPLE_QUERY_ACTION.name} ran successfully`).should( + "be.visible", + ); + + queryWritableDB( + `SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`, + dialect, + ).then(result => { + const row = result.rows[0]; + + expect(row.score).to.equal(22); + }); + }); + + it("should allow public sharing of actions and execution of public actions", () => { + const IMPLICIT_ACTION_NAME = "Update"; + + cy.get("@writableModelId").then(modelId => { + createAction({ + ...SAMPLE_WRITABLE_QUERY_ACTION, + model_id: modelId, + }); + createAction({ + type: "implicit", + kind: "row/update", + name: IMPLICIT_ACTION_NAME, + model_id: modelId, + }); + cy.visit(`/model/${modelId}/detail/actions`); + cy.wait("@getModel"); + }); + + enableSharingFor(SAMPLE_WRITABLE_QUERY_ACTION.name, { + publicUrlAlias: "queryActionPublicUrl", + }); + enableSharingFor(IMPLICIT_ACTION_NAME, { + publicUrlAlias: "implicitActionPublicUrl", + }); + + cy.signOut(); + + cy.get("@queryActionPublicUrl").then(url => { + cy.visit(url); + cy.findByLabelText(TEST_PARAMETER.name).type("1"); + cy.button(SAMPLE_QUERY_ACTION.name).click(); + cy.findByText(`${SAMPLE_WRITABLE_QUERY_ACTION.name} ran successfully`).should( + "be.visible", + ); + cy.findByRole("form").should("not.exist"); + cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist"); + + queryWritableDB( + `SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 1`, + dialect, + ).then(result => { + const row = result.rows[0]; + + expect(row.score).to.equal(22); + }); + }); + + cy.get("@implicitActionPublicUrl").then(url => { + cy.visit(url); + + // team 2 has 10 points, let's give them more + cy.findByLabelText("Id").type("2"); + cy.findByLabelText(/score/i).type("16"); + cy.findByLabelText(/team name/i).type("Bouncy Bears"); + + + cy.button(IMPLICIT_ACTION_NAME).click(); + cy.findByText(`${IMPLICIT_ACTION_NAME} ran successfully`).should( + "be.visible", + ); + cy.findByRole("form").should("not.exist"); + cy.button(IMPLICIT_ACTION_NAME).should("not.exist"); + + queryWritableDB( + `SELECT * FROM ${WRITABLE_TEST_TABLE} WHERE id = 2`, + dialect, + ).then(result => { + const row = result.rows[0]; + + expect(row.score).to.equal(16); + expect(row.team_name).to.equal("Bouncy Bears"); + // should not mutate form fields that we don't touch + expect(row.status).to.not.be.a('null'); + }); + }); + + cy.signInAsAdmin(); + cy.get("@writableModelId").then(modelId => { + cy.visit(`/model/${modelId}/detail/actions`); + cy.wait("@getModel"); + }); + + disableSharingFor(SAMPLE_QUERY_ACTION.name); + disableSharingFor(IMPLICIT_ACTION_NAME); + + cy.signOut(); + + cy.get("@queryActionPublicUrl").then(url => { + cy.visit(url); + cy.findByRole("form").should("not.exist"); + cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist"); + cy.findByText("Not found").should("be.visible"); + }); + + cy.get("@implicitActionPublicUrl").then(url => { + cy.visit(url); + cy.findByRole("form").should("not.exist"); + cy.button(SAMPLE_QUERY_ACTION.name).should("not.exist"); + cy.findByText("Not found").should("be.visible"); + }); + }); + }); +}); + function runActionFor(actionName) { cy.findByRole("listitem", { name: actionName }).within(() => { cy.icon("play").click();