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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import {
SeedlessOnboardingControllerErrorMessage,
Web3AuthNetwork,
} from './constants';
import { PasswordSyncError, RecoveryError } from './errors';
import {
PasswordSyncError,
RecoveryError,
SeedlessOnboardingError,
} from './errors';
import { projectLogger, createModuleLogger } from './logger';
import { SecretMetadata } from './SecretMetadata';
import type {
Expand Down Expand Up @@ -437,8 +441,11 @@ export class SeedlessOnboardingController<
return authenticationResult;
} catch (error) {
log('Error authenticating user', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
{
cause: error,
},
);
}
};
Expand Down Expand Up @@ -664,8 +671,11 @@ export class SeedlessOnboardingController<
);
} catch (error) {
log('Error changing password', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToChangePassword,
{
cause: error,
},
);
}
});
Expand Down Expand Up @@ -941,8 +951,11 @@ export class SeedlessOnboardingController<
})
.catch((error) => {
log('Error fetching auth pub key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey,
{
cause: error,
},
);
});
globalAuthPubKey = authPubKey;
Expand Down Expand Up @@ -1031,8 +1044,11 @@ export class SeedlessOnboardingController<
throw error;
}
log('Error persisting local encryption key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToPersistOprfKey,
{
cause: error,
},
);
}
}
Expand Down Expand Up @@ -1195,8 +1211,11 @@ export class SeedlessOnboardingController<
if (this.#isAuthTokenError(error)) {
throw error;
}
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchSecretMetadata,
{
cause: error,
},
);
}

Expand Down Expand Up @@ -1805,8 +1824,11 @@ export class SeedlessOnboardingController<
})
.catch((error) => {
log('Error fetching auth pub key', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey,
{
cause: error,
},
);
});
const isPasswordOutdated = await this.checkIsPasswordOutdated({
Expand Down Expand Up @@ -1844,8 +1866,11 @@ export class SeedlessOnboardingController<
refreshToken,
}).catch((error) => {
log('Error refreshing JWT tokens', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens,
{
cause: error,
},
);
});

Expand All @@ -1866,8 +1891,11 @@ export class SeedlessOnboardingController<
});
} catch (error) {
log('Error refreshing node auth tokens', error);
throw new Error(
throw new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
{
cause: error,
},
);
}
}
Expand Down
207 changes: 206 additions & 1 deletion packages/seedless-onboarding-controller/src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { TOPRFErrorCode } from '@metamask/toprf-secure-backup';

import { SeedlessOnboardingControllerErrorMessage } from './constants';
import { getErrorMessageFromTOPRFErrorCode } from './errors';
import {
getErrorMessageFromTOPRFErrorCode,
SeedlessOnboardingError,
} from './errors';

describe('getErrorMessageFromTOPRFErrorCode', () => {
it('returns TooManyLoginAttempts for RateLimitExceeded', () => {
Expand Down Expand Up @@ -49,3 +52,205 @@ describe('getErrorMessageFromTOPRFErrorCode', () => {
).toBe('fallback');
});
});

describe('SeedlessOnboardingError', () => {
describe('constructor', () => {
it('creates an error with just a message', () => {
const error = new SeedlessOnboardingError('Test error message');

expect(error.message).toBe('Test error message');
expect(error.name).toBe('SeedlessOnboardingControllerError');
expect(error.details).toBeUndefined();
expect(error.cause).toBeUndefined();
});

it('creates an error with a message from SeedlessOnboardingControllerErrorMessage enum', () => {
const error = new SeedlessOnboardingError(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
);

expect(error.message).toBe(
SeedlessOnboardingControllerErrorMessage.AuthenticationError,
);
expect(error.name).toBe('SeedlessOnboardingControllerError');
});

it('creates an error with message and details', () => {
const error = new SeedlessOnboardingError('Test error', {
details: 'Additional context for debugging',
});

expect(error.message).toBe('Test error');
expect(error.details).toBe('Additional context for debugging');
expect(error.cause).toBeUndefined();
});

it('creates an error with an Error instance as cause', () => {
const originalError = new Error('Original error');
const error = new SeedlessOnboardingError('Wrapped error', {
cause: originalError,
});

expect(error.message).toBe('Wrapped error');
expect(error.cause).toBe(originalError);
});

it('creates an error with a string as cause', () => {
const error = new SeedlessOnboardingError('Test error', {
cause: 'String cause message',
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe('String cause message');
});

it('creates an error with an object as cause (JSON serializable)', () => {
const causeObject = { code: 500, reason: 'Internal error' };
const error = new SeedlessOnboardingError('Test error', {
cause: causeObject,
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe(JSON.stringify(causeObject));
});

it('handles circular object as cause by using fallback message', () => {
const circularObject: Record<string, unknown> = { name: 'circular' };
circularObject.self = circularObject;

const error = new SeedlessOnboardingError('Test error', {
cause: circularObject,
});

expect(error.cause).toBeInstanceOf(Error);
expect(error.cause?.message).toBe('Unknown error');
});

it('creates an error with both details and cause', () => {
const originalError = new Error('Original');
const error = new SeedlessOnboardingError('Test error', {
details: 'Some details',
cause: originalError,
});

expect(error.message).toBe('Test error');
expect(error.details).toBe('Some details');
expect(error.cause).toBe(originalError);
});
});

describe('toJSON', () => {
it('serializes error with all properties', () => {
const originalError = new Error('Original error');
const error = new SeedlessOnboardingError('Test error', {
details: 'Debug info',
cause: originalError,
});

const json = error.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Test error');
expect(json.details).toBe('Debug info');
expect(json.cause).toStrictEqual({
name: 'Error',
message: 'Original error',
});
expect(json.stack).toBeDefined();
});

it('serializes error without optional properties', () => {
const error = new SeedlessOnboardingError('Simple error');

const json = error.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Simple error');
expect(json.details).toBeUndefined();
expect(json.cause).toBeUndefined();
expect(json.stack).toBeDefined();
});

it('serializes error with custom error type as cause', () => {
class CustomError extends Error {
constructor() {
super('Custom error message');
this.name = 'CustomError';
}
}
const customError = new CustomError();
const error = new SeedlessOnboardingError('Wrapper', {
cause: customError,
});

const json = error.toJSON();

expect(json.cause).toStrictEqual({
name: 'CustomError',
message: 'Custom error message',
});
});

it('serializes SeedlessOnboardingError cause with details preserved', () => {
const innerError = new SeedlessOnboardingError('Inner error', {
details: 'Inner debugging context',
});
const outerError = new SeedlessOnboardingError('Outer error', {
details: 'Outer debugging context',
cause: innerError,
});

const json = outerError.toJSON();

expect(json.name).toBe('SeedlessOnboardingControllerError');
expect(json.message).toBe('Outer error');
expect(json.details).toBe('Outer debugging context');
expect(json.cause).toStrictEqual({
name: 'SeedlessOnboardingControllerError',
message: 'Inner error',
details: 'Inner debugging context',
cause: undefined,
stack: innerError.stack,
});
});

it('serializes deeply nested SeedlessOnboardingError chain', () => {
const rootError = new Error('Root cause');
const level1 = new SeedlessOnboardingError('Level 1', {
details: 'Level 1 details',
cause: rootError,
});
const level2 = new SeedlessOnboardingError('Level 2', {
details: 'Level 2 details',
cause: level1,
});

const json = level2.toJSON();

expect(json.details).toBe('Level 2 details');
const level1Json = json.cause as Record<string, unknown>;
expect(level1Json.message).toBe('Level 1');
expect(level1Json.details).toBe('Level 1 details');
expect(level1Json.cause).toStrictEqual({
name: 'Error',
message: 'Root cause',
});
});
});

describe('inheritance', () => {
it('is an instance of Error', () => {
const error = new SeedlessOnboardingError('Test');

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(SeedlessOnboardingError);
});

it('has a proper stack trace', () => {
const error = new SeedlessOnboardingError('Test');

expect(error.stack).toBeDefined();
expect(error.stack).toContain('SeedlessOnboardingControllerError');
});
});
});
Loading
Loading