diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index 29203ec14c..92da67a5d4 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -564,6 +564,20 @@ describe('validateLink', () => { expect(fn).not.toHaveBeenCalled(); }); + it('throws an error if the snap being navigated to is not installed', () => { + const fn = jest.fn().mockReturnValue(false); + + expect(() => + validateLink( + 'metamask://snap/npm:@metamask/examplesnap/home', + fn, + jest.fn().mockReturnValue(false), + ), + ).toThrow('The snap being navigated to is not installed.'); + + expect(fn).not.toHaveBeenCalled(); + }); + it('throws an error for an invalid URL', () => { const fn = jest.fn().mockReturnValue(false); @@ -658,29 +672,39 @@ describe('validateTextLinks', () => { it('throws an error if an invalid link is found in text', () => { expect(() => - validateTextLinks('[test](http://foo.bar)', () => false), + validateTextLinks('[test](http://foo.bar)', () => false, jest.fn()), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', ); expect(() => - validateTextLinks('[[test]](http://foo.bar)', () => false), + validateTextLinks('[[test]](http://foo.bar)', () => false, jest.fn()), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', ); - expect(() => validateTextLinks('', () => false)).toThrow( + expect(() => + validateTextLinks('', () => false, jest.fn()), + ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', ); expect(() => - validateTextLinks('[test](http://foo.bar "foo bar baz")', () => false), + validateTextLinks( + '[test](http://foo.bar "foo bar baz")', + () => false, + jest.fn(), + ), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', ); expect(() => - validateTextLinks('[foo][1]\n\n[1]: http://foo.bar', () => false), + validateTextLinks( + '[foo][1]\n\n[1]: http://foo.bar', + () => false, + jest.fn(), + ), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', ); @@ -689,6 +713,7 @@ describe('validateTextLinks', () => { validateTextLinks( `[foo][1]\n\n[1]: http://foo.bar "foo bar baz"`, () => false, + jest.fn(), ), ).toThrow( 'Invalid URL: Protocol must be one of: https:, mailto:, metamask:.', diff --git a/packages/snaps-utils/src/url.test.ts b/packages/snaps-utils/src/url.test.ts index 8fceb3dfa8..956d481cfa 100644 --- a/packages/snaps-utils/src/url.test.ts +++ b/packages/snaps-utils/src/url.test.ts @@ -1,4 +1,10 @@ -import { parseMetaMaskUrl } from './url'; +import { isMetaMaskUrl, METAMASK_URL_REGEX, parseMetaMaskUrl } from './url'; + +describe('METAMASK_URL_REGEX', () => { + it('will throw for non-metamask urls', () => { + expect('random:foobar'.match(METAMASK_URL_REGEX)).toBeNull(); + }); +}); describe('parseMetaMaskUrl', () => { it('can parse a valid url with the client authority', () => { @@ -28,6 +34,16 @@ describe('parseMetaMaskUrl', () => { }); }); + it('can parse a valid url with a local snap', () => { + expect( + parseMetaMaskUrl('metamask://snap/local:http://localhost:8080/home'), + ).toStrictEqual({ + authority: 'snap', + path: '/home', + snapId: 'local:http://localhost:8080', + }); + }); + it('will throw on an invalid scheme', () => { expect(() => parseMetaMaskUrl('metmask://client/')).toThrow( 'Invalid MetaMask url.', @@ -53,3 +69,19 @@ describe('parseMetaMaskUrl', () => { ); }); }); + +describe('isMetaMaskUrl', () => { + it.each(['https://www.google.com', 'metamask://foo/'])( + 'will return false for non-metamask urls', + (url) => { + expect(isMetaMaskUrl(url)).toBe(false); + }, + ); + + it.each(['metamask://snap/home', 'metamask://client/'])( + 'will not throw for valid metamask urls', + (url) => { + expect(isMetaMaskUrl(url)).toBe(true); + }, + ); +}); diff --git a/packages/snaps-utils/src/url.ts b/packages/snaps-utils/src/url.ts index e3b9c7d5f5..5a3d36a548 100644 --- a/packages/snaps-utils/src/url.ts +++ b/packages/snaps-utils/src/url.ts @@ -45,21 +45,30 @@ export function parseMetaMaskUrl(url: MetaMaskUrl): { }; } else if (authority === 'snap') { const strippedPath = stripSnapPrefix(path.slice(1)); + const location = path.slice(1).startsWith('npm:') ? 'npm:' : 'local:'; const isNameSpaced = strippedPath.startsWith('@'); const pathTokens = strippedPath.split('/'); const lastPathToken = `/${pathTokens[pathTokens.length - 1]}`; - const partialSnapId = isNameSpaced - ? `${pathTokens[0]}/${pathTokens[1]}` - : pathTokens[0]; - const location = path.slice(1).startsWith('npm:') ? 'npm:' : 'local:'; + let partialSnapId; + if (location === 'local:') { + partialSnapId = `http://${pathTokens[2]}`; + assert( + pathTokens.length === 4 && SnapPaths.includes(lastPathToken), + 'Invalid snap path.', + ); + } else { + partialSnapId = isNameSpaced + ? `${pathTokens[0]}/${pathTokens[1]}` + : pathTokens[0]; + assert( + isNameSpaced + ? pathTokens.length === 3 && SnapPaths.includes(lastPathToken) + : pathTokens.length === 2 && SnapPaths.includes(lastPathToken), + 'Invalid snap path.', + ); + } const snapId = `${location}${partialSnapId}`; assertIsValidSnapId(snapId); - assert( - isNameSpaced - ? pathTokens.length === 3 && SnapPaths.includes(lastPathToken) - : pathTokens.length === 2 && SnapPaths.includes(lastPathToken), - 'Invalid snap path.', - ); return { authority, @@ -67,7 +76,9 @@ export function parseMetaMaskUrl(url: MetaMaskUrl): { path: lastPathToken, }; } - + // You don't ever actually reach this line, TS doesn't know that no other authority will make it past + // the regex statement + /* istanbul ignore next */ throw new Error('Invalid authority'); }