UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

202 lines 7.84 kB
// SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai'; import { describe, it } from 'mocha'; import { IntervalLock } from '../../../src/core/lock/interval-lock.js'; import { LockHolder } from '../../../src/core/lock/lock-holder.js'; import { NamespaceName } from '../../../src/types/namespace/namespace-name.js'; import { LockAcquisitionError } from '../../../src/core/lock/lock-acquisition-error.js'; import { StatusCodes } from 'http-status-codes'; import { Duration } from '../../../src/core/time/duration.js'; describe('IntervalLock', () => { it('should ignore a renew conflict when latest lease is still held by the same lock holder', async () => { const namespace = NamespaceName.of('lock-conflict-test'); const lockHolder = LockHolder.of('lock-user'); const leaseName = 'lock-conflict-test'; let readCallCounter = 0; let renewCallCounter = 0; let scheduleCallCounter = 0; const initialLease = createLease(namespace, leaseName, lockHolder.toJson(), '1'); const latestLease = createLease(namespace, leaseName, lockHolder.toJson(), '2'); const renewalService = { isScheduled: async () => false, schedule: async () => { scheduleCallCounter++; return 99; }, cancel: async () => true, cancelAll: async () => new Map(), calculateRenewalDelay: () => Duration.ofSeconds(1), }; const leases = { create: async () => { throw new Error('not used'); }, delete: async () => { throw new Error('not used'); }, read: async () => { readCallCounter++; return readCallCounter === 1 ? initialLease : latestLease; }, renew: async () => { renewCallCounter++; throw createConflictError(); }, transfer: async () => { throw new Error('not used'); }, }; const namespaces = { create: async () => true, delete: async () => true, list: async () => [], has: async () => true, }; const k8 = { namespaces: () => namespaces, leases: () => leases, }; const k8Factory = { getK8: () => k8, default: () => k8, }; const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20); await lock.renew(); expect(readCallCounter).to.equal(2); expect(renewCallCounter).to.equal(1); expect(scheduleCallCounter).to.equal(1); }); it('should fail renew when conflict resolution reads a lease owned by another holder', async () => { const namespace = NamespaceName.of('lock-conflict-fail-test'); const lockHolder = LockHolder.of('lock-user'); const otherLockHolder = LockHolder.of('other-user'); const leaseName = 'lock-conflict-fail-test'; let readCallCounter = 0; const initialLease = createLease(namespace, leaseName, lockHolder.toJson(), '1'); const conflictingLease = createLease(namespace, leaseName, otherLockHolder.toJson(), '2'); const renewalService = { isScheduled: async () => false, schedule: async () => 99, cancel: async () => true, cancelAll: async () => new Map(), calculateRenewalDelay: () => Duration.ofSeconds(1), }; const leases = { create: async () => { throw new Error('not used'); }, delete: async () => { throw new Error('not used'); }, read: async () => { readCallCounter++; return readCallCounter === 1 ? initialLease : conflictingLease; }, renew: async () => { throw createConflictError(); }, transfer: async () => { throw new Error('not used'); }, }; const namespaces = { create: async () => true, delete: async () => true, list: async () => [], has: async () => true, }; const k8 = { namespaces: () => namespaces, leases: () => leases, }; const k8Factory = { getK8: () => k8, default: () => k8, }; const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20); await expect(lock.renew()).to.be.rejectedWith(LockAcquisitionError); }); it('should stop renewal gracefully when namespace is being terminated (403 Forbidden)', async () => { const namespace = NamespaceName.of('lock-namespace-terminating-test'); const lockHolder = LockHolder.of('lock-user'); const leaseName = 'lock-namespace-terminating-test'; let createCallCounter = 0; let cancelCallCounter = 0; let cancelledScheduleId; const renewalService = { isScheduled: async () => false, schedule: async () => 99, cancel: async (scheduleId) => { cancelCallCounter++; cancelledScheduleId = scheduleId; return true; }, cancelAll: async () => new Map(), calculateRenewalDelay: () => Duration.ofSeconds(1), }; const leases = { create: async () => { createCallCounter++; if (createCallCounter === 1) { return createLease(namespace, leaseName, lockHolder.toJson(), '1'); } throw createForbiddenError(); }, delete: async () => { throw new Error('not used'); }, read: async () => undefined, renew: async () => { throw new Error('not used'); }, transfer: async () => { throw new Error('not used'); }, }; const namespaces = { create: async () => true, delete: async () => true, list: async () => [], has: async () => true, }; const k8 = { namespaces: () => namespaces, leases: () => leases, }; const k8Factory = { getK8: () => k8, default: () => k8, }; const lock = new IntervalLock(k8Factory, renewalService, lockHolder, namespace, leaseName, 20); // First renew: creates lease successfully and sets scheduleId = 99 await lock.renew(); expect(createCallCounter).to.equal(1); // Second renew: namespace is being terminated → 403 → should cancel schedule and return without error await lock.renew(); expect(createCallCounter).to.equal(2); expect(cancelCallCounter).to.equal(1); expect(cancelledScheduleId).to.equal(99); }); }); function createLease(namespace, leaseName, holderIdentity, resourceVersion) { return { namespace, leaseName, holderIdentity, durationSeconds: 20, acquireTime: new Date(), renewTime: new Date(), resourceVersion, }; } function createConflictError() { const error = new Error('Conflict while replacing lease'); error.meta = { statusCode: StatusCodes.CONFLICT }; return error; } function createForbiddenError() { const error = new Error('Forbidden: namespace is being terminated'); error.meta = { statusCode: StatusCodes.FORBIDDEN }; return error; } //# sourceMappingURL=interval-lock.test.js.map