Skip to content

Commit

Permalink
Merge branch 'main' into 231208-typescript-module-resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
MajorLift committed Jul 19, 2024
2 parents 59fe394 + ed7a9db commit c273024
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -284,28 +284,40 @@ export default class NotificationServicesController extends BaseController<
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:enablePushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:enablePushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to enable push notifications', e);
}
},
disablePushNotifications: async (UUIDs: string[]) => {
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:disablePushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:disablePushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to disable push notifications', e);
}
},
updatePushNotifications: async (UUIDs: string[]) => {
if (!this.#isPushIntegrated) {
return;
}
await this.messagingSystem.call(
'NotificationServicesPushController:updateTriggerPushNotifications',
UUIDs,
);
try {
await this.messagingSystem.call(
'NotificationServicesPushController:updateTriggerPushNotifications',
UUIDs,
);
} catch (e) {
log.error('Silently failed to update push notifications', e);
}
},
subscribe: () => {
if (!this.#isPushIntegrated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export function createMockNotificationEthSent(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -48,7 +49,8 @@ export function createMockNotificationEthReceived(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -82,7 +84,8 @@ export function createMockNotificationERC20Sent(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down Expand Up @@ -122,7 +125,8 @@ export function createMockNotificationERC20Received(): OnChainRawNotification {
chain_id: 1,
block_number: 17485840,
block_timestamp: '2022-03-01T00:00:00Z',
tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C',
tx_hash:
'0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6',
unread: true,
created_at: '2022-03-01T00:00:00Z',
address: '0x881D40237659C251811CEC9c364ef91dC08D300C',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { mockFetchFeatureAnnouncementNotifications } from '../__fixtures__/mockS
import { TRIGGER_TYPES } from '../constants/notification-schema';
import { getFeatureAnnouncementNotifications } from './feature-announcements';

// Mocked type for testing, allows overwriting TS to test erroneous values
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MockedType = any;

jest.mock('@contentful/rich-text-html-renderer', () => ({
documentToHtmlString: jest
.fn()
Expand All @@ -20,6 +24,27 @@ describe('Feature Announcement Notifications', () => {
jest.clearAllMocks();
});

it('should return an empty array if invalid environment provided', async () => {
mockFetchFeatureAnnouncementNotifications();

const assertEnvEmpty = async (
override: Partial<typeof featureAnnouncementsEnv>,
) => {
const result = await getFeatureAnnouncementNotifications({
...featureAnnouncementsEnv,
...override,
});
expect(result).toHaveLength(0);
};

await assertEnvEmpty({ accessToken: null as MockedType });
await assertEnvEmpty({ platform: null as MockedType });
await assertEnvEmpty({ spaceId: null as MockedType });
await assertEnvEmpty({ accessToken: '' });
await assertEnvEmpty({ platform: '' });
await assertEnvEmpty({ spaceId: '' });
});

it('should return an empty array if fetch fails', async () => {
const mockEndpoint = mockFetchFeatureAnnouncementNotifications({
status: 500,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import type { Entry, Asset } from 'contentful';
import log from 'loglevel';
import type { Entry, Asset, EntryCollection } from 'contentful';

import { TRIGGER_TYPES } from '../constants/notification-schema';
import { processFeatureAnnouncement } from '../processors/process-feature-announcement';
Expand Down Expand Up @@ -37,53 +36,30 @@ export type ContentfulResult = {
items?: TypeFeatureAnnouncement[];
};

const fetchFromContentful = async (
url: string,
retries = 3,
retryDelay = 1000,
): Promise<ContentfulResult | null> => {
let lastError: Error | null = null;

for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Fetch failed with status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
lastError = error;
}
if (i < retries - 1) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}

log.error(
`Error fetching from Contentful after ${retries} retries:`,
lastError,
);
return null;
};
const getFeatureAnnouncementUrl = (env: Env) =>
FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId)
.replace(DEFAULT_ACCESS_TOKEN, env.accessToken)
.replace(DEFAULT_CLIENT_ID, env.platform);

const fetchFeatureAnnouncementNotifications = async (
env: Env,
): Promise<FeatureAnnouncementRawNotification[]> => {
const url = FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId)
.replace(DEFAULT_ACCESS_TOKEN, env.accessToken)
.replace(DEFAULT_CLIENT_ID, env.platform);
const data = await fetchFromContentful(url);
const url = getFeatureAnnouncementUrl(env);

const data = await fetch(url)
.then((r) => r.json())
.catch(() => null);

if (!data) {
return [];
}

const findIncludedItem = (sysId: string) => {
const typedData: EntryCollection<ImageFields | TypeExtensionLinkFields> =
data;
const item =
data?.includes?.Entry?.find((i: Entry) => i?.sys?.id === sysId) ||
data?.includes?.Asset?.find((i: Asset) => i?.sys?.id === sysId);
typedData?.includes?.Entry?.find((i: Entry) => i?.sys?.id === sysId) ||
typedData?.includes?.Asset?.find((i: Asset) => i?.sys?.id === sysId);
return item ? item?.fields : null;
};

Expand All @@ -94,6 +70,7 @@ const fetchFeatureAnnouncementNotifications = async (
const imageFields = fields.image
? (findIncludedItem(fields.image.sys.id) as ImageFields['fields'])
: undefined;

const extensionLinkFields = fields.extensionLink
? (findIncludedItem(
fields.extensionLink.sys.id,
Expand Down Expand Up @@ -135,10 +112,14 @@ const fetchFeatureAnnouncementNotifications = async (
export async function getFeatureAnnouncementNotifications(
env: Env,
): Promise<INotification[]> {
const rawNotifications = await fetchFeatureAnnouncementNotifications(env);
const notifications = rawNotifications.map((notification) =>
processFeatureAnnouncement(notification),
);
if (env?.accessToken && env?.spaceId && env?.platform) {
const rawNotifications = await fetchFeatureAnnouncementNotifications(env);
const notifications = rawNotifications.map((notification) =>
processFeatureAnnouncement(notification),
);

return notifications;
}

return notifications;
return [];
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import log from 'loglevel';
import { v4 as uuidv4 } from 'uuid';

import {
Expand Down Expand Up @@ -425,65 +424,20 @@ export function toggleUserStorageTriggerStatus(
return userStorage;
}

/**
* Attempts to fetch a resource from the network, retrying the request up to a specified number of times
* in case of failure, with a delay between attempts.
*
* @param url - The resource URL.
* @param options - The options for the fetch request.
* @param retries - Maximum number of retry attempts. Defaults to 3.
* @param retryDelay - Delay between retry attempts in milliseconds. Defaults to 1000.
* @returns A Promise resolving to the Response object.
* @throws Will throw an error if the request fails after the specified number of retries.
*/
async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3,
retryDelay = 1000,
): Promise<Response> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Fetch failed with status: ${response.status}`);
}
return response;
} catch (error) {
log.error(`Attempt ${attempt} failed for fetch:`, error);
if (attempt < retries) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {
throw new Error(
`Fetching failed after ${retries} retries. Last error: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
}
}

throw new Error('Unexpected error in fetchWithRetry');
}

/**
* Performs an API call with automatic retries on failure.
*
* @param bearerToken - The JSON Web Token for authorization.
* @param endpoint - The URL of the API endpoint to call.
* @param method - The HTTP method ('POST' or 'DELETE').
* @param body - The body of the request. It should be an object that can be serialized to JSON.
* @param retries - The number of retry attempts in case of failure (default is 3).
* @param retryDelay - The delay between retries in milliseconds (default is 1000).
* @returns A Promise that resolves to the response of the fetch request.
*/
export async function makeApiCall<Body>(
bearerToken: string,
endpoint: string,
method: 'POST' | 'DELETE',
body: Body,
retries = 3,
retryDelay = 1000,
): Promise<Response> {
const options: RequestInit = {
method,
Expand All @@ -494,5 +448,5 @@ export async function makeApiCall<Body>(
body: JSON.stringify(body),
};

return fetchWithRetry(endpoint, options, retries, retryDelay);
return await fetch(endpoint, options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ describe('getPermissions RPC method', () => {

const engine = new JsonRpcEngine();
engine.push(
async (
(
req: JsonRpcRequest<[]>,
res: PendingJsonRpcResponse<PermissionConstraint[]>,
next,
end,
) => {
await implementation(req, res, next, end, {
// We intentionally do not await this promise; JsonRpcEngine won't await
// middleware anyway.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
implementation(req, res, next, end, {
getPermissionsForOrigin: mockGetPermissionsForOrigin,
});
},
Expand All @@ -44,13 +47,16 @@ describe('getPermissions RPC method', () => {

const engine = new JsonRpcEngine();
engine.push(
async (
(
req: JsonRpcRequest<[]>,
res: PendingJsonRpcResponse<PermissionConstraint[]>,
next,
end,
) => {
await implementation(req, res, next, end, {
// We intentionally do not await this promise; JsonRpcEngine won't await
// middleware anyway.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
implementation(req, res, next, end, {
getPermissionsForOrigin: mockGetPermissionsForOrigin,
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ describe('requestPermissions RPC method', () => {

const engine = new JsonRpcEngine();
engine.push<[RequestedPermissions], PermissionConstraint[]>(
async (req, res, next, end) => {
await implementation(req, res, next, end, {
(req, res, next, end) => {
// We intentionally do not await this promise; JsonRpcEngine won't await
// middleware anyway.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
implementation(req, res, next, end, {
requestPermissionsForOrigin: mockRequestPermissionsForOrigin,
});
},
Expand Down Expand Up @@ -102,8 +105,11 @@ describe('requestPermissions RPC method', () => {

const engine = new JsonRpcEngine();
engine.push<[RequestedPermissions], PermissionConstraint[]>(
async (req, res, next, end) => {
await implementation(req, res, next, end, {
(req, res, next, end) => {
// We intentionally do not await this promise; JsonRpcEngine won't await
// middleware anyway.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
implementation(req, res, next, end, {
requestPermissionsForOrigin: mockRequestPermissionsForOrigin,
});
},
Expand Down
Loading

0 comments on commit c273024

Please sign in to comment.