diff --git a/app/scripts/controllers/authentication/authentication-controller.test.ts b/app/scripts/controllers/authentication/authentication-controller.test.ts index 75ab54e8bedd..1a86cee38c82 100644 --- a/app/scripts/controllers/authentication/authentication-controller.test.ts +++ b/app/scripts/controllers/authentication/authentication-controller.test.ts @@ -4,12 +4,11 @@ import AuthenticationController, { AuthenticationControllerState, } from './authentication-controller'; import { - MOCK_ACCESS_TOKEN, - MOCK_LOGIN_RESPONSE, mockEndpointAccessToken, mockEndpointGetNonce, mockEndpointLogin, } from './mocks/mockServices'; +import { MOCK_ACCESS_TOKEN, MOCK_LOGIN_RESPONSE } from './mocks/mockResponses'; const mockSignedInState = (): AuthenticationControllerState => ({ isSignedIn: true, diff --git a/app/scripts/controllers/authentication/mocks/mockResponses.ts b/app/scripts/controllers/authentication/mocks/mockResponses.ts new file mode 100644 index 000000000000..4882bd81d812 --- /dev/null +++ b/app/scripts/controllers/authentication/mocks/mockResponses.ts @@ -0,0 +1,60 @@ +import { + AUTH_LOGIN_ENDPOINT, + AUTH_NONCE_ENDPOINT, + LoginResponse, + NonceResponse, + OAuthTokenResponse, + OIDC_TOKENS_ENDPOINT, +} from '../services'; + +type MockResponse = { + url: string; + requestMethod: 'GET' | 'POST' | 'PUT'; + response: unknown; +}; + +export const MOCK_NONCE = '4cbfqzoQpcNxVImGv'; +export const MOCK_NONCE_RESPONSE: NonceResponse = { + nonce: MOCK_NONCE, +}; + +export function getMockAuthNonceResponse() { + return { + url: AUTH_NONCE_ENDPOINT, + requestMethod: 'GET', + response: MOCK_NONCE_RESPONSE, + } satisfies MockResponse; +} + +export const MOCK_JWT = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; +export const MOCK_LOGIN_RESPONSE: LoginResponse = { + token: MOCK_JWT, + expires_in: new Date().toString(), + profile: { + identifier_id: 'MOCK_IDENTIFIER', + profile_id: 'MOCK_PROFILE_ID', + }, +}; + +export function getMockAuthLoginResponse() { + return { + url: AUTH_LOGIN_ENDPOINT, + requestMethod: 'POST', + response: MOCK_LOGIN_RESPONSE, + } satisfies MockResponse; +} + +export const MOCK_ACCESS_TOKEN = `MOCK_ACCESS_TOKEN-${MOCK_JWT}`; +export const MOCK_OATH_TOKEN_RESPONSE: OAuthTokenResponse = { + access_token: MOCK_ACCESS_TOKEN, + expires_in: new Date().getTime(), +}; + +export function getMockAuthAccessTokenResponse() { + return { + url: OIDC_TOKENS_ENDPOINT, + requestMethod: 'POST', + response: MOCK_OATH_TOKEN_RESPONSE, + } satisfies MockResponse; +} diff --git a/app/scripts/controllers/authentication/mocks/mockServices.ts b/app/scripts/controllers/authentication/mocks/mockServices.ts index 41a6c69c5b4a..69d3cf56c5d5 100644 --- a/app/scripts/controllers/authentication/mocks/mockServices.ts +++ b/app/scripts/controllers/authentication/mocks/mockServices.ts @@ -1,25 +1,19 @@ import nock from 'nock'; import { - AUTH_LOGIN_ENDPOINT, - AUTH_NONCE_ENDPOINT, - LoginResponse, - NonceResponse, - OAuthTokenResponse, - OIDC_TOKENS_ENDPOINT, -} from '../services'; + getMockAuthAccessTokenResponse, + getMockAuthLoginResponse, + getMockAuthNonceResponse, +} from './mockResponses'; type MockReply = { status: nock.StatusCode; body?: nock.Body; }; -export const MOCK_NONCE = '4cbfqzoQpcNxVImGv'; -const MOCK_NONCE_RESPONSE: NonceResponse = { - nonce: MOCK_NONCE, -}; export function mockEndpointGetNonce(mockReply?: MockReply) { - const reply = mockReply ?? { status: 200, body: MOCK_NONCE_RESPONSE }; - const mockNonceEndpoint = nock(AUTH_NONCE_ENDPOINT) + const mockResponse = getMockAuthNonceResponse(); + const reply = mockReply ?? { status: 200, body: mockResponse.response }; + const mockNonceEndpoint = nock(mockResponse.url) .get('') .query(true) .reply(reply.status, reply.body); @@ -27,33 +21,20 @@ export function mockEndpointGetNonce(mockReply?: MockReply) { return mockNonceEndpoint; } -export const MOCK_JWT = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; -export const MOCK_LOGIN_RESPONSE: LoginResponse = { - token: MOCK_JWT, - expires_in: new Date().toString(), - profile: { - identifier_id: 'MOCK_IDENTIFIER', - profile_id: 'MOCK_PROFILE_ID', - }, -}; export function mockEndpointLogin(mockReply?: MockReply) { - const reply = mockReply ?? { status: 200, body: MOCK_LOGIN_RESPONSE }; - const mockLoginEndpoint = nock(AUTH_LOGIN_ENDPOINT) + const mockResponse = getMockAuthLoginResponse(); + const reply = mockReply ?? { status: 200, body: mockResponse.response }; + const mockLoginEndpoint = nock(mockResponse.url) .post('') .reply(reply.status, reply.body); return mockLoginEndpoint; } -export const MOCK_ACCESS_TOKEN = `MOCK_ACCESS_TOKEN-${MOCK_JWT}`; -const MOCK_OATH_TOKEN_RESPONSE: OAuthTokenResponse = { - access_token: MOCK_ACCESS_TOKEN, - expires_in: new Date().getTime(), -}; export function mockEndpointAccessToken(mockReply?: MockReply) { - const reply = mockReply ?? { status: 200, body: MOCK_OATH_TOKEN_RESPONSE }; - const mockOidcTokensEndpoint = nock(OIDC_TOKENS_ENDPOINT) + const mockResponse = getMockAuthAccessTokenResponse(); + const reply = mockReply ?? { status: 200, body: mockResponse.response }; + const mockOidcTokensEndpoint = nock(mockResponse.url) .post('') .reply(reply.status, reply.body); diff --git a/app/scripts/controllers/authentication/services.test.ts b/app/scripts/controllers/authentication/services.test.ts index 00074d1369e8..759b3c535936 100644 --- a/app/scripts/controllers/authentication/services.test.ts +++ b/app/scripts/controllers/authentication/services.test.ts @@ -1,7 +1,5 @@ +import { MOCK_ACCESS_TOKEN, MOCK_JWT, MOCK_NONCE } from './mocks/mockResponses'; import { - MOCK_ACCESS_TOKEN, - MOCK_JWT, - MOCK_NONCE, mockEndpointAccessToken, mockEndpointGetNonce, mockEndpointLogin, diff --git a/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts b/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts index 6f172ee845b9..d93d48536c13 100644 --- a/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts +++ b/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts @@ -10,7 +10,7 @@ import { AuthenticationControllerGetBearerToken, AuthenticationControllerIsSignedIn, } from '../authentication/authentication-controller'; -import { MOCK_ACCESS_TOKEN } from '../authentication/mocks/mockServices'; +import { MOCK_ACCESS_TOKEN } from '../authentication/mocks/mockResponses'; import { UserStorageControllerGetStorageKey, UserStorageControllerPerformGetStorage, @@ -31,7 +31,6 @@ import { import { createMockFeatureAnnouncementAPIResult, createMockFeatureAnnouncementRaw, - mockFetchFeatureAnnouncementNotifications, } from './mocks/mock-feature-announcements'; import { MOCK_USER_STORAGE_ACCOUNT, @@ -39,11 +38,12 @@ import { createMockUserStorageWithTriggers, } from './mocks/mock-notification-user-storage'; import { + mockFetchFeatureAnnouncementNotifications, mockBatchCreateTriggers, mockBatchDeleteTriggers, mockListNotifications, mockMarkNotificationsAsRead, -} from './mocks/mock-onchain-notifications'; +} from './mocks/mockServices'; import { createMockNotificationEthSent } from './mocks/mock-raw-notifications'; import { processNotification } from './processors/process-notifications'; import * as OnChainNotifications from './services/onchain-notifications'; diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts index 7410b78ee3be..bf3a3fde2242 100644 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts @@ -1,28 +1,7 @@ -import nock from 'nock'; -import { - ContentfulResult, - FEATURE_ANNOUNCEMENT_URL, -} from '../services/feature-announcements'; +import { ContentfulResult } from '../services/feature-announcements'; import { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; import { TRIGGER_TYPES } from '../constants/notification-schema'; -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockFetchFeatureAnnouncementNotifications( - mockReply?: MockReply, -) { - const reply = mockReply ?? { status: 200, body: { items: [] } }; - const mockEndpoint = nock(FEATURE_ANNOUNCEMENT_URL) - .get('') - .query(true) - .reply(reply.status, reply.body); - - return mockEndpoint; -} - export function createMockFeatureAnnouncementAPIResult(): ContentfulResult { return { sys: { diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts deleted file mode 100644 index cd8f9e5979c9..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts +++ /dev/null @@ -1,58 +0,0 @@ -import nock from 'nock'; -import { - NOTIFICATION_API_LIST_ENDPOINT, - NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, - TRIGGER_API_BATCH_ENDPOINT, -} from '../services/onchain-notifications'; -import { createMockRawOnChainNotifications } from './mock-raw-notifications'; - -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockBatchCreateTriggers(mockReply?: MockReply) { - const reply = mockReply ?? { status: 204 }; - - const mockEndpoint = nock(TRIGGER_API_BATCH_ENDPOINT) - .post('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockBatchDeleteTriggers(mockReply?: MockReply) { - const reply = mockReply ?? { status: 204 }; - - const mockEndpoint = nock(TRIGGER_API_BATCH_ENDPOINT) - .delete('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockListNotifications(mockReply?: MockReply) { - const reply = mockReply ?? { - status: 200, - body: createMockRawOnChainNotifications(), - }; - - const mockEndpoint = nock(NOTIFICATION_API_LIST_ENDPOINT) - .post('') - .query(true) - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockMarkNotificationsAsRead(mockReply?: MockReply) { - const reply = mockReply ?? { - status: 200, - }; - - const mockEndpoint = nock(NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT) - .post('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts index 2c9f7749f501..f78b4a53a21f 100644 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts @@ -144,6 +144,8 @@ export function createMockNotificationERC721Sent() { data: { to: '0xf47f628fe3bd2595e9ab384bfffc3859b448e451', nft: { + // This is not a hex color + // eslint-disable-next-line @metamask/design-tokens/color-no-hex name: 'Captainz #8680', image: 'https://i.seadn.io/s/raw/files/ae0fc06714ff7fb40217340d8a242c0e.gif?w=500&auto=format', @@ -184,6 +186,8 @@ export function createMockNotificationERC721Received() { data: { to: '0xba7f3daa8adfdad686574406ab9bd5d2f0a49d2e', nft: { + // This is not a hex color + // eslint-disable-next-line @metamask/design-tokens/color-no-hex name: 'The Plague #2722', image: 'https://i.seadn.io/s/raw/files/a96f90ec8ebf55a2300c66a0c46d6a16.png?w=500&auto=format', diff --git a/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts b/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts new file mode 100644 index 000000000000..940646c10640 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts @@ -0,0 +1,59 @@ +import { FEATURE_ANNOUNCEMENT_API } from '../services/feature-announcements'; +import { + NOTIFICATION_API_LIST_ENDPOINT, + NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, + TRIGGER_API_BATCH_ENDPOINT, +} from '../services/onchain-notifications'; +import { createMockFeatureAnnouncementAPIResult } from './mock-feature-announcements'; +import { createMockRawOnChainNotifications } from './mock-raw-notifications'; + +type MockResponse = { + url: string; + requestMethod: 'GET' | 'POST' | 'PUT' | 'DELETE'; + response: unknown; +}; + +export const CONTENTFUL_RESPONSE = createMockFeatureAnnouncementAPIResult(); + +export function getMockFeatureAnnouncementResponse() { + return { + url: FEATURE_ANNOUNCEMENT_API, + requestMethod: 'GET', + response: CONTENTFUL_RESPONSE, + } satisfies MockResponse; +} + +export function getMockBatchCreateTriggersResponse() { + return { + url: TRIGGER_API_BATCH_ENDPOINT, + requestMethod: 'POST', + response: null, + } satisfies MockResponse; +} + +export function getMockBatchDeleteTriggersResponse() { + return { + url: TRIGGER_API_BATCH_ENDPOINT, + requestMethod: 'DELETE', + response: null, + } satisfies MockResponse; +} + +export const MOCK_RAW_ON_CHAIN_NOTIFICATIONS = + createMockRawOnChainNotifications(); + +export function getMockListNotificationsResponse() { + return { + url: NOTIFICATION_API_LIST_ENDPOINT, + requestMethod: 'POST', + response: MOCK_RAW_ON_CHAIN_NOTIFICATIONS, + } satisfies MockResponse; +} + +export function getMockMarkNotificationsAsReadResponse() { + return { + url: NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, + requestMethod: 'POST', + response: null, + } satisfies MockResponse; +} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts b/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts new file mode 100644 index 000000000000..84de50ec8555 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts @@ -0,0 +1,71 @@ +import nock from 'nock'; +import { + getMockBatchCreateTriggersResponse, + getMockBatchDeleteTriggersResponse, + getMockFeatureAnnouncementResponse, + getMockListNotificationsResponse, + getMockMarkNotificationsAsReadResponse, +} from './mockResponses'; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +export function mockFetchFeatureAnnouncementNotifications( + mockReply?: MockReply, +) { + const mockResponse = getMockFeatureAnnouncementResponse(); + const reply = mockReply ?? { status: 200, body: mockResponse.response }; + const mockEndpoint = nock(mockResponse.url) + .get('') + .query(true) + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockBatchCreateTriggers(mockReply?: MockReply) { + const mockResponse = getMockBatchCreateTriggersResponse(); + const reply = mockReply ?? { status: 204 }; + + const mockEndpoint = nock(mockResponse.url) + .post('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockBatchDeleteTriggers(mockReply?: MockReply) { + const mockResponse = getMockBatchDeleteTriggersResponse(); + const reply = mockReply ?? { status: 204 }; + + const mockEndpoint = nock(mockResponse.url) + .delete('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockListNotifications(mockReply?: MockReply) { + const mockResponse = getMockListNotificationsResponse(); + const reply = mockReply ?? { status: 200, body: mockResponse.response }; + + const mockEndpoint = nock(mockResponse.url) + .post('') + .query(true) + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockMarkNotificationsAsRead(mockReply?: MockReply) { + const mockResponse = getMockMarkNotificationsAsReadResponse(); + const reply = mockReply ?? { status: 200 }; + + const mockEndpoint = nock(mockResponse.url) + .post('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} diff --git a/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts b/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts index ea000423db0c..d8a178d065c5 100644 --- a/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts +++ b/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts @@ -1,8 +1,6 @@ import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { - createMockFeatureAnnouncementAPIResult, - mockFetchFeatureAnnouncementNotifications, -} from '../mocks/mock-feature-announcements'; +import { createMockFeatureAnnouncementAPIResult } from '../mocks/mock-feature-announcements'; +import { mockFetchFeatureAnnouncementNotifications } from '../mocks/mockServices'; import { getFeatureAnnouncementNotifications } from './feature-announcements'; jest.mock('@contentful/rich-text-html-renderer', () => ({ diff --git a/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts b/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts index 3d794c31bf4d..548646138e85 100644 --- a/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts +++ b/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts @@ -12,9 +12,10 @@ import { ImageFields } from '../types/feature-announcement/type-feature-announce import { TypeLinkFields } from '../types/feature-announcement/type-link'; import { TypeActionFields } from '../types/feature-announcement/type-action'; -const spaceId = process.env.CONTENTFUL_ACCESS_SPACE_ID || ''; +const spaceId = process.env.CONTENTFUL_ACCESS_SPACE_ID || ':space_id'; const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN || ''; -export const FEATURE_ANNOUNCEMENT_URL = `https://cdn.contentful.com/spaces/${spaceId}/environments/master/entries?access_token=${accessToken}&content_type=productAnnouncement&include=10&fields.clients=extension`; +export const FEATURE_ANNOUNCEMENT_API = `https://cdn.contentful.com/spaces/${spaceId}/environments/master/entries`; +export const FEATURE_ANNOUNCEMENT_URL = `${FEATURE_ANNOUNCEMENT_API}?access_token=${accessToken}&content_type=productAnnouncement&include=10&fields.clients=extension`; export type ContentfulResult = { includes?: { diff --git a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts index 3ee5b311a9f7..eb205c7e52aa 100644 --- a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts +++ b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts @@ -4,7 +4,7 @@ import { mockBatchDeleteTriggers, mockListNotifications, mockMarkNotificationsAsRead, -} from '../mocks/mock-onchain-notifications'; +} from '../mocks/mockServices'; import { MOCK_USER_STORAGE_ACCOUNT, MOCK_USER_STORAGE_CHAIN, diff --git a/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts b/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts new file mode 100644 index 000000000000..3ff965b3444f --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts @@ -0,0 +1,60 @@ +import { REGISTRATION_TOKENS_ENDPOINT } from '../services/endpoints'; +import { LinksResult } from '../services/services'; + +type MockResponse = { + url: string | RegExp; + requestMethod: 'GET' | 'POST' | 'PUT'; + response: unknown; +}; + +export const MOCK_REG_TOKEN = 'REG_TOKEN'; +export const MOCK_LINKS_RESPONSE: LinksResult = { + trigger_ids: ['1', '2', '3'], + registration_tokens: [ + { token: 'reg_token_1', platform: 'portfolio' }, + { token: 'reg_token_2', platform: 'extension' }, + ], +}; + +export function getMockRetrievePushNotificationLinksResponse() { + return { + url: REGISTRATION_TOKENS_ENDPOINT, + requestMethod: 'GET', + response: MOCK_LINKS_RESPONSE, + } satisfies MockResponse; +} + +export function getMockUpdatePushNotificationLinksResponse() { + return { + url: REGISTRATION_TOKENS_ENDPOINT, + requestMethod: 'POST', + response: null, + } satisfies MockResponse; +} + +export const MOCK_FCM_RESPONSE = { + name: '', + token: 'fcm-token', + web: { + endpoint: '', + p256dh: '', + auth: '', + applicationPubKey: '', + }, +}; + +export function getMockCreateFCMRegistrationTokenResponse() { + return { + url: /^https:\/\/fcmregistrations\.googleapis\.com\/v1\/projects\/.*$/u, + requestMethod: 'POST', + response: MOCK_FCM_RESPONSE, + } satisfies MockResponse; +} + +export function getMockDeleteFCMRegistrationTokenResponse() { + return { + url: /^https:\/\/fcmregistrations\.googleapis\.com\/v1\/projects\/.*$/u, + requestMethod: 'POST', + response: {}, + } satisfies MockResponse; +} diff --git a/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts b/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts new file mode 100644 index 000000000000..e0c3cb98d0c2 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts @@ -0,0 +1,36 @@ +import nock from 'nock'; +import { + getMockRetrievePushNotificationLinksResponse, + getMockUpdatePushNotificationLinksResponse, +} from './mockResponse'; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +export function mockEndpointGetPushNotificationLinks(mockReply?: MockReply) { + const mockResponse = getMockRetrievePushNotificationLinksResponse(); + const reply = mockReply ?? { + status: 200, + body: mockResponse.response, + }; + + const mockEndpoint = nock(mockResponse.url) + .get('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockEndpointUpdatePushNotificationLinks(mockReply?: MockReply) { + const mockResponse = getMockUpdatePushNotificationLinksResponse(); + const reply = mockReply ?? { + status: 200, + body: mockResponse.response, + }; + + const mockEndpoint = nock(mockResponse.url).post('').reply(reply.status); + + return mockEndpoint; +} diff --git a/app/scripts/controllers/push-platform-notifications/services/endpoints.ts b/app/scripts/controllers/push-platform-notifications/services/endpoints.ts new file mode 100644 index 000000000000..ff6520eb03a5 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/services/endpoints.ts @@ -0,0 +1,2 @@ +const url = process.env.PUSH_NOTIFICATIONS_SERVICE_URL; +export const REGISTRATION_TOKENS_ENDPOINT = `${url}/v1/link`; diff --git a/app/scripts/controllers/push-platform-notifications/services/services.test.ts b/app/scripts/controllers/push-platform-notifications/services/services.test.ts index f8277e1a358c..413c2d28caf8 100644 --- a/app/scripts/controllers/push-platform-notifications/services/services.test.ts +++ b/app/scripts/controllers/push-platform-notifications/services/services.test.ts @@ -1,67 +1,58 @@ +import * as FirebaseApp from 'firebase/app'; +import * as FirebaseMessaging from 'firebase/messaging'; +import * as FirebaseMessagingSW from 'firebase/messaging/sw'; +import { + mockEndpointGetPushNotificationLinks, + mockEndpointUpdatePushNotificationLinks, +} from '../mocks/mockServices'; import * as services from './services'; -type MockResponse = { - trigger_ids: string[]; - registration_tokens: services.RegToken[]; -}; +jest.mock('firebase/app'); +jest.mock('firebase/messaging'); +jest.mock('firebase/messaging/sw'); const MOCK_REG_TOKEN = 'REG_TOKEN'; const MOCK_NEW_REG_TOKEN = 'NEW_REG_TOKEN'; const MOCK_TRIGGERS = ['1', '2', '3']; -const MOCK_RESPONSE: MockResponse = { - trigger_ids: ['1', '2', '3'], - registration_tokens: [ - { token: 'reg_token_1', platform: 'portfolio' }, - { token: 'reg_token_2', platform: 'extension' }, - ], -}; const MOCK_JWT = 'MOCK_JWT'; describe('PushPlatformNotificationsServices', () => { describe('getPushNotificationLinks', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const utils = services; - it('Should return reg token links', async () => { - jest - .spyOn(services, 'getPushNotificationLinks') - .mockResolvedValue(MOCK_RESPONSE); - + const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks(); const res = await services.getPushNotificationLinks(MOCK_JWT); + expect(mockGetLinksEndpoint.isDone()).toBe(true); expect(res).toBeDefined(); expect(res?.trigger_ids).toBeDefined(); expect(res?.registration_tokens).toBeDefined(); }); it('Should return null if api call fails', async () => { - jest.spyOn(services, 'getPushNotificationLinks').mockResolvedValue(null); + const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks({ + status: 500, + }); + const res = await services.getPushNotificationLinks(MOCK_JWT); - const res = await utils.getPushNotificationLinks(MOCK_JWT); + expect(mockGetLinksEndpoint.isDone()).toBe(true); expect(res).toBeNull(); }); }); describe('updateLinksAPI', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('Should return true if links are updated', async () => { - jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(true); + const mockUpdateLinksEndpoint = mockEndpointUpdatePushNotificationLinks(); const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ { token: MOCK_NEW_REG_TOKEN, platform: 'extension' }, ]); + expect(mockUpdateLinksEndpoint.isDone()).toBe(true); expect(res).toBe(true); }); it('Should return false if links are not updated', async () => { - jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + mockEndpointUpdatePushNotificationLinks({ status: 500 }); const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ { token: MOCK_NEW_REG_TOKEN, platform: 'extension' }, @@ -72,14 +63,10 @@ describe('PushPlatformNotificationsServices', () => { }); describe('activatePushNotifications()', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('should append registration token when enabling push', async () => { - jest - .spyOn(services, 'activatePushNotifications') - .mockResolvedValue(MOCK_NEW_REG_TOKEN); + arrangeEndpoints(); + arrangeFCMMocks(); + const res = await services.activatePushNotifications( MOCK_JWT, MOCK_TRIGGERS, @@ -89,9 +76,8 @@ describe('PushPlatformNotificationsServices', () => { }); it('should fail if unable to get existing notification links', async () => { - jest - .spyOn(services, 'getPushNotificationLinks') - .mockResolvedValueOnce(null); + mockEndpointGetPushNotificationLinks({ status: 500 }); + const res = await services.activatePushNotifications( MOCK_JWT, MOCK_TRIGGERS, @@ -100,7 +86,9 @@ describe('PushPlatformNotificationsServices', () => { }); it('should fail if unable to create new reg token', async () => { - jest.spyOn(services, 'createRegToken').mockResolvedValueOnce(null); + arrangeEndpoints(); + const fcmMocks = arrangeFCMMocks(); + fcmMocks.getToken.mockRejectedValue(new Error('MOCK ERROR')); const res = await services.activatePushNotifications( MOCK_JWT, MOCK_TRIGGERS, @@ -108,25 +96,24 @@ describe('PushPlatformNotificationsServices', () => { expect(res).toBeNull(); }); - it('should fail if unable to update links', async () => { - jest.spyOn(services, 'updateLinksAPI').mockResolvedValueOnce(false); + it('should silently fail and return if failed to activate push notifications', async () => { + mockEndpointGetPushNotificationLinks(); + mockEndpointUpdatePushNotificationLinks({ status: 500 }); + arrangeFCMMocks(); const res = await services.activatePushNotifications( MOCK_JWT, MOCK_TRIGGERS, ); - expect(res).toBeNull(); - }); - afterEach(() => { - jest.restoreAllMocks(); + // We return the registration token, but we haven't updating the links. + // This can be redone at a later invocation (e.g. when the extension is re-initialized or notification setting changes) + expect(res).toBe(MOCK_NEW_REG_TOKEN); }); }); describe('deactivatePushNotifications()', () => { it('should fail if unable to get existing notification links', async () => { - jest - .spyOn(services, 'getPushNotificationLinks') - .mockResolvedValueOnce(null); + mockEndpointGetPushNotificationLinks({ status: 500 }); const res = await services.deactivatePushNotifications( MOCK_REG_TOKEN, @@ -138,10 +125,8 @@ describe('PushPlatformNotificationsServices', () => { }); it('should fail if unable to update links', async () => { - jest - .spyOn(services, 'getPushNotificationLinks') - .mockResolvedValue(MOCK_RESPONSE); - jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + mockEndpointGetPushNotificationLinks(); + mockEndpointUpdatePushNotificationLinks({ status: 500 }); const res = await services.deactivatePushNotifications( MOCK_REG_TOKEN, @@ -153,10 +138,9 @@ describe('PushPlatformNotificationsServices', () => { }); it('should fail if unable to delete reg token', async () => { - jest - .spyOn(services, 'getPushNotificationLinks') - .mockResolvedValueOnce(MOCK_RESPONSE); - jest.spyOn(services, 'deleteRegToken').mockResolvedValue(false); + arrangeEndpoints(); + const fcmMocks = arrangeFCMMocks(); + fcmMocks.deleteToken.mockRejectedValue(new Error('MOCK FAIL')); const res = await services.deactivatePushNotifications( MOCK_REG_TOKEN, @@ -166,22 +150,11 @@ describe('PushPlatformNotificationsServices', () => { expect(res).toBe(false); }); - - afterEach(() => { - jest.clearAllMocks(); - }); }); describe('updateTriggerPushNotifications()', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('should update triggers for push notifications', async () => { - jest.spyOn(services, 'updateTriggerPushNotifications').mockResolvedValue({ - isTriggersLinkedToPushNotifications: true, - fcmToken: 'fcm-token', - }); + arrangeEndpoints(); const res = await services.updateTriggerPushNotifications( MOCK_REG_TOKEN, @@ -191,15 +164,13 @@ describe('PushPlatformNotificationsServices', () => { expect(res).toEqual({ isTriggersLinkedToPushNotifications: true, - fcmToken: 'fcm-token', + fcmToken: expect.any(String), }); }); it('should fail if unable to update triggers', async () => { - jest.spyOn(services, 'updateTriggerPushNotifications').mockResolvedValue({ - isTriggersLinkedToPushNotifications: false, - fcmToken: undefined, - }); + mockEndpointGetPushNotificationLinks(); + mockEndpointUpdatePushNotificationLinks({ status: 500 }); const res = await services.updateTriggerPushNotifications( MOCK_REG_TOKEN, @@ -209,12 +180,53 @@ describe('PushPlatformNotificationsServices', () => { expect(res).toEqual({ isTriggersLinkedToPushNotifications: false, - fcmToken: undefined, + fcmToken: expect.any(String), }); }); - - afterEach(() => { - jest.restoreAllMocks(); - }); }); + + function arrangeEndpoints() { + const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks(); + const mockUpdateLinksEndpoint = mockEndpointUpdatePushNotificationLinks(); + + return { + mockGetLinksEndpoint, + mockUpdateLinksEndpoint, + }; + } + + function arrangeFCMMocks() { + const mockFirebaseApp: FirebaseApp.FirebaseApp = { + name: '', + automaticDataCollectionEnabled: false, + options: {}, + }; + const mockFirebaseMessaging: FirebaseMessagingSW.Messaging = { + app: mockFirebaseApp, + }; + + jest.spyOn(FirebaseApp, 'getApp').mockReturnValue(mockFirebaseApp); + jest.spyOn(FirebaseApp, 'initializeApp').mockReturnValue(mockFirebaseApp); + + const getMessaging = jest + .spyOn(FirebaseMessagingSW, 'getMessaging') + .mockReturnValue(mockFirebaseMessaging); + const onBackgroundMessage = jest + .spyOn(FirebaseMessagingSW, 'onBackgroundMessage') + .mockReturnValue(() => jest.fn()); + + const getToken = jest + .spyOn(FirebaseMessaging, 'getToken') + .mockResolvedValue(MOCK_NEW_REG_TOKEN); + const deleteToken = jest + .spyOn(FirebaseMessaging, 'deleteToken') + .mockResolvedValue(true); + + return { + getMessaging, + onBackgroundMessage, + getToken, + deleteToken, + }; + } }); diff --git a/app/scripts/controllers/push-platform-notifications/services/services.ts b/app/scripts/controllers/push-platform-notifications/services/services.ts index ffe908270b80..da15be11c8d8 100644 --- a/app/scripts/controllers/push-platform-notifications/services/services.ts +++ b/app/scripts/controllers/push-platform-notifications/services/services.ts @@ -13,9 +13,8 @@ import { NotificationUnion, } from '../../metamask-notifications/types/types'; import { processNotification } from '../../metamask-notifications/processors/process-notifications'; +import { REGISTRATION_TOKENS_ENDPOINT } from './endpoints'; -const url = process.env.PUSH_NOTIFICATIONS_SERVICE_URL; -const REGISTRATION_TOKENS_ENDPOINT = `${url}/v1/link`; const sw = self as unknown as ServiceWorkerGlobalScope; export type RegToken = { @@ -33,7 +32,7 @@ export type LinksResult = { * * @returns The Firebase app instance. */ -export async function createFirebaseApp(): Promise { +async function createFirebaseApp(): Promise { try { return getApp(); } catch { @@ -58,7 +57,7 @@ export async function createFirebaseApp(): Promise { * * @returns A promise that resolves with the Firebase Messaging service instance. */ -export async function getFirebaseMessaging(): Promise { +async function getFirebaseMessaging(): Promise { const app = await createFirebaseApp(); return getMessaging(app); } @@ -68,7 +67,7 @@ export async function getFirebaseMessaging(): Promise { * * @returns A promise that resolves with the registration token or null if an error occurs. */ -export async function createRegToken(): Promise { +async function createRegToken(): Promise { try { const messaging = await getFirebaseMessaging(); const token = await getToken(messaging, { @@ -86,7 +85,7 @@ export async function createRegToken(): Promise { * * @returns A promise that resolves with true if the token was successfully deleted, false otherwise. */ -export async function deleteRegToken(): Promise { +async function deleteRegToken(): Promise { try { const messaging = await getFirebaseMessaging(); await deleteToken(messaging); diff --git a/app/scripts/controllers/user-storage/mocks/mockResponses.ts b/app/scripts/controllers/user-storage/mocks/mockResponses.ts new file mode 100644 index 000000000000..c1b3896f446f --- /dev/null +++ b/app/scripts/controllers/user-storage/mocks/mockResponses.ts @@ -0,0 +1,35 @@ +import { createEntryPath } from '../schema'; +import { GetUserStorageResponse, USER_STORAGE_ENDPOINT } from '../services'; +import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage'; + +type MockResponse = { + url: string; + requestMethod: 'GET' | 'POST' | 'PUT'; + response: unknown; +}; + +export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath( + 'notification_settings', + MOCK_STORAGE_KEY, +)}`; + +const MOCK_GET_USER_STORAGE_RESPONSE: GetUserStorageResponse = { + HashedKey: 'HASHED_KEY', + Data: MOCK_ENCRYPTED_STORAGE_DATA, +}; + +export function getMockUserStorageGetResponse() { + return { + url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT, + requestMethod: 'GET', + response: MOCK_GET_USER_STORAGE_RESPONSE, + } satisfies MockResponse; +} + +export function getMockUserStoragePutResponse() { + return { + url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT, + requestMethod: 'PUT', + response: null, + } satisfies MockResponse; +} diff --git a/app/scripts/controllers/user-storage/mocks/mockServices.ts b/app/scripts/controllers/user-storage/mocks/mockServices.ts index 97ffa703dc9c..d612ebe6ed55 100644 --- a/app/scripts/controllers/user-storage/mocks/mockServices.ts +++ b/app/scripts/controllers/user-storage/mocks/mockServices.ts @@ -1,29 +1,22 @@ import nock from 'nock'; -import { USER_STORAGE_ENDPOINT, GetUserStorageResponse } from '../services'; -import { createEntryPath } from '../schema'; -import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage'; - -export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath( - 'notification_settings', - MOCK_STORAGE_KEY, -)}`; +import { + getMockUserStorageGetResponse, + getMockUserStoragePutResponse, +} from './mockResponses'; type MockReply = { status: nock.StatusCode; body?: nock.Body; }; -const MOCK_GET_USER_STORAGE_RESPONSE: GetUserStorageResponse = { - HashedKey: 'HASHED_KEY', - Data: MOCK_ENCRYPTED_STORAGE_DATA, -}; export function mockEndpointGetUserStorage(mockReply?: MockReply) { + const mockResponse = getMockUserStorageGetResponse(); const reply = mockReply ?? { status: 200, - body: MOCK_GET_USER_STORAGE_RESPONSE, + body: mockResponse.response, }; - const mockEndpoint = nock(MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT) + const mockEndpoint = nock(mockResponse.url) .get('') .reply(reply.status, reply.body); @@ -33,7 +26,8 @@ export function mockEndpointGetUserStorage(mockReply?: MockReply) { export function mockEndpointUpsertUserStorage( mockReply?: Pick, ) { - const mockEndpoint = nock(MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT) + const mockResponse = getMockUserStoragePutResponse(); + const mockEndpoint = nock(mockResponse.url) .put('') .reply(mockReply?.status ?? 204); return mockEndpoint; diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index 6e53931fa404..604b0bcd7954 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -24,6 +24,7 @@ const blacklistedHosts = [ const { mockEmptyStalelistAndHotlist, } = require('./tests/phishing-controller/mocks'); +const { mockNotificationServices } = require('./tests/notifications/mocks'); const emptyHtmlPage = () => ` @@ -593,6 +594,9 @@ async function setupMocking( }; }); + // Notification APIs + mockNotificationServices(server); + /** * Returns an array of alphanumerically sorted hostnames that were requested * during the current test suite. diff --git a/test/e2e/tests/notifications/mocks.ts b/test/e2e/tests/notifications/mocks.ts new file mode 100644 index 000000000000..cb14382f8763 --- /dev/null +++ b/test/e2e/tests/notifications/mocks.ts @@ -0,0 +1,83 @@ +import { Mockttp, RequestRuleBuilder } from 'mockttp'; +import { + getMockAuthNonceResponse, + getMockAuthLoginResponse, + getMockAuthAccessTokenResponse, +} from '../../../../app/scripts/controllers/authentication/mocks/mockResponses'; +import { + getMockUserStorageGetResponse, + getMockUserStoragePutResponse, +} from '../../../../app/scripts/controllers/user-storage/mocks/mockResponses'; +import { + getMockFeatureAnnouncementResponse, + getMockBatchCreateTriggersResponse, + getMockBatchDeleteTriggersResponse, + getMockListNotificationsResponse, + getMockMarkNotificationsAsReadResponse, +} from '../../../../app/scripts/controllers/metamask-notifications/mocks/mockResponses'; +import { + getMockRetrievePushNotificationLinksResponse, + getMockUpdatePushNotificationLinksResponse, + getMockCreateFCMRegistrationTokenResponse, + getMockDeleteFCMRegistrationTokenResponse, +} from '../../../../app/scripts/controllers/push-platform-notifications/mocks/mockResponse'; + +type MockResponse = { + url: string | RegExp; + requestMethod: 'GET' | 'POST' | 'PUT' | 'DELETE'; + response: unknown; +}; + +/** + * E2E mock setup for notification APIs (Auth, Storage, Notifications, Push Notifications) + * + * @param server - server obj used to mock our endpoints + */ +export function mockNotificationServices(server: Mockttp) { + // Auth + mockAPICall(server, getMockAuthNonceResponse()); + mockAPICall(server, getMockAuthLoginResponse()); + mockAPICall(server, getMockAuthAccessTokenResponse()); + + // Storage + mockAPICall(server, getMockUserStorageGetResponse()); + mockAPICall(server, getMockUserStoragePutResponse()); + + // Notifications + mockAPICall(server, getMockFeatureAnnouncementResponse()); + mockAPICall(server, getMockBatchCreateTriggersResponse()); + mockAPICall(server, getMockBatchDeleteTriggersResponse()); + mockAPICall(server, getMockListNotificationsResponse()); + mockAPICall(server, getMockMarkNotificationsAsReadResponse()); + + // Push Notifications + mockAPICall(server, getMockRetrievePushNotificationLinksResponse()); + mockAPICall(server, getMockUpdatePushNotificationLinksResponse()); + mockAPICall(server, getMockCreateFCMRegistrationTokenResponse()); + mockAPICall(server, getMockDeleteFCMRegistrationTokenResponse()); +} + +function mockAPICall(server: Mockttp, response: MockResponse) { + let requestRuleBuilder: RequestRuleBuilder | undefined; + + if (response.requestMethod === 'GET') { + requestRuleBuilder = server.forGet(response.url); + } + + if (response.requestMethod === 'POST') { + requestRuleBuilder = server.forPost(response.url); + } + + if (response.requestMethod === 'PUT') { + requestRuleBuilder = server.forPut(response.url); + } + + if (response.requestMethod === 'DELETE') { + requestRuleBuilder = server.forDelete(response.url); + } + + requestRuleBuilder?.thenCallback(() => ({ + statusCode: 200, + json: response.response, + })); +} diff --git a/test/env.js b/test/env.js index e80daf9441ea..8dbdd0aaa6bc 100644 --- a/test/env.js +++ b/test/env.js @@ -11,3 +11,5 @@ process.env.CONTENTFUL_ACCESS_TOKEN = 'MOCK_ACCESS_TOKEN'; process.env.TRIGGERS_SERVICE_URL = 'https://mock-test-triggers-api.metamask.io'; process.env.NOTIFICATIONS_SERVICE_URL = 'https://mock-test-notifications-api.metamask.io'; +process.env.PUSH_NOTIFICATIONS_SERVICE_URL = + 'https://mock-test-push-notifications-api.metamask.io';