UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

380 lines 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const WalletPermissionsManager_fixtures_1 = require("./WalletPermissionsManager.fixtures"); const WalletPermissionsManager_1 = require("../WalletPermissionsManager"); const globals_1 = require("@jest/globals"); // We mock the underlying @bsv/sdk references with our test fixtures: globals_1.jest.mock('@bsv/sdk', () => WalletPermissionsManager_fixtures_1.MockedBSV_SDK); /** * A lightweight helper that forces the manager to never find any on-chain token. * We do this so we can reliably test the request flow (i.e., that it truly initiates * a new permission request if no token is found). */ function mockNoTokensFound(manager) { globals_1.jest.spyOn(manager, 'findProtocolToken').mockResolvedValue(undefined); globals_1.jest.spyOn(manager, 'findBasketToken').mockResolvedValue(undefined); globals_1.jest.spyOn(manager, 'findCertificateToken').mockResolvedValue(undefined); globals_1.jest.spyOn(manager, 'findSpendingToken').mockResolvedValue(undefined); } describe('WalletPermissionsManager - Permission Request Flow & Active Requests', () => { let underlying; let manager; beforeEach(() => { underlying = (0, WalletPermissionsManager_fixtures_1.mockUnderlyingWallet)(); manager = new WalletPermissionsManager_1.WalletPermissionsManager(underlying, 'admin.test.com'); }); afterEach(() => { globals_1.jest.clearAllMocks(); }); /** * UNIT TESTS */ describe('Unit Tests: requestPermissionFlow & activeRequests map', () => { it('should coalesce parallel requests for the same resource into a single user prompt', async () => { // We want to test the underlying private method "requestPermissionFlow" indirectly // or we can test it via a public method that calls it. We'll do so via ensureProtocolPermission. // Force no token found => triggers a request flow mockNoTokensFound(manager); // Spy on the manager's "onProtocolPermissionRequested" callbacks const requestCallback = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', requestCallback); // Make two parallel calls for the same resource const callA = manager.ensureProtocolPermission({ originator: 'example.com', privileged: false, protocolID: [1, 'someproto'], counterparty: 'self', reason: 'UnitTest - same resource A', seekPermission: true, usageType: 'signing' }); const callB = manager.ensureProtocolPermission({ originator: 'example.com', privileged: false, protocolID: [1, 'someproto'], counterparty: 'self', reason: 'UnitTest - same resource B', seekPermission: true, usageType: 'signing' }); // Wait a short moment for the async request flow to trigger await new Promise(res => setTimeout(res, 5)); // We expect only one "onProtocolPermissionRequested" event for both calls expect(requestCallback).toHaveBeenCalledTimes(1); // Now let's deny the request: // Grab the requestID that the manager gave us from the callback param const callbackArg = requestCallback.mock.calls[0][0]; const requestID = callbackArg.requestID; expect(typeof requestID).toBe('string'); // manager-generated // Deny the request await manager.denyPermission(requestID); // Both calls should reject await expect(callA).rejects.toThrow(/Permission denied/); await expect(callB).rejects.toThrow(/Permission denied/); // Confirm activeRequests map is empty after denial const activeRequests = manager.activeRequests; expect(activeRequests.size).toBe(0); }); it('should generate two distinct user prompts for two different permission requests', async () => { // Force no tokens mockNoTokensFound(manager); // Spy on basket & protocol request callbacks const protocolRequestCb = globals_1.jest.fn(() => { }); const basketRequestCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', protocolRequestCb); manager.bindCallback('onBasketAccessRequested', basketRequestCb); // Make one call for protocol usage const pCall = manager.ensureProtocolPermission({ originator: 'example.com', privileged: false, protocolID: [1, 'proto-A'], counterparty: 'self', reason: 'Different request A', seekPermission: true, usageType: 'signing' }); // Make a second call for basket usage const bCall = manager.ensureBasketAccess({ originator: 'example.com', basket: 'some-basket', reason: 'Different request B', seekPermission: true, usageType: 'insertion' }); // Wait a moment for them to trigger await new Promise(res => setTimeout(res, 5)); // We expect one protocol request AND one basket request expect(protocolRequestCb).toHaveBeenCalledTimes(1); expect(basketRequestCb).toHaveBeenCalledTimes(1); // Deny protocol request const pReqID = protocolRequestCb.mock.calls[0][0].requestID; await manager.denyPermission(pReqID); // Deny basket request const bReqID = basketRequestCb.mock.calls[0][0].requestID; await manager.denyPermission(bReqID); // Both calls should have rejected await expect(pCall).rejects.toThrow(/Permission denied/); await expect(bCall).rejects.toThrow(/Permission denied/); // activeRequests is empty const activeRequests = manager.activeRequests; expect(activeRequests.size).toBe(0); }); it('should resolve all parallel requests when permission is granted, referencing the same requestID', async () => { // No tokens => triggers request flow mockNoTokensFound(manager); const requestCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', requestCb); // Parallel calls const promiseA = manager.ensureProtocolPermission({ originator: 'example.com', privileged: false, protocolID: [1, 'proto-X'], counterparty: 'anyone', reason: 'Test parallel grant A', seekPermission: true, usageType: 'encrypting' }); const promiseB = manager.ensureProtocolPermission({ originator: 'example.com', privileged: false, protocolID: [1, 'proto-X'], counterparty: 'anyone', reason: 'Test parallel grant B', seekPermission: true, usageType: 'encrypting' }); // Let the request event fire await new Promise(res => setTimeout(res, 5)); expect(requestCb).toHaveBeenCalledTimes(1); // Extract the requestID from the callback const { requestID } = requestCb.mock.calls[0][0]; // Now we grant permission for that same requestID // Because ephemeral is false by default, the manager will attempt to create on-chain tokens // We'll mock the internal createPermissionOnChain so it doesn't blow up const createOnChainSpy = globals_1.jest.spyOn(manager, 'createPermissionOnChain').mockResolvedValue(undefined); await manager.grantPermission({ requestID }); // Both calls should resolve with `true` (the manager returns a boolean) await expect(promiseA).resolves.toBe(true); await expect(promiseB).resolves.toBe(true); // activeRequests map is empty const activeRequests = manager.activeRequests; expect(activeRequests.size).toBe(0); // The manager tried to create an on-chain permission token once expect(createOnChainSpy).toHaveBeenCalledTimes(1); }); it('should reject only the matching request queue on deny if requestID is specified', async () => { // This scenario tests the manager's partial denial logic where we pass { requestID } // to only reject the queued requests with that ID, leaving others (with a different requestID) // in the queue. mockNoTokensFound(manager); // We do two separate calls for the same resource but at different times, resulting in separate queues. // Actually, the manager normally merges them into one queue if the resource is the same. // So let's do two different resources to ensure we get two separate keys. const protoCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', protoCb); // Resource 1 const p1Promise = manager.ensureProtocolPermission({ originator: 'siteA.com', privileged: false, protocolID: [1, 'proto-siteA'], counterparty: 'self', usageType: 'encrypting' }); await new Promise(res => setTimeout(res, 5)); const p1ReqID = protoCb.mock.calls[0][0].requestID; // At this point, resource 1 is pending in activeRequests. We'll not resolve it yet. // Resource 2 const p2Promise = manager.ensureProtocolPermission({ originator: 'siteB.com', privileged: false, protocolID: [1, 'proto-siteB'], counterparty: 'self', usageType: 'encrypting' }); await new Promise(res => setTimeout(res, 5)); // the second call triggers a second onProtocolPermissionRequested callback expect(protoCb).toHaveBeenCalledTimes(2); const p2ReqID = protoCb.mock.calls[1][0].requestID; // Deny the second request only await manager.denyPermission(p2ReqID); await expect(p2Promise).rejects.toThrow(/Permission denied/); // But the first request is still waiting const activeRequests = manager.activeRequests; expect(activeRequests.size).toBe(1); // Now let's deny the first request too await manager.denyPermission(p1ReqID); await expect(p1Promise).rejects.toThrow(/Permission denied/); // The queue is empty now expect(activeRequests.size).toBe(0); }); }); /** * INTEGRATION TESTS */ describe('Integration Tests: ephemeral vs. persistent tokens', () => { it('should not create a token if ephemeral=true, so subsequent calls re-trigger the request', async () => { // We'll do a "protocol" permission scenario: mockNoTokensFound(manager); // Bind the request callback const requestCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', requestCb); // Force any on-chain creation attempt to be spied on const createTokenSpy = globals_1.jest.spyOn(manager, 'createPermissionOnChain'); // 1) Call ensureProtocolPermission => triggers request const pCall1 = manager.ensureProtocolPermission({ originator: 'appdomain.com', privileged: false, protocolID: [1, 'ephemeral-proto'], counterparty: 'self', reason: 'test ephemeral #1', usageType: 'signing' }); // Wait for request callback await new Promise(res => setTimeout(res, 5)); expect(requestCb).toHaveBeenCalledTimes(1); const reqID1 = requestCb.mock.calls[0][0].requestID; // Grant ephemeral await manager.grantPermission({ requestID: reqID1, ephemeral: true }); // pCall1 is resolved await expect(pCall1).resolves.toBe(true); // Because ephemeral=true, we do NOT create an on-chain token expect(createTokenSpy).not.toHaveBeenCalled(); manager.permissionCache = new Map(); // 2) Immediately call ensureProtocolPermission again for the same resource // Because ephemeral usage didn't store a token, it should re-prompt. const pCall2 = manager.ensureProtocolPermission({ originator: 'appdomain.com', privileged: false, protocolID: [1, 'ephemeral-proto'], counterparty: 'self', reason: 'test ephemeral #2', usageType: 'signing' }); await new Promise(res => setTimeout(res, 5)); // We expect a new request callback expect(requestCb).toHaveBeenCalledTimes(2); // We'll deny the second request const reqID2 = requestCb.mock.calls[1][0].requestID; await manager.denyPermission(reqID2); await expect(pCall2).rejects.toThrow(/Permission denied/); }); it('should create a token if ephemeral=false, so subsequent calls do not re-trigger if unexpired', async () => { // We want the manager to truly create a token. We'll confirm that // subsequent calls for the same resource skip user prompt. mockNoTokensFound(manager); // We'll also ensure no token is found "the first time." // But on subsequent calls, we can mock that the manager sees the newly created token. // Let's spy on "createPermissionOnChain" so we can intercept the new token const createTokenSpy = globals_1.jest.spyOn(manager, 'createPermissionOnChain').mockResolvedValue(undefined); // no real on-chain creation // Spy on "findProtocolToken" so we can simulate that the second time it's called, // there's a valid token. We'll do this by setting the mock to return undefined the first time, // and a valid token the second time (or we can just rely on the manager's logic). let firstFindCall = true; globals_1.jest.spyOn(manager, 'findProtocolToken').mockImplementation(async () => { if (firstFindCall) { firstFindCall = false; return undefined; // first time triggers request } // second time => pretend we found a valid token const mockToken = { tx: [], txid: 'abcdef', outputIndex: 0, outputScript: '00', satoshis: 1, originator: 'persistentdomain.com', expiry: Math.floor(Date.now() / 1000) + 3600, // unexpired privileged: false, protocol: 'persist-proto', securityLevel: 1, counterparty: 'self' }; return mockToken; }); // We'll observe the request callback const requestCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', requestCb); // 1) First call => no token => triggers request const call1 = manager.ensureProtocolPermission({ originator: 'persistentdomain.com', privileged: false, protocolID: [1, 'persist-proto'], counterparty: 'self', reason: 'test persistent #1', usageType: 'signing' }); await new Promise(res => setTimeout(res, 5)); expect(requestCb).toHaveBeenCalledTimes(1); // Grant ephemeral=false => triggers createPermissionOnChain const reqID = requestCb.mock.calls[0][0].requestID; await manager.grantPermission({ requestID: reqID, ephemeral: false }); await expect(call1).resolves.toBe(true); expect(createTokenSpy).toHaveBeenCalledTimes(1); // 2) Second call => the manager should find the token we just "created" => no request prompt const call2 = manager.ensureProtocolPermission({ originator: 'persistentdomain.com', privileged: false, protocolID: [1, 'persist-proto'], counterparty: 'self', reason: 'test persistent #2', usageType: 'signing' }); // We do not expect a new user prompt => requestCb remains at 1 await new Promise(res => setTimeout(res, 5)); expect(requestCb).toHaveBeenCalledTimes(1); // The second call should resolve immediately, no prompt await expect(call2).resolves.toBe(true); }); it('should handle renewal if the found token is expired, passing previousToken in the request', async () => { // We'll test the "renewal" flow: // If the manager finds a token but it's expired, it sets { renewal: true, previousToken } in the request. // We'll mock findProtocolToken to return an expired token const expiredToken = { tx: [], txid: 'expiredTxid123', outputIndex: 0, outputScript: '76a914xxxx...88ac', satoshis: 1, originator: 'renewme.com', expiry: Math.floor(Date.now() / 1000) - 100, // in the past privileged: false, protocol: 'renew-proto', securityLevel: 1, counterparty: 'self' }; globals_1.jest.spyOn(manager, 'findProtocolToken').mockResolvedValue(expiredToken); // Spy on request callback const requestCb = globals_1.jest.fn(() => { }); manager.bindCallback('onProtocolPermissionRequested', requestCb); // We'll also spy on "renewPermissionOnChain" to see if it's called const renewSpy = globals_1.jest.spyOn(manager, 'renewPermissionOnChain').mockResolvedValue(undefined); // Call ensureProtocolPermission => sees expired token => triggers request with renewal const promise = manager.ensureProtocolPermission({ originator: 'renewme.com', privileged: false, protocolID: [1, 'renew-proto'], counterparty: 'self', reason: 'test renewal', usageType: 'encrypting' }); // Wait for request callback await new Promise(res => setTimeout(res, 10)); expect(requestCb).toHaveBeenCalledTimes(1); // Confirm the callback param includes `renewal=true` and `previousToken=expiredToken` const { renewal, previousToken } = requestCb.mock.calls[0][0]; expect(renewal).toBe(true); expect(previousToken.txid).toBe('expiredTxid123'); // Grant ephemeral=false => manager calls renewPermissionOnChain const { requestID } = requestCb.mock.calls[0][0]; await manager.grantPermission({ requestID, ephemeral: false }); await expect(promise).resolves.toBe(true); expect(renewSpy).toHaveBeenCalledTimes(1); // The first arg is the old token, second is request, etc. expect(renewSpy).toHaveBeenCalledWith(expiredToken, expect.objectContaining({ originator: 'renewme.com' }), expect.any(Number), undefined); }); }); }); //# sourceMappingURL=WalletPermissionsManager.flows.test.js.map