Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NOTIFY-998): add logic around the new get all feature entries endpoint #4626

Merged
merged 9 commits into from
Sep 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '../authentication/AuthenticationController';
import {
mockEndpointGetUserStorage,
mockEndpointGetUserStorageAllFeatureEntries,
mockEndpointUpsertUserStorage,
} from './__fixtures__/mockServices';
import {
Expand Down Expand Up @@ -120,6 +121,81 @@ describe('user-storage/user-storage-controller - performGetStorage() tests', ()
);
});

describe('user-storage/user-storage-controller - performGetStorageAllFeatureEntries() tests', () => {
const arrangeMocks = () => {
return {
messengerMocks: mockUserStorageMessenger(),
mockAPI: mockEndpointGetUserStorageAllFeatureEntries(),
};
};

it('returns users notification storage', async () => {
const { messengerMocks, mockAPI } = arrangeMocks();
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
});

const result = await controller.performGetStorageAllFeatureEntries(
'notifications',
);
mockAPI.done();
expect(result).toStrictEqual([MOCK_STORAGE_DATA]);
});

it('rejects if UserStorage is not enabled', async () => {
const { messengerMocks } = arrangeMocks();
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
state: {
isProfileSyncingEnabled: false,
isProfileSyncingUpdateLoading: false,
},
});

await expect(
controller.performGetStorageAllFeatureEntries('notifications'),
).rejects.toThrow(expect.any(Error));
});

it.each([
Prithpal-Sooriya marked this conversation as resolved.
Show resolved Hide resolved
[
'fails when no bearer token is found (auth errors)',
(messengerMocks: ReturnType<typeof mockUserStorageMessenger>) =>
messengerMocks.mockAuthGetBearerToken.mockRejectedValue(
new Error('MOCK FAILURE'),
),
],
[
'fails when no session identifier is found (auth errors)',
(messengerMocks: ReturnType<typeof mockUserStorageMessenger>) =>
messengerMocks.mockAuthGetSessionProfile.mockRejectedValue(
new Error('MOCK FAILURE'),
),
],
])(
'rejects on auth failure - %s',
async (
_: string,
arrangeFailureCase: (
messengerMocks: ReturnType<typeof mockUserStorageMessenger>,
) => void,
) => {
const { messengerMocks } = arrangeMocks();
arrangeFailureCase(messengerMocks);
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
});

await expect(
controller.performGetStorageAllFeatureEntries('notifications'),
).rejects.toThrow(expect.any(Error));
},
);
});

describe('user-storage/user-storage-controller - performSetStorage() tests', () => {
const arrangeMocks = (overrides?: { mockAPI?: nock.Scope }) => {
return {
Expand Down Expand Up @@ -202,7 +278,10 @@ describe('user-storage/user-storage-controller - performSetStorage() tests', ()

it('rejects if api call fails', async () => {
const { messengerMocks } = arrangeMocks({
mockAPI: mockEndpointUpsertUserStorage({ status: 500 }),
mockAPI: mockEndpointUpsertUserStorage(
'notifications.notificationSettings',
{ status: 500 },
),
});
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ import type {
AuthenticationControllerPerformSignOut,
} from '../authentication/AuthenticationController';
import { createSHA256Hash } from './encryption';
import type { UserStoragePath } from './schema';
import { getUserStorage, upsertUserStorage } from './services';
import type {
UserStoragePathWithFeatureAndKey,
UserStoragePathWithFeatureOnly,
} from './schema';
import {
getUserStorage,
getUserStorageAllFeatureEntries,
upsertUserStorage,
} from './services';

// TODO: fix external dependencies
export declare type NotificationServicesControllerDisableNotificationServices =
Expand Down Expand Up @@ -76,6 +83,7 @@ type CreateActionsObj<Controller extends keyof UserStorageController> = {
};
type ActionsObj = CreateActionsObj<
| 'performGetStorage'
| 'performGetStorageAllFeatureEntries'
| 'performSetStorage'
| 'getStorageKey'
| 'enableProfileSyncing'
Expand All @@ -90,6 +98,8 @@ export type Actions =
| UserStorageControllerGetStateAction;
export type UserStorageControllerPerformGetStorage =
ActionsObj['performGetStorage'];
export type UserStorageControllerPerformGetStorageAllFeatureEntries =
ActionsObj['performGetStorageAllFeatureEntries'];
export type UserStorageControllerPerformSetStorage =
ActionsObj['performSetStorage'];
export type UserStorageControllerGetStorageKey = ActionsObj['getStorageKey'];
Expand Down Expand Up @@ -234,6 +244,11 @@ export default class UserStorageController extends BaseController<
this.performGetStorage.bind(this),
);

this.messagingSystem.registerActionHandler(
'UserStorageController:performGetStorageAllFeatureEntries',
this.performGetStorageAllFeatureEntries.bind(this),
);

this.messagingSystem.registerActionHandler(
'UserStorageController:performSetStorage',
this.performSetStorage.bind(this),
Expand Down Expand Up @@ -330,7 +345,7 @@ export default class UserStorageController extends BaseController<
* @returns the decrypted string contents found from user storage (or null if not found)
*/
public async performGetStorage(
path: UserStoragePath,
path: UserStoragePathWithFeatureAndKey,
): Promise<string | null> {
this.#assertProfileSyncingEnabled();

Expand All @@ -346,6 +361,30 @@ export default class UserStorageController extends BaseController<
return result;
}

/**
* Allows retrieval of all stored data for a specific feature. Data stored is formatted as an array of strings.
* Developers can extend the entry path through the `schema.ts` file.
*
* @param path - string in the form of `${feature}` that matches schema
* @returns the array of decrypted string contents found from user storage (or null if not found)
*/
public async performGetStorageAllFeatureEntries(
path: UserStoragePathWithFeatureOnly,
): Promise<string[] | null> {
this.#assertProfileSyncingEnabled();

const { bearerToken, storageKey } =
await this.#getStorageKeyAndBearerToken();

const result = await getUserStorageAllFeatureEntries({
path,
bearerToken,
storageKey,
});

return result;
}

/**
* Allows storage of user data. Data stored must be string formatted.
* Developers can extend the entry path and entry name through the `schema.ts` file.
Expand All @@ -355,7 +394,7 @@ export default class UserStorageController extends BaseController<
* @returns nothing. NOTE that an error is thrown if fails to store data.
*/
public async performSetStorage(
path: UserStoragePath,
path: UserStoragePathWithFeatureAndKey,
value: string,
): Promise<void> {
this.#assertProfileSyncingEnabled();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type {
UserStoragePathWithFeatureAndKey,
UserStoragePathWithFeatureOnly,
} from '../schema';
import { createEntryPath } from '../schema';
import type { GetUserStorageResponse } from '../services';
import type {
GetUserStorageAllFeatureEntriesResponse,
GetUserStorageResponse,
} from '../services';
import { USER_STORAGE_ENDPOINT } from '../services';
import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage';

Expand All @@ -9,27 +16,57 @@ type MockResponse = {
response: unknown;
};

export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath(
'notifications.notificationSettings',
MOCK_STORAGE_KEY,
)}`;
export const getMockUserStorageEndpoint = (
path: UserStoragePathWithFeatureAndKey | UserStoragePathWithFeatureOnly,
) => {
if (path.split('.').length === 1) {
return `${USER_STORAGE_ENDPOINT}/${path}`;
}

return `${USER_STORAGE_ENDPOINT}${createEntryPath(
path as UserStoragePathWithFeatureAndKey,
MOCK_STORAGE_KEY,
)}`;
};

const MOCK_GET_USER_STORAGE_RESPONSE = (): GetUserStorageResponse => ({
HashedKey: 'HASHED_KEY',
Data: MOCK_ENCRYPTED_STORAGE_DATA(),
});

export const getMockUserStorageGetResponse = () => {
const MOCK_GET_USER_STORAGE_ALL_FEATURE_ENTRIES_RESPONSE =
(): GetUserStorageAllFeatureEntriesResponse => [
{
HashedKey: 'HASHED_KEY',
Data: MOCK_ENCRYPTED_STORAGE_DATA(),
},
];

export const getMockUserStorageGetResponse = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notificationSettings',
) => {
return {
url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT,
url: getMockUserStorageEndpoint(path),
requestMethod: 'GET',
response: MOCK_GET_USER_STORAGE_RESPONSE(),
} satisfies MockResponse;
};

export const getMockUserStoragePutResponse = () => {
export const getMockUserStorageAllFeatureEntriesResponse = (
path: UserStoragePathWithFeatureOnly = 'notifications',
) => {
return {
url: getMockUserStorageEndpoint(path),
requestMethod: 'GET',
response: MOCK_GET_USER_STORAGE_ALL_FEATURE_ENTRIES_RESPONSE(),
} satisfies MockResponse;
};

export const getMockUserStoragePutResponse = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notificationSettings',
) => {
return {
url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT,
url: getMockUserStorageEndpoint(path),
requestMethod: 'PUT',
response: null,
} satisfies MockResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
import nock from 'nock';

import type {
UserStoragePathWithFeatureAndKey,
UserStoragePathWithFeatureOnly,
} from '../schema';
import {
getMockUserStorageGetResponse,
getMockUserStoragePutResponse,
getMockUserStorageAllFeatureEntriesResponse,
} from './mockResponses';

type MockReply = {
status: nock.StatusCode;
body?: nock.Body;
};

export const mockEndpointGetUserStorage = (mockReply?: MockReply) => {
const mockResponse = getMockUserStorageGetResponse();
export const mockEndpointGetUserStorageAllFeatureEntries = (
path: UserStoragePathWithFeatureOnly = 'notifications',
mockReply?: MockReply,
) => {
const mockResponse = getMockUserStorageAllFeatureEntriesResponse(path);
const reply = mockReply ?? {
status: 200,
body: mockResponse.response,
};

const mockEndpoint = nock(mockResponse.url)
.get('')
.reply(reply.status, reply.body);

return mockEndpoint;
};

export const mockEndpointGetUserStorage = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notificationSettings',
mockReply?: MockReply,
) => {
const mockResponse = getMockUserStorageGetResponse(path);
const reply = mockReply ?? {
status: 200,
body: mockResponse.response,
Expand All @@ -25,9 +50,10 @@ export const mockEndpointGetUserStorage = (mockReply?: MockReply) => {
};

export const mockEndpointUpsertUserStorage = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notificationSettings',
mockReply?: Pick<MockReply, 'status'>,
) => {
const mockResponse = getMockUserStoragePutResponse();
const mockResponse = getMockUserStoragePutResponse(path);
const mockEndpoint = nock(mockResponse.url)
.put('')
.reply(mockReply?.status ?? 204);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,14 @@ describe('user-storage/schema.ts', () => {
key: 'notificationSettings',
});
});

it('should return feature and key from path with arbitrary key', () => {
const path = 'accounts.0x123';
const result = getFeatureAndKeyFromPath(path);
expect(result).toStrictEqual({
feature: 'accounts',
key: '0x123',
});
});
});
});
Loading
Loading