Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/contentstack-utilities/src/authentication-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class AuthenticationHandler {
if (error.response && error.response.status) {
switch (error.response.status) {
case 401:
if (maxRetryCount >= 2) {
const errorDetails = formatError(error);
ux.print(`Authentication failed after token refresh: ${errorDetails}`, { color: 'red' });
return;
}
// NOTE: Refresh the token if the type is OAuth.
const region: { cma: string; name: string; cda: string } = configHandler.get('region') || {};
if (region?.cma) {
Expand All @@ -67,12 +72,11 @@ class AuthenticationHandler {
hostName = hostName || region.cma;
const refreshed = await this.refreshToken(hostName);
if (refreshed) {
return this.refreshAccessToken(error, maxRetryCount); // Retry after refreshing the token
return this.refreshAccessToken(error, maxRetryCount + 1);
}

const errorDetails = formatError(error);
ux.print(`Authentication failed: ${errorDetails}`, { color: 'red' });
// For Basic Auth, exit immediately without retrying
return;
}
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//@ts-nocheck
import { expect } from 'chai';
import { createSandbox } from 'sinon';
import authenticationHandler from '../../src/authentication-handler';
import configHandler from '../../src/config-handler';
import cliux from '../../src/cli-ux';

describe('Authentication Handler', () => {
describe('refreshAccessToken - 401 handling', () => {
let sandbox;
let printStub;

beforeEach(() => {
sandbox = createSandbox();
printStub = sandbox.stub(cliux, 'print');
});

afterEach(() => {
sandbox.restore();
});

const printedMessages = () => printStub.getCalls().map((call) => String(call.args[0]));

it('should stop after a single refresh when the 401 persists', async () => {
// Regression guard: the 401 branch used to recurse with the same stale error and an
// unincremented counter, so a successful refresh that did not clear the 401 looped forever.
sandbox.stub(configHandler, 'get').returns({ cma: 'https://api.contentstack.io' });
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(true);

await authenticationHandler.refreshAccessToken({ response: { status: 401 } });

expect(refreshTokenStub.callCount).to.equal(1);
expect(printedMessages().some((msg) => msg.includes('Authentication failed after token refresh'))).to.be.true;
});

it('should not retry when the token refresh itself fails', async () => {
sandbox.stub(configHandler, 'get').returns({ cma: 'https://api.contentstack.io' });
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(false);

await authenticationHandler.refreshAccessToken({ response: { status: 401 } });

expect(refreshTokenStub.callCount).to.equal(1);
expect(printedMessages().some((msg) => msg.includes('Authentication failed'))).to.be.true;
});

it('should derive the host from a region cma that is not a URL', async () => {
sandbox.stub(configHandler, 'get').returns({ cma: 'api.contentstack.io' });
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(false);

await authenticationHandler.refreshAccessToken({ response: { status: 401 } });

expect(refreshTokenStub.calledOnceWith('api.contentstack.io')).to.be.true;
});

it('should not attempt a refresh when no region cma is configured', async () => {
sandbox.stub(configHandler, 'get').returns({});
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(true);

await authenticationHandler.refreshAccessToken({ response: { status: 401 } });

expect(refreshTokenStub.called).to.be.false;
});

it('should honour an already-exhausted retry count without refreshing', async () => {
sandbox.stub(configHandler, 'get').returns({ cma: 'https://api.contentstack.io' });
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(true);

await authenticationHandler.refreshAccessToken({ response: { status: 401 } }, 2);

expect(refreshTokenStub.called).to.be.false;
expect(printedMessages().some((msg) => msg.includes('Authentication failed after token refresh'))).to.be.true;
});
});

describe('refreshAccessToken - other statuses', () => {
let sandbox;
let printStub;

beforeEach(() => {
sandbox = createSandbox();
printStub = sandbox.stub(cliux, 'print');
});

afterEach(() => {
sandbox.restore();
});

it('should do nothing when the error carries no response', async () => {
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(true);

await authenticationHandler.refreshAccessToken(new Error('boom'));

expect(refreshTokenStub.called).to.be.false;
expect(printStub.called).to.be.false;
});

it('should do nothing for an unhandled status', async () => {
const refreshTokenStub = sandbox.stub(authenticationHandler, 'refreshToken').resolves(true);

await authenticationHandler.refreshAccessToken({ response: { status: 500 } });

expect(refreshTokenStub.called).to.be.false;
expect(printStub.called).to.be.false;
});

it('should still cap 429 retries at three attempts', async () => {
// The 429/408 branch is untouched by the 401 fix; this guards against regressing it.
await authenticationHandler.refreshAccessToken({ response: { status: 429 } });

const messages = printStub.getCalls().map((call) => String(call.args[0]));
expect(messages.some((msg) => msg.includes('Max retry attempts exceeded (3/3)'))).to.be.true;
});
});
});
Loading