import { Readable } from 'stream'; import { DataFactory } from 'n3'; import type { Quad } from 'n3'; import rdfDereferencer from 'rdf-dereference'; import { v4 } from 'uuid'; import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator'; import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage'; import { SOLID } from '../../../../src/util/Vocabularies'; const { literal, namedNode, quad } = DataFactory; jest.mock('uuid'); jest.mock('rdf-dereference', (): any => ({ dereference: jest.fn(), })); function quadToString(qq: Quad): string { const subPred = `<${qq.subject.value}> <${qq.predicate.value}>`; if (qq.object.termType === 'Literal') { return `${subPred} "${qq.object.value}"`; } return `${subPred} <${qq.object.value}>`; } describe('A TokenOwnershipValidator', (): void => { const rdfDereferenceMock: jest.Mocked = rdfDereferencer as any; const webId = 'http://alice.test.com/#me'; const token = 'randomlyGeneratedToken'; const tokenTriple = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token)); const tokenString = `${quadToString(tokenTriple)}.`; let storage: ExpiringStorage; let validator: TokenOwnershipValidator; function mockDereference(qq?: Quad): any { rdfDereferenceMock.dereference.mockImplementation((uri: string): any => ({ uri, quads: Readable.from(qq ? [ qq ] : []), exists: true, })); } beforeEach(async(): Promise => { const now = Date.now(); jest.spyOn(Date, 'now').mockReturnValue(now); (v4 as jest.Mock).mockReturnValue(token); const map = new Map(); storage = { get: jest.fn().mockImplementation((key: string): any => map.get(key)), set: jest.fn().mockImplementation((key: string, value: any): any => map.set(key, value)), delete: jest.fn().mockImplementation((key: string): any => map.delete(key)), } as any; mockDereference(); validator = new TokenOwnershipValidator(storage); }); it('errors if no token is stored in the storage.', async(): Promise => { // Even if the token is in the WebId, it will error since it's not in the storage mockDereference(tokenTriple); await expect(validator.handle({ webId })).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining(tokenString), details: { quad: tokenString }, })); expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(0); }); it('errors if the expected triple is missing.', async(): Promise => { // First call will add the token to the storage await expect(validator.handle({ webId })).rejects.toThrow(tokenString); expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(0); // Second call will fetch the WebId await expect(validator.handle({ webId })).rejects.toThrow(tokenString); expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(1); }); it('resolves if the WebId contains the verification triple.', async(): Promise => { mockDereference(tokenTriple); // First call will add the token to the storage await expect(validator.handle({ webId })).rejects.toThrow(tokenString); // Second call will succeed since it has the verification triple await expect(validator.handle({ webId })).resolves.toBeUndefined(); }); it('fails if the WebId contains the wrong verification triple.', async(): Promise => { const wrongQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal('wrongToken')); mockDereference(wrongQuad); // First call will add the token to the storage await expect(validator.handle({ webId })).rejects.toThrow(tokenString); // Second call will fail since it has the wrong verification triple await expect(validator.handle({ webId })).rejects.toThrow(tokenString); }); });