Repository: metabase/metabase
Pull request #56255: ADM497 db routing e2e tests
Author: login: fraserdrops
Labels: name: no-backport
description: Do not backport this PR to any branch
name: .Team/AdminWebapp
description: DEPRECATED  Please use .Team/UXWest instead
Created: 2025-04-04T00:35:54Z | Merged: 2025-04-10T04:26:14Z

This adds tests for usage of DB routing, adding to the current tests for Admin configuration. This tests
-  regular usage, ensuring users get routed to dbs matching their user attribute
- cases where the user has an attribute value that doesn't match a db, or just no attribute
- caching
- sandboxing (column-based)

Still to test are 
- sandboxing, question-based
- impersonation


Diff:
diff --git a/e2e/snapshot-creators/qa-db.cy.snap.js b/e2e/snapshot-creators/qa-db.cy.snap.js
index a72332853220..01838d8e9948 100644
--- a/e2e/snapshot-creators/qa-db.cy.snap.js
+++ b/e2e/snapshot-creators/qa-db.cy.snap.js
@@ -33,14 +33,14 @@ describe("qa databases snapshots", { tags: "@external" }, () => {
 
       restoreAndAuthenticate();
 
-      addMySQLDatabase();
+      addMySQLDatabase({});
       snapshot("mysql-8");
       deleteDatabase("mysqlID");
 
       restoreAndAuthenticate();
 
       setupWritableDB("mysql");
-      addMySQLDatabase("Writable MySQL8", true);
+      addMySQLDatabase({ displayName: "Writable MySQL8", writable: true });
       snapshot("mysql-writable");
       deleteDatabase("mysqlID");
 
diff --git a/e2e/support/helpers/e2e-qa-databases-helpers.js b/e2e/support/helpers/e2e-qa-databases-helpers.js
index 3b7272916d71..68c2ae39afb9 100644
--- a/e2e/support/helpers/e2e-qa-databases-helpers.js
+++ b/e2e/support/helpers/e2e-qa-databases-helpers.js
@@ -14,69 +14,103 @@ import { createQuestion } from "./api";
  **            QA DATABASES             **
  ******************************************/
 
-export function addMongoDatabase(name = "QA Mongo") {
+export function addMongoDatabase(displayName = "QA Mongo") {
   // https://hub.docker.com/layers/metabase/qa-databases/mongo-sample-4.4/images/sha256-8cdeaacf28c6f0a6f9fde42ce004fcc90200d706ac6afa996bdd40db78ec0305
-  addQADatabase("mongo", name, QA_MONGO_PORT);
+  return addQADatabase({
+    engine: "mongo",
+    displayName,
+    port: QA_MONGO_PORT,
+  });
 }
 
-export function addPostgresDatabase(name = "QA Postgres12", writable = false) {
+export function addPostgresDatabase(
+  displayName = "QA Postgres12",
+  writable = false,
+  dbName,
+  idAlias,
+) {
   // https://hub.docker.com/layers/metabase/qa-databases/postgres-sample-12/images/sha256-80bbef27dc52552d6dc64b52796ba356d7541e7bba172740336d7b8a64859cf8
-  addQADatabase("postgres", name, QA_POSTGRES_PORT, writable);
+  return addQADatabase({
+    engine: "postgres",
+    displayName,
+    dbName,
+    port: QA_POSTGRES_PORT,
+    enable_actions: writable,
+    idAlias,
+  });
 }
 
-export function addMySQLDatabase(name = "QA MySQL8", writable = false) {
+export function addMySQLDatabase({
+  displayName = "QA MySQL8",
+  writable = false,
+}) {
   // https://hub.docker.com/layers/metabase/qa-databases/mysql-sample-8/images/sha256-df67db50379ec59ac3a437b5205871f85ab519ce8d2cdc526e9313354d00f9d4
-  addQADatabase("mysql", name, QA_MYSQL_PORT, writable);
+  return addQADatabase({
+    engine: "mysql",
+    displayName: displayName,
+    port: QA_MYSQL_PORT,
+    enable_actions: writable,
+  });
 }
 
-function addQADatabase(engine, db_display_name, port, enable_actions = false) {
+function addQADatabase({
+  engine,
+  displayName,
+  dbName,
+  port,
+  enable_actions = false,
+  idAlias,
+}) {
   const PASS_KEY = engine === "mongo" ? "pass" : "password";
   const AUTH_DB = engine === "mongo" ? "admin" : null;
   const OPTIONS = engine === "mysql" ? "allowPublicKeyRetrieval=true" : null;
 
-  const db_name = enable_actions
-    ? WRITABLE_DB_CONFIG[engine].connection.database
-    : QA_DB_CREDENTIALS.database;
+  const db_name =
+    dbName ??
+    (enable_actions
+      ? WRITABLE_DB_CONFIG[engine].connection.database
+      : QA_DB_CREDENTIALS.database);
 
   const credentials = enable_actions
     ? WRITABLE_DB_CONFIG[engine].connection
     : QA_DB_CREDENTIALS;
 
   cy.log(`**-- Adding ${engine.toUpperCase()} DB --**`);
-  cy.request("POST", "/api/database", {
-    engine: engine,
-    name: db_display_name,
-    details: {
-      dbname: db_name,
-      host: credentials.host,
-      port: port,
-      user: credentials.user,
-      [PASS_KEY]: QA_DB_CREDENTIALS.password, // NOTE: we're inconsistent in where we use `pass` vs `password` as a key
-      authdb: AUTH_DB,
-      "additional-options": OPTIONS,
-      "use-srv": false,
-      "tunnel-enabled": false,
-    },
-    auto_run_queries: true,
-    is_full_sync: true,
-    schedules: {
-      cache_field_values: {
-        schedule_day: null,
-        schedule_frame: null,
-        schedule_hour: 0,
-        schedule_type: "daily",
+  return cy
+    .request("POST", "/api/database", {
+      engine: engine,
+      name: displayName,
+      details: {
+        dbname: db_name,
+        host: credentials.host,
+        port: port,
+        user: credentials.user,
+        [PASS_KEY]: QA_DB_CREDENTIALS.password, // NOTE: we're inconsistent in where we use `pass` vs `password` as a key
+        authdb: AUTH_DB,
+        "additional-options": OPTIONS,
+        "use-srv": false,
+        "tunnel-enabled": false,
       },
-      metadata_sync: {
-        schedule_day: null,
-        schedule_frame: null,
-        schedule_hour: null,
-        schedule_type: "hourly",
+      auto_run_queries: true,
+      is_full_sync: true,
+      schedules: {
+        cache_field_values: {
+          schedule_day: null,
+          schedule_frame: null,
+          schedule_hour: 0,
+          schedule_type: "daily",
+        },
+        metadata_sync: {
+          schedule_day: null,
+          schedule_frame: null,
+          schedule_hour: null,
+          schedule_type: "hourly",
+        },
       },
-    },
-  })
+    })
     .then(({ status, body }) => {
       expect(status).to.equal(200);
-      cy.wrap(body.id).as(`${engine}ID`);
+      cy.wrap(body.id).as(idAlias ?? `${engine}ID`);
     })
     .then((dbId) => {
       // Make sure we have all the metadata because we'll need to use it in tests
@@ -209,6 +243,12 @@ export function queryQADB(query, type = "postgres") {
   });
 }
 
+/**
+ * Executes a query against a writable database
+ * @param {string} query - The SQL query to execute
+ * @param {("postgres"|"mysql")} [type="postgres"] - Database type to connect to
+ * @returns {Cypress.Chainable<{rows: any[]}>} - Cypress chainable that resolves to query results
+ */
 export function queryWritableDB(query, type = "postgres") {
   return cy.task("connectAndQueryDB", {
     connectionConfig: WRITABLE_DB_CONFIG[type],
diff --git a/e2e/test/scenarios/admin/database-routing.cy.spec.ts b/e2e/test/scenarios/admin/database-routing/database-routing-admin.cy.spec.ts
similarity index 93%
rename from e2e/test/scenarios/admin/database-routing.cy.spec.ts
rename to e2e/test/scenarios/admin/database-routing/database-routing-admin.cy.spec.ts
index 0879bd008ef2..83a125164410 100644
--- a/e2e/test/scenarios/admin/database-routing.cy.spec.ts
+++ b/e2e/test/scenarios/admin/database-routing/database-routing-admin.cy.spec.ts
@@ -5,9 +5,14 @@ import {
   SAMPLE_DB_ID,
   USER_GROUPS,
 } from "e2e/support/cypress_data";
-import type { DatabaseData } from "metabase-types/api";
 
-import { interceptPerformanceRoutes } from "./performance/helpers/e2e-performance-helpers";
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
 
 const { H } = cy;
 const { ALL_USERS_GROUP } = USER_GROUPS;
@@ -507,57 +512,6 @@ function assertDbRoutingDisabled() {
     .should("exist");
 }
 
-const BASE_POSTGRES_MIRROR_DB_INFO = {
-  is_on_demand: false,
-  is_full_sync: true,
-  is_sample: false,
-  cache_ttl: null,
-  refingerprint: false,
-  auto_run_queries: true,
-  schedules: {},
-  details: {
-    host: "localhost",
-    port: QA_POSTGRES_PORT,
-    dbname: "sample",
-    user: "metabase",
-    "use-auth-provider": false,
-    password: "metasample123",
-    "schema-filters-type": "all",
-    ssl: false,
-    "tunnel-enabled": false,
-    "advanced-options": false,
-  },
-  name: "Destination DB",
-  engine: "postgres",
-};
-
-function configurDbRoutingViaAPI({
-  router_database_id,
-  user_attribute,
-}: {
-  router_database_id: number;
-  user_attribute: string | null;
-}) {
-  cy.request(
-    "PUT",
-    `/api/ee/database-routing/router-database/${router_database_id}`,
-    { user_attribute },
-  );
-}
-
-function createDestinationDatabasesViaAPI({
-  router_database_id,
-  databases,
-}: {
-  router_database_id: number;
-  databases: DatabaseData[];
-}) {
-  cy.request("POST", "/api/ee/database-routing/mirror-database", {
-    router_database_id,
-    mirrors: databases,
-  });
-}
-
 function setupModelPersistence() {
   interceptPerformanceRoutes();
   cy.visit("/admin");
diff --git a/e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts b/e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
new file mode 100644
index 000000000000..40d43054ce1b
--- /dev/null
+++ b/e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
@@ -0,0 +1,314 @@
+const { H } = cy;
+import _ from "underscore";
+
+import { USER_GROUPS } from "e2e/support/cypress_data";
+import { DataPermissionValue } from "metabase/admin/permissions/types";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  DB_ROUTER_USERS,
+  configurDbRoutingViaAPI,
+  createDbWithIdentifierTable,
+  createDestinationDatabasesViaAPI,
+  signInAs,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    H.addPostgresDatabase("lead", false, "lead", "leadDbId").then(function () {
+      configurDbRoutingViaAPI({
+        router_database_id: this.leadDbId,
+        user_attribute: "destination_database",
+      });
+      createDestinationDatabasesViaAPI({
+        router_database_id: this.leadDbId,
+        databases: [
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_one",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_one",
+            },
+          },
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_two",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_two",
+            },
+          },
+        ],
+      });
+
+      cy.request(
+        "GET",
+        `/api/database/${this.leadDbId}/metadata?include_hidden=true`,
+      ).then(({ body }) => {
+        const dbIdentifierTable = _.findWhere(body.tables, {
+          name: "db_identifier",
+        });
+        cy.wrap(dbIdentifierTable.id).as("leadDb_db_identifier_table_ID");
+        const colorField = _.findWhere(dbIdentifierTable.fields, {
+          name: "color",
+        });
+        cy.wrap(colorField.id).as("leadDb_color_field_ID");
+      });
+    });
+
+    H.addPostgresDatabase(
+      "destination_one",
+      false,
+      "destination_one",
+      "destinationOneDbId",
+    ).then(function () {
+      cy.request(
+        "GET",
+        `/api/database/${this.destinationOneDbId}/metadata?include_hidden=true`,
+      ).then(({ body }) => {
+        const dbIdentifierTable = _.findWhere(body.tables, {
+          name: "db_identifier",
+        });
+        cy.wrap(dbIdentifierTable.id).as(
+          "destinationOneDb_db_identifier_table_ID",
+        );
+        const colorField = _.findWhere(dbIdentifierTable.fields, {
+          name: "color",
+        });
+        cy.wrap(colorField.id).as("destinationOneDb_color_field_ID");
+      });
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Native Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.findByTestId("query-visualization-root").findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.findByTestId("query-visualization-root").findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+
+    cy.signInAsAdmin();
+    H.createQuestion({
+      name: "DB Identifier Name",
+      database: this.leadDbId,
+      query: {
+        "source-table": this.leadDb_db_identifier_table_ID,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User with wrong attribute");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.findByTestId("query-visualization-root").findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.findByTestId("query-visualization-root").findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+  });
+
+  it("should not leak cached data", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.request("PUT", "api/cache", {
+        model: "question",
+        model_id: questionId,
+        strategy: {
+          refresh_automatically: false,
+          unit: "hours",
+          duration: 24,
+          type: "duration",
+        },
+      });
+      cy.request("GET", `api/cache?model=question&id=${questionId}`);
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+    });
+  });
+
+  it("should work with sandboxing", function () {
+    cy.visit(`admin/permissions/data/group/${COLLECTION_GROUP}/database/3`);
+    H.createQuestion({
+      name: "Color",
+      database: this.leadDbId,
+      query: {
+        "source-table": this.leadDb_db_identifier_table_ID,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Sandboxing a destination db should have no effect");
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, 6);
+      // @ts-expect-error - this isn't typed yet
+      cy.sandboxTable({
+        table_id: this.destinationOneDb_db_identifier_table_ID,
+        group_id: COLLECTION_GROUP,
+        attribute_remappings: {
+          color: ["dimension", ["field", this.destinationOneDb_color_field_ID]],
+        },
+      });
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="color"]').should("contain", "blue");
+      cy.get('[data-column-id="color"]').should("contain", "red");
+
+      cy.signInAsAdmin();
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, this.leadDbId);
+      // @ts-expect-error - this isn't typed yet
+      cy.sandboxTable({
+        table_id: this.leadDb_db_identifier_table_ID,
+        group_id: COLLECTION_GROUP,
+        attribute_remappings: {
+          color: ["dimension", ["field", this.leadDb_color_field_ID]],
+        },
+      });
+
+      cy.log(
+        "Unrestricted access on the destination db should not affect sandboxing",
+      );
+      cy.updatePermissionsGraph({
+        [ALL_USERS_GROUP]: {
+          6: {
+            "view-data": DataPermissionValue.UNRESTRICTED,
+          },
+        },
+      });
+
+      cy.visit(`admin/permissions/data/group/${COLLECTION_GROUP}/database/3`);
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="color"]').should("contain", "blue");
+      cy.get('[data-column-id="color"]').should("not.contain", "red");
+    });
+  });
+});
diff --git a/e2e/test/scenarios/admin/database-routing/helpers/e2e-database-routing-helpers.ts b/e2e/test/scenarios/admin/database-routing/helpers/e2e-database-routing-helpers.ts
new file mode 100644
index 000000000000..7ec546edaeb6
--- /dev/null
+++ b/e2e/test/scenarios/admin/database-routing/helpers/e2e-database-routing-helpers.ts
@@ -0,0 +1,169 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { DatabaseData, User } from "metabase-types/api";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+export function configurDbRoutingViaAPI({
+  router_database_id,
+  user_attribute,
+}: {
+  router_database_id: number;
+  user_attribute: string | null;
+}) {
+  cy.request(
+    "PUT",
+    `/api/ee/database-routing/router-database/${router_database_id}`,
+    { user_attribute },
+  );
+}
+
+export function createDestinationDatabasesViaAPI({
+  router_database_id,
+  databases,
+}: {
+  router_database_id: number;
+  databases: DatabaseData[];
+}) {
+  cy.request("POST", "/api/ee/database-routing/mirror-database", {
+    router_database_id,
+    mirrors: databases,
+  });
+}
+
+export const BASE_POSTGRES_MIRROR_DB_INFO = {
+  is_on_demand: false,
+  is_full_sync: true,
+  is_sample: false,
+  cache_ttl: null,
+  refingerprint: false,
+  auto_run_queries: true,
+  schedules: {},
+  details: {
+    host: "localhost",
+    port: QA_POSTGRES_PORT,
+    dbname: "sample",
+    user: "metabase",
+    "use-auth-provider": false,
+    password: "metasample123",
+    "schema-filters-type": "all",
+    ssl: false,
+    "tunnel-enabled": false,
+    "advanced-options": false,
+  },
+  name: "Destination DB",
+  engine: "postgres",
+};
+
+export const DB_ROUTER_USERS = {
+  userA: {
+    first_name: "Don",
+    last_name: "RouterA",
+    email: "routerA@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_one",
+      color: "blue",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userB: {
+    first_name: "Tom",
+    last_name: "RouterB",
+    email: "routerB@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_two",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userWithMetabaseRouterAttr: {
+    first_name: "Router",
+    last_name: "Attribute",
+    email: "routerAttribute@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "__METABASE_ROUTER__",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userNoAttribute: {
+    first_name: "Jane",
+    last_name: "NoAttribute",
+    email: "noattribute@metabase.test",
+    password: "12341234",
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userWrongAttribute: {
+    first_name: "Bill",
+    last_name: "WrongAttribute",
+    email: "wrongattribute@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "wrong_destination",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+};
+
+export function signInAs(user: Partial<User> & { password: string }) {
+  cy.log(`Sign in as user via an API call: ${user.email}`);
+  return cy.request("POST", "/api/session", {
+    username: user.email,
+    password: user.password,
+  });
+}
+
+export function createDbWithIdentifierTable({ dbName }: { dbName: string }) {
+  H.queryWritableDB(
+    `SELECT datname from pg_database WHERE datname = '${dbName}'`,
+  ).then((res: { rows: any }) => {
+    if (res.rows.length === 0) {
+      H.queryWritableDB(`CREATE DATABASE ${dbName};`);
+    }
+  });
+
+  const dbConfig = {
+    client: "pg",
+    connection: {
+      ...QA_DB_CREDENTIALS,
+      port: QA_POSTGRES_PORT,
+      name: dbName,
+      database: dbName,
+    },
+  };
+  cy.task("connectAndQueryDB", {
+    connectionConfig: dbConfig,
+    query:
+      "DROP TABLE IF EXISTS db_identifier; CREATE TABLE db_identifier (name VARCHAR(50), color VARCHAR(20));",
+  });
+
+  cy.task("connectAndQueryDB", {
+    connectionConfig: dbConfig,
+    query: `INSERT INTO db_identifier VALUES ('${dbName}', 'blue'), ('${dbName}', 'red');`,
+  });
+
+  cy.task("connectAndQueryDB", {
+    connectionConfig: dbConfig,
+    query: "SELECT color FROM db_identifier;",
+  });
+}


Reviews:
user: login: iethree
body: Very nice work 👏  A few nits and suggestions, the one thing I really think we need here is some coverage on notebook questions, not just native ones.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T21:45:25Z
user: login: fraserdrops
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T03:33:51Z
user: login: iethree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:23:51Z
user: login: iethree
body: 👏  nice work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:31:20Z

Review comments:
diff_hunk: @@ -0,0 +1,235 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: 3,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: 3,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: 3,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with userB
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+
+      // Test with user with wrong attribute value
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+
+      // Test with user with no attribute
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+    });
+  });
+
+  it("should not leak cached data", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: 3,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.visit(`/question/${questionId}`);
+      openSidebarCacheStrategyForm("question");
+      durationRadioButton().click();
+      saveCacheStrategyForm({ strategyType: "duration", model: "database" });
+      // Test with user a
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with user b
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+    });
+  });
+});
+
+const DB_ROUTER_USERS = {
+  userA: {
+    first_name: "Don",
+    last_name: "RouterA",
+    email: "routerA@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_one",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+      { id: DATA_GROUP, is_group_manager: false },
+    ],
+  },
+
+  userB: {
+    first_name: "Tom",
+    last_name: "RouterB",
+    email: "routerB@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_two",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+      { id: DATA_GROUP, is_group_manager: false },
+    ],
+  },
+
+  userNoAttribute: {
+    first_name: "Jane",
+    last_name: "NoAttribute",
+    email: "noattribute@metabase.test",
+    password: "12341234",
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+      { id: DATA_GROUP, is_group_manager: false },
+    ],
+  },
+
+  userWrongAttribute: {
+    first_name: "Bill",
+    last_name: "WrongAttribute",
+    email: "wrongattribute@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "wrong_destination",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+      { id: DATA_GROUP, is_group_manager: false },
+    ],
+  },
+};
+
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: might as well put all of this 👇 into the helpers file. Nothing wrong with inline helpers, but if we have a helpers file, we might as well keep everything together.
created_at: 2025-04-07T21:08:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,235 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: 3,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: 3,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: since you did this in `beforeEach` it's superfluous here
created_at: 2025-04-07T21:10:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,235 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: 3,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: 3,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: 3,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: use `H.visitQuestion(questionId)`
created_at: 2025-04-07T21:12:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,235 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: 3,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: 3,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: 3,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: I would also assert that `destination_two` does not exist
created_at: 2025-04-07T21:15:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,235 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, DATA_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: 3,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: 3,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: 3,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with userB
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+
+      // Test with user with wrong attribute value
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: scope this to where you're expecting to find it, probably "query-visualization-root"
created_at: 2025-04-07T21:17:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with userB
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+
+      // Test with user with wrong attribute value
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+
+      // Test with user with no attribute
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+    });
+  });
+
+  it("should not leak cached data", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.visit(`/question/${questionId}`);
+      openSidebarCacheStrategyForm("question");
+      durationRadioButton().click();
+      saveCacheStrategyForm({ strategyType: "duration", model: "database" });
+      // Test with user a
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with user b
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+    });
+  });
+
+  it("should work with sandboxing", () => {
+    cy.signInAsAdmin();
+    H.createQuestion({
+      name: "Color",
+      database: LEAD_DATABASE_ID,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: QUESTION_ID } }) => {
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${QUESTION_ID}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="color"]').should("contain", "blue");
+      cy.get('[data-column-id="color"]').should("contain", "red");
+
+      cy.signInAsAdmin();
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, LEAD_DATABASE_ID);
+
+      cy.visit(`admin/permissions/data/group/${COLLECTION_GROUP}/database/3`);
+      H.modifyPermission("Db Identifier", 0, "Sandboxed");
+      H.modal().findByText("Pick a column").click();
+      H.popover().findByText("Color").click();
+      H.modal().findByText("Pick a user attribute").click();
+      H.popover().findByText("color").click();
+      H.modal().button("Save").click();
+      H.savePermissions();
+
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${QUESTION_ID}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="color"]').should("contain", "blue");
+      cy.get('[data-column-id="color"]').should("not.contain", "red");
+    });
+  });
+});
+
+const DB_ROUTER_USERS = {
+  userA: {
+    first_name: "Don",
+    last_name: "RouterA",
+    email: "routerA@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_one",
+      color: "blue",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userB: {
+    first_name: "Tom",
+    last_name: "RouterB",
+    email: "routerB@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "destination_two",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+
+  userNoAttribute: {
+    first_name: "Jane",
+    last_name: "NoAttribute",
+    email: "noattribute@metabase.test",
+    password: "12341234",
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+  userWrongAttribute: {
+    first_name: "Bill",
+    last_name: "WrongAttribute",
+    email: "wrongattribute@metabase.test",
+    password: "12341234",
+    login_attributes: {
+      destination_database: "wrong_destination",
+    },
+    user_group_memberships: [
+      { id: ALL_USERS_GROUP, is_group_manager: false },
+      { id: COLLECTION_GROUP, is_group_manager: false },
+    ],
+  },
+};
+
+function signInAs(user: Partial<User> & { password: string }) {
+  cy.log(`Sign in as user via an API call: ${user.email}`);
+  return cy.request("POST", "/api/session", {
+    username: user.email,
+    password: user.password,
+  });
+}
+
+function createDbWithIdentifierTable({ dbName }: { dbName: string }) {
+  H.queryWritableDB(
+    `SELECT datname from pg_database WHERE datname = '${dbName}'`,
+  ).then((res: { rows: any }) => {
+    if (res.rows.length === 0) {
+      H.queryWritableDB(`CREATE DATABASE ${dbName};`);
+    }
+  });
+
+  const dbConfig = {
+    client: "pg",
+    connection: {
+      ...QA_DB_CREDENTIALS,
+      port: QA_POSTGRES_PORT,
+      name: dbName,
+      database: dbName,
+    },
+  };
+  cy.task("connectAndQueryDB", {
+    connectionConfig: dbConfig,
+    query:
+      "CREATE TABLE IF NOT EXISTS db_identifier (name VARCHAR(50), color VARCHAR(20));",
+  });
+
+  cy.task("connectAndQueryDB", {
+    connectionConfig: dbConfig,
+    query: "DELETE FROM db_identifier;",
+  });
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: for better DX: maybe instead of deleting records, just drop the table if it exists. I ran this, and then when I ran it again after you changed the schema it all blew up
created_at: 2025-04-07T21:20:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: this could be fragile, we try not to hard code IDs if at all possible
created_at: 2025-04-07T21:22:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: we should run the same set of cases against a notebook question as well
created_at: 2025-04-07T21:41:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      // Test with userA
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      // Test with userB
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userB);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+
+      // Test with user with wrong attribute value
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+
+      // Test with user with no attribute
+      cy.signOut();
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      cy.visit(`/question/${questionId}`);
+      cy.findByText("No Mirror Database found for user attribute");
+    });
+  });
+
+  it("should not leak cached data", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.visit(`/question/${questionId}`);
+      openSidebarCacheStrategyForm("question");
+      durationRadioButton().click();
+      saveCacheStrategyForm({ strategyType: "duration", model: "database" });
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: just use the API to set up caching here. we're not testing that the form works, we're testing that data doesn't leak.
created_at: 2025-04-07T21:42:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: fraserdrops
body: > we should run the same set of cases against a notebook question as well

By notebook question do you mean using query builder aka `H.createQuestion()`? Why do you think the question type matters for testing where it gets routed?
created_at: 2025-04-08T03:33:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,273 @@
+const { H } = cy;
+import {
+  QA_DB_CREDENTIALS,
+  QA_POSTGRES_PORT,
+  USER_GROUPS,
+} from "e2e/support/cypress_data";
+import type { User } from "metabase-types/api";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+import {
+  durationRadioButton,
+  openSidebarCacheStrategyForm,
+  saveCacheStrategyForm,
+} from "../performance/helpers/e2e-strategy-form-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  configurDbRoutingViaAPI,
+  createDestinationDatabasesViaAPI,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+const LEAD_DATABASE_ID = 3;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead");
+    configurDbRoutingViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      user_attribute: "destination_database",
+    });
+    createDestinationDatabasesViaAPI({
+      router_database_id: LEAD_DATABASE_ID,
+      databases: [
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_one",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_one",
+          },
+        },
+        {
+          ...BASE_POSTGRES_MIRROR_DB_INFO,
+          name: "destination_two",
+          details: {
+            ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+            dbname: "destination_two",
+          },
+        },
+      ],
+    });
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", () => {
+    cy.signInAsAdmin();
+    H.createNativeQuestion({
+      database: LEAD_DATABASE_ID,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: I know the query processor handles native questions and notebook questions differently in a number of ways. Not sure if that's implicated here, but we should at least have some test coverage that proves that they behave the same way.
created_at: 2025-04-09T16:23:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+const { H } = cy;
+import { USER_GROUPS } from "e2e/support/cypress_data";
+import { DataPermissionValue } from "metabase/admin/permissions/types";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  DB_ROUTER_USERS,
+  configurDbRoutingViaAPI,
+  createDbWithIdentifierTable,
+  createDestinationDatabasesViaAPI,
+  signInAs,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead", "leadDbId").then(function () {
+      configurDbRoutingViaAPI({
+        router_database_id: this.leadDbId,
+        user_attribute: "destination_database",
+      });
+      createDestinationDatabasesViaAPI({
+        router_database_id: this.leadDbId,
+        databases: [
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_one",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_one",
+            },
+          },
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_two",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_two",
+            },
+          },
+        ],
+      });
+    });
+
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Native Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+
+    cy.signInAsAdmin();
+    H.createQuestion({
+      name: "DB Identifier Name",
+      database: this.leadDbId,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User with wrong attribute");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+  });
+
+  it("should not leak cached data", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.request("PUT", "api/cache", {
+        model: "question",
+        model_id: questionId,
+        strategy: {
+          refresh_automatically: false,
+          unit: "hours",
+          duration: 24,
+          type: "duration",
+        },
+      });
+      cy.request("GET", `api/cache?model=question&id=${questionId}`);
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+    });
+  });
+
+  it("should work with sandboxing", function () {
+    H.addPostgresDatabase("destination_one", false, "destination_one");
+    cy.visit(`admin/permissions/data/group/${COLLECTION_GROUP}/database/3`);
+    H.createQuestion({
+      name: "Color",
+      database: this.leadDbId,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Sandboxing a destination db should have no effect");
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, 6);
+      // @ts-expect-error - this isn't typed yet
+      cy.sandboxTable({
+        table_id: 23,
+        group_id: COLLECTION_GROUP,
+        attribute_remappings: {
+          color: ["dimension", ["field", 243]],
+        },
+      });
+
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="color"]').should("contain", "blue");
+      cy.get('[data-column-id="color"]').should("contain", "red");
+
+      cy.signInAsAdmin();
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, this.leadDbId);
+      // @ts-expect-error - this isn't typed yet
+      cy.sandboxTable({
+        table_id: 22,
+        group_id: COLLECTION_GROUP,
+        attribute_remappings: {
+          color: ["dimension", ["field", 241]],
+        },
+      });
+
+      cy.log(
+        "Unrestricted access on the destination db should not affect sandboxing",
+      );
+      cy.updatePermissionsGraph({
+        [ALL_USERS_GROUP]: {
+          6: {
+            "view-data": DataPermissionValue.UNRESTRICTED,
+          },
+        },
+      });
+
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: H.visitQuestion(questionId);
created_at: 2025-04-09T16:28:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+const { H } = cy;
+import { USER_GROUPS } from "e2e/support/cypress_data";
+import { DataPermissionValue } from "metabase/admin/permissions/types";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  DB_ROUTER_USERS,
+  configurDbRoutingViaAPI,
+  createDbWithIdentifierTable,
+  createDestinationDatabasesViaAPI,
+  signInAs,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead", "leadDbId").then(function () {
+      configurDbRoutingViaAPI({
+        router_database_id: this.leadDbId,
+        user_attribute: "destination_database",
+      });
+      createDestinationDatabasesViaAPI({
+        router_database_id: this.leadDbId,
+        databases: [
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_one",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_one",
+            },
+          },
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_two",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_two",
+            },
+          },
+        ],
+      });
+    });
+
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Native Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+
+    cy.signInAsAdmin();
+    H.createQuestion({
+      name: "DB Identifier Name",
+      database: this.leadDbId,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User with wrong attribute");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+  });
+
+  it("should not leak cached data", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      interceptPerformanceRoutes();
+      cy.request("PUT", "api/cache", {
+        model: "question",
+        model_id: questionId,
+        strategy: {
+          refresh_automatically: false,
+          unit: "hours",
+          duration: 24,
+          type: "duration",
+        },
+      });
+      cy.request("GET", `api/cache?model=question&id=${questionId}`);
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+    });
+  });
+
+  it("should work with sandboxing", function () {
+    H.addPostgresDatabase("destination_one", false, "destination_one");
+    cy.visit(`admin/permissions/data/group/${COLLECTION_GROUP}/database/3`);
+    H.createQuestion({
+      name: "Color",
+      database: this.leadDbId,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Sandboxing a destination db should have no effect");
+      H.blockUserGroupPermissions(ALL_USERS_GROUP, 6);
+      // @ts-expect-error - this isn't typed yet
+      cy.sandboxTable({
+        table_id: 23,
+        group_id: COLLECTION_GROUP,
+        attribute_remappings: {
+          color: ["dimension", ["field", 243]],
+        },
+      });
+
+      signInAs(DB_ROUTER_USERS.userA);
+      cy.visit(`/question/${questionId}`);
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: H.visitQuestion(questionId);
created_at: 2025-04-09T16:29:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,277 @@
+const { H } = cy;
+import { USER_GROUPS } from "e2e/support/cypress_data";
+import { DataPermissionValue } from "metabase/admin/permissions/types";
+
+import { interceptPerformanceRoutes } from "../performance/helpers/e2e-performance-helpers";
+
+import {
+  BASE_POSTGRES_MIRROR_DB_INFO,
+  DB_ROUTER_USERS,
+  configurDbRoutingViaAPI,
+  createDbWithIdentifierTable,
+  createDestinationDatabasesViaAPI,
+  signInAs,
+} from "./helpers/e2e-database-routing-helpers";
+
+const { ALL_USERS_GROUP, COLLECTION_GROUP } = USER_GROUPS;
+
+describe("admin > database > database routing", { tags: ["@external"] }, () => {
+  before(() => {
+    // For DB Routing it's important all the tables have the same schema
+    createDbWithIdentifierTable({ dbName: "lead" });
+    createDbWithIdentifierTable({ dbName: "destination_one" });
+    createDbWithIdentifierTable({ dbName: "destination_two" });
+
+    H.restore("postgres-writable");
+    cy.signInAsAdmin();
+    H.setTokenFeatures("all");
+    Object.values(DB_ROUTER_USERS).forEach((user) => {
+      // @ts-expect-error - this isn't typed yet
+      cy.createUserFromRawData(user);
+    });
+
+    // With DB routing we only add the primary db directly
+    // the other dbs get linked when they're added as destination dbs
+    H.addPostgresDatabase("lead", false, "lead", "leadDbId").then(function () {
+      configurDbRoutingViaAPI({
+        router_database_id: this.leadDbId,
+        user_attribute: "destination_database",
+      });
+      createDestinationDatabasesViaAPI({
+        router_database_id: this.leadDbId,
+        databases: [
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_one",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_one",
+            },
+          },
+          {
+            ...BASE_POSTGRES_MIRROR_DB_INFO,
+            name: "destination_two",
+            details: {
+              ...BASE_POSTGRES_MIRROR_DB_INFO.details,
+              dbname: "destination_two",
+            },
+          },
+        ],
+      });
+    });
+
+    H.snapshot("db-routing-3-dbs");
+  });
+
+  beforeEach(() => {
+    H.restore("db-routing-3-dbs" as any);
+    cy.signInAsAdmin();
+  });
+
+  it("should route users to the correct destination database", function () {
+    H.createNativeQuestion({
+      database: this.leadDbId,
+      name: "Native Identifier Name",
+      native: {
+        query: "SELECT name FROM db_identifier;",
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+      cy.log("User B");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
+    });
+
+    cy.signInAsAdmin();
+    H.createQuestion({
+      name: "DB Identifier Name",
+      database: this.leadDbId,
+      query: {
+        "source-table": 22,
+      },
+    }).then(({ body: { id: questionId } }) => {
+      cy.log("Admin should see primary db");
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      cy.log("User with __METABASE_ROUTER__ should see primary db");
+      signInAs(DB_ROUTER_USERS.userWithMetabaseRouterAttr);
+      cy.get('[data-column-id="name"]').should("contain", "lead");
+      cy.get('[data-column-id="name"]')
+        .should("not.contain", "destination_one")
+        .should("not.contain", "destination_two");
+
+      signInAs(DB_ROUTER_USERS.userA);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_one");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_two",
+      );
+
+      cy.log("User A");
+      signInAs(DB_ROUTER_USERS.userB);
+      H.visitQuestion(questionId);
+      cy.get('[data-column-id="name"]').should("contain", "destination_two");
+      cy.get('[data-column-id="name"]').should(
+        "not.contain",
+        "destination_one",
+      );
+
+      cy.log("User with wrong attribute");
+      signInAs(DB_ROUTER_USERS.userWrongAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "No Mirror Database found for user attribute",
+      );
+
+      cy.log("User with no attribute");
+      signInAs(DB_ROUTER_USERS.userNoAttribute);
+      H.visitQuestion(questionId);
+      cy.get('[data-testid="query-visualization-root"]').findByText(
+        "User attribute missing, cannot lookup Mirror Database",
+      );
path: e2e/test/scenarios/admin/database-routing/database-routing-usage.cy.spec.ts
user: login: iethree
body: There's a built in helper for getting by Test Id. 

We try to avoid `cy.get` in most cases - it can encourage using fragile properties like class names. We prefer accessible roles/labels/placeholders or just straight up test ids.

```suggestion
      cy.findByTestId("query-visualization-root").findByText(
        "User attribute missing, cannot lookup Mirror Database",
      );
```
created_at: 2025-04-09T16:30:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-04T00:36:10Z
body: 
  <details>
    <summary>What's black and white and read all over?</summary>
    <br>The newspaper. 🤣
  </details>

author_association: CONTRIBUTOR
user: login: trunk-io[bot]
created_at: 2025-04-04T01:16:13Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/4032_tests-09f1a7e6-purple)](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/2-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>admin > database > database routing EE feature visibility should show for users with db management permissions but prevent removal of destination d...</code> |  | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment&runConclusion=FAILURE&runId=1e62f5f2-ccd9-59d6-ac3c-4a134c63fc49) |
| <code>admin > database > database routing EE feature visibility should show for users with db management permissions but prevent removal of destination d...</code> |  | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment&runConclusion=FAILURE&runId=701961b7-1507-5d09-8fc1-df944705a1cd) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/56255?repo=metabase/metabase&commitHash=09f1a7e606d7bcbe3c8a045cf51206deede37c90&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: fraserdrops
created_at: 2025-04-07T19:56:43Z
body: @iethree ~~I really can't tell if the Mongo test is a legit failure.~~ 

Test is passing now 
author_association: CONTRIBUTOR
user: login: fraserdrops
created_at: 2025-04-07T21:44:51Z
body: The db routing test is already failing for me on the main feature branch
author_association: CONTRIBUTOR
user: login: iethree
created_at: 2025-04-09T16:33:05Z
body: check out this failure:

![Screenshot 2025-04-09 at 10 31 57 AM](https://github.com/user-attachments/assets/03a5f2a9-2c7d-4be6-9eec-2d1a498bddf6)

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-10T03:57:47Z
body: 
  ## e2e tests failed on `09f1a7e606d7bcbe3c8a045cf51206deede37c90-2`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/14371795811?pr=56255)

  File | Test Name
  ---- | ---------
  `database-routing-admin.cy.spec.ts` | admin > database > database routing > EE > feature visibility > should show for users with db management permissions but prevent removal of destination databases

author_association: CONTRIBUTOR
user: login: fraserdrops
created_at: 2025-04-10T04:26:03Z
body: @iethree That test is failing in the source branch already, I'll tackle it separately 


author_association: CONTRIBUTOR