@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
380 lines • 20.4 kB
JavaScript
;
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