Compare commits

..
Author SHA1 Message Date
iruy-fr f78c5a00dd FIX: surface failed MFA resets 2026-09-03 16:44:47 -03:00
4 changed files with 74 additions and 60 deletions
@@ -0,0 +1,57 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { Metadata } from '@grpc/grpc-js';
import { of } from 'rxjs';
import { AuthClientService } from './auth.service';
describe('AuthClientService.resetUsers', () => {
const logger = {
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
};
const resetUser = jest.fn();
let service: AuthClientService;
beforeEach(() => {
jest.clearAllMocks();
service = new AuthClientService({ logger } as any, {} as any);
(service as any).authService = { ResetUser: resetUser };
});
it('returns the DUC response when every requested user was reset', async () => {
const response = {
message: 'Users reset successfully',
successfulUsers: ['user-1'],
failedUsers: [],
};
resetUser.mockReturnValue(of(response));
await expect(
service.resetUsers(['user-1'], new Metadata()),
).resolves.toEqual(response);
});
it('returns a non-2xx error instead of masking failed resets', async () => {
const response = {
message: 'Some users failed',
successfulUsers: ['user-1'],
failedUsers: ['user-2'],
};
resetUser.mockReturnValue(of(response));
try {
await service.resetUsers(['user-1', 'user-2'], new Metadata());
fail('Expected resetUsers to reject');
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(HttpStatus.BAD_GATEWAY);
expect((error as HttpException).getResponse()).toEqual({
statusCode: HttpStatus.BAD_GATEWAY,
message: 'Failed to reset MFA for one or more users.',
successfulUsers: ['user-1'],
failedUsers: ['user-2'],
});
}
});
});
+16
View File
@@ -295,6 +295,22 @@ export class AuthClientService implements OnModuleInit {
this.authService.ResetUser({ users }, metadata),
);
if (response.failedUsers?.length > 0) {
this.logger.error('resetUsers - Partial or total failure', {
successfulUsers: response.successfulUsers,
failedUsers: response.failedUsers,
});
throw new HttpException(
{
statusCode: HttpStatus.BAD_GATEWAY,
message: 'Failed to reset MFA for one or more users.',
successfulUsers: response.successfulUsers,
failedUsers: response.failedUsers,
},
HttpStatus.BAD_GATEWAY,
);
}
this.logger.info('resetUsers - Success', { response });
return response;
} catch (error) {
@@ -1,59 +0,0 @@
let mockStrategyOptions: { scope: string };
jest.mock('passport-hubspot-oauth2', () => ({
Strategy: class {
constructor(options: { scope: string }) {
mockStrategyOptions = options;
}
authenticate() {}
},
}));
jest.mock('@nestjs/passport', () => ({
PassportStrategy: (Strategy) => Strategy,
}));
import { HubspotStrategy } from './hubspot-strategy';
import type { OauthSecrets } from 'src/utils/OauthSecrets';
import type { OauthService } from '../oauth.service';
describe('HubspotStrategy', () => {
it('requests the content scope without requesting Marketing write scopes', () => {
const oauthSecrets = {
hubspot: {
client_id: '',
client_secret: '',
redirect_uri: '',
},
} as OauthSecrets;
new HubspotStrategy(oauthSecrets, {} as OauthService);
const scopes = mockStrategyOptions.scope.split(' ');
expect(scopes).toEqual([
'tickets',
'automation',
'business-intelligence',
'oauth',
'forms',
'content',
'integration-sync',
'sales-email-read',
'crm.lists.read',
'crm.objects.contacts.read',
'crm.schemas.contacts.read',
'crm.objects.companies.read',
'crm.objects.deals.read',
'crm.schemas.companies.read',
'crm.schemas.deals.read',
'crm.objects.owners.read',
'crm.objects.quotes.read',
'crm.schemas.quotes.read',
'crm.objects.line_items.read',
'crm.schemas.line_items.read',
]);
expect(scopes).not.toContain('marketing.email.write');
});
});
@@ -17,7 +17,7 @@ export class HubspotStrategy extends PassportStrategy(Strategy) {
clientSecret: oauthSecrets.hubspot.client_secret,
callbackURL: oauthSecrets.hubspot.redirect_uri,
scope:
'tickets automation business-intelligence oauth forms content integration-sync sales-email-read crm.lists.read crm.objects.contacts.read crm.schemas.contacts.read crm.objects.companies.read crm.objects.deals.read crm.schemas.companies.read crm.schemas.deals.read crm.objects.owners.read crm.objects.quotes.read crm.schemas.quotes.read crm.objects.line_items.read crm.schemas.line_items.read',
'tickets automation business-intelligence oauth forms integration-sync sales-email-read crm.lists.read crm.objects.contacts.read crm.schemas.contacts.read crm.objects.companies.read crm.objects.deals.read crm.schemas.companies.read crm.schemas.deals.read crm.objects.owners.read crm.objects.quotes.read crm.schemas.quotes.read crm.objects.line_items.read crm.schemas.line_items.read',
passReqToCallback: true,
},
(accessToken, refreshToken, tokenInfo, profile, done) => {