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-1214): add support for DELETE ONE endpoint #4776

Merged
merged 5 commits into from
Oct 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mockEndpointGetUserStorageAllFeatureEntries,
mockEndpointUpsertUserStorage,
mockEndpointDeleteUserStorageAllFeatureEntries,
mockEndpointDeleteUserStorage,
} from './__fixtures__/mockServices';
import {
MOCK_STORAGE_DATA,
Expand Down Expand Up @@ -405,6 +406,98 @@ describe('user-storage/user-storage-controller - performBatchSetStorage() tests'
});
});

describe('user-storage/user-storage-controller - performDeleteStorage() tests', () => {
const arrangeMocks = async (mockResponseStatus?: number) => {
return {
messengerMocks: mockUserStorageMessenger(),
mockAPI: mockEndpointDeleteUserStorage(
'notifications.notification_settings',
mockResponseStatus ? { status: mockResponseStatus } : undefined,
),
};
};

it('deletes a user storage entry', async () => {
const { messengerMocks, mockAPI } = await arrangeMocks();
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
});

await controller.performDeleteStorage(
'notifications.notification_settings',
);
mockAPI.done();

expect(mockAPI.isDone()).toBe(true);
});

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

await expect(
controller.performDeleteStorage('notifications.notification_settings'),
).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 } = await arrangeMocks();
arrangeFailureCase(messengerMocks);
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
});

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

it('rejects if api call fails', async () => {
const { messengerMocks, mockAPI } = await arrangeMocks(500);
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
getMetaMetricsState: () => true,
});

await expect(
controller.performDeleteStorage('notifications.notification_settings'),
).rejects.toThrow(expect.any(Error));
mockAPI.done();
});
});

describe('user-storage/user-storage-controller - performDeleteStorageAllFeatureEntries() tests', () => {
const arrangeMocks = async (mockResponseStatus?: number) => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { startNetworkSyncing } from './network-syncing/controller-integration';
import {
batchUpsertUserStorage,
deleteUserStorage,
deleteUserStorageAllFeatureEntries,
getUserStorage,
getUserStorageAllFeatureEntries,
Expand Down Expand Up @@ -675,6 +676,27 @@ export default class UserStorageController extends BaseController<
});
}

/**
* Allows deletion of user data. Developers can extend the entry path and entry name through the `schema.ts` file.
*
* @param path - string in the form of `${feature}.${key}` that matches schema
* @returns nothing. NOTE that an error is thrown if fails to delete data.
*/
public async performDeleteStorage(
path: UserStoragePathWithFeatureAndKey,
): Promise<void> {
this.#assertProfileSyncingEnabled();

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

await deleteUserStorage({
path,
bearerToken,
storageKey,
});
}

/**
* Allows deletion of all user data entries for a specific feature.
* Developers can extend the entry path through the `schema.ts` file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ export const getMockUserStorageBatchPutResponse = (
} satisfies MockResponse;
};

export const deleteMockUserStorageResponse = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notification_settings',
) => {
return {
url: getMockUserStorageEndpoint(path),
requestMethod: 'DELETE',
response: null,
} satisfies MockResponse;
};

export const deleteMockUserStorageAllFeatureEntriesResponse = (
path: UserStoragePathWithFeatureOnly = 'notifications',
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getMockUserStorageAllFeatureEntriesResponse,
getMockUserStorageBatchPutResponse,
deleteMockUserStorageAllFeatureEntriesResponse,
deleteMockUserStorageResponse,
} from './mockResponses';

type MockReply = {
Expand Down Expand Up @@ -79,6 +80,20 @@ export const mockEndpointBatchUpsertUserStorage = (
return mockEndpoint;
};

export const mockEndpointDeleteUserStorage = (
path: UserStoragePathWithFeatureAndKey = 'notifications.notification_settings',
mockReply?: MockReply,
) => {
const mockResponse = deleteMockUserStorageResponse(path);
const reply = mockReply ?? {
status: 200,
};

const mockEndpoint = nock(mockResponse.url).delete('').reply(reply.status);

return mockEndpoint;
};

export const mockEndpointDeleteUserStorageAllFeatureEntries = (
path: UserStoragePathWithFeatureOnly = 'notifications',
mockReply?: MockReply,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mockEndpointGetUserStorageAllFeatureEntries,
mockEndpointBatchUpsertUserStorage,
mockEndpointDeleteUserStorageAllFeatureEntries,
mockEndpointDeleteUserStorage,
} from './__fixtures__/mockServices';
import {
MOCK_STORAGE_DATA,
Expand All @@ -19,6 +20,7 @@ import {
getUserStorageAllFeatureEntries,
upsertUserStorage,
deleteUserStorageAllFeatureEntries,
deleteUserStorage,
} from './services';

describe('user-storage/services.ts - getUserStorage() tests', () => {
Expand Down Expand Up @@ -244,6 +246,60 @@ describe('user-storage/services.ts - batchUpsertUserStorage() tests', () => {
});
});

describe('user-storage/services.ts - deleteUserStorage() tests', () => {
const actCallDeleteUserStorage = async () => {
return await deleteUserStorage({
path: 'notifications.notification_settings',
bearerToken: 'MOCK_BEARER_TOKEN',
storageKey: MOCK_STORAGE_KEY,
});
};

it('invokes delete endpoint with no errors', async () => {
const mockDeleteUserStorage = mockEndpointDeleteUserStorage(
'notifications.notification_settings',
);

await actCallDeleteUserStorage();

expect(mockDeleteUserStorage.isDone()).toBe(true);
});

it('throws error if unable to delete user storage', async () => {
const mockDeleteUserStorage = mockEndpointDeleteUserStorage(
'notifications.notification_settings',
{ status: 500 },
);

await expect(actCallDeleteUserStorage()).rejects.toThrow(expect.any(Error));
mockDeleteUserStorage.done();
});

it('throws error if feature not found', async () => {
const mockDeleteUserStorage = mockEndpointDeleteUserStorage(
'notifications.notification_settings',
{ status: 404 },
);

await expect(actCallDeleteUserStorage()).rejects.toThrow(
'user-storage - feature/entry not found',
);
mockDeleteUserStorage.done();
});

it('throws error if unable to get user storage', async () => {
const mockDeleteUserStorage = mockEndpointDeleteUserStorage(
'notifications.notification_settings',
{ status: 400 },
);

await expect(actCallDeleteUserStorage()).rejects.toThrow(
'user-storage - unable to delete data',
);
mockDeleteUserStorage.done();
});
});

describe('user-storage/services.ts - deleteUserStorageAllFeatureEntries() tests', () => {
const actCallDeleteUserStorageAllFeatureEntries = async () => {
return await deleteUserStorageAllFeatureEntries({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,35 @@ export async function batchUpsertUserStorage(
}
}

/**
* User Storage Service - Delete Storage Entry.
*
* @param opts - User Storage Options
*/
export async function deleteUserStorage(
opts: UserStorageOptions,
): Promise<void> {
const { bearerToken, path, storageKey } = opts;
const encryptedPath = createEntryPath(path, storageKey);
const url = new URL(`${USER_STORAGE_ENDPOINT}/${encryptedPath}`);

const userStorageResponse = await fetch(url.toString(), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${bearerToken}`,
},
});

if (userStorageResponse.status === 404) {
throw new Error('user-storage - feature/entry not found');
}
Prithpal-Sooriya marked this conversation as resolved.
Show resolved Hide resolved

if (!userStorageResponse.ok) {
throw new Error('user-storage - unable to delete data');
}
}

/**
* User Storage Service - Delete all storage entries for a specific feature.
*
Expand All @@ -259,12 +288,11 @@ export async function deleteUserStorageAllFeatureEntries(
},
});

// Acceptable error - since indicates feature does not exist.
if (userStorageResponse.status === 404) {
throw new Error('user-storage - feature not found');
}

if (userStorageResponse.status !== 200 || !userStorageResponse.ok) {
if (!userStorageResponse.ok) {
throw new Error('user-storage - unable to delete data');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ export const handleMockUserStoragePut = (
return mockEndpoint;
};

export const handleMockUserStorageDelete = async (mockReply?: MockReply) => {
const reply = mockReply ?? { status: 204 };
const mockEndpoint = nock(MOCK_STORAGE_URL)
.persist()
.delete(/.*/u)
.reply(reply.status);

return mockEndpoint;
};

export const handleMockUserStorageDeleteAllFeatureEntries = async (
mockReply?: MockReply,
) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/profile-sync-controller/src/sdk/user-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
handleMockUserStoragePut,
handleMockUserStorageGetAllFeatureEntries,
handleMockUserStorageDeleteAllFeatureEntries,
handleMockUserStorageDelete,
} from './__fixtures__/mock-userstorage';
import { arrangeAuth, typedMockFn } from './__fixtures__/test-utils';
import type { IBaseAuth } from './authentication-jwt-bearer/types';
Expand Down Expand Up @@ -133,6 +134,33 @@ describe('User Storage', () => {
expect(mockPut.isDone()).toBe(true);
});

it('user storage: delete one feature entry', async () => {
const { auth } = arrangeAuth('SRP', MOCK_SRP);
const { userStorage } = arrangeUserStorage(auth);

const mockDelete = await handleMockUserStorageDelete();

await userStorage.deleteItem('notifications.notification_settings');
expect(mockDelete.isDone()).toBe(true);
});

it('user storage: failed to delete one feature entry', async () => {
const { auth } = arrangeAuth('SRP', MOCK_SRP);
const { userStorage } = arrangeUserStorage(auth);

await handleMockUserStorageDelete({
status: 401,
body: {
message: 'failed to delete storage entry',
error: 'generic-error',
},
});

await expect(
userStorage.deleteItem('notifications.notification_settings'),
).rejects.toThrow(UserStorageError);
});

it('user storage: delete all feature entries', async () => {
const { auth } = arrangeAuth('SRP', MOCK_SRP);
const { userStorage } = arrangeUserStorage(auth);
Expand Down
Loading
Loading