@fabricorg/databricks-testkit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +179 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +174 -19
- package/dist/index.js.map +1 -1
- package/package.json +12 -14
- package/LICENSE +0 -21
package/dist/index.cjs
CHANGED
|
@@ -500,7 +500,7 @@ function looselyEqual(a, b) {
|
|
|
500
500
|
|
|
501
501
|
// src/mock-client.ts
|
|
502
502
|
function createMockDatabricksClient() {
|
|
503
|
-
const queues = { GET: [], POST: [] };
|
|
503
|
+
const queues = { GET: [], POST: [], DELETE: [] };
|
|
504
504
|
const calls = [];
|
|
505
505
|
function take(method, path) {
|
|
506
506
|
const index = queues[method].findIndex((q) => path.startsWith(q.pathPrefix));
|
|
@@ -524,6 +524,12 @@ function createMockDatabricksClient() {
|
|
|
524
524
|
if (queued.error) throw queued.error;
|
|
525
525
|
return queued.response;
|
|
526
526
|
},
|
|
527
|
+
async request(method, path, body, query) {
|
|
528
|
+
calls.push({ method, path, body, query });
|
|
529
|
+
const queued = take(method, path);
|
|
530
|
+
if (queued.error) throw queued.error;
|
|
531
|
+
return queued.response;
|
|
532
|
+
},
|
|
527
533
|
queue(method, pathPrefix, response) {
|
|
528
534
|
queues[method].push({ pathPrefix, response });
|
|
529
535
|
return client;
|
|
@@ -692,6 +698,26 @@ function sqlRoundTripCheck() {
|
|
|
692
698
|
}
|
|
693
699
|
};
|
|
694
700
|
}
|
|
701
|
+
function restrictedPrincipalCheck() {
|
|
702
|
+
return {
|
|
703
|
+
id: "restricted-principal",
|
|
704
|
+
description: "Live checks use the designated least-privilege service principal",
|
|
705
|
+
configured: (env) => Boolean(env.DBX_TEST_RESTRICTED_PRINCIPAL_ID),
|
|
706
|
+
async run({ env }) {
|
|
707
|
+
const expected = env.DBX_TEST_RESTRICTED_PRINCIPAL_ID?.toLowerCase();
|
|
708
|
+
const actual = env.DATABRICKS_CLIENT_ID?.toLowerCase();
|
|
709
|
+
if (!expected || !actual) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
"Restricted-principal check requires DBX_TEST_RESTRICTED_PRINCIPAL_ID and DATABRICKS_CLIENT_ID"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (actual !== expected) {
|
|
715
|
+
throw new Error(`Live suite is using principal '${actual}', expected '${expected}'`);
|
|
716
|
+
}
|
|
717
|
+
return { principalId: actual };
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
695
721
|
function assertDeltaContract(table, rows, columns) {
|
|
696
722
|
for (const expected of columns) {
|
|
697
723
|
const actual = rows.find((row) => String(row.col_name ?? row.column_name) === expected.name);
|
|
@@ -783,17 +809,21 @@ function notebookSubmitCheck() {
|
|
|
783
809
|
return {
|
|
784
810
|
id: "notebook",
|
|
785
811
|
description: "Databricks notebook submit run reaches SUCCESS",
|
|
786
|
-
configured: (env) => Boolean(
|
|
812
|
+
configured: (env) => Boolean(
|
|
813
|
+
env.DBX_TEST_NOTEBOOK_PATH && (env.DBX_TEST_CLUSTER_ID || env.DBX_TEST_NOTEBOOK_SERVERLESS === "1")
|
|
814
|
+
),
|
|
787
815
|
async run({ env, client }) {
|
|
788
|
-
if (!env.DBX_TEST_NOTEBOOK_PATH || !env.DBX_TEST_CLUSTER_ID) {
|
|
789
|
-
throw new Error(
|
|
816
|
+
if (!env.DBX_TEST_NOTEBOOK_PATH || !env.DBX_TEST_CLUSTER_ID && env.DBX_TEST_NOTEBOOK_SERVERLESS !== "1") {
|
|
817
|
+
throw new Error(
|
|
818
|
+
"Notebook submit requires DBX_TEST_NOTEBOOK_PATH and either DBX_TEST_CLUSTER_ID or DBX_TEST_NOTEBOOK_SERVERLESS=1"
|
|
819
|
+
);
|
|
790
820
|
}
|
|
791
821
|
const submitted = await client.post("/api/2.1/jobs/runs/submit", {
|
|
792
822
|
run_name: `fabric-experiments-notebook-${Date.now()}`,
|
|
793
823
|
tasks: [
|
|
794
824
|
{
|
|
795
825
|
task_key: "notebook",
|
|
796
|
-
existing_cluster_id: env.DBX_TEST_CLUSTER_ID,
|
|
826
|
+
...env.DBX_TEST_CLUSTER_ID ? { existing_cluster_id: env.DBX_TEST_CLUSTER_ID } : {},
|
|
797
827
|
notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
|
|
798
828
|
}
|
|
799
829
|
]
|
|
@@ -813,29 +843,59 @@ function dbtBuildCheck() {
|
|
|
813
843
|
return {
|
|
814
844
|
id: "dbt",
|
|
815
845
|
description: "Databricks dbt task reaches SUCCESS",
|
|
816
|
-
configured: (env) => Boolean(
|
|
846
|
+
configured: (env) => Boolean(
|
|
847
|
+
(env.DBX_TEST_DBT_GIT_URL || env.DBX_TEST_DBT_PROJECT_DIR) && env.DATABRICKS_WAREHOUSE_ID
|
|
848
|
+
),
|
|
817
849
|
async run({ env, client }) {
|
|
818
|
-
if (!env.DBX_TEST_DBT_GIT_URL || !env.DATABRICKS_WAREHOUSE_ID) {
|
|
819
|
-
throw new Error(
|
|
850
|
+
if (!env.DBX_TEST_DBT_GIT_URL && !env.DBX_TEST_DBT_PROJECT_DIR || !env.DATABRICKS_WAREHOUSE_ID) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
"dbt build requires DBX_TEST_DBT_GIT_URL or DBX_TEST_DBT_PROJECT_DIR, plus DATABRICKS_WAREHOUSE_ID"
|
|
853
|
+
);
|
|
820
854
|
}
|
|
821
|
-
const
|
|
855
|
+
const sourceVars = JSON.stringify({
|
|
856
|
+
source_catalog: env.DBX_TEST_DBT_SOURCE_CATALOG ?? env.DBX_TEST_CATALOG,
|
|
857
|
+
source_schema: env.DBX_TEST_DBT_SOURCE_SCHEMA
|
|
858
|
+
});
|
|
859
|
+
const serverless = env.DBX_TEST_DBT_SERVERLESS === "1";
|
|
860
|
+
const request = {
|
|
822
861
|
run_name: `fabric-experiments-dbt-${Date.now()}`,
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
862
|
+
...env.DBX_TEST_DBT_GIT_URL ? {
|
|
863
|
+
git_source: {
|
|
864
|
+
git_url: env.DBX_TEST_DBT_GIT_URL,
|
|
865
|
+
git_provider: env.DBX_TEST_DBT_GIT_PROVIDER ?? "gitHub",
|
|
866
|
+
git_branch: env.DBX_TEST_DBT_GIT_BRANCH ?? "main"
|
|
867
|
+
}
|
|
868
|
+
} : {},
|
|
828
869
|
tasks: [
|
|
829
870
|
{
|
|
830
871
|
task_key: "dbt_build",
|
|
872
|
+
...serverless ? { environment_key: "dbt" } : {},
|
|
831
873
|
dbt_task: {
|
|
832
|
-
commands: ["dbt deps",
|
|
874
|
+
commands: ["dbt deps", `dbt build --vars '${sourceVars}'`],
|
|
833
875
|
warehouse_id: env.DATABRICKS_WAREHOUSE_ID,
|
|
876
|
+
...env.DBX_TEST_CATALOG ? { catalog: env.DBX_TEST_CATALOG } : {},
|
|
877
|
+
...env.DBX_TEST_SCHEMA ? { schema: env.DBX_TEST_SCHEMA } : {},
|
|
878
|
+
...env.DBX_TEST_DBT_GIT_URL ? {} : { source: "WORKSPACE" },
|
|
834
879
|
...env.DBX_TEST_DBT_PROJECT_DIR ? { project_directory: env.DBX_TEST_DBT_PROJECT_DIR } : {}
|
|
835
880
|
}
|
|
836
881
|
}
|
|
837
|
-
]
|
|
838
|
-
|
|
882
|
+
],
|
|
883
|
+
...serverless ? {
|
|
884
|
+
environments: [
|
|
885
|
+
{
|
|
886
|
+
environment_key: "dbt",
|
|
887
|
+
spec: {
|
|
888
|
+
environment_version: "2",
|
|
889
|
+
dependencies: ["dbt-databricks>=1.8,<2"]
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
]
|
|
893
|
+
} : {}
|
|
894
|
+
};
|
|
895
|
+
const submitted = await client.post(
|
|
896
|
+
"/api/2.1/jobs/runs/submit",
|
|
897
|
+
request
|
|
898
|
+
);
|
|
839
899
|
if (!submitted.run_id) throw new Error("dbt submit returned no run_id");
|
|
840
900
|
const run = await databricks.waitForDatabricksRun(client, submitted.run_id, {
|
|
841
901
|
timeoutMs: Number(env.DBX_TEST_TIMEOUT_MS ?? 9e5),
|
|
@@ -843,7 +903,10 @@ function dbtBuildCheck() {
|
|
|
843
903
|
});
|
|
844
904
|
const result = run.state?.result_state;
|
|
845
905
|
if (result !== "SUCCESS") throw new Error(`dbt run ended in ${result ?? "UNKNOWN"}`);
|
|
846
|
-
return {
|
|
906
|
+
return {
|
|
907
|
+
runId: submitted.run_id,
|
|
908
|
+
source: env.DBX_TEST_DBT_GIT_URL ?? env.DBX_TEST_DBT_PROJECT_DIR
|
|
909
|
+
};
|
|
847
910
|
}
|
|
848
911
|
};
|
|
849
912
|
}
|
|
@@ -922,6 +985,99 @@ function lakebaseRoundTripCheck(options = {}) {
|
|
|
922
985
|
};
|
|
923
986
|
}
|
|
924
987
|
|
|
988
|
+
// src/lakebase-branch.ts
|
|
989
|
+
init_workspace_context();
|
|
990
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
991
|
+
var DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
992
|
+
function resolveLakebaseProject(env, explicit) {
|
|
993
|
+
const project = explicit ?? env.DBX_TEST_LAKEBASE_PROJECT;
|
|
994
|
+
if (!project || !/^projects\/[a-z0-9][a-z0-9-]{0,62}$/.test(project)) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
`Lakebase project must look like projects/fabric-experiments; got '${project ?? ""}'`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return project;
|
|
1000
|
+
}
|
|
1001
|
+
function toLakebaseBranchId(environmentName) {
|
|
1002
|
+
const normalized = environmentName.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1003
|
+
const prefixed = normalized.startsWith("fx-") ? normalized : `fx-${normalized}`;
|
|
1004
|
+
const trimmed = prefixed.slice(0, 63).replace(/-+$/g, "");
|
|
1005
|
+
if (!/^fx-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(trimmed)) {
|
|
1006
|
+
throw new Error(`Cannot derive a safe Lakebase branch ID from '${environmentName}'`);
|
|
1007
|
+
}
|
|
1008
|
+
return trimmed;
|
|
1009
|
+
}
|
|
1010
|
+
async function createEphemeralLakebaseBranch(options) {
|
|
1011
|
+
const env = options.env ?? process.env;
|
|
1012
|
+
const client = options.client ?? createClientFromEnv(env);
|
|
1013
|
+
const project = resolveLakebaseProject(env, options.project);
|
|
1014
|
+
const branchId = toLakebaseBranchId(options.branchId);
|
|
1015
|
+
const ttlSeconds = Math.max(60, Math.ceil(options.ttlMs / 1e3));
|
|
1016
|
+
const operation = await client.post(
|
|
1017
|
+
`/api/2.0/postgres/${project}/branches`,
|
|
1018
|
+
{ spec: { ttl: `${ttlSeconds}s` } },
|
|
1019
|
+
{ branch_id: branchId, replace_existing: false }
|
|
1020
|
+
);
|
|
1021
|
+
const branch = await waitForOperation(client, operation, options);
|
|
1022
|
+
const branchName = branch?.name ?? `${project}/branches/${branchId}`;
|
|
1023
|
+
const [endpointResult, databaseResult] = await Promise.all([
|
|
1024
|
+
client.get(`/api/2.0/postgres/${branchName}/endpoints`),
|
|
1025
|
+
client.get(`/api/2.0/postgres/${branchName}/databases`)
|
|
1026
|
+
]);
|
|
1027
|
+
const endpoint = endpointResult.endpoints?.find((item) => item.endpoint_id === "primary") ?? endpointResult.endpoints?.[0];
|
|
1028
|
+
const database = databaseResult.databases?.[0];
|
|
1029
|
+
const host = endpoint?.status?.hosts?.host;
|
|
1030
|
+
const postgresDatabase = database?.status?.postgres_database;
|
|
1031
|
+
if (!endpoint?.name || !host || !database?.name || !postgresDatabase) {
|
|
1032
|
+
throw new Error(`Lakebase branch '${branchName}' did not expose an endpoint and database`);
|
|
1033
|
+
}
|
|
1034
|
+
const createdAt = branch?.create_time ?? (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1035
|
+
return {
|
|
1036
|
+
project,
|
|
1037
|
+
branch: branchName,
|
|
1038
|
+
branchId,
|
|
1039
|
+
endpoint: endpoint.name,
|
|
1040
|
+
host,
|
|
1041
|
+
databaseResource: database.name,
|
|
1042
|
+
database: postgresDatabase,
|
|
1043
|
+
createdAt,
|
|
1044
|
+
expiresAt: new Date(Date.parse(createdAt) + ttlSeconds * 1e3).toISOString()
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async function deleteEphemeralLakebaseBranch(branch, options = {}) {
|
|
1048
|
+
if (!/^projects\/[a-z0-9-]+\/branches\/fx-[a-z0-9-]+$/.test(branch.branch)) {
|
|
1049
|
+
throw new Error(`Refusing to delete non-ephemeral Lakebase branch '${branch.branch}'`);
|
|
1050
|
+
}
|
|
1051
|
+
const env = options.env ?? process.env;
|
|
1052
|
+
const client = options.client ?? createClientFromEnv(env);
|
|
1053
|
+
const operation = await client.request(
|
|
1054
|
+
"DELETE",
|
|
1055
|
+
`/api/2.0/postgres/${branch.branch}`,
|
|
1056
|
+
void 0,
|
|
1057
|
+
{ purge: true }
|
|
1058
|
+
);
|
|
1059
|
+
await waitForOperation(client, operation, options);
|
|
1060
|
+
}
|
|
1061
|
+
async function waitForOperation(client, initial, options) {
|
|
1062
|
+
let operation = initial;
|
|
1063
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1064
|
+
while (!operation.done) {
|
|
1065
|
+
if (!operation.name) throw new Error("Lakebase operation did not return an operation name");
|
|
1066
|
+
if (Date.now() >= deadline) throw new Error(`Lakebase operation '${operation.name}' timed out`);
|
|
1067
|
+
await delay(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
1068
|
+
operation = await client.get(`/api/2.0/postgres/${operation.name}`);
|
|
1069
|
+
}
|
|
1070
|
+
if (operation.error) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`Lakebase operation failed${operation.error.code ? ` (${operation.error.code})` : ""}: ${operation.error.message ?? "unknown error"}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return operation.response;
|
|
1076
|
+
}
|
|
1077
|
+
function delay(ms) {
|
|
1078
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
925
1081
|
// src/databricks-http.ts
|
|
926
1082
|
init_workspace_context();
|
|
927
1083
|
async function databricksFetch(env, pathOrUrl, init = {}, fetchImpl = fetch) {
|
|
@@ -1115,6 +1271,7 @@ exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
|
|
|
1115
1271
|
exports.compareToGolden = compareToGolden;
|
|
1116
1272
|
exports.createClientFromEnv = createClientFromEnv;
|
|
1117
1273
|
exports.createDuckDbContext = createDuckDbContext;
|
|
1274
|
+
exports.createEphemeralLakebaseBranch = createEphemeralLakebaseBranch;
|
|
1118
1275
|
exports.createExecutionContext = createExecutionContext;
|
|
1119
1276
|
exports.createLakebaseTestProvider = createLakebaseTestProvider;
|
|
1120
1277
|
exports.createMockDatabricksClient = createMockDatabricksClient;
|
|
@@ -1122,6 +1279,7 @@ exports.createWorkspaceContext = createWorkspaceContext;
|
|
|
1122
1279
|
exports.customLiveCheck = customLiveCheck;
|
|
1123
1280
|
exports.dbtBuildCheck = dbtBuildCheck;
|
|
1124
1281
|
exports.defineLiveSuite = defineLiveSuite;
|
|
1282
|
+
exports.deleteEphemeralLakebaseBranch = deleteEphemeralLakebaseBranch;
|
|
1125
1283
|
exports.deleteVolumeFile = deleteVolumeFile;
|
|
1126
1284
|
exports.deltaContractCheck = deltaContractCheck;
|
|
1127
1285
|
exports.ensureVolumeDirectory = ensureVolumeDirectory;
|
|
@@ -1138,11 +1296,14 @@ exports.parseStatementRows = parseStatementRows;
|
|
|
1138
1296
|
exports.pipelineRefreshCheck = pipelineRefreshCheck;
|
|
1139
1297
|
exports.renderJUnit = renderJUnit;
|
|
1140
1298
|
exports.resolveFixtureRows = resolveFixtureRows;
|
|
1299
|
+
exports.resolveLakebaseProject = resolveLakebaseProject;
|
|
1141
1300
|
exports.resolveProfile = resolveProfile;
|
|
1142
1301
|
exports.resolveTokenProvider = resolveTokenProvider;
|
|
1302
|
+
exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
|
|
1143
1303
|
exports.runLiveSuite = runLiveSuite;
|
|
1144
1304
|
exports.secretScopeCheck = secretScopeCheck;
|
|
1145
1305
|
exports.sqlRoundTripCheck = sqlRoundTripCheck;
|
|
1306
|
+
exports.toLakebaseBranchId = toLakebaseBranchId;
|
|
1146
1307
|
exports.toNamedParameters = toNamedParameters;
|
|
1147
1308
|
exports.unityCatalogAccessCheck = unityCatalogAccessCheck;
|
|
1148
1309
|
exports.uploadVolumeFile = uploadVolumeFile;
|