Skip to content
Snippets Groups Projects
Unverified Commit e6d30aef authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

[E2E] Embedding feature flag (#32455)

* Adapt and improve embedding smoketests

This commit deals with the 'embedding is not yet enabled' section.

* Add test ids to embedding page

* Adapt and improve embedding smoketests

This commit deals with the 'embedding enabled' section.

* Expand embedding smoketests

- Make sure we can regenerate the token
- Make sure the old embedding url doesn't work after that

* Run full-app embedding tests only with token

* Improve assertions
parent d687abea
No related branches found
No related tags found
No related merge requests found
......@@ -3,13 +3,16 @@ import {
popover,
appBar,
restore,
setTokenFeatures,
describeEE,
} from "e2e/support/helpers";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
describe("scenarios > embedding > full app", () => {
describeEE("scenarios > embedding > full app", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
setTokenFeatures("all");
cy.intercept("POST", `/api/card/*/query`).as("getCardQuery");
cy.intercept("POST", "/api/dashboard/**/query").as("getDashCardQuery");
cy.intercept("GET", `/api/dashboard/*`).as("getDashboard");
......@@ -17,19 +20,30 @@ describe("scenarios > embedding > full app", () => {
});
describe("home page navigation", () => {
it("should show the top and side nav by default", () => {
visitUrl({ url: "/" });
cy.wait("@getXrayDashboard");
appBar()
.should("be.visible")
.within(() => {
cy.findByTestId("main-logo").should("be.visible");
cy.button(/New/).should("not.exist");
cy.findByPlaceholderText("Search").should("not.exist");
});
sideNav().should("be.visible");
});
it("should hide the top nav when nothing is shown", () => {
visitUrl({ url: "/", qs: { side_nav: false, logo: false } });
cy.wait("@getXrayDashboard");
appBar().should("not.exist");
});
it("should show the top nav by default", () => {
visitUrl({ url: "/" });
appBar().should("be.visible");
cy.findByTestId("main-logo").should("be.visible");
});
it("should hide the top nav by a param", () => {
it("should hide the top nav by an explicit param", () => {
visitUrl({ url: "/", qs: { top_nav: false } });
cy.wait("@getXrayDashboard");
appBar().should("not.exist");
});
......@@ -38,8 +52,10 @@ describe("scenarios > embedding > full app", () => {
url: "/question/" + ORDERS_QUESTION_ID,
qs: { breadcrumbs: false },
});
cy.findByTestId("main-logo").should("be.visible");
cy.wait("@getCardQuery");
appBar().within(() => {
cy.findByTestId("main-logo").should("be.visible");
cy.findByText("Our analytics").should("not.exist");
});
});
......@@ -54,28 +70,24 @@ describe("scenarios > embedding > full app", () => {
new_button: false,
},
});
cy.wait("@getXrayDashboard");
appBar().should("be.visible");
cy.button("Toggle sidebar").should("be.visible");
});
it("should show the top nav by a param", () => {
visitUrl({ url: "/" });
appBar().should("be.visible");
cy.findByTestId("main-logo").should("be.visible");
appBar().within(() => {
cy.button(/New/).should("not.exist");
cy.findByPlaceholderText("Search").should("not.exist");
});
sideNav().should("be.visible");
appBar()
.should("be.visible")
.within(() => {
cy.button("Toggle sidebar").should("be.visible").click();
});
sideNav().should("not.be.visible");
});
it("should hide the side nav by a param", () => {
visitUrl({ url: "/", qs: { side_nav: false } });
appBar().within(() => {
cy.findByTestId("main-logo").should("be.visible");
cy.button("Toggle sidebar").should("not.exist");
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Our analytics").should("not.exist");
sideNav().should("not.exist");
});
it("should show question creation controls by a param", () => {
......@@ -99,10 +111,11 @@ describe("scenarios > embedding > full app", () => {
cy.findByPlaceholderText("Search…").should("be.visible");
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Our analytics").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Orders in a dashboard").should("be.visible");
sideNav().findByText("Our analytics").click();
cy.findAllByRole("rowgroup")
.should("contain", "Orders in a dashboard")
.and("be.visible");
appBar().within(() => {
cy.findByPlaceholderText("Search…").should("be.visible");
......@@ -365,3 +378,7 @@ const addLinkClickBehavior = ({ dashboardId, linkTemplate }) => {
});
});
};
const sideNav = () => {
return cy.findByTestId("main-navbar-root");
};
import {
restore,
visitQuestion,
isEE,
isOSS,
visitDashboard,
setTokenFeatures,
modal,
visitIframe,
} from "e2e/support/helpers";
import { METABASE_SECRET_KEY } from "e2e/support/cypress_data";
import { ORDERS_QUESTION_ID } from "e2e/support/cypress_sample_instance_data";
const embeddingPage = "/admin/settings/embedding-in-other-applications";
const standalonePath =
"/admin/settings/embedding-in-other-applications/standalone";
const licenseUrl = "https://metabase.com/license/embedding";
const upgradeUrl = "https://www.metabase.com/upgrade";
const learnEmbeddingUrl =
"https://www.metabase.com/learn/embedding/embedding-charts-and-dashboards.html";
const embeddingDescription =
"Embed dashboards, questions, or the entire Metabase app into your application. Integrate with your server code to create a secure environment, limited to specific users or organizations.";
const licenseExplanations = [
`When you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds.`,
`Your should, however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.`,
];
describe("scenarios > embedding > smoke tests", () => {
// These tests will run on both OSS and EE instances. Both without a token!
describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
});
it("should not offer to share or embed models (metabase#20815)", () => {
cy.intercept("POST", "/api/dataset").as("dataset");
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
cy.visit(`/model/${ORDERS_QUESTION_ID}`);
cy.wait("@dataset");
cy.findByTestId("view-footer").within(() => {
cy.icon("download").should("exist");
cy.icon("bell").should("exist");
cy.icon("share").should("not.exist");
});
});
context("embedding disabled", () => {
beforeEach(() => {
// We enable embedding by default in the default snapshot that all tests are using.
......@@ -33,92 +52,140 @@ describe("scenarios > embedding > smoke tests", () => {
resetEmbedding();
});
it("should display the embedding page correctly", { tags: "@OSS" }, () => {
it("should display the embedding page correctly", () => {
cy.visit("/admin/settings/setup");
sidebar().within(() => {
cy.findByText("Embedding").click();
cy.findByRole("link", { name: "Embedding" }).click();
});
cy.location("pathname").should("eq", embeddingPage);
// Some info we provide to users before they enable embedding
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("More details");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("By enabling embedding you're agreeing to");
assertLinkMatchesUrl("our embedding license.", licenseUrl);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("More details").click();
licenseExplanations.forEach(licenseExplanation => {
cy.findByText(licenseExplanation);
cy.findByRole("heading", { name: "Embedding" });
cy.findByTestId("enable-embedding-setting").within(() => {
// Some info we provide to users before they enable embedding
cy.findByText(embeddingDescription);
cy.contains("By enabling embedding you're agreeing to");
assertLinkMatchesUrl("our embedding license.", licenseUrl);
cy.findByRole("tab")
.should("have.attr", "aria-expanded", "false")
.findByText("More details")
.click();
cy.findByRole("tab")
.should("have.attr", "aria-expanded", "true")
.within(() => {
licenseExplanations.forEach(licenseExplanation => {
cy.findByText(licenseExplanation);
});
});
cy.button("Enable").click();
});
cy.button("Enable").click();
// Let's examine the contents of the enabled embedding page (the url stays the same)
// The URL should stay the same
cy.location("pathname").should("eq", embeddingPage);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains(
"Allow questions, dashboards, and more to be embedded. Learn more.",
);
assertLinkMatchesUrl("Learn more.", learnEmbeddingUrl);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Enabled");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Standalone embeds").click();
if (isOSS) {
cy.findByTestId("enable-embedding-setting").within(() => {
cy.contains(
"In order to remove the Metabase logo from embeds, you can always upgrade to one of our paid plans.",
"Allow questions, dashboards, and more to be embedded. Learn more.",
);
assertLinkMatchesUrl("Learn more.", learnEmbeddingUrl);
assertLinkMatchesUrl("one of our paid plans.", upgradeUrl);
}
cy.findByRole("switch")
.should("be.checked")
.siblings()
.should("have.text", "Enabled");
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/Embedding secret key/i);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(
"Standalone Embed Secret Key used to sign JSON Web Tokens for requests to /api/embed endpoints. This lets you create a secure environment limited to specific users or organizations.",
cy.log(
"With the embedding enabled, we should now see two new sections on the main page",
);
cy.log("The first section: 'Standalone embeds'");
cy.findByTestId("-standalone-embeds-setting").within(() => {
cy.findByRole("link")
.should("have.attr", "href")
.and("eq", standalonePath);
cy.findByText("Standalone embeds");
cy.findByText(
"Securely embed individual questions and dashboards within other applications.",
);
cy.findByText("More details").click();
cy.location("pathname").should("eq", standalonePath);
});
cy.log("Standalone embeds page");
mainPage().within(() => {
cy.findByTestId("embedding-secret-key-setting").within(() => {
cy.findByText(/Embedding secret key/i);
cy.findByText(
"Standalone Embed Secret Key used to sign JSON Web Tokens for requests to /api/embed endpoints. This lets you create a secure environment limited to specific users or organizations.",
);
getTokenValue().should("have.length", 64);
cy.button("Regenerate key");
});
cy.findByTestId("-embedded-dashboards-setting").within(() => {
cy.findByText(/Embedded dashboards/i);
cy.findByText("No dashboards have been embedded yet.");
});
getTokenValue().should("have.length", 64);
cy.findByTestId("-embedded-questions-setting")
.within(() => {
cy.findByText(/Embedded questions/i);
cy.findByText("No questions have been embedded yet.");
})
.next()
.within(() => {
// FE unit tests are making sure this section doesn't exist when a valid token is provided,
// so we don't have to do it here usign a conditional logic
cy.contains(
"In order to remove the Metabase logo from embeds, you can always upgrade to one of our paid plans.",
);
assertLinkMatchesUrl("one of our paid plans.", upgradeUrl);
});
});
cy.button("Regenerate key");
cy.go("back");
cy.location("pathname").should("eq", embeddingPage);
// List of all embedded dashboards and questions
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/Embedded dashboards/i);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("No dashboards have been embedded yet.");
cy.log("The second section: 'Full-app embedding'");
cy.findByTestId("-full-app-embedding-setting").within(() => {
const fullAppEmbeddingPath =
"/admin/settings/embedding-in-other-applications/full-app";
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(/Embedded questions/i);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("No questions have been embedded yet.");
cy.findByRole("link")
.should("have.attr", "href")
.and("eq", fullAppEmbeddingPath);
// Full app embedding section (available only for EE version and in PRO hosted plans)
if (isEE) {
sidebar().within(() => {
cy.findByText("Embedding").click();
});
cy.findByText("Full-app embedding").click();
cy.findByText(/Paid/i);
cy.findByText("Full-app embedding");
cy.findByText(
"With this Pro/Enterprise feature you can embed the full Metabase app. Enable your users to drill-through to charts, browse collections, and use the graphical query builder.",
);
cy.findByText("More details").click();
cy.location("pathname").should("eq", fullAppEmbeddingPath);
});
cy.log("Full-app embedding page");
mainPage().within(() => {
cy.findByText(/Embedding the entire Metabase app/i);
// Full app embedding is only available for specific premium tokens
cy.contains(
"With some of our paid plans, you can embed the full Metabase app and enable your users to drill-through to charts, browse collections, and use the graphical query builder. You can also get priority support, more tools to help you share your insights with your teams and powerful options to help you create seamless, interactive data experiences for your customers.",
);
cy.findByTestId("embedding-app-origin-setting").should("not.exist");
cy.contains(
"Enter the origins for the websites or web apps where you want to allow embedding, separated by a space. Here are the exact specifications for what can be entered.",
).should("not.exist");
cy.findByPlaceholderText("https://*.example.com").should("not.exist");
}
});
});
it("should not let you embed the question", () => {
visitQuestion("1");
visitQuestion(ORDERS_QUESTION_ID);
cy.icon("share").click();
ensureEmbeddingIsDisabled();
......@@ -126,14 +193,13 @@ describe("scenarios > embedding > smoke tests", () => {
it("should not let you embed the dashboard", () => {
visitDashboard(1);
cy.icon("share").click();
ensureEmbeddingIsDisabled();
});
});
context("embedding enabled", { tags: "@OSS" }, () => {
context("embedding enabled", () => {
["question", "dashboard"].forEach(object => {
it(`should be able to publish/embed and then unpublish a ${object} without filters`, () => {
const embeddableObject = object === "question" ? "card" : "dashboard";
......@@ -144,103 +210,140 @@ describe("scenarios > embedding > smoke tests", () => {
cy.intercept("GET", `/api/${embeddableObject}/embeddable`).as(
"currentlyEmbeddedObject",
);
isEE && setTokenFeatures("all");
visitAndEnableSharing(object);
if (isEE) {
cy.findByText("Font");
}
if (isOSS) {
cy.findByText("Font").should("not.exist");
}
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Parameters");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(
/This (question|dashboard) doesn't have any parameters to configure yet./,
);
cy.findByTestId("embedding-settings").within(() => {
cy.findByRole("heading", { name: "Style" });
cy.findByRole("heading", { name: "Appearance" });
cy.findByRole("heading", { name: "Font" }).should("not.exist");
cy.findByRole("heading", { name: "Download data" }).should(
"not.exist",
);
cy.findByText("Parameters");
cy.findByText(
/This (question|dashboard) doesn't have any parameters to configure yet./,
);
cy.findByText("Parameters");
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(
/You will need to publish this (question|dashboard) before you can embed it in another application./,
);
cy.findByTestId("embedding-preview").within(() => {
cy.findByText(
/You will need to publish this (question|dashboard) before you can embed it in another application./,
);
cy.button("Publish").click();
cy.wait("@embedObject");
cy.button("Publish").click();
cy.wait("@embedObject");
});
visitIframe();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains(objectName);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("37.65");
if (isEE) {
cy.contains("Powered by Metabase").should("not.exist");
} else {
cy.contains("Powered by Metabase")
.closest("a")
.should("have.attr", "href")
cy.findByTestId("embed-frame").within(() => {
cy.findByRole("heading", { name: objectName });
cy.get(".cellData").contains("37.65");
});
cy.findByRole("contentinfo").within(() => {
cy.findByRole("link")
.should("have.text", "Powered by Metabase")
.and("have.attr", "href")
.and("eq", "https://metabase.com/");
}
});
cy.log(
`Make sure the ${object} shows up in the standalone embeds page`,
);
cy.signInAsAdmin();
cy.visit(embeddingPage);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Standalone embeds").click();
cy.visit(standalonePath);
cy.wait("@currentlyEmbeddedObject");
const sectionName = new RegExp(`Embedded ${object}s`, "i");
const sectionTestId = new RegExp(`-embedded-${object}s-setting`);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains(sectionName)
.closest("li")
cy.findByTestId(sectionTestId)
.find("tbody tr")
.should("have.length", 1)
.and("contain", objectName);
cy.log(`Unpublish ${object}`);
visitAndEnableSharing(object);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Danger zone");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(
/This will disable embedding for this (question|dashboard)./,
);
cy.button("Unpublish").click();
cy.wait("@embedObject");
cy.findByTestId("embedding-settings").within(() => {
cy.findByRole("heading", { name: "Danger zone" });
cy.findByText(`This will disable embedding for this ${object}.`);
cy.button("Unpublish").click();
cy.wait("@embedObject");
});
visitIframe();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Embedding is not enabled for this object.");
cy.findByTestId("embed-frame").findByText(
"Embedding is not enabled for this object.",
);
cy.signInAsAdmin();
cy.visit(embeddingPage);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Standalone embeds").click();
cy.visit(standalonePath);
cy.wait("@currentlyEmbeddedObject");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains(/No (questions|dashboards) have been embedded yet./);
mainPage()
.findAllByText(/No (questions|dashboards) have been embedded yet./)
.should("have.length", 2);
});
});
});
it("should not offer to share or embed models (metabase#20815)", () => {
cy.intercept("POST", "/api/dataset").as("dataset");
it("should regenerate embedding token and invalidate previous embed url", () => {
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
enable_embedding: true,
});
visitAndEnableSharing("question");
cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
cy.document().then(doc => {
const iframe = doc.querySelector("iframe");
cy.visit(`/model/${ORDERS_QUESTION_ID}`);
cy.wait("@dataset");
cy.signOut();
cy.visit(iframe.src);
cy.findByTestId("embed-frame").contains("37.65");
cy.signInAsAdmin();
cy.visit(standalonePath);
cy.findByLabelText("Embedding secret key").should(
"have.value",
METABASE_SECRET_KEY,
);
cy.button("Regenerate key").click();
cy.icon("share").should("not.exist");
modal().within(() => {
cy.intercept("GET", "/api/util/random_token").as("regenerateKey");
cy.findByRole("heading", { name: "Regenerate embedding key?" });
cy.findByText(
"This will cause existing embeds to stop working until they are updated with the new key.",
);
cy.findByText("Are you sure you want to do this?");
cy.button("Yes").click();
});
cy.wait("@regenerateKey").then(
({
response: {
body: { token },
},
}) => {
expect(token).to.have.length(64);
expect(token).to.not.eq(METABASE_SECRET_KEY);
cy.findByDisplayValue(token);
},
);
cy.log("Visit the embedding url generated with the old token");
cy.visit(iframe.src);
cy.findByTestId("embed-frame").findByText(
"Message seems corrupt or manipulated.",
);
});
});
});
});
......@@ -277,7 +380,7 @@ function ensureEmbeddingIsDisabled() {
function visitAndEnableSharing(object) {
if (object === "question") {
visitQuestion("1");
visitQuestion(ORDERS_QUESTION_ID);
cy.icon("share").click();
cy.findByText(/Embed in your application/).click();
}
......@@ -293,3 +396,7 @@ function visitAndEnableSharing(object) {
function sidebar() {
return cy.get(".AdminList");
}
function mainPage() {
return sidebar().next();
}
......@@ -33,7 +33,10 @@ const AdvancedEmbedPane = ({
onDiscard,
}) => (
<div className="full flex">
<div className="flex-full p4 flex flex-column">
<div
className="flex-full p4 flex flex-column"
data-testid="embedding-preview"
>
{!resource.enable_embedding ||
!_.isEqual(resource.embedding_params, embeddingParams) ? (
<div className="mb2 p2 bordered rounded flex align-center flex-no-shrink">
......
......@@ -43,6 +43,7 @@ const AdvancedSettingsPane = ({
return (
<div
data-testid="embedding-settings"
className={cx(className, "p4 full-height flex flex-column bg-light")}
style={{ width: 400 }}
>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment