@lightdash/cli 2.228.1 → 2.229.1

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.
Files changed (177) hide show
  1. package/dist/.tsbuildinfo.build +1 -0
  2. package/package.json +4 -4
  3. package/dist/.tsbuildinfo +0 -1
  4. package/dist/config.test.d.ts +0 -2
  5. package/dist/config.test.d.ts.map +0 -1
  6. package/dist/config.test.js +0 -72
  7. package/dist/dbt/context.test.d.ts +0 -2
  8. package/dist/dbt/context.test.d.ts.map +0 -1
  9. package/dist/dbt/context.test.js +0 -152
  10. package/dist/dbt/manifest.test.d.ts +0 -2
  11. package/dist/dbt/manifest.test.d.ts.map +0 -1
  12. package/dist/dbt/manifest.test.js +0 -170
  13. package/dist/dbt/models.test.d.ts +0 -2
  14. package/dist/dbt/models.test.d.ts.map +0 -1
  15. package/dist/dbt/models.test.js +0 -139
  16. package/dist/dbt/profiles.test.d.ts +0 -2
  17. package/dist/dbt/profiles.test.d.ts.map +0 -1
  18. package/dist/dbt/profiles.test.js +0 -50
  19. package/dist/dbt/targets/athena.test.d.ts +0 -2
  20. package/dist/dbt/targets/athena.test.d.ts.map +0 -1
  21. package/dist/dbt/targets/athena.test.js +0 -78
  22. package/dist/dbt/targets/duckdb.test.d.ts +0 -2
  23. package/dist/dbt/targets/duckdb.test.d.ts.map +0 -1
  24. package/dist/dbt/targets/duckdb.test.js +0 -149
  25. package/dist/dbt/targets/redshift.test.d.ts +0 -2
  26. package/dist/dbt/targets/redshift.test.d.ts.map +0 -1
  27. package/dist/dbt/targets/redshift.test.js +0 -198
  28. package/dist/dbt/templating.test.d.ts +0 -2
  29. package/dist/dbt/templating.test.d.ts.map +0 -1
  30. package/dist/dbt/templating.test.js +0 -51
  31. package/dist/dbt/validation.test.d.ts +0 -2
  32. package/dist/dbt/validation.test.d.ts.map +0 -1
  33. package/dist/dbt/validation.test.js +0 -67
  34. package/dist/handlers/apps/appCodeFiles.test.d.ts +0 -2
  35. package/dist/handlers/apps/appCodeFiles.test.d.ts.map +0 -1
  36. package/dist/handlers/apps/appCodeFiles.test.js +0 -391
  37. package/dist/handlers/apps/appsDownload.test.d.ts +0 -2
  38. package/dist/handlers/apps/appsDownload.test.d.ts.map +0 -1
  39. package/dist/handlers/apps/appsDownload.test.js +0 -639
  40. package/dist/handlers/apps/build.test.d.ts +0 -2
  41. package/dist/handlers/apps/build.test.d.ts.map +0 -1
  42. package/dist/handlers/apps/build.test.js +0 -252
  43. package/dist/handlers/apps/createApp.test.d.ts +0 -2
  44. package/dist/handlers/apps/createApp.test.d.ts.map +0 -1
  45. package/dist/handlers/apps/createApp.test.js +0 -340
  46. package/dist/handlers/apps/pathContainment.test.d.ts +0 -2
  47. package/dist/handlers/apps/pathContainment.test.d.ts.map +0 -1
  48. package/dist/handlers/apps/pathContainment.test.js +0 -66
  49. package/dist/handlers/apps/preview.test.d.ts +0 -2
  50. package/dist/handlers/apps/preview.test.d.ts.map +0 -1
  51. package/dist/handlers/apps/preview.test.js +0 -353
  52. package/dist/handlers/apps/previewProxy.test.d.ts +0 -2
  53. package/dist/handlers/apps/previewProxy.test.d.ts.map +0 -1
  54. package/dist/handlers/apps/previewProxy.test.js +0 -178
  55. package/dist/handlers/apps/scaffolding.test.d.ts +0 -2
  56. package/dist/handlers/apps/scaffolding.test.d.ts.map +0 -1
  57. package/dist/handlers/apps/scaffolding.test.js +0 -151
  58. package/dist/handlers/apps/uploadRetry.test.d.ts +0 -2
  59. package/dist/handlers/apps/uploadRetry.test.d.ts.map +0 -1
  60. package/dist/handlers/apps/uploadRetry.test.js +0 -103
  61. package/dist/handlers/apps/validate.test.d.ts +0 -2
  62. package/dist/handlers/apps/validate.test.d.ts.map +0 -1
  63. package/dist/handlers/apps/validate.test.js +0 -526
  64. package/dist/handlers/compile.test.d.ts +0 -2
  65. package/dist/handlers/compile.test.d.ts.map +0 -1
  66. package/dist/handlers/compile.test.js +0 -151
  67. package/dist/handlers/compileProject.test.d.ts +0 -2
  68. package/dist/handlers/compileProject.test.d.ts.map +0 -1
  69. package/dist/handlers/compileProject.test.js +0 -637
  70. package/dist/handlers/compileWarehouseColumnValidation.test.d.ts +0 -2
  71. package/dist/handlers/compileWarehouseColumnValidation.test.d.ts.map +0 -1
  72. package/dist/handlers/compileWarehouseColumnValidation.test.js +0 -493
  73. package/dist/handlers/connectSnowflake.test.d.ts +0 -2
  74. package/dist/handlers/connectSnowflake.test.d.ts.map +0 -1
  75. package/dist/handlers/connectSnowflake.test.js +0 -320
  76. package/dist/handlers/contentAsCode/fileNames.test.d.ts +0 -2
  77. package/dist/handlers/contentAsCode/fileNames.test.d.ts.map +0 -1
  78. package/dist/handlers/contentAsCode/fileNames.test.js +0 -79
  79. package/dist/handlers/contentAsCode/registry.test.d.ts +0 -2
  80. package/dist/handlers/contentAsCode/registry.test.d.ts.map +0 -1
  81. package/dist/handlers/contentAsCode/registry.test.js +0 -18
  82. package/dist/handlers/contentAsCode/resource.test.d.ts +0 -2
  83. package/dist/handlers/contentAsCode/resource.test.d.ts.map +0 -1
  84. package/dist/handlers/contentAsCode/resource.test.js +0 -121
  85. package/dist/handlers/dbt/getDbtVersion.test.d.ts +0 -2
  86. package/dist/handlers/dbt/getDbtVersion.test.d.ts.map +0 -1
  87. package/dist/handlers/dbt/getDbtVersion.test.js +0 -106
  88. package/dist/handlers/deploy.test.d.ts +0 -2
  89. package/dist/handlers/deploy.test.d.ts.map +0 -1
  90. package/dist/handlers/deploy.test.js +0 -244
  91. package/dist/handlers/download.test.d.ts +0 -2
  92. package/dist/handlers/download.test.d.ts.map +0 -1
  93. package/dist/handlers/download.test.js +0 -1739
  94. package/dist/handlers/exportChartImage.test.d.ts +0 -2
  95. package/dist/handlers/exportChartImage.test.d.ts.map +0 -1
  96. package/dist/handlers/exportChartImage.test.js +0 -55
  97. package/dist/handlers/homepagesAsCode.test.d.ts +0 -2
  98. package/dist/handlers/homepagesAsCode.test.d.ts.map +0 -1
  99. package/dist/handlers/homepagesAsCode.test.js +0 -72
  100. package/dist/handlers/lint.test.d.ts +0 -2
  101. package/dist/handlers/lint.test.d.ts.map +0 -1
  102. package/dist/handlers/lint.test.js +0 -221
  103. package/dist/handlers/login.test.d.ts +0 -2
  104. package/dist/handlers/login.test.d.ts.map +0 -1
  105. package/dist/handlers/login.test.js +0 -24
  106. package/dist/handlers/metadataFile.test.d.ts +0 -2
  107. package/dist/handlers/metadataFile.test.d.ts.map +0 -1
  108. package/dist/handlers/metadataFile.test.js +0 -46
  109. package/dist/handlers/organizationContent/customRoles.test.d.ts +0 -2
  110. package/dist/handlers/organizationContent/customRoles.test.d.ts.map +0 -1
  111. package/dist/handlers/organizationContent/customRoles.test.js +0 -179
  112. package/dist/handlers/organizationContent/groups.test.d.ts +0 -2
  113. package/dist/handlers/organizationContent/groups.test.d.ts.map +0 -1
  114. package/dist/handlers/organizationContent/groups.test.js +0 -135
  115. package/dist/handlers/organizationContent/index.test.d.ts +0 -2
  116. package/dist/handlers/organizationContent/index.test.d.ts.map +0 -1
  117. package/dist/handlers/organizationContent/index.test.js +0 -212
  118. package/dist/handlers/organizationContent/themes.test.d.ts +0 -2
  119. package/dist/handlers/organizationContent/themes.test.d.ts.map +0 -1
  120. package/dist/handlers/organizationContent/themes.test.js +0 -180
  121. package/dist/handlers/organizationContent/users.test.d.ts +0 -2
  122. package/dist/handlers/organizationContent/users.test.d.ts.map +0 -1
  123. package/dist/handlers/organizationContent/users.test.js +0 -155
  124. package/dist/handlers/preAggregateAudit.test.d.ts +0 -2
  125. package/dist/handlers/preAggregateAudit.test.d.ts.map +0 -1
  126. package/dist/handlers/preAggregateAudit.test.js +0 -102
  127. package/dist/handlers/renameHandler.test.d.ts +0 -2
  128. package/dist/handlers/renameHandler.test.d.ts.map +0 -1
  129. package/dist/handlers/renameHandler.test.js +0 -189
  130. package/dist/handlers/resolveProjectFlag.test.d.ts +0 -2
  131. package/dist/handlers/resolveProjectFlag.test.d.ts.map +0 -1
  132. package/dist/handlers/resolveProjectFlag.test.js +0 -48
  133. package/dist/handlers/selectProject.test.d.ts +0 -2
  134. package/dist/handlers/selectProject.test.d.ts.map +0 -1
  135. package/dist/handlers/selectProject.test.js +0 -224
  136. package/dist/handlers/setProject.test.d.ts +0 -2
  137. package/dist/handlers/setProject.test.d.ts.map +0 -1
  138. package/dist/handlers/setProject.test.js +0 -63
  139. package/dist/handlers/slugUpdate.test.d.ts +0 -2
  140. package/dist/handlers/slugUpdate.test.d.ts.map +0 -1
  141. package/dist/handlers/slugUpdate.test.js +0 -265
  142. package/dist/handlers/timestampConversion.test.d.ts +0 -2
  143. package/dist/handlers/timestampConversion.test.d.ts.map +0 -1
  144. package/dist/handlers/timestampConversion.test.js +0 -87
  145. package/dist/handlers/upgradeCheck.test.d.ts +0 -2
  146. package/dist/handlers/upgradeCheck.test.d.ts.map +0 -1
  147. package/dist/handlers/upgradeCheck.test.js +0 -227
  148. package/dist/handlers/validate.test.d.ts +0 -2
  149. package/dist/handlers/validate.test.d.ts.map +0 -1
  150. package/dist/handlers/validate.test.js +0 -318
  151. package/dist/handlers/validateSpaceFilter.test.d.ts +0 -2
  152. package/dist/handlers/validateSpaceFilter.test.d.ts.map +0 -1
  153. package/dist/handlers/validateSpaceFilter.test.js +0 -79
  154. package/dist/handlers/warehouseConnection.test.d.ts +0 -2
  155. package/dist/handlers/warehouseConnection.test.d.ts.map +0 -1
  156. package/dist/handlers/warehouseConnection.test.js +0 -75
  157. package/dist/lightdash-config/lightdash-config.test.d.ts +0 -2
  158. package/dist/lightdash-config/lightdash-config.test.d.ts.map +0 -1
  159. package/dist/lightdash-config/lightdash-config.test.js +0 -69
  160. package/dist/prompts/fuzzySearchPrompt.test.d.ts +0 -2
  161. package/dist/prompts/fuzzySearchPrompt.test.d.ts.map +0 -1
  162. package/dist/prompts/fuzzySearchPrompt.test.js +0 -53
  163. package/dist/refsArgument.test.d.ts +0 -2
  164. package/dist/refsArgument.test.d.ts.map +0 -1
  165. package/dist/refsArgument.test.js +0 -42
  166. package/dist/releaseSafety/index.test.d.ts +0 -2
  167. package/dist/releaseSafety/index.test.d.ts.map +0 -1
  168. package/dist/releaseSafety/index.test.js +0 -154
  169. package/dist/releaseSafety/version.test.d.ts +0 -2
  170. package/dist/releaseSafety/version.test.d.ts.map +0 -1
  171. package/dist/releaseSafety/version.test.js +0 -22
  172. package/dist/skills.test.d.ts +0 -2
  173. package/dist/skills.test.d.ts.map +0 -1
  174. package/dist/skills.test.js +0 -112
  175. package/dist/terminal/contentAsCodeOutput.test.d.ts +0 -2
  176. package/dist/terminal/contentAsCodeOutput.test.d.ts.map +0 -1
  177. package/dist/terminal/contentAsCodeOutput.test.js +0 -231
@@ -1,135 +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 crypto_1 = require("crypto");
6
- const fs_1 = require("fs");
7
- const yaml = tslib_1.__importStar(require("js-yaml"));
8
- const os = tslib_1.__importStar(require("os"));
9
- const path = tslib_1.__importStar(require("path"));
10
- const vitest_1 = require("vitest");
11
- const apiClient_1 = require("../dbt/apiClient");
12
- const groups_1 = require("./groups");
13
- vitest_1.vi.mock('../dbt/apiClient', async (importOriginal) => ({
14
- ...(await importOriginal()),
15
- lightdashApi: vitest_1.vi.fn(),
16
- }));
17
- describe('groups as code', () => {
18
- let tmpDir;
19
- const group = (name, members = []) => ({
20
- version: 1,
21
- name,
22
- members,
23
- });
24
- const writeGroup = async (filename, value) => {
25
- const folder = (0, groups_1.getGroupsFolder)(tmpDir);
26
- await fs_1.promises.mkdir(folder, { recursive: true });
27
- await fs_1.promises.writeFile(path.join(folder, filename), typeof value === 'string'
28
- ? value
29
- : yaml.dump(value, { sortKeys: true }));
30
- };
31
- beforeEach(async () => {
32
- vitest_1.vi.mocked(apiClient_1.lightdashApi).mockReset();
33
- tmpDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'groups-test-'));
34
- });
35
- afterEach(async () => {
36
- vitest_1.vi.restoreAllMocks();
37
- await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
38
- });
39
- it('downloads deterministic groups, empty groups, and collision-safe filenames', async () => {
40
- const folder = (0, groups_1.getGroupsFolder)(tmpDir);
41
- await fs_1.promises.mkdir(folder, { recursive: true });
42
- await fs_1.promises.writeFile(path.join(folder, 'stale.yml'), 'stale: true');
43
- vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
44
- groups: [
45
- group('Finance?', ['z@example.com', 'a@example.com']),
46
- group('Empty'),
47
- group('Finance!'),
48
- ],
49
- });
50
- await expect((0, groups_1.downloadGroups)('organization-uuid', tmpDir)).resolves.toBe(3);
51
- const financeBangHash = (0, crypto_1.createHash)('sha256')
52
- .update('Finance!')
53
- .digest('hex')
54
- .slice(0, 8);
55
- const financeQuestionHash = (0, crypto_1.createHash)('sha256')
56
- .update('Finance?')
57
- .digest('hex')
58
- .slice(0, 8);
59
- expect((await fs_1.promises.readdir(folder)).sort()).toStrictEqual([
60
- 'empty.yml',
61
- `finance-${financeBangHash}.yml`,
62
- `finance-${financeQuestionHash}.yml`,
63
- 'stale.yml',
64
- ].sort());
65
- expect(yaml.load(await fs_1.promises.readFile(path.join(folder, `finance-${financeQuestionHash}.yml`), 'utf8'))).toStrictEqual({
66
- version: 1,
67
- name: 'Finance?',
68
- members: ['a@example.com', 'z@example.com'],
69
- });
70
- expect(yaml.load(await fs_1.promises.readFile(path.join(folder, 'empty.yml'), 'utf8'))).toStrictEqual({ version: 1, name: 'Empty', members: [] });
71
- });
72
- it('rejects duplicate exact names but preserves case-sensitive identity', async () => {
73
- await writeGroup('one.yml', group('Finance'));
74
- await writeGroup('two.yml', group('Finance'));
75
- await expect((0, groups_1.readGroupFiles)(tmpDir)).rejects.toThrow('Duplicate group name "Finance"');
76
- await fs_1.promises.rm((0, groups_1.getGroupsFolder)(tmpDir), { recursive: true });
77
- await writeGroup('upper.yml', group('Finance'));
78
- await writeGroup('lower.yml', group('finance'));
79
- await expect((0, groups_1.readGroupFiles)(tmpDir)).resolves.toHaveLength(2);
80
- });
81
- it('is a backward-compatible no-op when the groups folder is missing', async () => {
82
- await expect((0, groups_1.uploadGroups)('organization-uuid', tmpDir)).resolves.toMatchObject({
83
- created: 0,
84
- updated: 0,
85
- unchanged: 0,
86
- failed: 0,
87
- dependencySkipped: 0,
88
- });
89
- expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
90
- });
91
- it('continues after malformed and API-failing group files', async () => {
92
- await writeGroup('bad-yaml.yml', 'name: [unterminated');
93
- await writeGroup('api-error.yml', group('API error'));
94
- await writeGroup('valid.yml', group('Valid'));
95
- vitest_1.vi.mocked(apiClient_1.lightdashApi)
96
- .mockRejectedValueOnce(new Error('unknown member'))
97
- .mockResolvedValueOnce({ action: common_1.PromotionAction.CREATE });
98
- await expect((0, groups_1.uploadGroups)('organization-uuid', tmpDir)).resolves.toMatchObject({
99
- created: 1,
100
- updated: 0,
101
- unchanged: 0,
102
- failed: 2,
103
- });
104
- expect(apiClient_1.lightdashApi).toHaveBeenCalledTimes(2);
105
- });
106
- it('reports all promotion actions and sends the portable body', async () => {
107
- await writeGroup('create.yml', group('Create'));
108
- await writeGroup('same.yml', group('Same'));
109
- await writeGroup('update.yml', group('Update', ['member@example.com']));
110
- vitest_1.vi.mocked(apiClient_1.lightdashApi)
111
- .mockResolvedValueOnce({ action: common_1.PromotionAction.CREATE })
112
- .mockResolvedValueOnce({
113
- action: common_1.PromotionAction.NO_CHANGES,
114
- })
115
- .mockResolvedValueOnce({ action: common_1.PromotionAction.UPDATE });
116
- await expect((0, groups_1.uploadGroups)('organization-uuid', tmpDir)).resolves.toMatchObject({
117
- created: 1,
118
- updated: 1,
119
- unchanged: 1,
120
- failed: 0,
121
- });
122
- expect(apiClient_1.lightdashApi).toHaveBeenNthCalledWith(3, {
123
- method: 'POST',
124
- url: '/api/v2/orgs/organization-uuid/code/groups',
125
- body: expect.any(String),
126
- });
127
- expect(JSON.parse(vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[2][0].body)).toStrictEqual(group('Update', ['member@example.com']));
128
- });
129
- it('counts files skipped because a dependency phase failed', async () => {
130
- await writeGroup('one.yml', group('One'));
131
- await writeGroup('two.yml', 'invalid: [yaml');
132
- await fs_1.promises.writeFile(path.join((0, groups_1.getGroupsFolder)(tmpDir), 'ignored.yaml'), 'name: ignored');
133
- await expect((0, groups_1.countDependencySkippedGroups)(tmpDir)).resolves.toBe(2);
134
- });
135
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/index.test.ts"],"names":[],"mappings":""}
@@ -1,212 +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 analytics_1 = require("../../analytics/analytics");
6
- const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
7
- const customRoles_1 = require("./customRoles");
8
- const groups_1 = require("./groups");
9
- const index_1 = require("./index");
10
- const themes_1 = require("./themes");
11
- const users_1 = require("./users");
12
- vi.mock('./customRoles', async (importOriginal) => ({
13
- ...(await importOriginal()),
14
- downloadCustomRoles: vi.fn(),
15
- uploadCustomRoles: vi.fn(),
16
- }));
17
- vi.mock('./users', async (importOriginal) => ({
18
- ...(await importOriginal()),
19
- downloadUsers: vi.fn(),
20
- uploadUsers: vi.fn(),
21
- }));
22
- vi.mock('./groups', async (importOriginal) => ({
23
- ...(await importOriginal()),
24
- downloadGroups: vi.fn(),
25
- uploadGroups: vi.fn(),
26
- countDependencySkippedGroups: vi.fn(),
27
- }));
28
- vi.mock('./themes', async (importOriginal) => ({
29
- ...(await importOriginal()),
30
- downloadThemes: vi.fn(),
31
- prepareThemeUploads: vi.fn(),
32
- uploadThemes: vi.fn(),
33
- }));
34
- describe('organization content download', () => {
35
- const spinner = {
36
- start: vi.fn(),
37
- succeed: vi.fn(),
38
- warn: vi.fn(),
39
- stop: vi.fn(),
40
- fail: vi.fn(),
41
- };
42
- const config = {
43
- user: {
44
- userUuid: 'user-uuid',
45
- organizationUuid: 'organization-uuid',
46
- },
47
- };
48
- beforeEach(() => {
49
- vi.clearAllMocks();
50
- vi.spyOn(globalState_1.default, 'startSpinner').mockReturnValue(spinner);
51
- vi.spyOn(globalState_1.default, 'log').mockImplementation(() => undefined);
52
- vi.spyOn(globalState_1.default, 'debug').mockImplementation(() => undefined);
53
- vi.spyOn(analytics_1.LightdashAnalytics, 'track').mockResolvedValue(undefined);
54
- vi.mocked(customRoles_1.downloadCustomRoles).mockResolvedValue(2);
55
- vi.mocked(users_1.downloadUsers).mockResolvedValue(3);
56
- vi.mocked(groups_1.downloadGroups).mockResolvedValue(4);
57
- vi.mocked(themes_1.downloadThemes).mockResolvedValue(5);
58
- });
59
- it('skips groups when the group service is disabled', async () => {
60
- vi.mocked(groups_1.downloadGroups).mockRejectedValue(new common_1.LightdashError({
61
- message: 'Group service is not enabled',
62
- name: 'ForbiddenError',
63
- statusCode: 403,
64
- data: {},
65
- }));
66
- await expect((0, index_1.downloadOrganizationContent)({ config })).resolves.toBeUndefined();
67
- expect(spinner.warn).toHaveBeenCalledOnce();
68
- expect(spinner.fail).not.toHaveBeenCalled();
69
- expect(globalState_1.default.debug).toHaveBeenCalledWith('> Warning: groups were not downloaded because the group service is not enabled');
70
- });
71
- it('reports a duration for every downloaded resource', async () => {
72
- await (0, index_1.downloadOrganizationContent)({ config });
73
- expect(spinner.succeed).toHaveBeenCalledTimes(4);
74
- spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
75
- });
76
- it('still fails for other group download errors', async () => {
77
- vi.mocked(groups_1.downloadGroups).mockRejectedValue(new Error('Network error'));
78
- await expect((0, index_1.downloadOrganizationContent)({ config })).rejects.toThrow('Network error');
79
- expect(spinner.fail).toHaveBeenCalledOnce();
80
- });
81
- });
82
- describe('organization content upload sequencing', () => {
83
- const spinner = {
84
- start: vi.fn(),
85
- succeed: vi.fn(),
86
- warn: vi.fn(),
87
- stop: vi.fn(),
88
- fail: vi.fn(),
89
- };
90
- const config = {
91
- user: {
92
- userUuid: 'user-uuid',
93
- organizationUuid: 'organization-uuid',
94
- },
95
- };
96
- const customRoleSummary = (failed = 0) => ({
97
- created: 0,
98
- updated: 0,
99
- unchanged: 0,
100
- failed,
101
- failures: failed > 0 ? [{ message: 'role failure' }] : [],
102
- });
103
- const userSummary = (failed = 0) => ({
104
- created: 0,
105
- updated: 0,
106
- unchanged: 0,
107
- awaitingAuthentication: 0,
108
- invited: 0,
109
- skippedAuthenticated: 0,
110
- skippedDisabled: 0,
111
- skippedValidInvite: 0,
112
- failed,
113
- failures: failed > 0 ? [{ message: 'user failure' }] : [],
114
- });
115
- const groupSummary = (failed = 0) => ({
116
- created: 0,
117
- updated: 0,
118
- unchanged: 0,
119
- failed,
120
- dependencySkipped: 0,
121
- failures: failed > 0 ? [{ message: 'group failure' }] : [],
122
- });
123
- const themeSummary = (failed = 0) => ({
124
- created: failed > 0 ? 1 : 0,
125
- updated: 0,
126
- unchanged: 0,
127
- failed,
128
- completedSlugs: failed > 0 ? ['processed-theme'] : [],
129
- failures: failed > 0 ? [{ message: 'theme failure' }] : [],
130
- });
131
- beforeEach(() => {
132
- vi.clearAllMocks();
133
- vi.spyOn(globalState_1.default, 'startSpinner').mockReturnValue(spinner);
134
- vi.spyOn(globalState_1.default, 'log').mockImplementation(() => undefined);
135
- vi.spyOn(analytics_1.LightdashAnalytics, 'track').mockResolvedValue(undefined);
136
- vi.mocked(customRoles_1.uploadCustomRoles).mockResolvedValue(customRoleSummary());
137
- vi.mocked(users_1.uploadUsers).mockResolvedValue(userSummary());
138
- vi.mocked(groups_1.uploadGroups).mockResolvedValue(groupSummary());
139
- vi.mocked(themes_1.prepareThemeUploads).mockResolvedValue([]);
140
- vi.mocked(themes_1.uploadThemes).mockResolvedValue(themeSummary());
141
- vi.mocked(groups_1.countDependencySkippedGroups).mockResolvedValue(2);
142
- });
143
- it('preflights themes, then awaits organization resource uploads in order', async () => {
144
- const phaseOrder = [];
145
- vi.mocked(themes_1.prepareThemeUploads).mockImplementation(async () => {
146
- phaseOrder.push('themes:preflight:start', 'themes:preflight:end');
147
- return [];
148
- });
149
- vi.mocked(customRoles_1.uploadCustomRoles).mockImplementation(async () => {
150
- phaseOrder.push('roles:start', 'roles:end');
151
- return customRoleSummary();
152
- });
153
- vi.mocked(users_1.uploadUsers).mockImplementation(async () => {
154
- phaseOrder.push('users:start', 'users:end');
155
- return userSummary();
156
- });
157
- vi.mocked(groups_1.uploadGroups).mockImplementation(async () => {
158
- phaseOrder.push('groups:start', 'groups:end');
159
- return groupSummary();
160
- });
161
- vi.mocked(themes_1.uploadThemes).mockImplementation(async () => {
162
- phaseOrder.push('themes:start', 'themes:end');
163
- return themeSummary();
164
- });
165
- await (0, index_1.uploadOrganizationContent)({ config });
166
- expect(phaseOrder).toStrictEqual([
167
- 'themes:preflight:start',
168
- 'themes:preflight:end',
169
- 'roles:start',
170
- 'roles:end',
171
- 'users:start',
172
- 'users:end',
173
- 'groups:start',
174
- 'groups:end',
175
- 'themes:start',
176
- 'themes:end',
177
- ]);
178
- });
179
- it('reports a duration for every uploaded resource', async () => {
180
- await (0, index_1.uploadOrganizationContent)({ config });
181
- expect(spinner.succeed).toHaveBeenCalledTimes(4);
182
- spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
183
- });
184
- it('does not start users or groups when custom roles fail', async () => {
185
- vi.mocked(customRoles_1.uploadCustomRoles).mockResolvedValue(customRoleSummary(1));
186
- await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed custom roles');
187
- expect(users_1.uploadUsers).not.toHaveBeenCalled();
188
- expect(groups_1.uploadGroups).not.toHaveBeenCalled();
189
- expect(themes_1.uploadThemes).not.toHaveBeenCalled();
190
- expect(groups_1.countDependencySkippedGroups).toHaveBeenCalledOnce();
191
- });
192
- it('does not start groups when users fail', async () => {
193
- vi.mocked(users_1.uploadUsers).mockResolvedValue(userSummary(1));
194
- await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed users');
195
- expect(groups_1.uploadGroups).not.toHaveBeenCalled();
196
- expect(themes_1.uploadThemes).not.toHaveBeenCalled();
197
- expect(groups_1.countDependencySkippedGroups).toHaveBeenCalledOnce();
198
- });
199
- it('does not mutate organization resources when theme preflight fails', async () => {
200
- vi.mocked(themes_1.prepareThemeUploads).mockRejectedValue(new Error('Invalid theme'));
201
- await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Invalid theme');
202
- expect(customRoles_1.uploadCustomRoles).not.toHaveBeenCalled();
203
- expect(users_1.uploadUsers).not.toHaveBeenCalled();
204
- expect(groups_1.uploadGroups).not.toHaveBeenCalled();
205
- expect(themes_1.uploadThemes).not.toHaveBeenCalled();
206
- });
207
- it('fails after reporting a partial theme upload', async () => {
208
- vi.mocked(themes_1.uploadThemes).mockResolvedValue(themeSummary(1));
209
- await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed themes: 1 created, 0 updated, 0 unchanged, 1 failed');
210
- expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('processed-theme'));
211
- });
212
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=themes.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"themes.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/themes.test.ts"],"names":[],"mappings":""}
@@ -1,180 +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 node_fetch_1 = require("node-fetch");
7
- const os = tslib_1.__importStar(require("os"));
8
- const path = tslib_1.__importStar(require("path"));
9
- const yaml_1 = require("yaml");
10
- const apiClient_1 = require("../dbt/apiClient");
11
- const themes_1 = require("./themes");
12
- vi.mock('../dbt/apiClient', () => ({
13
- lightdashApi: vi.fn(),
14
- lightdashRawApi: vi.fn(),
15
- }));
16
- const manifest = (slug) => ({
17
- codeVersion: 1,
18
- slug,
19
- name: 'Acme Brand',
20
- description: null,
21
- extraInstructions: null,
22
- });
23
- const createThemeDirectory = async (contentRoot, slug) => {
24
- const directory = path.join(contentRoot, 'themes', slug);
25
- await Promise.all(['css', 'fonts', 'images', 'instructions'].map((folder) => fs_1.promises.mkdir(path.join(directory, folder), { recursive: true })));
26
- await fs_1.promises.writeFile(path.join(directory, 'lightdash-theme.yml'), (0, yaml_1.stringify)(manifest(slug)));
27
- await fs_1.promises.writeFile(path.join(directory, 'css', 'theme.css'), ':root { --brand: #123456; }');
28
- return directory;
29
- };
30
- describe('organization theme content-as-code', () => {
31
- let temporaryRoot;
32
- beforeEach(async () => {
33
- vi.clearAllMocks();
34
- vi.mocked(apiClient_1.lightdashApi).mockResolvedValue([]);
35
- temporaryRoot = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'themes-test-'));
36
- });
37
- afterEach(async () => {
38
- await fs_1.promises.rm(temporaryRoot, { recursive: true, force: true });
39
- });
40
- it('treats a missing themes directory as an empty upload', async () => {
41
- await expect((0, themes_1.prepareThemeUploads)(temporaryRoot)).resolves.toEqual([]);
42
- });
43
- it('rejects nested paths during preflight', async () => {
44
- const themeDirectory = await createThemeDirectory(temporaryRoot, 'acme-brand');
45
- await fs_1.promises.mkdir(path.join(themeDirectory, 'css', 'nested'));
46
- await expect((0, themes_1.prepareThemeUploads)(temporaryRoot)).rejects.toThrow('without symlinks or nested directories');
47
- });
48
- it('rejects symlinked files during preflight', async () => {
49
- const themeDirectory = await createThemeDirectory(temporaryRoot, 'acme-brand');
50
- await fs_1.promises.symlink(path.join(themeDirectory, 'css', 'theme.css'), path.join(themeDirectory, 'css', 'linked.css'));
51
- await expect((0, themes_1.prepareThemeUploads)(temporaryRoot)).rejects.toThrow('without symlinks or nested directories');
52
- });
53
- it('accepts Git-dropped empty directories and ignores local filesystem artifacts', async () => {
54
- const themeDirectory = await createThemeDirectory(temporaryRoot, 'acme-brand');
55
- await Promise.all(['fonts', 'images', 'instructions'].map((folder) => fs_1.promises.rmdir(path.join(themeDirectory, folder))));
56
- await Promise.all([
57
- fs_1.promises.writeFile(path.join(temporaryRoot, 'themes', '.DS_Store'), 'metadata'),
58
- fs_1.promises.writeFile(path.join(themeDirectory, '.DS_Store'), 'metadata'),
59
- fs_1.promises.writeFile(path.join(themeDirectory, 'css', '.DS_Store'), 'metadata'),
60
- fs_1.promises.mkdir(path.join(temporaryRoot, 'themes', '.lightdash-theme-ABC123')),
61
- fs_1.promises.mkdir(path.join(temporaryRoot, 'themes', 'acme-brand.backup-00000000-0000-4000-8000-000000000001')),
62
- ]);
63
- const prepared = await (0, themes_1.prepareThemeUploads)(temporaryRoot);
64
- expect(prepared).toHaveLength(1);
65
- expect(prepared[0].manifest.slug).toBe('acme-brand');
66
- });
67
- it('round-trips a downloaded package and replaces stale theme files', async () => {
68
- const sourceRoot = path.join(temporaryRoot, 'source');
69
- const destinationRoot = path.join(temporaryRoot, 'destination');
70
- await createThemeDirectory(sourceRoot, 'acme-brand');
71
- const [prepared] = await (0, themes_1.prepareThemeUploads)(sourceRoot);
72
- const staleDirectory = await createThemeDirectory(destinationRoot, 'acme-brand');
73
- await createThemeDirectory(destinationRoot, 'remotely-deleted');
74
- await fs_1.promises.writeFile(path.join(staleDirectory, 'stale.txt'), 'stale');
75
- vi.mocked(apiClient_1.lightdashApi).mockResolvedValue([
76
- { slug: 'acme-brand' },
77
- ]);
78
- vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(prepared.archive));
79
- await expect((0, themes_1.downloadThemes)(destinationRoot)).resolves.toBe(1);
80
- await expect(fs_1.promises.readFile(path.join(destinationRoot, 'themes', 'acme-brand', 'css', 'theme.css'), 'utf8')).resolves.toContain('--brand');
81
- await expect(fs_1.promises.stat(path.join(destinationRoot, 'themes', 'acme-brand', 'stale.txt'))).rejects.toMatchObject({ code: 'ENOENT' });
82
- await expect(fs_1.promises.stat(path.join(destinationRoot, 'themes', 'remotely-deleted'))).rejects.toMatchObject({ code: 'ENOENT' });
83
- const [downloaded] = await (0, themes_1.prepareThemeUploads)(destinationRoot);
84
- expect(downloaded.archive).toEqual(prepared.archive);
85
- });
86
- it('keeps the previous local theme set when a package download fails', async () => {
87
- const sourceRoot = path.join(temporaryRoot, 'source');
88
- const destinationRoot = path.join(temporaryRoot, 'destination');
89
- await createThemeDirectory(sourceRoot, 'first-theme');
90
- const [prepared] = await (0, themes_1.prepareThemeUploads)(sourceRoot);
91
- const existingDirectory = await createThemeDirectory(destinationRoot, 'existing-theme');
92
- await fs_1.promises.writeFile(path.join(existingDirectory, 'local-marker.txt'), 'keep me');
93
- vi.mocked(apiClient_1.lightdashApi).mockResolvedValue([
94
- { slug: 'first-theme' },
95
- { slug: 'second-theme' },
96
- ]);
97
- vi.mocked(apiClient_1.lightdashRawApi)
98
- .mockResolvedValueOnce(new node_fetch_1.Response(prepared.archive))
99
- .mockResolvedValueOnce(new node_fetch_1.Response(Buffer.from('not a tar')));
100
- await expect((0, themes_1.downloadThemes)(destinationRoot)).rejects.toThrow();
101
- await expect(fs_1.promises.readFile(path.join(existingDirectory, 'local-marker.txt'), 'utf8')).resolves.toBe('keep me');
102
- await expect(fs_1.promises.stat(path.join(destinationRoot, 'themes', 'first-theme'))).rejects.toMatchObject({ code: 'ENOENT' });
103
- });
104
- it('reports completed and failed imports without claiming full success', async () => {
105
- await createThemeDirectory(temporaryRoot, 'first-theme');
106
- await createThemeDirectory(temporaryRoot, 'second-theme');
107
- const prepared = await (0, themes_1.prepareThemeUploads)(temporaryRoot);
108
- vi.mocked(apiClient_1.lightdashRawApi)
109
- .mockResolvedValueOnce(new node_fetch_1.Response(JSON.stringify({
110
- status: 'ok',
111
- results: {
112
- slug: 'first-theme',
113
- action: common_1.PromotionAction.CREATE,
114
- },
115
- })))
116
- .mockRejectedValueOnce(new Error('remote failure'));
117
- const summary = await (0, themes_1.uploadThemes)(prepared);
118
- expect(summary).toEqual({
119
- created: 1,
120
- updated: 0,
121
- unchanged: 0,
122
- failed: 1,
123
- completedSlugs: ['first-theme'],
124
- failures: [
125
- {
126
- message: 'Failed to upload theme "second-theme": remote failure',
127
- },
128
- ],
129
- });
130
- });
131
- it('reports the action returned for every successful import', async () => {
132
- await createThemeDirectory(temporaryRoot, 'created-theme');
133
- await createThemeDirectory(temporaryRoot, 'updated-theme');
134
- await createThemeDirectory(temporaryRoot, 'unchanged-theme');
135
- const prepared = await (0, themes_1.prepareThemeUploads)(temporaryRoot);
136
- for (const theme of prepared) {
137
- const actionBySlug = {
138
- 'created-theme': common_1.PromotionAction.CREATE,
139
- 'updated-theme': common_1.PromotionAction.UPDATE,
140
- 'unchanged-theme': common_1.PromotionAction.NO_CHANGES,
141
- };
142
- vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValueOnce(new node_fetch_1.Response(JSON.stringify({
143
- status: 'ok',
144
- results: {
145
- slug: theme.manifest.slug,
146
- action: actionBySlug[theme.manifest.slug],
147
- },
148
- })));
149
- }
150
- await expect((0, themes_1.uploadThemes)(prepared)).resolves.toMatchObject({
151
- created: 1,
152
- updated: 1,
153
- unchanged: 1,
154
- failed: 0,
155
- });
156
- });
157
- it('classifies successful imports returned by older servers', async () => {
158
- await createThemeDirectory(temporaryRoot, 'existing-theme');
159
- await createThemeDirectory(temporaryRoot, 'new-theme');
160
- const prepared = await (0, themes_1.prepareThemeUploads)(temporaryRoot);
161
- vi.mocked(apiClient_1.lightdashApi).mockResolvedValue([
162
- { slug: 'existing-theme' },
163
- ]);
164
- vi.mocked(apiClient_1.lightdashRawApi)
165
- .mockResolvedValueOnce(new node_fetch_1.Response(JSON.stringify({
166
- status: 'ok',
167
- results: { slug: 'existing-theme' },
168
- })))
169
- .mockResolvedValueOnce(new node_fetch_1.Response(JSON.stringify({
170
- status: 'ok',
171
- results: { slug: 'new-theme' },
172
- })));
173
- await expect((0, themes_1.uploadThemes)(prepared)).resolves.toMatchObject({
174
- created: 1,
175
- updated: 1,
176
- unchanged: 0,
177
- failed: 0,
178
- });
179
- });
180
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=users.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"users.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/users.test.ts"],"names":[],"mappings":""}