Skip to content
Merged
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
25 changes: 17 additions & 8 deletions plugins/sandbox/src/api/RegistrationBackendClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isValidCountryCode, isValidPhoneNumber } from '../utils/phone-utils';
import { CommonResponse, SignupData } from '../types';
import { SecureFetchApi } from './SecureFetchClient';
import { SandboxEnvironment } from '../const';
import UserSignupError from './errors/UserSignupError';

export type RegistrationBackendClientOptions = {
configApi: ConfigApi;
Expand Down Expand Up @@ -67,20 +68,28 @@ export class RegistrationBackendClient implements RegistrationService {
);
};

/**
* Get the "UserSignup" resource for the current user.
* @returns the current user's signup resource or code undefined it is not
* found.
*/
getSignUpData = async (): Promise<SignupData | undefined> => {
const signupURL = await this.signupAPI();
const response = await this.secureFetchApi.fetch(signupURL, {
method: 'GET',
});
if (!response.ok) {
if (response.status === 404) {
return undefined;
}
throw new Error(
`Unexpected status code: ${response.status} ${response.statusText}`,
);
if (response.ok) {
return response.json();
}
return response.json();

// The user signup is not found, we return undefined to signal that it is
// safe to create it instead.
if (response.status === 404) {
return undefined;
}

// At this point we need to signal the user that something went wrong.
throw await UserSignupError.fromResponse(response);
Comment thread
MikelAlejoBR marked this conversation as resolved.
};

getRecaptchaToken = async (): Promise<string> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,8 @@
import { ConfigApi } from '@backstage/core-plugin-api';
import { RegistrationBackendClient } from '../RegistrationBackendClient';
import { SecureFetchApi } from '../SecureFetchClient';

// Helper to create a mock Response object
const createMockResponse = (options: {
ok: boolean;
status?: number;
statusText?: string;
json?: () => Promise<any>;
}): Response => {
const { ok, status = 200, statusText = '', json } = options;
return {
ok,
status,
statusText,
headers: new Headers(),
redirected: false,
type: 'basic',
url: 'http://mock',
json: json || (() => Promise.resolve({})),
text: () => Promise.resolve(''),
blob: () => Promise.resolve(new Blob()),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
formData: () => Promise.resolve(new FormData()),
bodyUsed: false,
body: null,
clone: function () {
return this;
},
} as Response;
};
import { createMockResponse } from '../../test-utils/mockResponse';
import UserSignupError from '../errors/UserSignupError';

describe('RegistrationBackendClient', () => {
let mockConfigApi: jest.Mocked<ConfigApi>;
Expand Down Expand Up @@ -103,7 +76,7 @@ describe('RegistrationBackendClient', () => {
expect(result).toEqual(mockData);
});

it('should return undefined on 404 response', async () => {
it('should return undefined on a 404 response', async () => {
mockConfigApi.getString.mockReturnValue('http://api');
mockSecureFetchApi.fetch.mockResolvedValue(
createMockResponse({
Expand All @@ -116,7 +89,7 @@ describe('RegistrationBackendClient', () => {
expect(result).toBeUndefined();
});

it('should throw error on other unsuccessful responses', async () => {
it('should throw a UserSignupError on a 500 response', async () => {
mockConfigApi.getString.mockReturnValue('http://api');
mockSecureFetchApi.fetch.mockResolvedValue(
createMockResponse({
Expand All @@ -126,8 +99,28 @@ describe('RegistrationBackendClient', () => {
}),
);

await expect(client.getSignUpData()).rejects.toThrow(UserSignupError);
await expect(client.getSignUpData()).rejects.toThrow(
'Unable to sign you up into Developer Sandbox. Please contact devsandbox@redhat.com',
);
});

it('should throw a UserSignupError with the correct message for a known error', async () => {
mockConfigApi.getString.mockReturnValue('http://api');
mockSecureFetchApi.fetch.mockResolvedValue(
createMockResponse({
ok: false,
status: 403,
json: () =>
Promise.resolve({
message: 'user has been suspended',
}),
}),
);

await expect(client.getSignUpData()).rejects.toThrow(UserSignupError);
await expect(client.getSignUpData()).rejects.toThrow(
'Unexpected status code: 500 Server Error',
'Access to the Developer Sandbox has been suspended due to suspicious activity or detected abuse',
);
});
});
Expand Down
165 changes: 165 additions & 0 deletions plugins/sandbox/src/api/__tests__/UserSignupError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { createMockResponse } from '../../test-utils/mockResponse';
import UserSignupError from '../errors/UserSignupError';

describe('user signup error', () => {
const defaultErrorMessage: string =
'Unable to sign you up into Developer Sandbox. Please contact devsandbox@redhat.com';

it('sets a message when using the main constructor', () => {
const errorMsg = 'test error msg';

// Call the function under test.
const userSignupError = new UserSignupError(errorMsg);

expect(userSignupError.message).toBe(errorMsg);
});

it('returns the default error message on internal server errors', async () => {
const response = createMockResponse({
ok: false,
status: 500,
});

// Call the function under test.
const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(defaultErrorMessage);
});

it('returns the default error message when the response body is not valid JSON', async () => {
const response = createMockResponse({
ok: false,
status: 400,
json: () => Promise.reject(new Error('invalid json')),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(defaultErrorMessage);
});

it('returns the default error message when the response body is null', async () => {
const response = createMockResponse({
ok: false,
status: 400,
json: () => Promise.resolve(null),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(defaultErrorMessage);
});

it('returns the default error message when the response body has no message field', async () => {
const response = createMockResponse({
ok: false,
status: 400,
json: () => Promise.resolve({ details: 'some detail' }),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(defaultErrorMessage);
});

it('returns the invalid code message with details', async () => {
const response = createMockResponse({
ok: false,
status: 403,
json: () =>
Promise.resolve({
message: 'invalid code provided',
details: 'code ABC123 is not recognized',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(
'The provided activation code is invalid: code ABC123 is not recognized',
);
});

it('returns the suspended message', async () => {
const response = createMockResponse({
ok: false,
status: 403,
json: () =>
Promise.resolve({
message: 'user has been suspended',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(
'Access to the Developer Sandbox has been suspended due to suspicious activity or detected abuse',
);
});

it('returns the denied message', async () => {
const response = createMockResponse({
ok: false,
status: 403,
json: () =>
Promise.resolve({
message: 'user has been denied',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(
'Access to the Developer Sandbox has been denied',
);
});

it('returns the admin not allowed message', async () => {
const response = createMockResponse({
ok: false,
status: 403,
json: () =>
Promise.resolve({
message: 'failed to create usersignup for admin-user',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(
'A CRT admin is not allowed to sign up',
);
});

it('returns the already signed up message', async () => {
const response = createMockResponse({
ok: false,
status: 409,
json: () =>
Promise.resolve({
message: 'there is already an active UserSignup with such a username',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(
'An account is already signed up to Developer Sandbox with your username',
);
});

it('returns the default error message for an unrecognized message', async () => {
const response = createMockResponse({
ok: false,
status: 400,
json: () =>
Promise.resolve({
message: 'some completely unexpected error from the backend',
}),
});

const userSignupError = await UserSignupError.fromResponse(response);

expect(userSignupError.message).toBe(defaultErrorMessage);
});
});
66 changes: 66 additions & 0 deletions plugins/sandbox/src/api/errors/UserSignupError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { CommonResponse } from '../../types';

/**
* Defines the error type that is useful for problems with the user's signup.
*/
export default class UserSignupError extends Error {
constructor(message: string) {
super(message);
}

/**
* Creates the error from the given response, by attempting to read and set
* a user friendly error message.
* @param response the response to process.
* @returns a user friendly error message.
*/
static async fromResponse(response: Response): Promise<UserSignupError> {
const defaultErrorMessage: string =
'Unable to sign you up into Developer Sandbox. Please contact devsandbox@redhat.com';

if (response.status === 500) {
return new UserSignupError(defaultErrorMessage);
}

let body: CommonResponse | undefined;
try {
body = await response.json();
} catch {
return new UserSignupError(defaultErrorMessage);
}

if (!body || !body.message) {
return new UserSignupError(defaultErrorMessage);
}

const { message, details } = body;
switch (true) {
case message.includes('invalid code'): {
const baseErrMessage = 'The provided activation code is invalid';
if (details) {
return new UserSignupError(`${baseErrMessage}: ${details}`);
} else {
return new UserSignupError(baseErrMessage);
}
}
case message.includes('has been suspended'):
return new UserSignupError(
'Access to the Developer Sandbox has been suspended due to suspicious activity or detected abuse',
);
case message.includes('has been denied'):
return new UserSignupError(
'Access to the Developer Sandbox has been denied',
);
case message.includes('failed to create usersignup for'):
return new UserSignupError('A CRT admin is not allowed to sign up');
case message.includes(
'there is already an active UserSignup with such a username',
):
Comment on lines +46 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only out of curiosity - couldn't we simply:

  1. Adjust the messages that are returned from reg-service
  2. And show the message that is included in the error

instead of matching based on the message content?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but the problem I see is that the registration service might return some non-user friendly error messages containing information that the user doesn't really care about. So if we take just the message or details and post it as an alert in the UI, it might contain stuff like "error creating UserSignup resource", or "error retrieving K8s object Y", or whatever.

I personally don't like much either having to parse the error messages, but I think it provides the most user friendly error messages without exposing internals that they do not care or know about.

The other option I thought about is modifying the error messages to include some codes that the UI can interpret, or even better, refactor the errors we return from the back end so that they're always user friendly, which I guess it should be. However I didn't think that the effort would be worth it?

If you think otherwise, though, I'd be very much willing to refactor it

return new UserSignupError(
'An account is already signed up to Developer Sandbox with your username',
);
default:
return new UserSignupError(defaultErrorMessage);
}
}
}
Loading
Loading