From 1e764419fb4535b0e171d3225ab091140b53e1c9 Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Wed, 5 Oct 2022 21:40:02 -0700 Subject: [PATCH] retab auto-verifier.ts and auto-verifier.test.ts --- src/client/tests/webapp/auto-verifier.test.ts | 1046 ++++++++--------- src/client/webapp/auto-verifier.ts | 4 +- 2 files changed, 525 insertions(+), 525 deletions(-) diff --git a/src/client/tests/webapp/auto-verifier.test.ts b/src/client/tests/webapp/auto-verifier.test.ts index 71e9519..9fc361a 100644 --- a/src/client/tests/webapp/auto-verifier.test.ts +++ b/src/client/tests/webapp/auto-verifier.test.ts @@ -5,546 +5,546 @@ import Util from '../../webapp/util'; jest.mock('@electron/remote', () => ({ getGlobal: jest.fn() })); class BasicWE implements WithEquals { - static fromIds(ids: number[]): BasicWE[] { - return ids.map(id => new BasicWE(id)); - } + static fromIds(ids: number[]): BasicWE[] { + return ids.map(id => new BasicWE(id)); + } - static fromCombos(combos: { id: string | number, extra: string | null }[]): BasicWE[] { - return combos.map(({ id, extra }) => new BasicWE(id, extra)); - } + static fromCombos(combos: { id: string | number, extra: string | null }[]): BasicWE[] { + return combos.map(({ id, extra }) => new BasicWE(id, extra)); + } - id: string; - constructor(public id_: string | number, public readonly extra: string | null = null) { this.id = id_ + '' } + id: string; + constructor(public id_: string | number, public readonly extra: string | null = null) { this.id = id_ + '' } - equals(other: BasicWE) { - return this.id == other.id && this.extra == other.extra; - } + equals(other: BasicWE) { + return this.id == other.id && this.extra == other.extra; + } } describe('getChanges tests', () => { - test('both null', () => { - const primary = null; - const trusted = null; - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: [], - updated: [], - deleted: [], - }); - }); + test('both null', () => { + const primary = null; + const trusted = null; + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: [], + updated: [], + deleted: [], + }); + }); - test('null primary gives trusted as added', () => { - const primary = null; - const trusted = BasicWE.fromIds([2, 3, 4]); - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: BasicWE.fromIds([2, 3, 4]), - updated: [], - deleted: [], - }); - }); - - test('null trusted gives primary as deleted', () => { - const primary = BasicWE.fromIds([2, 3, 4]); - const trusted = null; - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: [], - updated: [], - deleted: BasicWE.fromIds([2, 3, 4]), - }); - }); + test('null primary gives trusted as added', () => { + const primary = null; + const trusted = BasicWE.fromIds([2, 3, 4]); + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: BasicWE.fromIds([2, 3, 4]), + updated: [], + deleted: [], + }); + }); + + test('null trusted gives primary as deleted', () => { + const primary = BasicWE.fromIds([2, 3, 4]); + const trusted = null; + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: [], + updated: [], + deleted: BasicWE.fromIds([2, 3, 4]), + }); + }); - test('equal', () => { - const primary = BasicWE.fromIds([2, 3, 4]); - const trusted = BasicWE.fromIds([2, 3, 4]); - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: [], - updated: [], - deleted: [], - }); - }); + test('equal', () => { + const primary = BasicWE.fromIds([2, 3, 4]); + const trusted = BasicWE.fromIds([2, 3, 4]); + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: [], + updated: [], + deleted: [], + }); + }); - test('added', () => { - const primary = BasicWE.fromIds([2, 3, 4]); - const trusted = BasicWE.fromIds([2, 3, 4, 5, 6]); - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: BasicWE.fromIds([5, 6]), - updated: [], - deleted: [], - }); - }); + test('added', () => { + const primary = BasicWE.fromIds([2, 3, 4]); + const trusted = BasicWE.fromIds([2, 3, 4, 5, 6]); + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: BasicWE.fromIds([5, 6]), + updated: [], + deleted: [], + }); + }); - test('changed', () => { - const primary = BasicWE.fromCombos([ { id: 2, extra: 'a' }, { id: 3, extra: 'b' }, { id: 4, extra: 'c' }, { id: 5, extra: 'd' } ]); - const trusted = BasicWE.fromCombos([ { id: 2, extra: '*' }, { id: 3, extra: 'b' }, { id: 4, extra: '*' }, { id: 5, extra: 'd' } ]); - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: [], - updated: [ - { oldDataPoint: new BasicWE(2, 'a'), newDataPoint: new BasicWE(2, '*') }, - { oldDataPoint: new BasicWE(4, 'c'), newDataPoint: new BasicWE(4, '*') }, - ], - deleted: [], - }); - }); + test('changed', () => { + const primary = BasicWE.fromCombos([ { id: 2, extra: 'a' }, { id: 3, extra: 'b' }, { id: 4, extra: 'c' }, { id: 5, extra: 'd' } ]); + const trusted = BasicWE.fromCombos([ { id: 2, extra: '*' }, { id: 3, extra: 'b' }, { id: 4, extra: '*' }, { id: 5, extra: 'd' } ]); + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: [], + updated: [ + { oldDataPoint: new BasicWE(2, 'a'), newDataPoint: new BasicWE(2, '*') }, + { oldDataPoint: new BasicWE(4, 'c'), newDataPoint: new BasicWE(4, '*') }, + ], + deleted: [], + }); + }); - test('deleted', () => { - const primary = BasicWE.fromIds([2, 3, 4]); - const trusted = BasicWE.fromIds([2]); - const changes = AutoVerifier.getChanges(primary, trusted); - expect(changes).toEqual({ - added: [], - updated: [], - deleted: BasicWE.fromIds([3, 4]), - }); - }); + test('deleted', () => { + const primary = BasicWE.fromIds([2, 3, 4]); + const trusted = BasicWE.fromIds([2]); + const changes = AutoVerifier.getChanges(primary, trusted); + expect(changes).toEqual({ + added: [], + updated: [], + deleted: BasicWE.fromIds([3, 4]), + }); + }); }); describe('constructor/factory tests', () => { - test('constructor', () => { - const av = new AutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(null); - expect(av.trustedStatus).toBe('none') - }); + test('constructor', () => { + const av = new AutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(null); + expect(av.trustedStatus).toBe('none') + }); - test('createStandardListAutoVerifier', () => { - const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(null); - expect(av.trustedStatus).toBe('none') - }); + test('createStandardListAutoVerifier', () => { + const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(null); + expect(av.trustedStatus).toBe('none') + }); - test('createStandardSingleAutoVerifier', () => { - const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(null); - expect(av.trustedStatus).toBe('none') - }); + test('createStandardSingleAutoVerifier', () => { + const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(null); + expect(av.trustedStatus).toBe('none') + }); }); describe('fetchAndVerifyIfNeeded tests', () => { - class ManualPromise { - promise: Promise; - resolve: (value: T) => void; - reject: (reason?: any) => void; - - constructor() { - this.resolve = undefined as unknown as (value: T) => void; - this.reject = undefined as unknown as (reason?: any) => void; - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - } - - function getManualsAndMocks(): { - nextPrimary: () => ManualPromise, - nextTrusted: () => ManualPromise, - nextEnsureTrustedFuncReady: () => ManualPromise, - nextVerify: () => ManualPromise, - primaryFunc: jest.Mock, []>, - trustedFunc: jest.Mock, []>, - ensureTrustedFuncReadyFunc: jest.Mock, []>, - verifyFunc: jest.Mock, [primaryResult: BasicWE | null, trustedResult: BasicWE | null]>, - } { - const manuals: { - primary: ManualPromise[], - trusted: ManualPromise[], - ensureTrustedFuncReady: ManualPromise[], - verify: ManualPromise[], - } = { - primary: [], - trusted: [], - ensureTrustedFuncReady: [], - verify: [], - }; - - const calls: { - primary: number, - trusted: number, - ensureTrustedFuncReady: number, - verify: number, - } = { - primary: 0, - trusted: 0, - ensureTrustedFuncReady: 0, - verify: 0 - }; - - function getNext(manualList: ManualPromise[]) { - const next = new ManualPromise(); - manualList.push(next); - return next; - } - - return { - nextPrimary: () => getNext(manuals.primary), - nextTrusted: () => getNext(manuals.trusted), - nextEnsureTrustedFuncReady: () => getNext(manuals.ensureTrustedFuncReady), - nextVerify: () => getNext(manuals.verify), - primaryFunc: jest.fn(() => manuals.primary[calls.primary++]!.promise), - trustedFunc: jest.fn(() => manuals.trusted[calls.trusted++]!.promise), - ensureTrustedFuncReadyFunc: jest.fn(() => manuals.ensureTrustedFuncReady[calls.ensureTrustedFuncReady++]!.promise), - verifyFunc: jest.fn((_primaryResult: BasicWE | null, _trustedResult: BasicWE | null) => manuals.verify[calls.verify++]!.promise), - } - } - - async function disjoint() { - await Util.sleep(0); - } - - test('primary null, then trusted with value - cache miss, return from server', async () => { - const { - nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - const trusted = nextTrusted(); - const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); - const verify = nextVerify(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('fetching'); - expect(primaryFunc).toHaveBeenCalled(); - expect(trustedFunc).toHaveBeenCalled(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - primary.resolve(null); - await disjoint(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - ensureTrustedFuncReady.resolve(); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(verifyFunc).toHaveBeenCalledTimes(0); - trusted.resolve(new BasicWE(2)); - await disjoint(); - - expect(verifyFunc).toHaveBeenCalledWith(null, new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(result).toBeUndefined(); - verify.resolve(true); - await disjoint(); - - expect(result).toEqual(new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verified'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - }); - - test('primary with value, then trusted with value - cache hit, verify with server', async () => { - const { - nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - const trusted = nextTrusted(); - const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); - const verify = nextVerify(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('fetching'); - expect(primaryFunc).toHaveBeenCalled(); - expect(trustedFunc).toHaveBeenCalled(); // TODO: Is this the source of the problem? - trustedFunc needs to wait to be called until ensureTrustedFuncReadyFunc is resolved - expect(result).toBeUndefined(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - primary.resolve(new BasicWE(2)); - await disjoint(); - - expect(result).toEqual(new BasicWE(2)); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - ensureTrustedFuncReady.resolve(); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(verifyFunc).toHaveBeenCalledTimes(0); - trusted.resolve(new BasicWE(2)); - await disjoint(); - - expect(verifyFunc).toHaveBeenCalledWith(new BasicWE(2), new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - verify.resolve(true); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verified'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - }); - - test('primary null, then trusted with null - cache miss, server confirms miss', async () => { - const { - nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - const trusted = nextTrusted(); - const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); - const verify = nextVerify(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('fetching'); - expect(primaryFunc).toHaveBeenCalled(); - expect(trustedFunc).toHaveBeenCalled(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - primary.resolve(null); - await disjoint(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - ensureTrustedFuncReady.resolve(); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(verifyFunc).toHaveBeenCalledTimes(0); - trusted.resolve(null); - await disjoint(); - - expect(verifyFunc).toHaveBeenCalledWith(null, null); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(result).toBeUndefined(); - verify.resolve(true); - await disjoint(); - - expect(result).toBeNull(); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('none'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - }); - - test('primary with value, then trusted with null - cache hit, server deleted', async () => { - const { - nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - const trusted = nextTrusted(); - const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); - const verify = nextVerify(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('fetching'); - expect(primaryFunc).toHaveBeenCalled(); - expect(trustedFunc).toHaveBeenCalled(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - expect(result).toBeUndefined(); - primary.resolve(new BasicWE(2)); - await disjoint(); - - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); - expect(result).toEqual(new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - ensureTrustedFuncReady.resolve(); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(verifyFunc).toHaveBeenCalledTimes(0); - trusted.resolve(null); - await disjoint(); - - expect(verifyFunc).toHaveBeenCalledWith(new BasicWE(2), null); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - verify.resolve(true); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('none'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - }); - - test('primary with null, lazy - lazy cache miss, need to ping server', async () => { - const { - nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - const trusted = nextTrusted(); - const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); - const verify = nextVerify(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(true); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(null); // trustedFunc will never be called - expect(av.trustedStatus).toBe('none'); - expect(primaryFunc).toHaveBeenCalled(); - - expect(trustedFunc).toHaveBeenCalledTimes(0); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - primary.resolve(null); - await disjoint(); - - expect(trustedFunc).toHaveBeenCalled(); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); // notably, we will never publicly see it in 'fetching' - - ensureTrustedFuncReady.resolve(); - await disjoint(); - - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(verifyFunc).toHaveBeenCalledTimes(0); - trusted.resolve(new BasicWE(2)); - await disjoint(); - - expect(verifyFunc).toHaveBeenCalledWith(null, new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verifying'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - - expect(result).toBeUndefined(); - verify.resolve(true); - await disjoint(); - - expect(result).toEqual(new BasicWE(2)); - expect(av.primaryPromise).toBe(null); - expect(av.trustedPromise).toBe(trusted.promise); - expect(av.trustedStatus).toBe('verified'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(1); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); - expect(verifyFunc).toHaveBeenCalledTimes(1); - }); - - test('primary with value, lazy - lazy cache hit, no need to ping server', async () => { - const { - nextPrimary, - primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc - } = getManualsAndMocks(); - - const primary = nextPrimary(); - - const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); - const resultPromise = av.fetchAndVerifyIfNeeded(true); - let result: BasicWE | null | undefined = undefined; - resultPromise.then(value => { result = value; }); - - expect(av.primaryPromise).toBe(primary.promise); - expect(av.trustedPromise).toBe(null); // trustedFunc will never be called - expect(av.trustedStatus).toBe('none'); - expect(primaryFunc).toHaveBeenCalled(); - - expect(result).toBeUndefined(); - primary.resolve(new BasicWE(2)); - await disjoint(); - - expect(result).toEqual(new BasicWE(2)); - expect(av.trustedStatus).toBe('none'); - - expect(primaryFunc).toHaveBeenCalledTimes(1); - expect(trustedFunc).toHaveBeenCalledTimes(0); - expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); - expect(verifyFunc).toHaveBeenCalledTimes(0); - }); - - // Make sure to do try/catch errors/rejections, verification failures, unverifies in the middle - // Multiple tryResolveTrustedPromises + class ManualPromise { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: any) => void; + + constructor() { + this.resolve = undefined as unknown as (value: T) => void; + this.reject = undefined as unknown as (reason?: any) => void; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + } + + function getManualsAndMocks(): { + nextPrimary: () => ManualPromise, + nextTrusted: () => ManualPromise, + nextEnsureTrustedFuncReady: () => ManualPromise, + nextVerify: () => ManualPromise, + primaryFunc: jest.Mock, []>, + trustedFunc: jest.Mock, []>, + ensureTrustedFuncReadyFunc: jest.Mock, []>, + verifyFunc: jest.Mock, [primaryResult: BasicWE | null, trustedResult: BasicWE | null]>, + } { + const manuals: { + primary: ManualPromise[], + trusted: ManualPromise[], + ensureTrustedFuncReady: ManualPromise[], + verify: ManualPromise[], + } = { + primary: [], + trusted: [], + ensureTrustedFuncReady: [], + verify: [], + }; + + const calls: { + primary: number, + trusted: number, + ensureTrustedFuncReady: number, + verify: number, + } = { + primary: 0, + trusted: 0, + ensureTrustedFuncReady: 0, + verify: 0 + }; + + function getNext(manualList: ManualPromise[]) { + const next = new ManualPromise(); + manualList.push(next); + return next; + } + + return { + nextPrimary: () => getNext(manuals.primary), + nextTrusted: () => getNext(manuals.trusted), + nextEnsureTrustedFuncReady: () => getNext(manuals.ensureTrustedFuncReady), + nextVerify: () => getNext(manuals.verify), + primaryFunc: jest.fn(() => manuals.primary[calls.primary++]!.promise), + trustedFunc: jest.fn(() => manuals.trusted[calls.trusted++]!.promise), + ensureTrustedFuncReadyFunc: jest.fn(() => manuals.ensureTrustedFuncReady[calls.ensureTrustedFuncReady++]!.promise), + verifyFunc: jest.fn((_primaryResult: BasicWE | null, _trustedResult: BasicWE | null) => manuals.verify[calls.verify++]!.promise), + } + } + + async function disjoint() { + await Util.sleep(0); + } + + test('primary null, then trusted with value - cache miss, return from server', async () => { + const { + nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + const trusted = nextTrusted(); + const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); + const verify = nextVerify(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('fetching'); + expect(primaryFunc).toHaveBeenCalled(); + expect(trustedFunc).toHaveBeenCalled(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + primary.resolve(null); + await disjoint(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + ensureTrustedFuncReady.resolve(); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(verifyFunc).toHaveBeenCalledTimes(0); + trusted.resolve(new BasicWE(2)); + await disjoint(); + + expect(verifyFunc).toHaveBeenCalledWith(null, new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(result).toBeUndefined(); + verify.resolve(true); + await disjoint(); + + expect(result).toEqual(new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verified'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + }); + + test('primary with value, then trusted with value - cache hit, verify with server', async () => { + const { + nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + const trusted = nextTrusted(); + const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); + const verify = nextVerify(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('fetching'); + expect(primaryFunc).toHaveBeenCalled(); + expect(trustedFunc).toHaveBeenCalled(); // TODO: Is this the source of the problem? - trustedFunc needs to wait to be called until ensureTrustedFuncReadyFunc is resolved + expect(result).toBeUndefined(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + primary.resolve(new BasicWE(2)); + await disjoint(); + + expect(result).toEqual(new BasicWE(2)); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + ensureTrustedFuncReady.resolve(); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(verifyFunc).toHaveBeenCalledTimes(0); + trusted.resolve(new BasicWE(2)); + await disjoint(); + + expect(verifyFunc).toHaveBeenCalledWith(new BasicWE(2), new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + verify.resolve(true); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verified'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + }); + + test('primary null, then trusted with null - cache miss, server confirms miss', async () => { + const { + nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + const trusted = nextTrusted(); + const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); + const verify = nextVerify(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('fetching'); + expect(primaryFunc).toHaveBeenCalled(); + expect(trustedFunc).toHaveBeenCalled(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + primary.resolve(null); + await disjoint(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + ensureTrustedFuncReady.resolve(); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(verifyFunc).toHaveBeenCalledTimes(0); + trusted.resolve(null); + await disjoint(); + + expect(verifyFunc).toHaveBeenCalledWith(null, null); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(result).toBeUndefined(); + verify.resolve(true); + await disjoint(); + + expect(result).toBeNull(); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('none'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + }); + + test('primary with value, then trusted with null - cache hit, server deleted', async () => { + const { + nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + const trusted = nextTrusted(); + const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); + const verify = nextVerify(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('fetching'); + expect(primaryFunc).toHaveBeenCalled(); + expect(trustedFunc).toHaveBeenCalled(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + expect(result).toBeUndefined(); + primary.resolve(new BasicWE(2)); + await disjoint(); + + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); + expect(result).toEqual(new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + ensureTrustedFuncReady.resolve(); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(verifyFunc).toHaveBeenCalledTimes(0); + trusted.resolve(null); + await disjoint(); + + expect(verifyFunc).toHaveBeenCalledWith(new BasicWE(2), null); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + verify.resolve(true); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('none'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + }); + + test('primary with null, lazy - lazy cache miss, need to ping server', async () => { + const { + nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + const trusted = nextTrusted(); + const ensureTrustedFuncReady = nextEnsureTrustedFuncReady(); + const verify = nextVerify(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(true); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(null); // trustedFunc will never be called + expect(av.trustedStatus).toBe('none'); + expect(primaryFunc).toHaveBeenCalled(); + + expect(trustedFunc).toHaveBeenCalledTimes(0); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + primary.resolve(null); + await disjoint(); + + expect(trustedFunc).toHaveBeenCalled(); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled(); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); // notably, we will never publicly see it in 'fetching' + + ensureTrustedFuncReady.resolve(); + await disjoint(); + + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(verifyFunc).toHaveBeenCalledTimes(0); + trusted.resolve(new BasicWE(2)); + await disjoint(); + + expect(verifyFunc).toHaveBeenCalledWith(null, new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verifying'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + + expect(result).toBeUndefined(); + verify.resolve(true); + await disjoint(); + + expect(result).toEqual(new BasicWE(2)); + expect(av.primaryPromise).toBe(null); + expect(av.trustedPromise).toBe(trusted.promise); + expect(av.trustedStatus).toBe('verified'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(1); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1); + expect(verifyFunc).toHaveBeenCalledTimes(1); + }); + + test('primary with value, lazy - lazy cache hit, no need to ping server', async () => { + const { + nextPrimary, + primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc + } = getManualsAndMocks(); + + const primary = nextPrimary(); + + const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); + const resultPromise = av.fetchAndVerifyIfNeeded(true); + let result: BasicWE | null | undefined = undefined; + resultPromise.then(value => { result = value; }); + + expect(av.primaryPromise).toBe(primary.promise); + expect(av.trustedPromise).toBe(null); // trustedFunc will never be called + expect(av.trustedStatus).toBe('none'); + expect(primaryFunc).toHaveBeenCalled(); + + expect(result).toBeUndefined(); + primary.resolve(new BasicWE(2)); + await disjoint(); + + expect(result).toEqual(new BasicWE(2)); + expect(av.trustedStatus).toBe('none'); + + expect(primaryFunc).toHaveBeenCalledTimes(1); + expect(trustedFunc).toHaveBeenCalledTimes(0); + expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0); + expect(verifyFunc).toHaveBeenCalledTimes(0); + }); + + // Make sure to do try/catch errors/rejections, verification failures, unverifies in the middle + // Multiple tryResolveTrustedPromises }); diff --git a/src/client/webapp/auto-verifier.ts b/src/client/webapp/auto-verifier.ts index c93e951..f96b26c 100644 --- a/src/client/webapp/auto-verifier.ts +++ b/src/client/webapp/auto-verifier.ts @@ -202,7 +202,7 @@ export class AutoVerifier { /* istanbul ignore next */ if (debug) LOG.debug(fetchId + ": created trusted promise, set to 'fetching'"); this.trustedStatus = 'fetching'; - // TODO: The solution may be to merge the ensureTrustedFuncReady into the trustedPromise + // TODO: The solution may be to merge the ensureTrustedFuncReady into the trustedPromise this.trustedPromise = this.trustedFunc(); } @@ -270,7 +270,7 @@ export class AutoVerifier { new Error(), ); if (this.trustedPromise === null) { - /* istanbul ignore next */ + /* istanbul ignore next */ if (debug) LOG.debug(fetchId + ': re-fetching since trustedPromise was null'); this.trustedStatus = 'fetching'; this.trustedPromise = this.trustedFunc();