Skip to content

Commit d1f2490

Browse files
Copilotramya18101
andcommitted
fix: null guard in calculateDryRunChanges, early validation in deploy.ts, and missing test coverage
Agent-Logs-Url: https://github.com/auth0/auth0-deploy-cli/sessions/abf3cd2a-de7c-4059-8fec-d5a9d16faf8c Co-authored-by: ramya18101 <62586490+ramya18101@users.noreply.github.com>
1 parent c616b52 commit d1f2490

4 files changed

Lines changed: 139 additions & 10 deletions

File tree

src/tools/calculateDryRunChanges.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,12 @@ export function calculateDryRunChanges({
311311
const create: Asset[] = [];
312312
const conflicts: Asset[] = [];
313313

314-
const localAssets: Asset[] = (Array.isArray(assets) ? [...assets] : [assets]).map((asset) =>
315-
type === 'tenant' ? normalizeTenantForDryRun(asset) : asset
316-
);
317-
const remoteAssets: Asset[] = (Array.isArray(existing) ? [...existing] : [existing]).map(
318-
(asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset)
319-
);
314+
const localAssets: Asset[] = (Array.isArray(assets) ? [...assets] : [assets])
315+
.filter((asset): asset is Asset => asset != null)
316+
.map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
317+
const remoteAssets: Asset[] = (Array.isArray(existing) ? [...existing] : [existing])
318+
.filter((asset): asset is Asset => asset != null)
319+
.map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
320320

321321
// Helper: returns true if a local and remote asset share at least one identifier value
322322
const assetsMatch = (localAsset: Asset, remoteAsset: Asset) =>

src/tools/deploy.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export default async function deploy(
2424
const dryRunMode = config('AUTH0_DRY_RUN');
2525
// Normalize boolean true (EA compat) → 'preview'
2626
const effectiveMode = dryRunMode === true || dryRunMode === 'true' ? 'preview' : dryRunMode;
27+
28+
if (dryRunMode && effectiveMode !== 'preview') {
29+
throw new Error(`Invalid AUTH0_DRY_RUN value: ${dryRunMode}. Use true or 'preview'.`);
30+
}
31+
2732
const isInteractive = !!config('AUTH0_DRY_RUN_INTERACTIVE');
2833
const shouldApplyAfterPreview = isTruthy(config('AUTH0_DRY_RUN_APPLY'));
2934

@@ -47,10 +52,6 @@ export default async function deploy(
4752
}
4853
}
4954

50-
if (dryRunMode && effectiveMode !== 'preview') {
51-
throw new Error(`Invalid AUTH0_DRY_RUN value: ${dryRunMode}. Use true or 'preview'.`);
52-
}
53-
5455
// Process changes
5556
await auth0.processChanges();
5657

test/tools/calculateDryRunChanges.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,4 +727,73 @@ describe('#dryRunFormatAssets additional paths', () => {
727727
// Without clients, the databases branch is skipped — enabled_clients remain as names
728728
expect(assets.databases![0].enabled_clients).to.deep.equal(['App One']);
729729
});
730+
731+
it('should preserve non-empty branding templates array', async () => {
732+
const auth0Client = mockMgmtClient() as any;
733+
auth0Client.clients.list = () => [];
734+
735+
const assets = await dryRunFormatAssets(
736+
{
737+
branding: {
738+
logo_url: 'https://example.com/logo.png',
739+
templates: [{ template: 'login', body: '<html>...</html>' }],
740+
},
741+
},
742+
auth0Client
743+
);
744+
745+
expect(assets.branding!.templates).to.have.length(1);
746+
});
747+
748+
it('should leave actions without a deployed field unchanged', async () => {
749+
const auth0Client = mockMgmtClient() as any;
750+
auth0Client.clients.list = () => [];
751+
752+
const assets = await dryRunFormatAssets(
753+
{
754+
actions: [{ name: 'action-no-deployed', runtime: 'node18' }],
755+
},
756+
auth0Client
757+
);
758+
759+
expect(assets.actions![0]).to.not.have.property('deployed');
760+
expect(assets.actions![0]).to.not.have.property('all_changes_deployed');
761+
});
762+
});
763+
764+
describe('#calculateDryRunChanges null-safety', () => {
765+
it('should treat all local assets as creates when existing is null', () => {
766+
const changes = calculateDryRunChanges({
767+
type: 'clients',
768+
assets: [{ name: 'New App', app_type: 'spa' }],
769+
existing: null,
770+
identifiers: ['name'],
771+
ignoreDryRunFields: [],
772+
});
773+
774+
expect(changes.create).to.have.length(1);
775+
expect(changes.create[0].name).to.equal('New App');
776+
expect(changes.update).to.have.length(0);
777+
expect(changes.del).to.have.length(0);
778+
});
779+
780+
it('should preserve non-empty tenant flags during dry-run comparison', () => {
781+
const changes = calculateDryRunChanges({
782+
type: 'tenant',
783+
assets: {
784+
friendly_name: 'Auth0 test',
785+
flags: { mfa_show_factor_list_on_enrollment: true },
786+
},
787+
existing: {
788+
friendly_name: 'Auth0 test',
789+
flags: { mfa_show_factor_list_on_enrollment: true },
790+
},
791+
identifiers: ['friendly_name'],
792+
ignoreDryRunFields: [],
793+
});
794+
795+
expect(changes.update).to.have.length(0);
796+
expect(changes.create).to.have.length(0);
797+
expect(changes.del).to.have.length(0);
798+
});
730799
});

test/tools/deploy.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,63 @@ describe('#tools deploy dry-run modes', () => {
8888
sinon.assert.calledOnceWithExactly(dryRunStub, { interactive: false });
8989
sinon.assert.notCalled(processChangesStub);
9090
});
91+
92+
it('should throw before validate when AUTH0_DRY_RUN is an invalid value', async () => {
93+
const validateStub = sandbox.stub(Auth0.prototype, 'validate').resolves();
94+
sandbox.stub(Auth0.prototype, 'processChanges').resolves();
95+
96+
const config = buildConfig({ AUTH0_DRY_RUN: 'not-a-mode' });
97+
98+
try {
99+
await deploy({}, {} as any, config);
100+
throw new Error('Expected deploy to throw');
101+
} catch (err) {
102+
sinon.assert.notCalled(validateStub);
103+
if ((err as Error).message === 'Expected deploy to throw') throw err;
104+
}
105+
});
106+
107+
it('should call processChanges and return handler summary when dry-run is not set', async () => {
108+
sandbox.stub(Auth0.prototype, 'validate').resolves();
109+
const dryRunStub = sandbox.stub(Auth0.prototype, 'dryRun').resolves(true);
110+
const processChangesStub = sandbox.stub(Auth0.prototype, 'processChanges').resolves();
111+
112+
const config = buildConfig({});
113+
const result = await deploy({}, {} as any, config);
114+
115+
sinon.assert.notCalled(dryRunStub);
116+
sinon.assert.calledOnce(processChangesStub);
117+
sinon.assert.match(result, sinon.match.object);
118+
});
119+
120+
it('should pass interactive=true to dryRun when AUTH0_DRY_RUN_INTERACTIVE is set', async () => {
121+
sandbox.stub(Auth0.prototype, 'validate').resolves();
122+
const dryRunStub = sandbox.stub(Auth0.prototype, 'dryRun').resolves(false);
123+
sandbox.stub(Auth0.prototype, 'processChanges').resolves();
124+
125+
const config = buildConfig({
126+
AUTH0_DRY_RUN: 'preview',
127+
AUTH0_DRY_RUN_INTERACTIVE: true,
128+
AUTH0_DRY_RUN_APPLY: false,
129+
});
130+
131+
await deploy({}, {} as any, config);
132+
133+
sinon.assert.calledOnceWithExactly(dryRunStub, { interactive: true });
134+
});
135+
136+
it('should normalize AUTH0_DRY_RUN=true (boolean) to preview mode', async () => {
137+
sandbox.stub(Auth0.prototype, 'validate').resolves();
138+
const dryRunStub = sandbox.stub(Auth0.prototype, 'dryRun').resolves(false);
139+
sandbox.stub(Auth0.prototype, 'processChanges').resolves();
140+
141+
const config = buildConfig({
142+
AUTH0_DRY_RUN: true,
143+
AUTH0_DRY_RUN_INTERACTIVE: false,
144+
});
145+
146+
await deploy({}, {} as any, config);
147+
148+
sinon.assert.calledOnceWithExactly(dryRunStub, { interactive: false });
149+
});
91150
});

0 commit comments

Comments
 (0)