@egulatee/pulumi-stack-alias 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,463 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as pulumi from "@pulumi/pulumi";
3
+ import {
4
+ matchesPattern,
5
+ createStackAlias,
6
+ createConditionalAlias,
7
+ createSimpleAlias,
8
+ } from "../src/alias";
9
+
10
+ // Mock Pulumi runtime functions
11
+ vi.mock("@pulumi/pulumi", async () => {
12
+ const actual = await vi.importActual("@pulumi/pulumi");
13
+ return {
14
+ ...actual,
15
+ getOrganization: vi.fn(() => "test-org"),
16
+ getProject: vi.fn(() => "test-project"),
17
+ getStack: vi.fn(() => "dev"),
18
+ StackReference: vi.fn(),
19
+ Output: actual.Output,
20
+ output: actual.output,
21
+ };
22
+ });
23
+
24
+ describe("Pattern Matching", () => {
25
+ describe("matchesPattern", () => {
26
+ it("should match exact project and stack", () => {
27
+ expect(matchesPattern("myproject/dev", "myproject", "dev")).toBe(true);
28
+ expect(matchesPattern("myproject/dev", "myproject", "prod")).toBe(false);
29
+ expect(matchesPattern("myproject/dev", "other", "dev")).toBe(false);
30
+ });
31
+
32
+ it("should match project wildcard", () => {
33
+ expect(matchesPattern("*/dev", "myproject", "dev")).toBe(true);
34
+ expect(matchesPattern("*/dev", "otherproject", "dev")).toBe(true);
35
+ expect(matchesPattern("*/dev", "anyproject", "prod")).toBe(false);
36
+ });
37
+
38
+ it("should match stack wildcard", () => {
39
+ expect(matchesPattern("myproject/*", "myproject", "dev")).toBe(true);
40
+ expect(matchesPattern("myproject/*", "myproject", "prod")).toBe(true);
41
+ expect(matchesPattern("myproject/*", "myproject", "staging")).toBe(true);
42
+ expect(matchesPattern("myproject/*", "other", "dev")).toBe(false);
43
+ });
44
+
45
+ it("should match double wildcard", () => {
46
+ expect(matchesPattern("*/*", "anyproject", "anystack")).toBe(true);
47
+ expect(matchesPattern("*/*", "project", "stack")).toBe(true);
48
+ });
49
+
50
+ it("should match suffix wildcard patterns", () => {
51
+ expect(matchesPattern("*/*-dev", "myproject", "app-dev")).toBe(true);
52
+ expect(matchesPattern("*/*-dev", "myproject", "service-dev")).toBe(true);
53
+ expect(matchesPattern("*/*-dev", "myproject", "dev")).toBe(false); // "dev" doesn't end with "-dev"
54
+ expect(matchesPattern("*/*-dev", "myproject", "prod")).toBe(false);
55
+ expect(matchesPattern("*/*-dev", "myproject", "development")).toBe(false);
56
+ });
57
+
58
+ it("should match prefix wildcard patterns", () => {
59
+ expect(matchesPattern("*/prod-*", "myproject", "prod-us")).toBe(true);
60
+ expect(matchesPattern("*/prod-*", "myproject", "prod-eu")).toBe(true);
61
+ expect(matchesPattern("*/prod-*", "myproject", "prod")).toBe(false); // "prod" doesn't start with "prod-"
62
+ expect(matchesPattern("*/prod-*", "myproject", "staging-prod")).toBe(false);
63
+ expect(matchesPattern("*/prod-*", "myproject", "dev")).toBe(false);
64
+ });
65
+
66
+ it("should match complex wildcard combinations", () => {
67
+ expect(matchesPattern("app-*/staging-*", "app-service", "staging-v1")).toBe(true);
68
+ expect(matchesPattern("app-*/*-prod", "app-api", "service-prod")).toBe(true);
69
+ expect(matchesPattern("*-infra/*-shared", "my-infra", "vpc-shared")).toBe(true);
70
+ });
71
+
72
+ it("should be case-sensitive", () => {
73
+ expect(matchesPattern("MyProject/Dev", "MyProject", "Dev")).toBe(true);
74
+ expect(matchesPattern("MyProject/Dev", "myproject", "dev")).toBe(false);
75
+ });
76
+
77
+ it("should throw error for invalid pattern format", () => {
78
+ expect(() => matchesPattern("invalid", "project", "stack")).toThrow(
79
+ 'Invalid pattern format: "invalid". Expected "projectPattern/stackPattern".'
80
+ );
81
+ expect(() => matchesPattern("project/", "project", "stack")).toThrow(
82
+ 'Invalid pattern format: "project/". Expected "projectPattern/stackPattern".'
83
+ );
84
+ expect(() => matchesPattern("/stack", "project", "stack")).toThrow(
85
+ 'Invalid pattern format: "/stack". Expected "projectPattern/stackPattern".'
86
+ );
87
+ });
88
+ });
89
+ });
90
+
91
+ describe("createStackAlias", () => {
92
+ let mockStackReference: any;
93
+ let mockRequireOutput: any;
94
+
95
+ beforeEach(() => {
96
+ vi.clearAllMocks();
97
+
98
+ mockRequireOutput = vi.fn();
99
+
100
+ mockStackReference = vi.fn().mockImplementation((stackName: string) => {
101
+ return {
102
+ stackName,
103
+ requireOutput: mockRequireOutput,
104
+ };
105
+ });
106
+
107
+ (pulumi.StackReference as any).mockImplementation(mockStackReference);
108
+ (pulumi.getOrganization as any).mockReturnValue("test-org");
109
+ });
110
+
111
+ afterEach(() => {
112
+ vi.restoreAllMocks();
113
+ });
114
+
115
+ it("should create StackReference with correct full name", () => {
116
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
117
+
118
+ createStackAlias({
119
+ targetProject: "infrastructure",
120
+ targetStack: "shared",
121
+ outputs: ["vpcId"],
122
+ });
123
+
124
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/shared");
125
+ });
126
+
127
+ it("should use custom org when provided", () => {
128
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
129
+
130
+ createStackAlias({
131
+ targetOrg: "custom-org",
132
+ targetProject: "infrastructure",
133
+ targetStack: "shared",
134
+ outputs: ["vpcId"],
135
+ });
136
+
137
+ expect(mockStackReference).toHaveBeenCalledWith("custom-org/infrastructure/shared");
138
+ });
139
+
140
+ it("should re-export all specified outputs", () => {
141
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
142
+
143
+ createStackAlias({
144
+ targetProject: "infrastructure",
145
+ targetStack: "shared",
146
+ outputs: ["vpcId", "endpoint", "clusterName"],
147
+ });
148
+
149
+ expect(mockRequireOutput).toHaveBeenCalledWith("vpcId");
150
+ expect(mockRequireOutput).toHaveBeenCalledWith("endpoint");
151
+ expect(mockRequireOutput).toHaveBeenCalledWith("clusterName");
152
+ expect(mockRequireOutput).toHaveBeenCalledTimes(3);
153
+ });
154
+
155
+ it("should return Pulumi Outputs", () => {
156
+ const outputValue = pulumi.output("test-value");
157
+ mockRequireOutput.mockReturnValue(outputValue);
158
+
159
+ const result = createStackAlias({
160
+ targetProject: "infrastructure",
161
+ targetStack: "shared",
162
+ outputs: ["vpcId"],
163
+ });
164
+
165
+ expect(result.vpcId).toBe(outputValue);
166
+ });
167
+
168
+ it("should handle empty outputs array", () => {
169
+ const result = createStackAlias({
170
+ targetProject: "infrastructure",
171
+ targetStack: "shared",
172
+ outputs: [],
173
+ });
174
+
175
+ expect(result).toEqual({});
176
+ expect(mockRequireOutput).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it("should work with single output", () => {
180
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
181
+
182
+ const result = createStackAlias({
183
+ targetProject: "infrastructure",
184
+ targetStack: "shared",
185
+ outputs: ["vpcId"],
186
+ });
187
+
188
+ expect(Object.keys(result)).toHaveLength(1);
189
+ expect(result.vpcId).toBeDefined();
190
+ });
191
+
192
+ it("should work with many outputs", () => {
193
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
194
+
195
+ const result = createStackAlias({
196
+ targetProject: "infrastructure",
197
+ targetStack: "shared",
198
+ outputs: ["output1", "output2", "output3", "output4", "output5"],
199
+ });
200
+
201
+ expect(Object.keys(result)).toHaveLength(5);
202
+ expect(mockRequireOutput).toHaveBeenCalledTimes(5);
203
+ });
204
+
205
+ it("should use requireOutput instead of getOutput", () => {
206
+ mockRequireOutput.mockReturnValue(pulumi.output("test-value"));
207
+
208
+ createStackAlias({
209
+ targetProject: "infrastructure",
210
+ targetStack: "shared",
211
+ outputs: ["vpcId"],
212
+ });
213
+
214
+ // Verify requireOutput was called (not getOutput)
215
+ expect(mockRequireOutput).toHaveBeenCalled();
216
+ });
217
+ });
218
+
219
+ describe("createConditionalAlias", () => {
220
+ let mockStackReference: any;
221
+ let mockRequireOutput: any;
222
+
223
+ beforeEach(() => {
224
+ vi.clearAllMocks();
225
+
226
+ mockRequireOutput = vi.fn().mockReturnValue(pulumi.output("test-value"));
227
+
228
+ mockStackReference = vi.fn().mockImplementation((stackName: string) => {
229
+ return {
230
+ stackName,
231
+ requireOutput: mockRequireOutput,
232
+ };
233
+ });
234
+
235
+ (pulumi.StackReference as any).mockImplementation(mockStackReference);
236
+ (pulumi.getOrganization as any).mockReturnValue("test-org");
237
+ (pulumi.getProject as any).mockReturnValue("test-project");
238
+ (pulumi.getStack as any).mockReturnValue("dev");
239
+ });
240
+
241
+ afterEach(() => {
242
+ vi.restoreAllMocks();
243
+ });
244
+
245
+ it("should use first matching pattern", () => {
246
+ createConditionalAlias({
247
+ targetProject: "infrastructure",
248
+ patterns: [
249
+ { pattern: "*/dev", target: "shared" },
250
+ { pattern: "*/staging", target: "shared" },
251
+ { pattern: "*/prod", target: "prod" },
252
+ ],
253
+ outputs: ["vpcId"],
254
+ });
255
+
256
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/shared");
257
+ });
258
+
259
+ it("should evaluate patterns in order", () => {
260
+ (pulumi.getStack as any).mockReturnValue("prod");
261
+
262
+ createConditionalAlias({
263
+ targetProject: "infrastructure",
264
+ patterns: [
265
+ { pattern: "*/staging", target: "shared" },
266
+ { pattern: "*/prod", target: "prod" },
267
+ { pattern: "*/*", target: "fallback" },
268
+ ],
269
+ outputs: ["vpcId"],
270
+ });
271
+
272
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/prod");
273
+ });
274
+
275
+ it("should use defaultTarget when no pattern matches", () => {
276
+ (pulumi.getStack as any).mockReturnValue("unknown-stack");
277
+
278
+ createConditionalAlias({
279
+ targetProject: "infrastructure",
280
+ patterns: [
281
+ { pattern: "*/dev", target: "shared" },
282
+ { pattern: "*/prod", target: "prod" },
283
+ ],
284
+ defaultTarget: "fallback",
285
+ outputs: ["vpcId"],
286
+ });
287
+
288
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/fallback");
289
+ });
290
+
291
+ it("should throw error when no pattern matches and no defaultTarget", () => {
292
+ (pulumi.getStack as any).mockReturnValue("unknown-stack");
293
+
294
+ expect(() => {
295
+ createConditionalAlias({
296
+ targetProject: "infrastructure",
297
+ patterns: [
298
+ { pattern: "*/dev", target: "shared" },
299
+ { pattern: "*/prod", target: "prod" },
300
+ ],
301
+ outputs: ["vpcId"],
302
+ });
303
+ }).toThrow("No matching pattern found for test-project/unknown-stack");
304
+ });
305
+
306
+ it("should work with complex pattern rules", () => {
307
+ (pulumi.getProject as any).mockReturnValue("app-service");
308
+ (pulumi.getStack as any).mockReturnValue("feature-ephemeral");
309
+
310
+ createConditionalAlias({
311
+ targetProject: "infrastructure",
312
+ patterns: [
313
+ { pattern: "app-*/*-ephemeral", target: "shared" },
314
+ { pattern: "*/prod", target: "prod" },
315
+ ],
316
+ outputs: ["vpcId"],
317
+ });
318
+
319
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/shared");
320
+ });
321
+
322
+ it("should support custom organization", () => {
323
+ createConditionalAlias({
324
+ targetProject: "infrastructure",
325
+ targetOrg: "custom-org",
326
+ patterns: [{ pattern: "*/dev", target: "shared" }],
327
+ outputs: ["vpcId"],
328
+ });
329
+
330
+ expect(mockStackReference).toHaveBeenCalledWith("custom-org/infrastructure/shared");
331
+ });
332
+
333
+ it("should re-export all specified outputs", () => {
334
+ createConditionalAlias({
335
+ targetProject: "infrastructure",
336
+ patterns: [{ pattern: "*/dev", target: "shared" }],
337
+ outputs: ["vpcId", "endpoint", "clusterName"],
338
+ });
339
+
340
+ expect(mockRequireOutput).toHaveBeenCalledWith("vpcId");
341
+ expect(mockRequireOutput).toHaveBeenCalledWith("endpoint");
342
+ expect(mockRequireOutput).toHaveBeenCalledWith("clusterName");
343
+ });
344
+
345
+ it("should work with different stack contexts", () => {
346
+ (pulumi.getStack as any).mockReturnValue("staging");
347
+
348
+ createConditionalAlias({
349
+ targetProject: "infrastructure",
350
+ patterns: [
351
+ { pattern: "*/dev", target: "dev" },
352
+ { pattern: "*/staging", target: "staging-canonical" },
353
+ { pattern: "*/prod", target: "prod" },
354
+ ],
355
+ outputs: ["vpcId"],
356
+ });
357
+
358
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/staging-canonical");
359
+ });
360
+
361
+ it("should delegate to createStackAlias", () => {
362
+ const result = createConditionalAlias({
363
+ targetProject: "infrastructure",
364
+ patterns: [{ pattern: "*/dev", target: "shared" }],
365
+ outputs: ["vpcId"],
366
+ });
367
+
368
+ // Verify it returns the same shape as createStackAlias
369
+ expect(result).toHaveProperty("vpcId");
370
+ expect(result.vpcId).toBeDefined();
371
+ });
372
+
373
+ it("should match using current project and stack", () => {
374
+ (pulumi.getProject as any).mockReturnValue("specific-project");
375
+ (pulumi.getStack as any).mockReturnValue("specific-stack");
376
+
377
+ createConditionalAlias({
378
+ targetProject: "infrastructure",
379
+ patterns: [
380
+ { pattern: "specific-project/specific-stack", target: "matched" },
381
+ { pattern: "*/*", target: "fallback" },
382
+ ],
383
+ outputs: ["vpcId"],
384
+ });
385
+
386
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/matched");
387
+ });
388
+ });
389
+
390
+ describe("createSimpleAlias", () => {
391
+ let mockStackReference: any;
392
+ let mockRequireOutput: any;
393
+
394
+ beforeEach(() => {
395
+ vi.clearAllMocks();
396
+
397
+ mockRequireOutput = vi.fn().mockReturnValue(pulumi.output("test-value"));
398
+
399
+ mockStackReference = vi.fn().mockImplementation((stackName: string) => {
400
+ return {
401
+ stackName,
402
+ requireOutput: mockRequireOutput,
403
+ };
404
+ });
405
+
406
+ (pulumi.StackReference as any).mockImplementation(mockStackReference);
407
+ (pulumi.getOrganization as any).mockReturnValue("test-org");
408
+ });
409
+
410
+ afterEach(() => {
411
+ vi.restoreAllMocks();
412
+ });
413
+
414
+ it("should create alias with simplified API", () => {
415
+ createSimpleAlias("infrastructure", "shared", ["vpcId"]);
416
+
417
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/shared");
418
+ expect(mockRequireOutput).toHaveBeenCalledWith("vpcId");
419
+ });
420
+
421
+ it("should use current organization", () => {
422
+ (pulumi.getOrganization as any).mockReturnValue("my-org");
423
+
424
+ createSimpleAlias("infrastructure", "shared", ["vpcId"]);
425
+
426
+ expect(mockStackReference).toHaveBeenCalledWith("my-org/infrastructure/shared");
427
+ });
428
+
429
+ it("should work with single output", () => {
430
+ const result = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
431
+
432
+ expect(Object.keys(result)).toHaveLength(1);
433
+ expect(result.vpcId).toBeDefined();
434
+ });
435
+
436
+ it("should work with many outputs", () => {
437
+ const result = createSimpleAlias("infrastructure", "shared", [
438
+ "vpcId",
439
+ "endpoint",
440
+ "clusterName",
441
+ ]);
442
+
443
+ expect(Object.keys(result)).toHaveLength(3);
444
+ expect(result.vpcId).toBeDefined();
445
+ expect(result.endpoint).toBeDefined();
446
+ expect(result.clusterName).toBeDefined();
447
+ });
448
+
449
+ it("should return Pulumi Outputs", () => {
450
+ const result = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
451
+
452
+ expect(result.vpcId).toBeDefined();
453
+ expect(mockRequireOutput).toHaveBeenCalledWith("vpcId");
454
+ });
455
+
456
+ it("should delegate to createStackAlias", () => {
457
+ const result = createSimpleAlias("infrastructure", "shared", ["vpcId"]);
458
+
459
+ // Verify it returns the same shape as createStackAlias
460
+ expect(result).toHaveProperty("vpcId");
461
+ expect(mockStackReference).toHaveBeenCalledWith("test-org/infrastructure/shared");
462
+ });
463
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["text", "json", "html"],
10
+ exclude: [
11
+ "node_modules/",
12
+ "dist/",
13
+ "tests/",
14
+ "examples/",
15
+ "*.config.ts",
16
+ ],
17
+ },
18
+ },
19
+ });