Skip to content

Commit 2c711c0

Browse files
committed
tests for dry run output and error handling
1 parent be935c0 commit 2c711c0

6 files changed

Lines changed: 730 additions & 3 deletions

File tree

docs/using-dry-run.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The Auth0 Deploy CLI supports a "dry run" mode that allows you to preview all potential changes to your Auth0 tenant without actually applying them. This feature provides a safety net for validating configurations and understanding the impact of deployments before they occur.
44

5-
[Discussions thread](https://github.com/auth0/auth0-deploy-cli/discussions/1092?sort=new)
5+
[Discussions thread](https://github.com/auth0/auth0-deploy-cli/discussions/1149)
66

77
## Usage
88

test/tools/auth0/handlers/dryRun.tests.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import DatabasesHandler from '../../../../src/tools/auth0/handlers/databases';
66
import HooksHandler from '../../../../src/tools/auth0/handlers/hooks';
77
import RulesConfigsHandler from '../../../../src/tools/auth0/handlers/rulesConfigs';
88
import RulesHandler from '../../../../src/tools/auth0/handlers/rules';
9+
import DefaultHandler from '../../../../src/tools/auth0/handlers/default';
910
import constants from '../../../../src/tools/constants';
11+
import { configFactory } from '../../../../src/configFactory';
1012
import { mockPagedData } from '../../../utils';
1113

1214
const pool = {
@@ -298,3 +300,86 @@ describe('#handler dryRunChanges', () => {
298300
expect(changes.update[0].secrets).to.equal(undefined);
299301
});
300302
});
303+
304+
describe('#getResourceName', () => {
305+
function createHandler(type: string): DefaultHandler {
306+
return new DefaultHandler({
307+
// @ts-ignore test stub
308+
config: configFactory(),
309+
type,
310+
id: 'id',
311+
identifiers: ['id', 'name'],
312+
});
313+
}
314+
315+
it('should return name when present', () => {
316+
const handler = createHandler('clients');
317+
expect(handler.getResourceName({ name: 'My App' })).to.equal('My App');
318+
});
319+
320+
it('should return display_name when name is absent', () => {
321+
const handler = createHandler('clients');
322+
expect(handler.getResourceName({ display_name: 'My Display Name' })).to.equal(
323+
'My Display Name'
324+
);
325+
});
326+
327+
it('should return template when name and display_name are absent', () => {
328+
const handler = createHandler('emailTemplates');
329+
expect(handler.getResourceName({ template: 'verify_email' })).to.equal('verify_email');
330+
});
331+
332+
it('should return email property as fallback', () => {
333+
const handler = createHandler('users');
334+
expect(handler.getResourceName({ email: 'test@example.com' })).to.equal('test@example.com');
335+
});
336+
337+
it('should format clientGrants as "client_id -> audience"', () => {
338+
const handler = createHandler('clientGrants');
339+
expect(
340+
handler.getResourceName({ client_id: 'cli_123', audience: 'https://api.example.com' })
341+
).to.equal('cli_123 -> https://api.example.com');
342+
});
343+
344+
it('should return name or type for guardianFactors', () => {
345+
const handler = createHandler('guardianFactors');
346+
expect(handler.getResourceName({ type: 'sms' })).to.equal('sms');
347+
});
348+
349+
it('should return template for emailTemplates type when no name', () => {
350+
const handler = createHandler('emailTemplates');
351+
expect(handler.getResourceName({ template: 'welcome_email' })).to.equal('welcome_email');
352+
});
353+
354+
it('should return description or priority for networkACLs', () => {
355+
const handler = createHandler('networkACLs');
356+
expect(handler.getResourceName({ description: 'Block list' })).to.equal('Block list');
357+
expect(handler.getResourceName({ priority: 10 })).to.equal('priority:10');
358+
});
359+
360+
it('should return domain for customDomains', () => {
361+
const handler = createHandler('customDomains');
362+
expect(handler.getResourceName({ domain: 'auth.example.com' })).to.equal('auth.example.com');
363+
});
364+
365+
it('should return "{type} settings" for singleton resource types', () => {
366+
const singletonTypes = [
367+
'tenant',
368+
'attackProtection',
369+
'branding',
370+
'emailProvider',
371+
'guardianPhoneFactorSelectedProvider',
372+
'guardianPolicies',
373+
'riskAssessment',
374+
];
375+
singletonTypes.forEach((type) => {
376+
const handler = createHandler(type);
377+
expect(handler.getResourceName({})).to.equal(`${type} settings`);
378+
});
379+
});
380+
381+
it('should return "unnamed resource" as fallback', () => {
382+
const handler = createHandler('unknownType');
383+
expect(handler.getResourceName({})).to.equal('unnamed resource');
384+
});
385+
});

test/tools/auth0/handlers/tenant.tests.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,45 @@ describe('#tenant handler', () => {
226226
expect(() => removeUnallowedTenantFlags({})).to.not.throw();
227227
});
228228
});
229+
230+
describe('#tenant processChanges dry-run', () => {
231+
it('should convert session durations from hours to minutes in update payload', async () => {
232+
let capturedPayload: any = null;
233+
const auth0 = {
234+
tenants: {
235+
settings: {
236+
get: () => ({
237+
friendly_name: 'Test',
238+
session_lifetime_in_minutes: 60,
239+
idle_session_lifetime_in_minutes: 120,
240+
}),
241+
update: (data: any) => {
242+
capturedPayload = data;
243+
return Promise.resolve(data);
244+
},
245+
},
246+
},
247+
};
248+
249+
// @ts-ignore — standard test pattern
250+
const handler = new tenantHandler({ client: auth0 });
251+
const stageFn = Object.getPrototypeOf(handler).processChanges;
252+
253+
await stageFn.apply(handler, [
254+
{
255+
tenant: {
256+
friendly_name: 'Updated Test',
257+
session_lifetime: 2,
258+
idle_session_lifetime: 3,
259+
},
260+
},
261+
]);
262+
263+
expect(capturedPayload).to.not.be.null;
264+
expect(capturedPayload.session_lifetime_in_minutes).to.equal(120);
265+
expect(capturedPayload.idle_session_lifetime_in_minutes).to.equal(180);
266+
expect(capturedPayload).to.not.have.property('session_lifetime');
267+
expect(capturedPayload).to.not.have.property('idle_session_lifetime');
268+
});
269+
});
229270
});

test/tools/auth0/index.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,196 @@ describe('#Auth0 class', () => {
130130
expect(output).to.match(/ ResourceServers\s+ UPDATE\s+ Role test API\s+/);
131131
expect(output).to.match(/[]+/);
132132
});
133+
134+
it('should output "No changes detected" when all handlers report no changes', async () => {
135+
process.env.AUTH0_DEBUG = 'true';
136+
137+
sandbox
138+
.stub(calculateDryRunChanges, 'dryRunFormatAssets')
139+
.callsFake(async (assets) => assets);
140+
141+
const printedMessages: string[] = [];
142+
sandbox.stub(utils, 'printCLIMessage').callsFake((message: string) => {
143+
printedMessages.push(message);
144+
});
145+
146+
const auth0 = new Auth0(mockEmptyClient, mockEmptyAssets, (key) => {
147+
const config = {
148+
AUTH0_DOMAIN: 'example-tenant.auth0.com',
149+
AUTH0_INPUT_FILE: './tenant.yaml',
150+
};
151+
return config[key];
152+
});
153+
154+
auth0.handlers = [
155+
{
156+
type: 'clients',
157+
dryRunChanges: async () => ({ create: [], update: [], del: [] }),
158+
getResourceName: (item: { name: string }) => item.name,
159+
},
160+
] as any;
161+
162+
const hasChanges = await auth0.dryRun();
163+
const output = printedMessages[printedMessages.length - 1].replace(
164+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'),
165+
''
166+
);
167+
168+
expect(hasChanges).to.equal(false);
169+
expect(output).to.include('No changes detected');
170+
});
171+
172+
it('should show DELETE with asterisk note when AUTH0_ALLOW_DELETE is false', async () => {
173+
process.env.AUTH0_DEBUG = 'true';
174+
175+
sandbox
176+
.stub(calculateDryRunChanges, 'dryRunFormatAssets')
177+
.callsFake(async (assets) => assets);
178+
179+
const printedMessages: string[] = [];
180+
sandbox.stub(utils, 'printCLIMessage').callsFake((message: string) => {
181+
printedMessages.push(message);
182+
});
183+
184+
const auth0 = new Auth0(mockEmptyClient, mockEmptyAssets, (key) => {
185+
const config = {
186+
AUTH0_ALLOW_DELETE: false,
187+
AUTH0_DOMAIN: 'example-tenant.auth0.com',
188+
AUTH0_INPUT_FILE: './tenant.yaml',
189+
};
190+
return config[key];
191+
});
192+
193+
auth0.handlers = [
194+
{
195+
type: 'clients',
196+
dryRunChanges: async () => ({
197+
create: [],
198+
update: [],
199+
del: [{ name: 'Old App' }],
200+
}),
201+
getResourceName: (item: { name: string }) => item.name,
202+
},
203+
] as any;
204+
205+
const hasChanges = await auth0.dryRun();
206+
const output = printedMessages[printedMessages.length - 1].replace(
207+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'),
208+
''
209+
);
210+
211+
expect(hasChanges).to.equal(true);
212+
expect(output).to.include('Requires AUTH0_ALLOW_DELETE to be enabled');
213+
expect(output).to.include('DELETE');
214+
});
215+
216+
it('should show CREATE changes in the table', async () => {
217+
process.env.AUTH0_DEBUG = 'true';
218+
219+
sandbox
220+
.stub(calculateDryRunChanges, 'dryRunFormatAssets')
221+
.callsFake(async (assets) => assets);
222+
223+
const printedMessages: string[] = [];
224+
sandbox.stub(utils, 'printCLIMessage').callsFake((message: string) => {
225+
printedMessages.push(message);
226+
});
227+
228+
const auth0 = new Auth0(mockEmptyClient, mockEmptyAssets, (key) => {
229+
const config = {
230+
AUTH0_DOMAIN: 'example-tenant.auth0.com',
231+
AUTH0_INPUT_FILE: './tenant.yaml',
232+
};
233+
return config[key];
234+
});
235+
236+
auth0.handlers = [
237+
{
238+
type: 'actions',
239+
dryRunChanges: async () => ({
240+
create: [{ name: 'New Action' }],
241+
update: [],
242+
del: [],
243+
}),
244+
getResourceName: (item: { name: string }) => item.name,
245+
},
246+
] as any;
247+
248+
const hasChanges = await auth0.dryRun();
249+
const output = printedMessages[printedMessages.length - 1].replace(
250+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'),
251+
''
252+
);
253+
254+
expect(hasChanges).to.equal(true);
255+
expect(output).to.include('CREATE');
256+
expect(output).to.include('New Action');
257+
});
258+
259+
it('should propagate handler errors with type and stage annotations', async () => {
260+
process.env.AUTH0_DEBUG = 'true';
261+
262+
sandbox
263+
.stub(calculateDryRunChanges, 'dryRunFormatAssets')
264+
.callsFake(async (assets) => assets);
265+
266+
sandbox.stub(utils, 'printCLIMessage');
267+
268+
const auth0 = new Auth0(mockEmptyClient, mockEmptyAssets, (key) => {
269+
const config = {
270+
AUTH0_DOMAIN: 'example-tenant.auth0.com',
271+
};
272+
return config[key];
273+
});
274+
275+
auth0.handlers = [
276+
{
277+
type: 'clients',
278+
dryRunChanges: async () => {
279+
throw new Error('API failure');
280+
},
281+
getResourceName: (item: { name: string }) => item.name,
282+
},
283+
] as any;
284+
285+
try {
286+
await auth0.dryRun();
287+
expect.fail('Expected dryRun to throw');
288+
} catch (err) {
289+
expect(err.message).to.equal('API failure');
290+
expect(err.type).to.equal('clients');
291+
expect(err.stage).to.equal('dryRun');
292+
}
293+
});
294+
295+
it('should use default input path when AUTH0_INPUT_FILE is not set', async () => {
296+
process.env.AUTH0_DEBUG = 'true';
297+
298+
sandbox
299+
.stub(calculateDryRunChanges, 'dryRunFormatAssets')
300+
.callsFake(async (assets) => assets);
301+
302+
const printedMessages: string[] = [];
303+
sandbox.stub(utils, 'printCLIMessage').callsFake((message: string) => {
304+
printedMessages.push(message);
305+
});
306+
307+
const auth0 = new Auth0(mockEmptyClient, mockEmptyAssets, (key) => {
308+
const config = {
309+
AUTH0_DOMAIN: 'example-tenant.auth0.com',
310+
};
311+
return config[key];
312+
});
313+
314+
auth0.handlers = [] as any;
315+
316+
await auth0.dryRun();
317+
const output = printedMessages[printedMessages.length - 1].replace(
318+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'),
319+
''
320+
);
321+
322+
expect(output).to.include('./tenant-config-directory/');
323+
});
133324
});
134325
});

0 commit comments

Comments
 (0)