@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,66 +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 os = tslib_1.__importStar(require("os"));
|
|
7
|
-
const path = tslib_1.__importStar(require("path"));
|
|
8
|
-
const pathContainment_1 = require("./pathContainment");
|
|
9
|
-
const describePosix = process.platform === 'win32' ? describe.skip : describe;
|
|
10
|
-
const mkTmpDir = (prefix) => fs_1.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
11
|
-
describePosix('canonicalizeForContainment', () => {
|
|
12
|
-
it('returns the realpath of an existing directory unchanged when there are no symlinks', async () => {
|
|
13
|
-
const dir = await mkTmpDir('ld-canon-plain-');
|
|
14
|
-
await expect((0, pathContainment_1.canonicalizeForContainment)(dir)).resolves.toBe(await fs_1.promises.realpath(dir));
|
|
15
|
-
});
|
|
16
|
-
it('resolves a symlinked existing directory to its real target', async () => {
|
|
17
|
-
const target = await mkTmpDir('ld-canon-target-');
|
|
18
|
-
const linkParent = await mkTmpDir('ld-canon-linkparent-');
|
|
19
|
-
const link = path.join(linkParent, 'link');
|
|
20
|
-
await fs_1.promises.symlink(target, link);
|
|
21
|
-
await expect((0, pathContainment_1.canonicalizeForContainment)(link)).resolves.toBe(await fs_1.promises.realpath(target));
|
|
22
|
-
});
|
|
23
|
-
it('walks up to the nearest existing ancestor and rejoins non-existent segments', async () => {
|
|
24
|
-
const target = await mkTmpDir('ld-canon-target-');
|
|
25
|
-
const linkParent = await mkTmpDir('ld-canon-linkparent-');
|
|
26
|
-
const link = path.join(linkParent, 'link');
|
|
27
|
-
await fs_1.promises.symlink(target, link);
|
|
28
|
-
const nonExistent = path.join(link, 'not', 'yet', 'created');
|
|
29
|
-
await expect((0, pathContainment_1.canonicalizeForContainment)(nonExistent)).resolves.toBe(path.join(await fs_1.promises.realpath(target), 'not', 'yet', 'created'));
|
|
30
|
-
});
|
|
31
|
-
it('rethrows a non-ENOENT realpath failure instead of treating it as non-existent', async () => {
|
|
32
|
-
const dir = await mkTmpDir('ld-canon-notdir-');
|
|
33
|
-
const filePath = path.join(dir, 'a-file');
|
|
34
|
-
await fs_1.promises.writeFile(filePath, 'not a directory');
|
|
35
|
-
const impossiblePath = path.join(filePath, 'nested', 'more');
|
|
36
|
-
await expect((0, pathContainment_1.canonicalizeForContainment)(impossiblePath)).rejects.toMatchObject({ code: 'ENOTDIR' });
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describePosix('assertOutDirSafeToClear', () => {
|
|
40
|
-
it('is a no-op when outDir does not exist yet', async () => {
|
|
41
|
-
const appDir = await mkTmpDir('ld-safeclear-app-');
|
|
42
|
-
const outDirParent = await mkTmpDir('ld-safeclear-outparent-');
|
|
43
|
-
const outDir = path.join(outDirParent, 'dist');
|
|
44
|
-
await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).resolves.toBeUndefined();
|
|
45
|
-
});
|
|
46
|
-
it('does not throw for a disjoint existing outDir', async () => {
|
|
47
|
-
const appDir = await mkTmpDir('ld-safeclear-app-');
|
|
48
|
-
const outDir = await mkTmpDir('ld-safeclear-outdir-');
|
|
49
|
-
await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).resolves.toBeUndefined();
|
|
50
|
-
});
|
|
51
|
-
it('throws ParameterError when an existing outDir canonically contains appDir', async () => {
|
|
52
|
-
const outDir = await mkTmpDir('ld-safeclear-container-');
|
|
53
|
-
const appDir = path.join(outDir, 'app');
|
|
54
|
-
await fs_1.promises.mkdir(appDir);
|
|
55
|
-
await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).rejects.toThrow(common_1.ParameterError);
|
|
56
|
-
});
|
|
57
|
-
it('throws ParameterError when outDir is reached via a symlinked ancestor that contains appDir', async () => {
|
|
58
|
-
const parentDir = await mkTmpDir('ld-safeclear-parent-');
|
|
59
|
-
const appDir = path.join(parentDir, 'app');
|
|
60
|
-
await fs_1.promises.mkdir(appDir);
|
|
61
|
-
const linkParent = await mkTmpDir('ld-safeclear-linkparent-');
|
|
62
|
-
const link = path.join(linkParent, 'link-to-parent');
|
|
63
|
-
await fs_1.promises.symlink(parentDir, link);
|
|
64
|
-
await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, link)).rejects.toThrow(common_1.ParameterError);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"preview.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/preview.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const execa_1 = tslib_1.__importDefault(require("execa"));
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
7
|
-
const os = tslib_1.__importStar(require("os"));
|
|
8
|
-
const path = tslib_1.__importStar(require("path"));
|
|
9
|
-
const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
|
|
10
|
-
const utils_1 = require("../utils");
|
|
11
|
-
const appCodeFiles_1 = require("./appCodeFiles");
|
|
12
|
-
const preview_1 = require("./preview");
|
|
13
|
-
vi.mock('execa');
|
|
14
|
-
vi.mock('inquirer', () => ({ default: { prompt: vi.fn() } }));
|
|
15
|
-
const previewBundle = {
|
|
16
|
-
manifest: {
|
|
17
|
-
codeVersion: 1,
|
|
18
|
-
appUuid: 'app-uuid-1',
|
|
19
|
-
projectUuid: 'proj-uuid-1',
|
|
20
|
-
version: 2,
|
|
21
|
-
name: 'Preview App',
|
|
22
|
-
description: '',
|
|
23
|
-
template: null,
|
|
24
|
-
downloadedAt: '2026-07-01T00:00:00.000Z',
|
|
25
|
-
},
|
|
26
|
-
files: [],
|
|
27
|
-
};
|
|
28
|
-
describe('getAuthHeader', () => {
|
|
29
|
-
it('uses ApiKey authentication for personal access tokens', () => {
|
|
30
|
-
expect((0, utils_1.getAuthHeader)('ldpat_secret')).toBe('ApiKey ldpat_secret');
|
|
31
|
-
});
|
|
32
|
-
it('uses Bearer authentication for service-account tokens', () => {
|
|
33
|
-
expect((0, utils_1.getAuthHeader)('ldsvc_secret')).toBe('Bearer ldsvc_secret');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe('buildPreviewChildEnv', () => {
|
|
37
|
-
it('passes through only required host variables and no parent secrets', () => {
|
|
38
|
-
const env = (0, preview_1.buildPreviewChildEnv)({
|
|
39
|
-
serverUrl: 'http://localhost:3000',
|
|
40
|
-
projectUuid: 'proj-uuid-1',
|
|
41
|
-
proxyPort: 45678,
|
|
42
|
-
proxyNonce: 'nonce-abc',
|
|
43
|
-
browserImageOrigins: ['https://tiles.example.com'],
|
|
44
|
-
parentEnv: {
|
|
45
|
-
HOME: '/home/developer',
|
|
46
|
-
PATH: '/usr/bin',
|
|
47
|
-
LIGHTDASH_API_KEY: 'ldpat_parent_secret',
|
|
48
|
-
LIGHTDASH_PROXY_AUTHORIZATION: 'Basic parent-secret',
|
|
49
|
-
NPM_TOKEN: 'npm_parent_secret',
|
|
50
|
-
VITE_LIGHTDASH_API_KEY: 'ldpat_browser_secret',
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
expect(env).toEqual({
|
|
54
|
-
HOME: '/home/developer',
|
|
55
|
-
PATH: '/usr/bin',
|
|
56
|
-
VITE_LIGHTDASH_URL: 'http://localhost:3000',
|
|
57
|
-
VITE_LIGHTDASH_API_KEY: 'nonce-abc',
|
|
58
|
-
VITE_LIGHTDASH_PROJECT_UUID: 'proj-uuid-1',
|
|
59
|
-
LIGHTDASH_PREVIEW_PROXY_TARGET: 'http://127.0.0.1:45678',
|
|
60
|
-
LIGHTDASH_PREVIEW_PROXY_NONCE: 'nonce-abc',
|
|
61
|
-
LIGHTDASH_PREVIEW_BROWSER_IMAGE_ORIGINS: '["https://tiles.example.com"]',
|
|
62
|
-
});
|
|
63
|
-
expect(env).not.toHaveProperty('LIGHTDASH_API_KEY');
|
|
64
|
-
expect(env).not.toHaveProperty('LIGHTDASH_PROXY_AUTHORIZATION');
|
|
65
|
-
expect(env).not.toHaveProperty('NPM_TOKEN');
|
|
66
|
-
for (const value of Object.values(env)) {
|
|
67
|
-
expect(value).not.toMatch(/^ld(pat|svc)_/);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
it('strips a trailing slash from the server url', () => {
|
|
71
|
-
const env = (0, preview_1.buildPreviewChildEnv)({
|
|
72
|
-
serverUrl: 'http://localhost:3000/',
|
|
73
|
-
projectUuid: 'p',
|
|
74
|
-
proxyPort: 1,
|
|
75
|
-
proxyNonce: 'n',
|
|
76
|
-
});
|
|
77
|
-
expect(env.VITE_LIGHTDASH_URL).toBe('http://localhost:3000');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('fetchBrowserImageOrigins', () => {
|
|
81
|
-
it('returns opted-in no-auth origins linked by manifest slug', async () => {
|
|
82
|
-
const connection = {
|
|
83
|
-
slug: 'public-tiles',
|
|
84
|
-
type: 'none',
|
|
85
|
-
origin: 'https://tiles.example.com',
|
|
86
|
-
allowBrowserImages: true,
|
|
87
|
-
};
|
|
88
|
-
const origins = await (0, preview_1.fetchBrowserImageOrigins)({
|
|
89
|
-
manifest: {
|
|
90
|
-
...previewBundle.manifest,
|
|
91
|
-
externalConnections: [
|
|
92
|
-
{ alias: 'tiles', connectionSlug: 'public-tiles' },
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
projectUuid: 'proj-uuid-1',
|
|
96
|
-
serverUrl: 'https://lightdash.example.com',
|
|
97
|
-
authorization: 'ApiKey token',
|
|
98
|
-
fetchFn: vi.fn().mockResolvedValue({
|
|
99
|
-
ok: true,
|
|
100
|
-
json: async () => ({ results: [connection] }),
|
|
101
|
-
}),
|
|
102
|
-
});
|
|
103
|
-
expect(origins).toEqual(['https://tiles.example.com']);
|
|
104
|
-
});
|
|
105
|
-
it('fails closed for malformed response shapes', async () => {
|
|
106
|
-
const origins = await (0, preview_1.fetchBrowserImageOrigins)({
|
|
107
|
-
manifest: {
|
|
108
|
-
...previewBundle.manifest,
|
|
109
|
-
externalConnections: [
|
|
110
|
-
{ alias: 'tiles', connectionSlug: 'public-tiles' },
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
projectUuid: 'proj-uuid-1',
|
|
114
|
-
serverUrl: 'https://lightdash.example.com',
|
|
115
|
-
authorization: 'ApiKey token',
|
|
116
|
-
fetchFn: vi.fn().mockResolvedValue({
|
|
117
|
-
ok: true,
|
|
118
|
-
json: async () => ({ results: 'not-an-array' }),
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
expect(origins).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
it('ignores malformed connections and unsafe origins', async () => {
|
|
124
|
-
const origins = await (0, preview_1.fetchBrowserImageOrigins)({
|
|
125
|
-
manifest: {
|
|
126
|
-
...previewBundle.manifest,
|
|
127
|
-
externalConnections: [
|
|
128
|
-
{ alias: 'tiles', connectionSlug: 'public-tiles' },
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
projectUuid: 'proj-uuid-1',
|
|
132
|
-
serverUrl: 'https://lightdash.example.com',
|
|
133
|
-
authorization: 'ApiKey token',
|
|
134
|
-
fetchFn: vi.fn().mockResolvedValue({
|
|
135
|
-
ok: true,
|
|
136
|
-
json: async () => ({
|
|
137
|
-
results: [
|
|
138
|
-
null,
|
|
139
|
-
{
|
|
140
|
-
slug: 'public-tiles',
|
|
141
|
-
type: 'none',
|
|
142
|
-
origin: 'not-a-url',
|
|
143
|
-
allowBrowserImages: true,
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
slug: 'public-tiles',
|
|
147
|
-
type: 'none',
|
|
148
|
-
origin: 'https://tiles.example.com/path',
|
|
149
|
-
allowBrowserImages: true,
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
slug: 'public-tiles',
|
|
153
|
-
type: 'none',
|
|
154
|
-
origin: 'https://tiles.example.com',
|
|
155
|
-
allowBrowserImages: true,
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
}),
|
|
159
|
-
}),
|
|
160
|
-
});
|
|
161
|
-
expect(origins).toEqual(['https://tiles.example.com']);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
describe('assertScaffoldingSupportsPreviewProxy', () => {
|
|
165
|
-
it('passes when vite.config.js reads the proxy target', async () => {
|
|
166
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
167
|
-
await fs_1.promises.writeFile(path.join(dir, 'vite.config.js'), 'const t = env.LIGHTDASH_PREVIEW_PROXY_TARGET;');
|
|
168
|
-
await expect((0, preview_1.assertScaffoldingSupportsPreviewProxy)(dir)).resolves.toBeUndefined();
|
|
169
|
-
});
|
|
170
|
-
it('rejects pre-proxy scaffolding with a re-download instruction', async () => {
|
|
171
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
172
|
-
await fs_1.promises.writeFile(path.join(dir, 'vite.config.js'), 'const k = env.LIGHTDASH_PREVIEW_API_KEY;');
|
|
173
|
-
await expect((0, preview_1.assertScaffoldingSupportsPreviewProxy)(dir)).rejects.toThrow(/Re-download the app/);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
describe('resolvePreviewTarget', () => {
|
|
177
|
-
it('defaults appDir to cwd and project to the current CLI project', async () => {
|
|
178
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
179
|
-
await (0, appCodeFiles_1.writeBundleToDir)(dir, previewBundle);
|
|
180
|
-
const target = await (0, preview_1.resolvePreviewTarget)({
|
|
181
|
-
cwd: dir,
|
|
182
|
-
currentProjectUuid: 'current-proj',
|
|
183
|
-
});
|
|
184
|
-
expect(target.appDir).toBe(dir);
|
|
185
|
-
expect(target.projectUuid).toBe('current-proj');
|
|
186
|
-
});
|
|
187
|
-
it('resolves a relative path arg and prefers --project over the current project', async () => {
|
|
188
|
-
const parent = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
189
|
-
const appDir = path.join(parent, 'apps', 'my-app');
|
|
190
|
-
await (0, appCodeFiles_1.writeBundleToDir)(appDir, previewBundle);
|
|
191
|
-
const target = await (0, preview_1.resolvePreviewTarget)({
|
|
192
|
-
pathArg: 'apps/my-app',
|
|
193
|
-
projectFlag: 'other-proj',
|
|
194
|
-
currentProjectUuid: 'current-proj',
|
|
195
|
-
cwd: parent,
|
|
196
|
-
});
|
|
197
|
-
expect(target.appDir).toBe(appDir);
|
|
198
|
-
expect(target.projectUuid).toBe('other-proj');
|
|
199
|
-
});
|
|
200
|
-
it('requires --project when the CLI has no current project', async () => {
|
|
201
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
202
|
-
await (0, appCodeFiles_1.writeBundleToDir)(dir, previewBundle);
|
|
203
|
-
await expect((0, preview_1.resolvePreviewTarget)({ cwd: dir })).rejects.toThrow(/Pass '--project <uuid or slug>'/);
|
|
204
|
-
});
|
|
205
|
-
it('errors clearly when the folder has no manifest', async () => {
|
|
206
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
207
|
-
await expect((0, preview_1.resolvePreviewTarget)({ cwd: dir })).rejects.toThrow(/No lightdash-app\.yml found in/);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
describe('assertNodeModulesPresent', () => {
|
|
211
|
-
it('passes when node_modules exists', async () => {
|
|
212
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
213
|
-
await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
|
|
214
|
-
await expect((0, preview_1.assertNodeModulesPresent)(dir)).resolves.toBeUndefined();
|
|
215
|
-
});
|
|
216
|
-
it('errors telling the user to npm install, without installing', async () => {
|
|
217
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
218
|
-
await expect((0, preview_1.assertNodeModulesPresent)(dir)).rejects.toThrow(/Run 'npm install' in .*, or rerun with --assume-yes/);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('resolvePreviewPort', () => {
|
|
222
|
-
it('returns undefined when no port is given', () => {
|
|
223
|
-
expect((0, preview_1.resolvePreviewPort)(undefined)).toBeUndefined();
|
|
224
|
-
});
|
|
225
|
-
it('parses a valid port', () => {
|
|
226
|
-
expect((0, preview_1.resolvePreviewPort)('4000')).toBe(4000);
|
|
227
|
-
});
|
|
228
|
-
it.each(['0', '-1', '65536', 'abc', '1.5'])('rejects invalid port %s', (raw) => {
|
|
229
|
-
expect(() => (0, preview_1.resolvePreviewPort)(raw)).toThrow(/--port must be an integer/);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
describe('ensureNodeModules', () => {
|
|
233
|
-
const promptMock = inquirer_1.default.prompt;
|
|
234
|
-
let dir;
|
|
235
|
-
beforeEach(async () => {
|
|
236
|
-
dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
237
|
-
vi.mocked(execa_1.default).mockReset();
|
|
238
|
-
promptMock.mockReset();
|
|
239
|
-
vi.spyOn(globalState_1.default, 'log').mockImplementation(() => { });
|
|
240
|
-
vi.spyOn(globalState_1.default, 'isNonInteractive').mockReturnValue(false);
|
|
241
|
-
});
|
|
242
|
-
afterEach(async () => {
|
|
243
|
-
vi.restoreAllMocks();
|
|
244
|
-
await fs_1.promises.rm(dir, { recursive: true, force: true });
|
|
245
|
-
});
|
|
246
|
-
it('does nothing when node_modules exists', async () => {
|
|
247
|
-
await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
|
|
248
|
-
await (0, preview_1.ensureNodeModules)({ appDir: dir, assumeYes: false });
|
|
249
|
-
expect(execa_1.default).not.toHaveBeenCalled();
|
|
250
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
251
|
-
});
|
|
252
|
-
it('fails fast without installing when not interactive', async () => {
|
|
253
|
-
vi.spyOn(globalState_1.default, 'isNonInteractive').mockReturnValue(true);
|
|
254
|
-
await expect((0, preview_1.ensureNodeModules)({ appDir: dir, assumeYes: false })).rejects.toThrow(/Run 'npm install'/);
|
|
255
|
-
expect(execa_1.default).not.toHaveBeenCalled();
|
|
256
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
257
|
-
});
|
|
258
|
-
it('installs without prompting with --assume-yes', async () => {
|
|
259
|
-
await (0, preview_1.ensureNodeModules)({ appDir: dir, assumeYes: true });
|
|
260
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
261
|
-
expect(execa_1.default).toHaveBeenCalledWith('npm', [
|
|
262
|
-
'install',
|
|
263
|
-
'--include=dev',
|
|
264
|
-
'--ignore-scripts',
|
|
265
|
-
'--no-package-lock',
|
|
266
|
-
], expect.objectContaining({ cwd: dir }));
|
|
267
|
-
});
|
|
268
|
-
it('cancels without installing when the warning prompt is declined', async () => {
|
|
269
|
-
const stdinTty = process.stdin.isTTY;
|
|
270
|
-
const stdoutTty = process.stdout.isTTY;
|
|
271
|
-
process.stdin.isTTY = true;
|
|
272
|
-
process.stdout.isTTY = true;
|
|
273
|
-
promptMock.mockResolvedValueOnce({ confirmed: false });
|
|
274
|
-
try {
|
|
275
|
-
await expect((0, preview_1.ensureNodeModules)({ appDir: dir, assumeYes: false })).rejects.toThrow('Preview cancelled.');
|
|
276
|
-
}
|
|
277
|
-
finally {
|
|
278
|
-
process.stdin.isTTY = stdinTty;
|
|
279
|
-
process.stdout.isTTY = stdoutTty;
|
|
280
|
-
}
|
|
281
|
-
expect(promptMock).toHaveBeenCalledTimes(1);
|
|
282
|
-
expect(execa_1.default).not.toHaveBeenCalled();
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
describe('projectNotFoundMessage', () => {
|
|
286
|
-
it('names the project, the server, and both remedies', () => {
|
|
287
|
-
const msg = (0, preview_1.projectNotFoundMessage)({
|
|
288
|
-
projectUuid: 'proj-uuid-1',
|
|
289
|
-
serverUrl: 'https://cloud.example.com',
|
|
290
|
-
});
|
|
291
|
-
expect(msg).toContain('proj-uuid-1');
|
|
292
|
-
expect(msg).toContain('https://cloud.example.com');
|
|
293
|
-
expect(msg).toMatch(/lightdash login/);
|
|
294
|
-
expect(msg).toMatch(/--project/);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
describe('preflightPreviewRequest', () => {
|
|
298
|
-
it('reports network failures separately from rejected credentials', async () => {
|
|
299
|
-
const fetchFn = vi
|
|
300
|
-
.fn()
|
|
301
|
-
.mockRejectedValue(new Error('connection refused'));
|
|
302
|
-
await expect((0, preview_1.preflightPreviewRequest)({
|
|
303
|
-
apiPath: '/api/v1/user',
|
|
304
|
-
serverUrl: 'http://localhost:8080',
|
|
305
|
-
authorization: 'ApiKey test',
|
|
306
|
-
fetchFn,
|
|
307
|
-
})).rejects.toThrow('Could not connect to http://localhost:8080 while starting preview: connection refused');
|
|
308
|
-
});
|
|
309
|
-
it('returns false for an HTTP rejection', async () => {
|
|
310
|
-
const fetchFn = vi
|
|
311
|
-
.fn()
|
|
312
|
-
.mockResolvedValue({ ok: false });
|
|
313
|
-
await expect((0, preview_1.preflightPreviewRequest)({
|
|
314
|
-
apiPath: '/api/v1/user',
|
|
315
|
-
serverUrl: 'http://localhost:8080',
|
|
316
|
-
authorization: 'ApiKey test',
|
|
317
|
-
fetchFn,
|
|
318
|
-
})).resolves.toBe(false);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
describe('removeLegacyPreviewCredential', () => {
|
|
322
|
-
it('removes only the obsolete credential and preserves other settings', async () => {
|
|
323
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
324
|
-
const envPath = path.join(dir, '.env.local');
|
|
325
|
-
await fs_1.promises.writeFile(envPath, [
|
|
326
|
-
'VITE_CUSTOM_SETTING=keep-me',
|
|
327
|
-
'LIGHTDASH_PREVIEW_API_KEY=ldpat_remove_me',
|
|
328
|
-
'# LIGHTDASH_PREVIEW_API_KEY=commented-out',
|
|
329
|
-
'ANOTHER_SETTING=also-keep-me',
|
|
330
|
-
'',
|
|
331
|
-
].join('\n'));
|
|
332
|
-
await expect((0, preview_1.removeLegacyPreviewCredential)(dir)).resolves.toBe(true);
|
|
333
|
-
await expect(fs_1.promises.readFile(envPath, 'utf-8')).resolves.toBe([
|
|
334
|
-
'VITE_CUSTOM_SETTING=keep-me',
|
|
335
|
-
'# LIGHTDASH_PREVIEW_API_KEY=commented-out',
|
|
336
|
-
'ANOTHER_SETTING=also-keep-me',
|
|
337
|
-
'',
|
|
338
|
-
].join('\n'));
|
|
339
|
-
});
|
|
340
|
-
it('deletes an env file that contains only the obsolete credential', async () => {
|
|
341
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
342
|
-
const envPath = path.join(dir, '.env.local');
|
|
343
|
-
await fs_1.promises.writeFile(envPath, 'export LIGHTDASH_PREVIEW_API_KEY=ldpat_remove_me\n');
|
|
344
|
-
await expect((0, preview_1.removeLegacyPreviewCredential)(dir)).resolves.toBe(true);
|
|
345
|
-
await expect(fs_1.promises.stat(envPath)).rejects.toMatchObject({
|
|
346
|
-
code: 'ENOENT',
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
it('does nothing when there is no legacy credential', async () => {
|
|
350
|
-
const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-prev-'));
|
|
351
|
-
await expect((0, preview_1.removeLegacyPreviewCredential)(dir)).resolves.toBe(false);
|
|
352
|
-
});
|
|
353
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"previewProxy.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/previewProxy.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,178 +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 http = tslib_1.__importStar(require("http"));
|
|
6
|
-
const previewProxy_1 = require("./previewProxy");
|
|
7
|
-
const PROJECT_UUID = 'proj-uuid-1';
|
|
8
|
-
const NONCE = 'test-nonce';
|
|
9
|
-
const API_KEY = 'ldpat_secret';
|
|
10
|
-
const AUTHORIZATION = `ApiKey ${API_KEY}`;
|
|
11
|
-
/** Records every request and replies 200 with a JSON echo. */
|
|
12
|
-
const startFakeUpstream = () => {
|
|
13
|
-
const seen = [];
|
|
14
|
-
const server = http.createServer((req, res) => {
|
|
15
|
-
const chunks = [];
|
|
16
|
-
req.on('data', (chunk) => chunks.push(chunk));
|
|
17
|
-
req.on('end', () => {
|
|
18
|
-
seen.push({
|
|
19
|
-
method: req.method ?? '',
|
|
20
|
-
url: req.url ?? '',
|
|
21
|
-
headers: req.headers,
|
|
22
|
-
body: Buffer.concat(chunks).toString('utf-8'),
|
|
23
|
-
});
|
|
24
|
-
res.writeHead(200, {
|
|
25
|
-
'content-type': 'application/json',
|
|
26
|
-
'set-cookie': 'upstream=1',
|
|
27
|
-
});
|
|
28
|
-
res.end(JSON.stringify({ status: 'ok' }));
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
server.listen(0, '127.0.0.1', () => {
|
|
33
|
-
const { port } = server.address();
|
|
34
|
-
resolve({
|
|
35
|
-
url: `http://127.0.0.1:${port}`,
|
|
36
|
-
seen,
|
|
37
|
-
close: () => new Promise((r) => {
|
|
38
|
-
server.close(() => r());
|
|
39
|
-
server.closeAllConnections();
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
describe('startPreviewProxy', () => {
|
|
46
|
-
let upstream;
|
|
47
|
-
let proxy;
|
|
48
|
-
const proxyFetch = (path, init = {}) => {
|
|
49
|
-
const { nonce = NONCE, authorization = `ApiKey ${NONCE}`, ...rest } = init;
|
|
50
|
-
const headers = new Headers(rest.headers);
|
|
51
|
-
if (nonce !== null)
|
|
52
|
-
headers.set(previewProxy_1.PREVIEW_PROXY_NONCE_HEADER, nonce);
|
|
53
|
-
if (authorization !== null) {
|
|
54
|
-
headers.set('authorization', authorization);
|
|
55
|
-
}
|
|
56
|
-
return fetch(`http://127.0.0.1:${proxy.port}${path}`, {
|
|
57
|
-
...rest,
|
|
58
|
-
headers,
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
beforeEach(async () => {
|
|
62
|
-
upstream = await startFakeUpstream();
|
|
63
|
-
proxy = await (0, previewProxy_1.startPreviewProxy)({
|
|
64
|
-
upstreamUrl: upstream.url,
|
|
65
|
-
authorization: AUTHORIZATION,
|
|
66
|
-
projectUuid: PROJECT_UUID,
|
|
67
|
-
nonce: NONCE,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
afterEach(async () => {
|
|
71
|
-
await proxy.close();
|
|
72
|
-
await upstream.close();
|
|
73
|
-
});
|
|
74
|
-
it('forwards an allowlisted route with the credential attached', async () => {
|
|
75
|
-
const res = await proxyFetch(`/api/v2/projects/${PROJECT_UUID}/query/metric-query`, {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: {
|
|
78
|
-
'content-type': 'application/json',
|
|
79
|
-
authorization: `ApiKey ${NONCE}`,
|
|
80
|
-
},
|
|
81
|
-
body: JSON.stringify({ exploreName: 'orders' }),
|
|
82
|
-
});
|
|
83
|
-
expect(res.status).toBe(200);
|
|
84
|
-
expect(upstream.seen).toHaveLength(1);
|
|
85
|
-
const forwarded = upstream.seen[0];
|
|
86
|
-
expect(forwarded.method).toBe('POST');
|
|
87
|
-
expect(forwarded.body).toBe('{"exploreName":"orders"}');
|
|
88
|
-
// The browser-side scoped capability is replaced by the real credential…
|
|
89
|
-
expect(forwarded.headers.authorization).toBe(AUTHORIZATION);
|
|
90
|
-
// …and the run-scoped nonce never travels upstream.
|
|
91
|
-
expect(forwarded.headers[previewProxy_1.PREVIEW_PROXY_NONCE_HEADER]).toBeUndefined();
|
|
92
|
-
});
|
|
93
|
-
it('stamps the signed-download header on schedule-download and nothing else', async () => {
|
|
94
|
-
await proxyFetch(`/api/v2/projects/${PROJECT_UUID}/query/some-query-uuid/schedule-download`, {
|
|
95
|
-
method: 'POST',
|
|
96
|
-
headers: {
|
|
97
|
-
'content-type': 'application/json',
|
|
98
|
-
authorization: `ApiKey ${NONCE}`,
|
|
99
|
-
},
|
|
100
|
-
body: JSON.stringify({ type: 'csv' }),
|
|
101
|
-
});
|
|
102
|
-
await proxyFetch(`/api/v2/projects/${PROJECT_UUID}/query/some-query-uuid`);
|
|
103
|
-
expect(upstream.seen).toHaveLength(2);
|
|
104
|
-
expect(upstream.seen[0].headers[common_1.LightdashSignedDownloadHeader.toLowerCase()]).toBe('true');
|
|
105
|
-
expect(upstream.seen[1].headers[common_1.LightdashSignedDownloadHeader.toLowerCase()]).toBeUndefined();
|
|
106
|
-
});
|
|
107
|
-
it('preserves the query string on result polling', async () => {
|
|
108
|
-
const res = await proxyFetch(`/api/v2/projects/${PROJECT_UUID}/query/some-query-uuid?page=2&pageSize=500`);
|
|
109
|
-
expect(res.status).toBe(200);
|
|
110
|
-
expect(upstream.seen[0].url).toBe(`/api/v2/projects/${PROJECT_UUID}/query/some-query-uuid?page=2&pageSize=500`);
|
|
111
|
-
});
|
|
112
|
-
it('rejects non-SDK routes without contacting upstream', async () => {
|
|
113
|
-
const res = await proxyFetch('/api/v1/org/projects');
|
|
114
|
-
expect(res.status).toBe(403);
|
|
115
|
-
const body = (await res.json());
|
|
116
|
-
expect(body.message).toMatch(/not a data-app SDK route/);
|
|
117
|
-
expect(upstream.seen).toHaveLength(0);
|
|
118
|
-
});
|
|
119
|
-
it('rejects allowlisted routes aimed at a different project', async () => {
|
|
120
|
-
const res = await proxyFetch('/api/v2/projects/other-project/query/metric-query', { method: 'POST' });
|
|
121
|
-
expect(res.status).toBe(403);
|
|
122
|
-
const body = (await res.json());
|
|
123
|
-
expect(body.message).toMatch(/pinned to proj-uuid-1/);
|
|
124
|
-
expect(upstream.seen).toHaveLength(0);
|
|
125
|
-
});
|
|
126
|
-
it('rejects allowlisted paths with the wrong method', async () => {
|
|
127
|
-
const res = await proxyFetch('/api/v1/user', { method: 'POST' });
|
|
128
|
-
expect(res.status).toBe(403);
|
|
129
|
-
expect(upstream.seen).toHaveLength(0);
|
|
130
|
-
});
|
|
131
|
-
it('rejects requests without the per-run nonce', async () => {
|
|
132
|
-
const missing = await proxyFetch('/api/v1/user', { nonce: null });
|
|
133
|
-
expect(missing.status).toBe(401);
|
|
134
|
-
const wrong = await proxyFetch('/api/v1/user', { nonce: 'guessed' });
|
|
135
|
-
expect(wrong.status).toBe(401);
|
|
136
|
-
expect(upstream.seen).toHaveLength(0);
|
|
137
|
-
});
|
|
138
|
-
it('rejects requests without the browser-side scoped capability', async () => {
|
|
139
|
-
const missing = await proxyFetch('/api/v1/user', {
|
|
140
|
-
authorization: null,
|
|
141
|
-
});
|
|
142
|
-
expect(missing.status).toBe(401);
|
|
143
|
-
const wrong = await proxyFetch('/api/v1/user', {
|
|
144
|
-
authorization: 'ApiKey guessed',
|
|
145
|
-
});
|
|
146
|
-
expect(wrong.status).toBe(401);
|
|
147
|
-
expect(upstream.seen).toHaveLength(0);
|
|
148
|
-
});
|
|
149
|
-
it('does not pass upstream cookies back to the page', async () => {
|
|
150
|
-
const res = await proxyFetch('/api/v1/user');
|
|
151
|
-
expect(res.status).toBe(200);
|
|
152
|
-
expect(res.headers.get('set-cookie')).toBeNull();
|
|
153
|
-
});
|
|
154
|
-
it('forwards service-account and reverse-proxy authorization', async () => {
|
|
155
|
-
const serviceProxy = await (0, previewProxy_1.startPreviewProxy)({
|
|
156
|
-
upstreamUrl: upstream.url,
|
|
157
|
-
authorization: 'Bearer ldsvc_secret',
|
|
158
|
-
proxyAuthorization: 'Basic reverse-proxy-secret',
|
|
159
|
-
projectUuid: PROJECT_UUID,
|
|
160
|
-
nonce: 'service-nonce',
|
|
161
|
-
});
|
|
162
|
-
try {
|
|
163
|
-
const res = await fetch(`http://127.0.0.1:${serviceProxy.port}/api/v1/user`, {
|
|
164
|
-
headers: {
|
|
165
|
-
[previewProxy_1.PREVIEW_PROXY_NONCE_HEADER]: 'service-nonce',
|
|
166
|
-
authorization: 'ApiKey service-nonce',
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
expect(res.status).toBe(200);
|
|
170
|
-
const forwarded = upstream.seen.at(-1);
|
|
171
|
-
expect(forwarded?.headers.authorization).toBe('Bearer ldsvc_secret');
|
|
172
|
-
expect(forwarded?.headers['proxy-authorization']).toBe('Basic reverse-proxy-secret');
|
|
173
|
-
}
|
|
174
|
-
finally {
|
|
175
|
-
await serviceProxy.close();
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scaffolding.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/scaffolding.test.ts"],"names":[],"mappings":""}
|