@lightdash/cli 2.228.1 → 2.229.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/.tsbuildinfo.build +1 -0
- package/package.json +4 -4
- package/dist/.tsbuildinfo +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -72
- package/dist/dbt/context.test.d.ts +0 -2
- package/dist/dbt/context.test.d.ts.map +0 -1
- package/dist/dbt/context.test.js +0 -152
- package/dist/dbt/manifest.test.d.ts +0 -2
- package/dist/dbt/manifest.test.d.ts.map +0 -1
- package/dist/dbt/manifest.test.js +0 -170
- package/dist/dbt/models.test.d.ts +0 -2
- package/dist/dbt/models.test.d.ts.map +0 -1
- package/dist/dbt/models.test.js +0 -139
- package/dist/dbt/profiles.test.d.ts +0 -2
- package/dist/dbt/profiles.test.d.ts.map +0 -1
- package/dist/dbt/profiles.test.js +0 -50
- package/dist/dbt/targets/athena.test.d.ts +0 -2
- package/dist/dbt/targets/athena.test.d.ts.map +0 -1
- package/dist/dbt/targets/athena.test.js +0 -78
- package/dist/dbt/targets/duckdb.test.d.ts +0 -2
- package/dist/dbt/targets/duckdb.test.d.ts.map +0 -1
- package/dist/dbt/targets/duckdb.test.js +0 -149
- package/dist/dbt/targets/redshift.test.d.ts +0 -2
- package/dist/dbt/targets/redshift.test.d.ts.map +0 -1
- package/dist/dbt/targets/redshift.test.js +0 -198
- package/dist/dbt/templating.test.d.ts +0 -2
- package/dist/dbt/templating.test.d.ts.map +0 -1
- package/dist/dbt/templating.test.js +0 -51
- package/dist/dbt/validation.test.d.ts +0 -2
- package/dist/dbt/validation.test.d.ts.map +0 -1
- package/dist/dbt/validation.test.js +0 -67
- package/dist/handlers/apps/appCodeFiles.test.d.ts +0 -2
- package/dist/handlers/apps/appCodeFiles.test.d.ts.map +0 -1
- package/dist/handlers/apps/appCodeFiles.test.js +0 -391
- package/dist/handlers/apps/appsDownload.test.d.ts +0 -2
- package/dist/handlers/apps/appsDownload.test.d.ts.map +0 -1
- package/dist/handlers/apps/appsDownload.test.js +0 -639
- package/dist/handlers/apps/build.test.d.ts +0 -2
- package/dist/handlers/apps/build.test.d.ts.map +0 -1
- package/dist/handlers/apps/build.test.js +0 -252
- package/dist/handlers/apps/createApp.test.d.ts +0 -2
- package/dist/handlers/apps/createApp.test.d.ts.map +0 -1
- package/dist/handlers/apps/createApp.test.js +0 -340
- package/dist/handlers/apps/pathContainment.test.d.ts +0 -2
- package/dist/handlers/apps/pathContainment.test.d.ts.map +0 -1
- package/dist/handlers/apps/pathContainment.test.js +0 -66
- package/dist/handlers/apps/preview.test.d.ts +0 -2
- package/dist/handlers/apps/preview.test.d.ts.map +0 -1
- package/dist/handlers/apps/preview.test.js +0 -353
- package/dist/handlers/apps/previewProxy.test.d.ts +0 -2
- package/dist/handlers/apps/previewProxy.test.d.ts.map +0 -1
- package/dist/handlers/apps/previewProxy.test.js +0 -178
- package/dist/handlers/apps/scaffolding.test.d.ts +0 -2
- package/dist/handlers/apps/scaffolding.test.d.ts.map +0 -1
- package/dist/handlers/apps/scaffolding.test.js +0 -151
- package/dist/handlers/apps/uploadRetry.test.d.ts +0 -2
- package/dist/handlers/apps/uploadRetry.test.d.ts.map +0 -1
- package/dist/handlers/apps/uploadRetry.test.js +0 -103
- package/dist/handlers/apps/validate.test.d.ts +0 -2
- package/dist/handlers/apps/validate.test.d.ts.map +0 -1
- package/dist/handlers/apps/validate.test.js +0 -526
- package/dist/handlers/compile.test.d.ts +0 -2
- package/dist/handlers/compile.test.d.ts.map +0 -1
- package/dist/handlers/compile.test.js +0 -151
- package/dist/handlers/compileProject.test.d.ts +0 -2
- package/dist/handlers/compileProject.test.d.ts.map +0 -1
- package/dist/handlers/compileProject.test.js +0 -637
- package/dist/handlers/compileWarehouseColumnValidation.test.d.ts +0 -2
- package/dist/handlers/compileWarehouseColumnValidation.test.d.ts.map +0 -1
- package/dist/handlers/compileWarehouseColumnValidation.test.js +0 -493
- package/dist/handlers/connectSnowflake.test.d.ts +0 -2
- package/dist/handlers/connectSnowflake.test.d.ts.map +0 -1
- package/dist/handlers/connectSnowflake.test.js +0 -320
- package/dist/handlers/contentAsCode/fileNames.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/fileNames.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/fileNames.test.js +0 -79
- package/dist/handlers/contentAsCode/registry.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/registry.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/registry.test.js +0 -18
- package/dist/handlers/contentAsCode/resource.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/resource.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/resource.test.js +0 -121
- package/dist/handlers/dbt/getDbtVersion.test.d.ts +0 -2
- package/dist/handlers/dbt/getDbtVersion.test.d.ts.map +0 -1
- package/dist/handlers/dbt/getDbtVersion.test.js +0 -106
- package/dist/handlers/deploy.test.d.ts +0 -2
- package/dist/handlers/deploy.test.d.ts.map +0 -1
- package/dist/handlers/deploy.test.js +0 -244
- package/dist/handlers/download.test.d.ts +0 -2
- package/dist/handlers/download.test.d.ts.map +0 -1
- package/dist/handlers/download.test.js +0 -1739
- package/dist/handlers/exportChartImage.test.d.ts +0 -2
- package/dist/handlers/exportChartImage.test.d.ts.map +0 -1
- package/dist/handlers/exportChartImage.test.js +0 -55
- package/dist/handlers/homepagesAsCode.test.d.ts +0 -2
- package/dist/handlers/homepagesAsCode.test.d.ts.map +0 -1
- package/dist/handlers/homepagesAsCode.test.js +0 -72
- package/dist/handlers/lint.test.d.ts +0 -2
- package/dist/handlers/lint.test.d.ts.map +0 -1
- package/dist/handlers/lint.test.js +0 -221
- package/dist/handlers/login.test.d.ts +0 -2
- package/dist/handlers/login.test.d.ts.map +0 -1
- package/dist/handlers/login.test.js +0 -24
- package/dist/handlers/metadataFile.test.d.ts +0 -2
- package/dist/handlers/metadataFile.test.d.ts.map +0 -1
- package/dist/handlers/metadataFile.test.js +0 -46
- package/dist/handlers/organizationContent/customRoles.test.d.ts +0 -2
- package/dist/handlers/organizationContent/customRoles.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/customRoles.test.js +0 -179
- package/dist/handlers/organizationContent/groups.test.d.ts +0 -2
- package/dist/handlers/organizationContent/groups.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/groups.test.js +0 -135
- package/dist/handlers/organizationContent/index.test.d.ts +0 -2
- package/dist/handlers/organizationContent/index.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/index.test.js +0 -212
- package/dist/handlers/organizationContent/themes.test.d.ts +0 -2
- package/dist/handlers/organizationContent/themes.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/themes.test.js +0 -180
- package/dist/handlers/organizationContent/users.test.d.ts +0 -2
- package/dist/handlers/organizationContent/users.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/users.test.js +0 -155
- package/dist/handlers/preAggregateAudit.test.d.ts +0 -2
- package/dist/handlers/preAggregateAudit.test.d.ts.map +0 -1
- package/dist/handlers/preAggregateAudit.test.js +0 -102
- package/dist/handlers/renameHandler.test.d.ts +0 -2
- package/dist/handlers/renameHandler.test.d.ts.map +0 -1
- package/dist/handlers/renameHandler.test.js +0 -189
- package/dist/handlers/resolveProjectFlag.test.d.ts +0 -2
- package/dist/handlers/resolveProjectFlag.test.d.ts.map +0 -1
- package/dist/handlers/resolveProjectFlag.test.js +0 -48
- package/dist/handlers/selectProject.test.d.ts +0 -2
- package/dist/handlers/selectProject.test.d.ts.map +0 -1
- package/dist/handlers/selectProject.test.js +0 -224
- package/dist/handlers/setProject.test.d.ts +0 -2
- package/dist/handlers/setProject.test.d.ts.map +0 -1
- package/dist/handlers/setProject.test.js +0 -63
- package/dist/handlers/slugUpdate.test.d.ts +0 -2
- package/dist/handlers/slugUpdate.test.d.ts.map +0 -1
- package/dist/handlers/slugUpdate.test.js +0 -265
- package/dist/handlers/timestampConversion.test.d.ts +0 -2
- package/dist/handlers/timestampConversion.test.d.ts.map +0 -1
- package/dist/handlers/timestampConversion.test.js +0 -87
- package/dist/handlers/upgradeCheck.test.d.ts +0 -2
- package/dist/handlers/upgradeCheck.test.d.ts.map +0 -1
- package/dist/handlers/upgradeCheck.test.js +0 -227
- package/dist/handlers/validate.test.d.ts +0 -2
- package/dist/handlers/validate.test.d.ts.map +0 -1
- package/dist/handlers/validate.test.js +0 -318
- package/dist/handlers/validateSpaceFilter.test.d.ts +0 -2
- package/dist/handlers/validateSpaceFilter.test.d.ts.map +0 -1
- package/dist/handlers/validateSpaceFilter.test.js +0 -79
- package/dist/handlers/warehouseConnection.test.d.ts +0 -2
- package/dist/handlers/warehouseConnection.test.d.ts.map +0 -1
- package/dist/handlers/warehouseConnection.test.js +0 -75
- package/dist/lightdash-config/lightdash-config.test.d.ts +0 -2
- package/dist/lightdash-config/lightdash-config.test.d.ts.map +0 -1
- package/dist/lightdash-config/lightdash-config.test.js +0 -69
- package/dist/prompts/fuzzySearchPrompt.test.d.ts +0 -2
- package/dist/prompts/fuzzySearchPrompt.test.d.ts.map +0 -1
- package/dist/prompts/fuzzySearchPrompt.test.js +0 -53
- package/dist/refsArgument.test.d.ts +0 -2
- package/dist/refsArgument.test.d.ts.map +0 -1
- package/dist/refsArgument.test.js +0 -42
- package/dist/releaseSafety/index.test.d.ts +0 -2
- package/dist/releaseSafety/index.test.d.ts.map +0 -1
- package/dist/releaseSafety/index.test.js +0 -154
- package/dist/releaseSafety/version.test.d.ts +0 -2
- package/dist/releaseSafety/version.test.d.ts.map +0 -1
- package/dist/releaseSafety/version.test.js +0 -22
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -112
- package/dist/terminal/contentAsCodeOutput.test.d.ts +0 -2
- package/dist/terminal/contentAsCodeOutput.test.d.ts.map +0 -1
- package/dist/terminal/contentAsCodeOutput.test.js +0 -231
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"exportChartImage.test.d.ts","sourceRoot":"","sources":["../../src/handlers/exportChartImage.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const config_1 = require("../config");
|
|
4
|
-
const apiClient_1 = require("./dbt/apiClient");
|
|
5
|
-
const exportChartImage_1 = require("./exportChartImage");
|
|
6
|
-
vi.mock('../config', () => ({ getConfig: vi.fn() }));
|
|
7
|
-
vi.mock('./dbt/apiClient', () => ({ lightdashApi: vi.fn() }));
|
|
8
|
-
vi.mock('../globalState', () => ({
|
|
9
|
-
default: {
|
|
10
|
-
setVerbose: vi.fn(),
|
|
11
|
-
startSpinner: vi.fn(() => ({ warn: vi.fn() })),
|
|
12
|
-
},
|
|
13
|
-
}));
|
|
14
|
-
describe('exportChartImageHandler', () => {
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.mocked(apiClient_1.lightdashApi).mockResolvedValue(undefined);
|
|
17
|
-
});
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
vi.clearAllMocks();
|
|
20
|
-
});
|
|
21
|
-
it('scopes a slug to the selected project', async () => {
|
|
22
|
-
vi.mocked(config_1.getConfig).mockResolvedValue({
|
|
23
|
-
context: {
|
|
24
|
-
project: '22222222-2222-4222-8222-222222222222',
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
await (0, exportChartImage_1.exportChartImageHandler)('shared-slug', { output: 'chart.png' });
|
|
28
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
|
|
29
|
-
method: 'POST',
|
|
30
|
-
url: '/api/v1/saved/shared-slug/export?projectUuid=22222222-2222-4222-8222-222222222222',
|
|
31
|
-
body: undefined,
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
it('does not scope UUID exports to the selected project', async () => {
|
|
35
|
-
vi.mocked(config_1.getConfig).mockResolvedValue({
|
|
36
|
-
context: {
|
|
37
|
-
project: '22222222-2222-4222-8222-222222222222',
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
await (0, exportChartImage_1.exportChartImageHandler)('11111111-1111-4111-8111-111111111111', {
|
|
41
|
-
output: 'chart.png',
|
|
42
|
-
});
|
|
43
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
|
|
44
|
-
method: 'POST',
|
|
45
|
-
url: '/api/v1/saved/11111111-1111-4111-8111-111111111111/export',
|
|
46
|
-
body: undefined,
|
|
47
|
-
});
|
|
48
|
-
expect(config_1.getConfig).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
it('rejects a slug without project context', async () => {
|
|
51
|
-
vi.mocked(config_1.getConfig).mockResolvedValue({});
|
|
52
|
-
await expect((0, exportChartImage_1.exportChartImageHandler)('shared-slug', { output: 'chart.png' })).rejects.toThrow('A project is required when exporting by slug');
|
|
53
|
-
expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"homepagesAsCode.test.d.ts","sourceRoot":"","sources":["../../src/handlers/homepagesAsCode.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const common_1 = require("@lightdash/common");
|
|
4
|
-
const vitest_1 = require("vitest");
|
|
5
|
-
const resource_1 = require("./contentAsCode/resource");
|
|
6
|
-
const apiClient_1 = require("./dbt/apiClient");
|
|
7
|
-
const homepagesAsCode_1 = require("./homepagesAsCode");
|
|
8
|
-
vitest_1.vi.mock('./dbt/apiClient', () => ({ lightdashApi: vitest_1.vi.fn() }));
|
|
9
|
-
vitest_1.vi.mock('./contentAsCode/resource', () => ({
|
|
10
|
-
readCodeResourceFiles: vitest_1.vi.fn(),
|
|
11
|
-
writeCodeResourceDocuments: vitest_1.vi.fn(),
|
|
12
|
-
assertCodeResourceFilesValid: vitest_1.vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
vitest_1.vi.mock('../globalState', () => ({ default: { log: vitest_1.vi.fn() } }));
|
|
15
|
-
const document = {
|
|
16
|
-
contentType: common_1.ContentAsCodeType.HOMEPAGE,
|
|
17
|
-
version: 1,
|
|
18
|
-
name: 'Home / Team',
|
|
19
|
-
config: { version: 1, rows: [] },
|
|
20
|
-
publication: null,
|
|
21
|
-
};
|
|
22
|
-
(0, vitest_1.describe)('homepage CLI download/upload', () => {
|
|
23
|
-
(0, vitest_1.beforeEach)(() => vitest_1.vi.clearAllMocks());
|
|
24
|
-
(0, vitest_1.it)('requests exact names and preserves other files during a filtered download', async () => {
|
|
25
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValue({
|
|
26
|
-
homepages: [document],
|
|
27
|
-
missingNames: [],
|
|
28
|
-
});
|
|
29
|
-
await (0, homepagesAsCode_1.downloadHomepages)('project', [document.name], '/tmp/homepage-code');
|
|
30
|
-
(0, vitest_1.expect)(apiClient_1.lightdashApi).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
|
|
31
|
-
url: '/api/v1/projects/project/code/homepages?names=Home+%2F+Team',
|
|
32
|
-
}));
|
|
33
|
-
(0, vitest_1.expect)(resource_1.writeCodeResourceDocuments).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
|
|
34
|
-
documents: [document],
|
|
35
|
-
pruneOtherDocuments: false,
|
|
36
|
-
}));
|
|
37
|
-
});
|
|
38
|
-
(0, vitest_1.it)('skips unavailable homepages only for implicit include-all downloads', async () => {
|
|
39
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockRejectedValue(new common_1.ForbiddenError());
|
|
40
|
-
await (0, vitest_1.expect)((0, homepagesAsCode_1.downloadHomepages)('project', [], undefined, true)).resolves.toBe(0);
|
|
41
|
-
await (0, vitest_1.expect)((0, homepagesAsCode_1.downloadHomepages)('project', [], undefined, false)).rejects.toThrow();
|
|
42
|
-
(0, vitest_1.expect)(resource_1.writeCodeResourceDocuments).not.toHaveBeenCalled();
|
|
43
|
-
});
|
|
44
|
-
(0, vitest_1.it)('passes the publication option explicitly and reports unchanged uploads', async () => {
|
|
45
|
-
vitest_1.vi.mocked(resource_1.readCodeResourceFiles).mockResolvedValue({
|
|
46
|
-
files: [{ filePath: 'home.yml', document }],
|
|
47
|
-
failures: [],
|
|
48
|
-
});
|
|
49
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValue({
|
|
50
|
-
action: common_1.PromotionAction.NO_CHANGES,
|
|
51
|
-
});
|
|
52
|
-
await (0, vitest_1.expect)((0, homepagesAsCode_1.uploadHomepages)('project', [], {}, false)).resolves.toEqual({ 'homepages unchanged': 1 });
|
|
53
|
-
(0, vitest_1.expect)(apiClient_1.lightdashApi).toHaveBeenLastCalledWith(vitest_1.expect.objectContaining({
|
|
54
|
-
method: 'POST',
|
|
55
|
-
url: '/api/v1/projects/project/code/homepages/Home%20%2F%20Team?publish=false',
|
|
56
|
-
}));
|
|
57
|
-
await (0, homepagesAsCode_1.uploadHomepages)('project', [], {}, true);
|
|
58
|
-
(0, vitest_1.expect)(apiClient_1.lightdashApi).toHaveBeenLastCalledWith(vitest_1.expect.objectContaining({
|
|
59
|
-
url: '/api/v1/projects/project/code/homepages/Home%20%2F%20Team?publish=true',
|
|
60
|
-
}));
|
|
61
|
-
});
|
|
62
|
-
(0, vitest_1.it)('fails missing selections and records API failures for a nonzero CLI exit', async () => {
|
|
63
|
-
vitest_1.vi.mocked(resource_1.readCodeResourceFiles).mockResolvedValue({
|
|
64
|
-
files: [{ filePath: 'home.yml', document }],
|
|
65
|
-
failures: [],
|
|
66
|
-
});
|
|
67
|
-
await (0, vitest_1.expect)((0, homepagesAsCode_1.uploadHomepages)('project', ['Missing'], {}, false)).rejects.toThrow('not found locally');
|
|
68
|
-
(0, vitest_1.expect)(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
69
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockRejectedValue(new Error('Missing reference'));
|
|
70
|
-
await (0, vitest_1.expect)((0, homepagesAsCode_1.uploadHomepages)('project', [], {}, false)).resolves.toEqual({ 'homepages with errors': 1 });
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lint.test.d.ts","sourceRoot":"","sources":["../../src/handlers/lint.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
const os = tslib_1.__importStar(require("os"));
|
|
6
|
-
const path = tslib_1.__importStar(require("path"));
|
|
7
|
-
const lint_1 = require("./lint");
|
|
8
|
-
vi.mock('../analytics/analytics', () => ({
|
|
9
|
-
categorizeError: vi.fn(),
|
|
10
|
-
LightdashAnalytics: {
|
|
11
|
-
track: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
},
|
|
13
|
-
}));
|
|
14
|
-
describe('lintHandler', () => {
|
|
15
|
-
let tempDir;
|
|
16
|
-
let output;
|
|
17
|
-
beforeEach(async () => {
|
|
18
|
-
tempDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'lightdash-lint-'));
|
|
19
|
-
output = [];
|
|
20
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
|
21
|
-
output.push(args.map(String).join(' '));
|
|
22
|
-
});
|
|
23
|
-
vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
24
|
-
throw new Error('process.exit');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
vi.restoreAllMocks();
|
|
29
|
-
await fs_1.promises.rm(tempDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
const expectLintFailure = async () => {
|
|
32
|
-
await expect((0, lint_1.lintHandler)({ path: tempDir, format: 'json' })).rejects.toThrow('process.exit');
|
|
33
|
-
expect(process.exit).toHaveBeenCalledWith(2);
|
|
34
|
-
return output.join('\n');
|
|
35
|
-
};
|
|
36
|
-
test('fails for a dashboard upload file missing its required version', async () => {
|
|
37
|
-
const dashboardsDir = path.join(tempDir, 'dashboards');
|
|
38
|
-
await fs_1.promises.mkdir(dashboardsDir);
|
|
39
|
-
await fs_1.promises.writeFile(path.join(dashboardsDir, 'missing-version.yml'), [
|
|
40
|
-
'name: Missing version',
|
|
41
|
-
'slug: missing-version',
|
|
42
|
-
'spaceSlug: shared',
|
|
43
|
-
'tiles: []',
|
|
44
|
-
'tabs: []',
|
|
45
|
-
].join('\n'));
|
|
46
|
-
const lintOutput = await expectLintFailure();
|
|
47
|
-
expect(lintOutput).toContain('missing-version.yml');
|
|
48
|
-
expect(lintOutput).toContain("Missing required property 'version'");
|
|
49
|
-
});
|
|
50
|
-
test('fails for a chart missing its required version alongside a valid sibling', async () => {
|
|
51
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
52
|
-
const dashboardsDir = path.join(tempDir, 'dashboards');
|
|
53
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
54
|
-
await fs_1.promises.mkdir(dashboardsDir);
|
|
55
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'missing-version.yml'), [
|
|
56
|
-
'name: Missing version',
|
|
57
|
-
'slug: missing-version',
|
|
58
|
-
'spaceSlug: shared',
|
|
59
|
-
'tableName: orders',
|
|
60
|
-
'metricQuery:',
|
|
61
|
-
' exploreName: orders',
|
|
62
|
-
' dimensions: []',
|
|
63
|
-
' metrics: []',
|
|
64
|
-
' filters: {}',
|
|
65
|
-
' sorts: []',
|
|
66
|
-
' limit: 500',
|
|
67
|
-
' tableCalculations: []',
|
|
68
|
-
' additionalMetrics: []',
|
|
69
|
-
' customDimensions: []',
|
|
70
|
-
'chartConfig:',
|
|
71
|
-
' type: table',
|
|
72
|
-
' config: {}',
|
|
73
|
-
].join('\n'));
|
|
74
|
-
await fs_1.promises.writeFile(path.join(dashboardsDir, 'valid.yml'), [
|
|
75
|
-
'name: Valid dashboard',
|
|
76
|
-
'slug: valid-dashboard',
|
|
77
|
-
'spaceSlug: shared',
|
|
78
|
-
'tiles: []',
|
|
79
|
-
'tabs: []',
|
|
80
|
-
'version: 1',
|
|
81
|
-
].join('\n'));
|
|
82
|
-
await expect((0, lint_1.lintHandler)({ path: tempDir, format: 'cli' })).rejects.toThrow('process.exit');
|
|
83
|
-
expect(process.exit).toHaveBeenCalledWith(2);
|
|
84
|
-
const lintOutput = output.join('\n');
|
|
85
|
-
expect(lintOutput).toContain('missing-version.yml');
|
|
86
|
-
expect(lintOutput).toContain("Missing required property 'version'");
|
|
87
|
-
expect(lintOutput).toContain('2 Lightdash Code files');
|
|
88
|
-
expect(lintOutput).toContain('1 valid');
|
|
89
|
-
expect(lintOutput).toContain('1 invalid');
|
|
90
|
-
});
|
|
91
|
-
test('fails when a chart upload file cannot be parsed', async () => {
|
|
92
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
93
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
94
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'invalid-yaml.yml'), ['name: Invalid YAML', 'slug: invalid-yaml', 'sql: ]'].join('\n'));
|
|
95
|
-
const lintOutput = await expectLintFailure();
|
|
96
|
-
expect(lintOutput).toContain('invalid-yaml.yml');
|
|
97
|
-
expect(lintOutput).toContain('chart/parse');
|
|
98
|
-
expect(lintOutput).toContain('"startLine": 3');
|
|
99
|
-
});
|
|
100
|
-
test('reports empty chart upload files as invalid', async () => {
|
|
101
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
102
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
103
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'empty.yml'), '');
|
|
104
|
-
const lintOutput = await expectLintFailure();
|
|
105
|
-
expect(lintOutput).toContain('empty.yml');
|
|
106
|
-
expect(lintOutput).toContain('chart/type');
|
|
107
|
-
});
|
|
108
|
-
test('validates loose chart files by contentType', async () => {
|
|
109
|
-
await fs_1.promises.writeFile(path.join(tempDir, 'loose-chart.yml'), [
|
|
110
|
-
'contentType: chart',
|
|
111
|
-
'name: Missing version',
|
|
112
|
-
'metricQuery: {}',
|
|
113
|
-
].join('\n'));
|
|
114
|
-
const lintOutput = await expectLintFailure();
|
|
115
|
-
expect(lintOutput).toContain('loose-chart.yml');
|
|
116
|
-
expect(lintOutput).toContain("Missing required property 'version'");
|
|
117
|
-
});
|
|
118
|
-
test('validates SQL chart content as a dashboard when placed in dashboards', async () => {
|
|
119
|
-
const dashboardsDir = path.join(tempDir, 'dashboards');
|
|
120
|
-
await fs_1.promises.mkdir(dashboardsDir);
|
|
121
|
-
await fs_1.promises.writeFile(path.join(dashboardsDir, 'sql-chart.yml'), [
|
|
122
|
-
'contentType: sql_chart',
|
|
123
|
-
'name: Misplaced SQL chart',
|
|
124
|
-
'sql: SELECT 1',
|
|
125
|
-
].join('\n'));
|
|
126
|
-
const lintOutput = await expectLintFailure();
|
|
127
|
-
expect(lintOutput).toContain('sql-chart.yml');
|
|
128
|
-
expect(lintOutput).toContain("Missing required property 'version'");
|
|
129
|
-
});
|
|
130
|
-
test('accepts structurally recognized SQL charts selected for upload', async () => {
|
|
131
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
132
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
133
|
-
await fs_1.promises.writeFile(path.join(tempDir, 'sql-chart.yml'), [
|
|
134
|
-
'contentType: sql_chart',
|
|
135
|
-
'name: Loose SQL chart',
|
|
136
|
-
'sql: SELECT 1',
|
|
137
|
-
].join('\n'));
|
|
138
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'sql-chart.yml'), ['name: Folder SQL chart', 'sql: SELECT 1'].join('\n'));
|
|
139
|
-
await (0, lint_1.lintHandler)({ path: tempDir, format: 'cli' });
|
|
140
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
141
|
-
expect(output.join('\n')).toContain('All Lightdash Code files are valid!');
|
|
142
|
-
});
|
|
143
|
-
test('ignores loose YAML files upload would not select', async () => {
|
|
144
|
-
await fs_1.promises.writeFile(path.join(tempDir, 'unrelated.yml'), ['version: 1', 'metricQuery: {}', 'name: Not uploadable'].join('\n'));
|
|
145
|
-
await (0, lint_1.lintHandler)({ path: tempDir, format: 'cli' });
|
|
146
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
147
|
-
expect(output.join('\n')).toContain('No Lightdash Code files found.');
|
|
148
|
-
});
|
|
149
|
-
test('accepts merge keys that upload resolves with js-yaml', async () => {
|
|
150
|
-
const dashboardsDir = path.join(tempDir, 'dashboards');
|
|
151
|
-
await fs_1.promises.mkdir(dashboardsDir);
|
|
152
|
-
await fs_1.promises.writeFile(path.join(dashboardsDir, 'merged-tabs.yml'), [
|
|
153
|
-
'contentType: dashboard',
|
|
154
|
-
'name: Merged tabs',
|
|
155
|
-
'slug: merged-tabs',
|
|
156
|
-
'spaceSlug: shared',
|
|
157
|
-
'version: 1',
|
|
158
|
-
'tiles: []',
|
|
159
|
-
'tabs:',
|
|
160
|
-
' - &tab',
|
|
161
|
-
' name: Overview',
|
|
162
|
-
' order: 0',
|
|
163
|
-
' - <<: *tab',
|
|
164
|
-
' name: Details',
|
|
165
|
-
' order: 1',
|
|
166
|
-
].join('\n'));
|
|
167
|
-
await (0, lint_1.lintHandler)({ path: tempDir, format: 'cli' });
|
|
168
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
169
|
-
expect(output.join('\n')).toContain('All Lightdash Code files are valid!');
|
|
170
|
-
});
|
|
171
|
-
test('reports parse errors for loose content files and models', async () => {
|
|
172
|
-
const modelsDir = path.join(tempDir, 'models');
|
|
173
|
-
await fs_1.promises.mkdir(modelsDir);
|
|
174
|
-
await fs_1.promises.writeFile(path.join(tempDir, 'loose-chart.yml'), [
|
|
175
|
-
'contentType: chart',
|
|
176
|
-
'name: Broken chart',
|
|
177
|
-
'bad: [unterminated',
|
|
178
|
-
].join('\n'));
|
|
179
|
-
await fs_1.promises.writeFile(path.join(modelsDir, 'broken-model.yml'), ['type: model', 'name: Broken model', 'bad: [unterminated'].join('\n'));
|
|
180
|
-
const lintOutput = await expectLintFailure();
|
|
181
|
-
expect(lintOutput).toContain('loose-chart.yml');
|
|
182
|
-
expect(lintOutput).toContain('chart/parse');
|
|
183
|
-
expect(lintOutput).toContain('broken-model.yml');
|
|
184
|
-
expect(lintOutput).toContain('model/parse');
|
|
185
|
-
});
|
|
186
|
-
test('reports .yaml content files as unsupported', async () => {
|
|
187
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
188
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
189
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'unsupported.yaml'), 'name: Unsupported extension');
|
|
190
|
-
const lintOutput = await expectLintFailure();
|
|
191
|
-
expect(lintOutput).toContain('unsupported.yaml');
|
|
192
|
-
expect(lintOutput).toContain('chart/extension');
|
|
193
|
-
expect(lintOutput).toContain("Content files must use the '.yml' extension");
|
|
194
|
-
});
|
|
195
|
-
test('defaults strict content discovery to the upload root', async () => {
|
|
196
|
-
const thirdPartyChartsDir = path.join(tempDir, 'vendor', 'charts');
|
|
197
|
-
await fs_1.promises.mkdir(thirdPartyChartsDir, { recursive: true });
|
|
198
|
-
await fs_1.promises.writeFile(path.join(thirdPartyChartsDir, 'unrelated.yml'), 'name: Not Lightdash content');
|
|
199
|
-
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
200
|
-
await (0, lint_1.lintHandler)({ format: 'cli' });
|
|
201
|
-
expect(process.exit).not.toHaveBeenCalled();
|
|
202
|
-
expect(output.join('\n')).toContain('No Lightdash Code files found.');
|
|
203
|
-
});
|
|
204
|
-
test('discovers content when run from inside the download root', async () => {
|
|
205
|
-
const chartsDir = path.join(tempDir, 'charts');
|
|
206
|
-
await fs_1.promises.mkdir(chartsDir);
|
|
207
|
-
await fs_1.promises.writeFile(path.join(chartsDir, 'missing-version.yml'), 'name: Missing version');
|
|
208
|
-
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
209
|
-
await expect((0, lint_1.lintHandler)({ format: 'cli' })).rejects.toThrow('process.exit');
|
|
210
|
-
expect(output.join('\n')).toContain('missing-version.yml');
|
|
211
|
-
expect(output.join('\n')).toContain("Missing required property 'version'");
|
|
212
|
-
});
|
|
213
|
-
test('shows the filename when linting a single file', async () => {
|
|
214
|
-
const dashboardsDir = path.join(tempDir, 'dashboards');
|
|
215
|
-
await fs_1.promises.mkdir(dashboardsDir);
|
|
216
|
-
const dashboardPath = path.join(dashboardsDir, 'single-file.yml');
|
|
217
|
-
await fs_1.promises.writeFile(dashboardPath, 'name: Missing version');
|
|
218
|
-
await expect((0, lint_1.lintHandler)({ path: dashboardPath, format: 'cli' })).rejects.toThrow('process.exit');
|
|
219
|
-
expect(output.join('\n')).toContain('✗ single-file.yml');
|
|
220
|
-
});
|
|
221
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"login.test.d.ts","sourceRoot":"","sources":["../../src/handlers/login.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const login_1 = require("./login");
|
|
4
|
-
describe('isApiKeyEnvVarShadowing', () => {
|
|
5
|
-
it('is false when LIGHTDASH_API_KEY is not set', () => {
|
|
6
|
-
expect((0, login_1.isApiKeyEnvVarShadowing)(undefined, {})).toBe(false);
|
|
7
|
-
expect((0, login_1.isApiKeyEnvVarShadowing)('ldpat_from_login', {})).toBe(false);
|
|
8
|
-
});
|
|
9
|
-
it('is true when an interactive login saves a token the env var will shadow', () => {
|
|
10
|
-
expect((0, login_1.isApiKeyEnvVarShadowing)(undefined, {
|
|
11
|
-
LIGHTDASH_API_KEY: 'ldpat_stale_env',
|
|
12
|
-
})).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
it('is false for `lightdash login --token $LIGHTDASH_API_KEY`', () => {
|
|
15
|
-
expect((0, login_1.isApiKeyEnvVarShadowing)('ldpat_ci', {
|
|
16
|
-
LIGHTDASH_API_KEY: 'ldpat_ci',
|
|
17
|
-
})).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
it('is true when --token is a different token to the env var', () => {
|
|
20
|
-
expect((0, login_1.isApiKeyEnvVarShadowing)('ldpat_explicit', {
|
|
21
|
-
LIGHTDASH_API_KEY: 'ldpat_stale_env',
|
|
22
|
-
})).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"metadataFile.test.d.ts","sourceRoot":"","sources":["../../src/handlers/metadataFile.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
const os = tslib_1.__importStar(require("os"));
|
|
6
|
-
const path = tslib_1.__importStar(require("path"));
|
|
7
|
-
const metadataFile_1 = require("./metadataFile");
|
|
8
|
-
describe('writeMetadataFile', () => {
|
|
9
|
-
let tmpDir;
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
tmpDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-metadata-'));
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
it('creates the output root when it does not exist yet', async () => {
|
|
17
|
-
const baseDir = path.join(tmpDir, 'brand-new-dir');
|
|
18
|
-
await (0, metadataFile_1.writeMetadataFile)(baseDir, {
|
|
19
|
-
version: 1,
|
|
20
|
-
charts: {},
|
|
21
|
-
dashboards: {},
|
|
22
|
-
});
|
|
23
|
-
const written = JSON.parse(await fs_1.promises.readFile(path.join(baseDir, metadataFile_1.METADATA_FILENAME), 'utf-8'));
|
|
24
|
-
expect(written).toEqual({ version: 1, charts: {}, dashboards: {} });
|
|
25
|
-
});
|
|
26
|
-
it('merges with existing metadata', async () => {
|
|
27
|
-
await (0, metadataFile_1.writeMetadataFile)(tmpDir, {
|
|
28
|
-
version: 1,
|
|
29
|
-
charts: { 'chart-a': '2026-01-01T00:00:00Z' },
|
|
30
|
-
dashboards: {},
|
|
31
|
-
});
|
|
32
|
-
await (0, metadataFile_1.writeMetadataFile)(tmpDir, {
|
|
33
|
-
version: 1,
|
|
34
|
-
charts: { 'chart-b': '2026-02-02T00:00:00Z' },
|
|
35
|
-
dashboards: { 'dash-a': '2026-02-02T00:00:00Z' },
|
|
36
|
-
});
|
|
37
|
-
expect(await (0, metadataFile_1.readMetadataFile)(tmpDir)).toEqual({
|
|
38
|
-
version: 1,
|
|
39
|
-
charts: {
|
|
40
|
-
'chart-a': '2026-01-01T00:00:00Z',
|
|
41
|
-
'chart-b': '2026-02-02T00:00:00Z',
|
|
42
|
-
},
|
|
43
|
-
dashboards: { 'dash-a': '2026-02-02T00:00:00Z' },
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"customRoles.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/customRoles.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const common_1 = require("@lightdash/common");
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const yaml = tslib_1.__importStar(require("js-yaml"));
|
|
7
|
-
const os = tslib_1.__importStar(require("os"));
|
|
8
|
-
const path = tslib_1.__importStar(require("path"));
|
|
9
|
-
const vitest_1 = require("vitest");
|
|
10
|
-
const apiClient_1 = require("../dbt/apiClient");
|
|
11
|
-
const customRoles_1 = require("./customRoles");
|
|
12
|
-
const index_1 = require("./index");
|
|
13
|
-
vitest_1.vi.mock('../dbt/apiClient', async (importOriginal) => ({
|
|
14
|
-
...(await importOriginal()),
|
|
15
|
-
lightdashApi: vitest_1.vi.fn(),
|
|
16
|
-
}));
|
|
17
|
-
describe('custom roles as code', () => {
|
|
18
|
-
let tmpDir;
|
|
19
|
-
const writeCustomRole = async (filename, role) => {
|
|
20
|
-
const folder = (0, customRoles_1.getCustomRolesFolder)(tmpDir);
|
|
21
|
-
await fs_1.promises.mkdir(folder, { recursive: true });
|
|
22
|
-
await fs_1.promises.writeFile(path.join(folder, filename), yaml.dump(role, { sortKeys: true }));
|
|
23
|
-
};
|
|
24
|
-
const customRole = (name, overrides = {}) => ({
|
|
25
|
-
version: 1,
|
|
26
|
-
name,
|
|
27
|
-
description: null,
|
|
28
|
-
level: 'project',
|
|
29
|
-
scopes: ['view:Dashboard'],
|
|
30
|
-
...overrides,
|
|
31
|
-
});
|
|
32
|
-
beforeEach(async () => {
|
|
33
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockReset();
|
|
34
|
-
tmpDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'custom-roles-test-'));
|
|
35
|
-
});
|
|
36
|
-
afterEach(async () => {
|
|
37
|
-
vitest_1.vi.restoreAllMocks();
|
|
38
|
-
await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
|
|
39
|
-
});
|
|
40
|
-
it('uses the standard organization content folder by default', () => {
|
|
41
|
-
expect((0, index_1.getOrganizationContentFolder)()).toBe(path.join(process.cwd(), 'lightdash'));
|
|
42
|
-
expect((0, index_1.getOrganizationContentFolder)(tmpDir)).toBe(tmpDir);
|
|
43
|
-
expect((0, customRoles_1.getCustomRolesFolder)(tmpDir)).toBe(path.join(tmpDir, 'custom-roles'));
|
|
44
|
-
});
|
|
45
|
-
it('downloads portable custom role YAML from the code endpoint', async () => {
|
|
46
|
-
const folder = (0, customRoles_1.getCustomRolesFolder)(tmpDir);
|
|
47
|
-
await fs_1.promises.mkdir(folder, { recursive: true });
|
|
48
|
-
await fs_1.promises.writeFile(path.join(folder, 'stale.yml'), 'stale: true');
|
|
49
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
50
|
-
customRoles: [
|
|
51
|
-
customRole('Custom Role Name', {
|
|
52
|
-
description: 'A useful role',
|
|
53
|
-
level: 'organization',
|
|
54
|
-
scopes: ['manage:Space', 'view:Dashboard'],
|
|
55
|
-
}),
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
const total = await (0, customRoles_1.downloadCustomRoles)('organization-uuid', tmpDir);
|
|
59
|
-
expect(total).toBe(1);
|
|
60
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
|
|
61
|
-
method: 'GET',
|
|
62
|
-
url: '/api/v2/orgs/organization-uuid/code/roles',
|
|
63
|
-
body: undefined,
|
|
64
|
-
});
|
|
65
|
-
expect(yaml.load(await fs_1.promises.readFile(path.join(tmpDir, 'custom-roles', 'custom-role-name.yml'), 'utf8'))).toStrictEqual({
|
|
66
|
-
version: 1,
|
|
67
|
-
name: 'Custom Role Name',
|
|
68
|
-
description: 'A useful role',
|
|
69
|
-
level: 'organization',
|
|
70
|
-
scopes: ['manage:Space', 'view:Dashboard'],
|
|
71
|
-
});
|
|
72
|
-
expect((await fs_1.promises.readdir(folder)).sort()).toStrictEqual([
|
|
73
|
-
'custom-role-name.yml',
|
|
74
|
-
'stale.yml',
|
|
75
|
-
]);
|
|
76
|
-
});
|
|
77
|
-
it('uses stable hashes for normalized filename collisions', async () => {
|
|
78
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
79
|
-
customRoles: [customRole('Data Admin'), customRole('Data--Admin')],
|
|
80
|
-
});
|
|
81
|
-
await (0, customRoles_1.downloadCustomRoles)('organization-uuid', tmpDir);
|
|
82
|
-
const filenames = (await fs_1.promises.readdir((0, customRoles_1.getCustomRolesFolder)(tmpDir))).sort();
|
|
83
|
-
expect(filenames).toHaveLength(2);
|
|
84
|
-
expect(filenames.every((filename) => /^data-admin-[a-f0-9]{8}\.yml$/.test(filename))).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it('reuses an existing filename owned by the same identity', async () => {
|
|
87
|
-
await writeCustomRole('hand-authored-name.yml', customRole('Data Admin'));
|
|
88
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
89
|
-
customRoles: [customRole('Data Admin')],
|
|
90
|
-
});
|
|
91
|
-
await (0, customRoles_1.downloadCustomRoles)('organization-uuid', tmpDir);
|
|
92
|
-
expect(await fs_1.promises.readdir((0, customRoles_1.getCustomRolesFolder)(tmpDir))).toStrictEqual([
|
|
93
|
-
'hand-authored-name.yml',
|
|
94
|
-
]);
|
|
95
|
-
});
|
|
96
|
-
it('counts backend create, update, and no-op actions', async () => {
|
|
97
|
-
await writeCustomRole('a.yml', customRole('Role A'));
|
|
98
|
-
await writeCustomRole('b.yml', customRole('Role B'));
|
|
99
|
-
await writeCustomRole('c.yml', customRole('Role C'));
|
|
100
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi)
|
|
101
|
-
.mockResolvedValueOnce({ action: common_1.PromotionAction.CREATE })
|
|
102
|
-
.mockResolvedValueOnce({ action: common_1.PromotionAction.UPDATE })
|
|
103
|
-
.mockResolvedValueOnce({
|
|
104
|
-
action: common_1.PromotionAction.NO_CHANGES,
|
|
105
|
-
});
|
|
106
|
-
const summary = await (0, customRoles_1.uploadCustomRoles)('organization-uuid', tmpDir);
|
|
107
|
-
expect(summary).toStrictEqual({
|
|
108
|
-
created: 1,
|
|
109
|
-
updated: 1,
|
|
110
|
-
unchanged: 1,
|
|
111
|
-
failed: 0,
|
|
112
|
-
failures: [],
|
|
113
|
-
});
|
|
114
|
-
expect(apiClient_1.lightdashApi).toHaveBeenNthCalledWith(1, {
|
|
115
|
-
method: 'POST',
|
|
116
|
-
url: '/api/v2/orgs/organization-uuid/code/roles',
|
|
117
|
-
body: expect.any(String),
|
|
118
|
-
});
|
|
119
|
-
expect(JSON.parse(vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[0][0].body)).toStrictEqual(customRole('Role A'));
|
|
120
|
-
});
|
|
121
|
-
it('is a no-op when the custom roles folder is missing or empty', async () => {
|
|
122
|
-
await expect((0, customRoles_1.uploadCustomRoles)('organization-uuid', tmpDir)).resolves.toStrictEqual({
|
|
123
|
-
created: 0,
|
|
124
|
-
updated: 0,
|
|
125
|
-
unchanged: 0,
|
|
126
|
-
failed: 0,
|
|
127
|
-
failures: [],
|
|
128
|
-
});
|
|
129
|
-
await fs_1.promises.mkdir((0, customRoles_1.getCustomRolesFolder)(tmpDir), { recursive: true });
|
|
130
|
-
await expect((0, customRoles_1.uploadCustomRoles)('organization-uuid', tmpDir)).resolves.toStrictEqual({
|
|
131
|
-
created: 0,
|
|
132
|
-
updated: 0,
|
|
133
|
-
unchanged: 0,
|
|
134
|
-
failed: 0,
|
|
135
|
-
failures: [],
|
|
136
|
-
});
|
|
137
|
-
expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
it('rejects duplicate names before calling the API', async () => {
|
|
140
|
-
await writeCustomRole('one.yml', customRole('Duplicate role'));
|
|
141
|
-
await writeCustomRole('two.yml', customRole('Duplicate role'));
|
|
142
|
-
await expect((0, customRoles_1.readCustomRoleFiles)(tmpDir)).rejects.toThrow('Duplicate custom role name "Duplicate role"');
|
|
143
|
-
expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
144
|
-
});
|
|
145
|
-
it('continues after the backend rejects one role', async () => {
|
|
146
|
-
await writeCustomRole('a-invalid.yml', customRole('Invalid role', { scopes: ['delete:VirtualViewsss'] }));
|
|
147
|
-
await writeCustomRole('b-valid.yml', customRole('Valid role'));
|
|
148
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi)
|
|
149
|
-
.mockRejectedValueOnce(new Error('Unknown custom role scopes: delete:VirtualViewsss'))
|
|
150
|
-
.mockResolvedValueOnce({ action: common_1.PromotionAction.UPDATE });
|
|
151
|
-
const summary = await (0, customRoles_1.uploadCustomRoles)('organization-uuid', tmpDir);
|
|
152
|
-
expect(summary).toMatchObject({
|
|
153
|
-
created: 0,
|
|
154
|
-
updated: 1,
|
|
155
|
-
unchanged: 0,
|
|
156
|
-
failed: 1,
|
|
157
|
-
});
|
|
158
|
-
expect(summary.failures[0].message).toContain('Unknown custom role scopes: delete:VirtualViewsss');
|
|
159
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledTimes(2);
|
|
160
|
-
});
|
|
161
|
-
it('skips custom roles when the target has no Enterprise license', async () => {
|
|
162
|
-
await writeCustomRole('role.yml', customRole('Enterprise role'));
|
|
163
|
-
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockRejectedValueOnce(new common_1.LightdashError({
|
|
164
|
-
message: 'Custom roles require a Lightdash Enterprise license',
|
|
165
|
-
name: 'ForbiddenError',
|
|
166
|
-
statusCode: 403,
|
|
167
|
-
data: {},
|
|
168
|
-
}));
|
|
169
|
-
const summary = await (0, customRoles_1.uploadCustomRoles)('organization-uuid', tmpDir);
|
|
170
|
-
expect(summary).toMatchObject({
|
|
171
|
-
created: 0,
|
|
172
|
-
updated: 0,
|
|
173
|
-
unchanged: 0,
|
|
174
|
-
skipped: 1,
|
|
175
|
-
failed: 0,
|
|
176
|
-
failures: [],
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"groups.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/groups.test.ts"],"names":[],"mappings":""}
|