From 0c76ed493422a62ac911e05a415aad6a3bcb99c2 Mon Sep 17 00:00:00 2001 From: imblue-dabadee Date: Thu, 13 Feb 2025 15:25:07 +1100 Subject: [PATCH] tests for phishing controller --- .../src/PhishingController.test.ts | 147 +++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/packages/phishing-controller/src/PhishingController.test.ts b/packages/phishing-controller/src/PhishingController.test.ts index 35fdfb27014..c72f08ee1e8 100644 --- a/packages/phishing-controller/src/PhishingController.test.ts +++ b/packages/phishing-controller/src/PhishingController.test.ts @@ -1,7 +1,7 @@ import { Messenger } from '@metamask/base-controller'; import { strict as assert } from 'assert'; import nock from 'nock'; -import * as sinon from 'sinon'; +import sinon from 'sinon'; import { ListNames, @@ -13,6 +13,8 @@ import { type PhishingControllerOptions, CLIENT_SIDE_DETECION_BASE_URL, C2_DOMAIN_BLOCKLIST_ENDPOINT, + PHISHING_DETECTION_BASE_URL, + PHISHING_DETECTION_SCAN_ENDPOINT, } from './PhishingController'; import { formatHostnameToUrl } from './tests/utils'; import { PhishingDetectorResultType } from './types'; @@ -2365,4 +2367,147 @@ describe('PhishingController', () => { expect(controller.state.whitelist).toHaveLength(1); }); }); + + describe('scanUrl', () => { + let controller: PhishingController; + let clock: sinon.SinonFakeTimers; + const testUrl: string = 'https://example.com'; + const mockResponse = { + domainName: 'example.com', + recommendedAction: 'NONE', + riskFactors: [], + verified: true, + status: 'COMPLETE', + }; + + beforeEach(() => { + controller = getPhishingController(); + clock = sinon.useFakeTimers(); + }); + + it('should return the scan result', async () => { + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: 'example.com' }) + .reply(200, mockResponse); + + const response = await controller.scanUrl(testUrl); + expect(response).toMatchObject(mockResponse); + expect(scope.isDone()).toBe(true); + }); + + it('should throw error on failed request', async () => { + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: 'example.com' }) + .reply(500); + + await expect(controller.scanUrl(testUrl)).rejects.toThrow( + 'request failed: 500 Internal Server Error', + ); + expect(scope.isDone()).toBe(true); + }); + + it('should timeout after 8000ms', async () => { + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: testUrl }) + .delayConnection(10000) + .reply(200, {}); + + const promise = controller.scanUrl(testUrl); + clock.tick(8000); + await expect(promise).rejects.toThrow( + 'request failed: Request timed out', + ); + expect(scope.isDone()).toBe(false); + }); + + it('should only send hostname when URL contains query parameters', async () => { + const urlWithQuery = + 'https://example.com/path?param1=value1¶m2=value2'; + const expectedHostname = 'example.com'; + + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: expectedHostname }) + .reply(200, mockResponse); + + const response = await controller.scanUrl(urlWithQuery); + expect(response).toMatchObject(mockResponse); + expect(scope.isDone()).toBe(true); + }); + + it('should only send hostname when URL contains hash fragments', async () => { + const urlWithHash = 'https://example.com/page#section1'; + const expectedHostname = 'example.com'; + + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: expectedHostname }) + .reply(200, mockResponse); + + const response = await controller.scanUrl(urlWithHash); + expect(response).toMatchObject(mockResponse); + expect(scope.isDone()).toBe(true); + }); + + it('should only send hostname for complex URLs with multiple parameters', async () => { + const complexUrl = + 'https://sub.example.com:8080/path/to/page?q=search&utm_source=test#top'; + const expectedHostname = 'sub.example.com'; + + const subdomainResponse = { + ...mockResponse, + domainName: 'sub.example.com', + }; + + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: expectedHostname }) + .reply(200, subdomainResponse); + + const response = await controller.scanUrl(complexUrl); + expect(response).toMatchObject(subdomainResponse); + expect(scope.isDone()).toBe(true); + }); + + it('should throw error for invalid URLs', async () => { + const invalidUrls = [ + 'not-a-url', + 'http://', + 'https://', + 'example', + 'http://.', + 'http://..', + 'http://../', + 'http://?', + 'http://??', + 'http://??/', + 'http://#', + 'http://##', + 'http://##/', + ]; + + for (const invalidUrl of invalidUrls) { + await expect(controller.scanUrl(invalidUrl)).rejects.toThrow( + 'Invalid URL provided', + ); + } + }); + + it('should handle URLs with authentication parameters correctly', async () => { + const urlWithAuth = 'https://user:pass@example.com/secure'; + const expectedHostname = 'example.com'; + + const scope = nock(PHISHING_DETECTION_BASE_URL) + .get(`/${PHISHING_DETECTION_SCAN_ENDPOINT}`) + .query({ url: expectedHostname }) + .reply(200, mockResponse); + + const response = await controller.scanUrl(urlWithAuth); + expect(response).toMatchObject(mockResponse); + expect(scope.isDone()).toBe(true); + }); + }); });