Skip to content

Commit 631ec23

Browse files
authored
fix: slas client list returns empty list when tenant doesn't exist (#138)
When listing SLAS clients for a tenant that doesn't exist yet, the API returns a 401 ClientTenantException. Instead of surfacing this confusing error, check if the tenant exists and return an empty list if not. Closes #126
1 parent d991db9 commit 631ec23

4 files changed

Lines changed: 93 additions & 21 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-cli': patch
3+
---
4+
5+
`slas client list` now returns an empty list instead of erroring when the SLAS tenant doesn't exist yet.

packages/b2c-cli/src/commands/slas/client/list.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,16 @@ export default class SlasClientList extends SlasClientCommand<typeof SlasClientL
6969
});
7070

7171
if (error) {
72-
this.error(
73-
t('commands.slas.client.list.error', 'Failed to list SLAS clients: {{message}}', {
74-
message: formatApiError(error, response),
75-
}),
76-
);
72+
const tenantExists = await this.checkTenantExists(slasClient, tenantId);
73+
if (tenantExists) {
74+
this.error(
75+
t('commands.slas.client.list.error', 'Failed to list SLAS clients: {{message}}', {
76+
message: formatApiError(error, response),
77+
}),
78+
);
79+
}
80+
// Tenant doesn't exist — no clients to list, fall through to empty handling
81+
this.logger.debug({tenantId}, 'Tenant does not exist yet, returning empty client list');
7782
}
7883

7984
const clients = ((data as {data?: Client[]})?.data ?? []).map((client) => normalizeClientResponse(client));

packages/b2c-cli/src/utils/slas/client.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,22 @@ export function formatApiError(error: unknown, response: Response): string {
107107
*/
108108
export abstract class SlasClientCommand<T extends typeof Command> extends OAuthCommand<T> {
109109
/**
110-
* Ensure tenant exists, creating it if necessary.
111-
* This is required before creating SLAS clients.
110+
* Check if a tenant exists.
111+
* Returns true if the tenant exists, false if not found.
112+
* Throws (via this.error) if an unexpected error occurs.
112113
*/
113-
protected async ensureTenantExists(slasClient: SlasClient, tenantId: string): Promise<void> {
114-
// Try to get the tenant first
114+
protected async checkTenantExists(slasClient: SlasClient, tenantId: string): Promise<boolean> {
115115
const {error, response} = await slasClient.GET('/tenants/{tenantId}', {
116116
params: {
117117
path: {tenantId},
118118
},
119119
});
120120

121-
// If tenant exists, we're done
122121
if (!error) {
123-
return;
122+
this.logger.debug({tenantId}, 'Tenant exists');
123+
return true;
124124
}
125125

126-
// Check if this is a "tenant not found" error (SLAS returns 400 with TenantNotFoundException)
127126
const isTenantNotFound =
128127
response.status === 404 ||
129128
(response.status === 400 &&
@@ -132,13 +131,27 @@ export abstract class SlasClientCommand<T extends typeof Command> extends OAuthC
132131
'exception_name' in error &&
133132
(error as {exception_name?: string}).exception_name === 'TenantNotFoundException');
134133

135-
// If it's not a tenant-not-found error, something else went wrong
136-
if (!isTenantNotFound) {
137-
this.error(
138-
t('commands.slas.client.create.tenantError', 'Failed to check tenant: {{message}}', {
139-
message: formatApiError(error, response),
140-
}),
141-
);
134+
if (isTenantNotFound) {
135+
this.logger.debug({tenantId, status: response.status}, 'Tenant not found');
136+
return false;
137+
}
138+
139+
this.error(
140+
t('commands.slas.client.create.tenantError', 'Failed to check tenant: {{message}}', {
141+
message: formatApiError(error, response),
142+
}),
143+
);
144+
}
145+
146+
/**
147+
* Ensure tenant exists, creating it if necessary.
148+
* This is required before creating SLAS clients.
149+
*/
150+
protected async ensureTenantExists(slasClient: SlasClient, tenantId: string): Promise<void> {
151+
const tenantExists = await this.checkTenantExists(slasClient, tenantId);
152+
153+
if (tenantExists) {
154+
return;
142155
}
143156

144157
// Tenant doesn't exist, create it with placeholder values

packages/b2c-cli/test/commands/slas/client/list.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,16 @@ describe('slas client list', () => {
4949
expect(result.clients).to.deep.equal([]);
5050
});
5151

52-
it('calls command.error on API error', async () => {
52+
it('calls command.error on API error when tenant exists', async () => {
5353
const command: any = await createCommand({'tenant-id': 'abcd_123'}, {});
5454

5555
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
5656

57-
const getStub = sinon.stub().resolves({data: undefined, error: {message: 'boom'}});
57+
const getStub = sinon.stub();
58+
// First call: list clients - returns error
59+
getStub.onFirstCall().resolves({data: undefined, error: {message: 'boom'}, response: {status: 401}});
60+
// Second call: check tenant - tenant exists
61+
getStub.onSecondCall().resolves({data: {tenantId: 'abcd_123'}, error: undefined});
5862
sinon.stub(command, 'getSlasClient').returns({GET: getStub} as any);
5963

6064
const errorStub = stubErrorToThrow(command);
@@ -66,4 +70,49 @@ describe('slas client list', () => {
6670
expect(errorStub.calledOnce).to.equal(true);
6771
}
6872
});
73+
74+
it('returns empty clients list when list errors and tenant does not exist (404)', async () => {
75+
const command: any = await createCommand({'tenant-id': 'abcd_123'}, {});
76+
77+
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
78+
79+
const getStub = sinon.stub();
80+
// First call: list clients - returns error
81+
getStub.onFirstCall().resolves({data: undefined, error: {message: 'boom'}, response: {status: 401}});
82+
// Second call: check tenant - tenant not found (404)
83+
getStub.onSecondCall().resolves({error: {message: 'not found'}, response: {status: 404}});
84+
sinon.stub(command, 'getSlasClient').returns({GET: getStub} as any);
85+
sinon.stub(command, 'jsonEnabled').returns(true);
86+
87+
const errorStub = sinon.stub(command, 'error');
88+
89+
const result = await command.run();
90+
91+
expect(result.clients).to.deep.equal([]);
92+
expect(errorStub.called).to.equal(false);
93+
});
94+
95+
it('returns empty clients list when list errors and tenant does not exist (400 TenantNotFoundException)', async () => {
96+
const command: any = await createCommand({'tenant-id': 'abcd_123'}, {});
97+
98+
sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
99+
100+
const getStub = sinon.stub();
101+
// First call: list clients - returns error
102+
getStub.onFirstCall().resolves({data: undefined, error: {message: 'boom'}, response: {status: 401}});
103+
// Second call: check tenant - tenant not found (400 + TenantNotFoundException)
104+
getStub.onSecondCall().resolves({
105+
error: {exception_name: 'TenantNotFoundException'},
106+
response: {status: 400},
107+
});
108+
sinon.stub(command, 'getSlasClient').returns({GET: getStub} as any);
109+
sinon.stub(command, 'jsonEnabled').returns(true);
110+
111+
const errorStub = sinon.stub(command, 'error');
112+
113+
const result = await command.run();
114+
115+
expect(result.clients).to.deep.equal([]);
116+
expect(errorStub.called).to.equal(false);
117+
});
69118
});

0 commit comments

Comments
 (0)