Skip to content

Commit

Permalink
feat(NOTIFY-998): add logic around the new get all feature entries en…
Browse files Browse the repository at this point in the history
…dpoint (#4626)

## Explanation

This PR adds support for the `GET /api/v1/userstorage/:feature`
endpoint, both for the SDK and `UserStorageController`.

## References


[NOTIFY-998](https://consensyssoftware.atlassian.net/jira/software/projects/NOTIFY/boards/616?selectedIssue=NOTIFY-998)

**NOTE**: this PR is the base on which #4629 is built upon. You can skip
merging this one if you merge #4629.

## Changelog

### `@metamask/profile-sync-controller`

- **ADDED**: added SDK and controller support for the new `GET
/api/v1/userstorage/:feature` endpoint.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate


[NOTIFY-998]:
https://consensyssoftware.atlassian.net/browse/NOTIFY-998?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
mathieuartu authored Sep 2, 2024
1 parent 25f460d commit b43ae02
Show file tree
Hide file tree
Showing 11 changed files with 504 additions and 43 deletions.
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([
[
'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

0 comments on commit b43ae02

Please sign in to comment.