Skip to content

Commit

Permalink
Fix bug in getTechPreviewDwnEndpoints() that sometimes returned only …
Browse files Browse the repository at this point in the history
…1 endpoint

Signed-off-by: Frank Hinek <[email protected]>
  • Loading branch information
frankhinek committed May 28, 2023
1 parent 5c08c5d commit b259d24
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 34 deletions.
37 changes: 14 additions & 23 deletions packages/dids/tests/did-ion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,21 @@ import { DidIonApi } from '../src/did-ion.js';
const DidIon = new DidIonApi();

describe('DidIonApi', () => {
it('works', async () => {
const didState = await DidIon.create();
describe('create()', () => {
it('returns a valid didState', async () => {
const didState = await DidIon.create();

expect(didState.id).to.exist;
expect(didState.internalId).to.exist;
expect(didState.keys).to.exist;
expect(didState.id).to.exist;
expect(didState.internalId).to.exist;
expect(didState.keys).to.exist;

for (let key of didState.keys) {
expect(key.id).to.exist;
expect(key.controller).to.exist;
expect(key.publicKeyJwk).to.exist;
expect(key.privateKeyJwk).to.exist;
expect(key.type).to.exist;
}
});

it('works when using dwn configuration', async () => {
const ionCreateOptions = await DidIonApi.generateDwnConfiguration(['https://dwn.tbddev.org/dwn0']);

try {
// TODO: write specific assertions
const _didState = await DidIon.create(ionCreateOptions);
} catch(e) {
expect.fail(e.message);
}
for (let key of didState.keys) {
expect(key.id).to.exist;
expect(key.controller).to.exist;
expect(key.publicKeyJwk).to.exist;
expect(key.privateKeyJwk).to.exist;
expect(key.type).to.exist;
}
});
});
});
68 changes: 68 additions & 0 deletions packages/dids/tests/tech-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { DwnServiceEndpoint } from '../src/types.js';

import { expect } from 'chai';

import { DidIonApi } from '../src/did-ion.js';

describe('Tech Preview', function () {
describe('generateDwnConfiguration()', () => {
it('returns keys and services with two DWN URLs', async () => {
const ionCreateOptions = await DidIonApi.generateDwnConfiguration([
'https://dwn.tbddev.test/dwn0',
'https://dwn.tbddev.test/dwn1'
]);

expect(ionCreateOptions).to.have.property('keys');
expect(ionCreateOptions.keys).to.have.lengthOf(2);
let encryptionKey = ionCreateOptions.keys!.find(key => key.id === 'enc');
expect(encryptionKey).to.exist;
let authorizationKey = ionCreateOptions.keys!.find(key => key.id === 'authz');
expect(authorizationKey).to.exist;

expect(ionCreateOptions).to.have.property('services');
expect(ionCreateOptions.services).to.have.lengthOf(1);

const [ service ] = ionCreateOptions.services!;
expect(service.id).to.equal('dwn');
expect(service).to.have.property('serviceEndpoint');

const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint;
expect(serviceEndpoint).to.have.property('nodes');
expect(serviceEndpoint.nodes).to.have.lengthOf(2);
expect(serviceEndpoint).to.have.property('messageAuthorizationKeys');
expect(serviceEndpoint!.messageAuthorizationKeys![0]).to.equal(`#${authorizationKey!.id}`);
expect(serviceEndpoint).to.have.property('recordEncryptionKeys');
expect(serviceEndpoint!.recordEncryptionKeys![0]).to.equal(`#${encryptionKey!.id}`);
});

it('returns keys and services with one DWN URLs', async () => {
const ionCreateOptions = await DidIonApi.generateDwnConfiguration([
'https://dwn.tbddev.test/dwn0'
]);

const [ service ] = ionCreateOptions.services!;
expect(service.id).to.equal('dwn');
expect(service).to.have.property('serviceEndpoint');

const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint;
expect(serviceEndpoint).to.have.property('nodes');
expect(serviceEndpoint.nodes).to.have.lengthOf(1);
expect(serviceEndpoint).to.have.property('messageAuthorizationKeys');
expect(serviceEndpoint).to.have.property('recordEncryptionKeys');
});

it('returns keys and services with 0 DWN URLs', async () => {
const ionCreateOptions = await DidIonApi.generateDwnConfiguration([]);

const [ service ] = ionCreateOptions.services!;
expect(service.id).to.equal('dwn');
expect(service).to.have.property('serviceEndpoint');

const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint;
expect(serviceEndpoint).to.have.property('nodes');
expect(serviceEndpoint.nodes).to.have.lengthOf(0);
expect(serviceEndpoint).to.have.property('messageAuthorizationKeys');
expect(serviceEndpoint).to.have.property('recordEncryptionKeys');
});
});
});
24 changes: 13 additions & 11 deletions packages/web5/src/web5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,9 @@ export class Web5 {

if (!profile) {
const dwnUrls = options.techPreview?.dwnEndpoints || await Web5.getTechPreviewDwnEndpoints();
let ionCreateOptions;

if (dwnUrls.length > 0) {
ionCreateOptions = await DidIonApi.generateDwnConfiguration(dwnUrls);
}

const ionCreateOptions = await DidIonApi.generateDwnConfiguration(dwnUrls);
const defaultProfileDid = await this.did.create('ion', ionCreateOptions);

// setting id & name as the app's did to make migration easier
profile = await profileApi.createProfile({
name : appDidState.id,
Expand Down Expand Up @@ -122,15 +118,19 @@ export class Web5 {
static async getTechPreviewDwnEndpoints(): Promise<string[]> {
const response = await fetch('https://dwn.tbddev.org/.well-known/did.json');

// Return an empty array if dwn.tbddev.org is not responding.
if (!response.ok) { return []; }

const didDoc = await response.json();
const [ service ] = didUtils.getServices(didDoc, { id: '#dwn', type: 'DecentralizedWebNode' });
const { nodes } = <DwnServiceEndpoint>service.serviceEndpoint;

// allocate up to 2 nodes for a user.
const numNodesToAllocate = Math.min(Math.floor(nodes.length / 2), 2);
const dwnUrls = new Set([]);
const dwnUrls = new Set<string>();
let attempts = 0;
const numNodesToAllocate = Math.min(nodes.length, 2);

for (let i = 0; i < numNodesToAllocate; i += 1) {
while(dwnUrls.size < numNodesToAllocate && attempts < nodes.length) {
const nodeIdx = getRandomInt(0, nodes.length);
const dwnUrl = nodes[nodeIdx];

Expand All @@ -139,9 +139,11 @@ export class Web5 {
if (healthCheck.status === 200) {
dwnUrls.add(dwnUrl);
}
} catch(e) {
// ignore healthcheck failures and try the next node.
} catch(e: unknown) {
// Ignore healthcheck failures and try the next node.
}

attempts++;
}

return Array.from(dwnUrls);
Expand Down
14 changes: 14 additions & 0 deletions packages/web5/tests/chai-plugins.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference types="chai" />

declare namespace Chai {

// For BDD API
interface Assertion extends LanguageChains, NumericComparison, TypeComparison {
url: Assertion;
}

// For Assert API
interface AssertStatic {
isUrl: (actual: any) => Assertion;
}
}
150 changes: 150 additions & 0 deletions packages/web5/tests/tech-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import sinon from 'sinon';
import chai, { expect } from 'chai';

import { chaiUrl } from './test-utils/chai-plugins.js';
import { Web5 } from '../src/web5.js';

chai.use(chaiUrl);

describe('Tech Preview', () => {

describe('web5.getTechPreviewDwnEndpoints()', () => {

let fetchStub: sinon.SinonStub;

let mockDwnEndpoints: Array<string>;

let tbdWellKnownOkResponse = {
status : 200,
ok : true,
json : async () => Promise.resolve({
id : 'did:web:dwn.tbddev.org',
service : [
{
id : '#dwn',
serviceEndpoint : {
nodes: mockDwnEndpoints
},
type: 'DecentralizedWebNode'
}
]
})
};

let tbdWellKnownBadResponse = {
status : 400,
ok : false
};

let dwnServerHealthOkResponse = {
status : 200,
ok : true,
json : async () => Promise.resolve({ok: true})
};

let dwnServerHealthBadResponse = {
status : 400,
ok : false
};

beforeEach(() => {
mockDwnEndpoints = [
'https://dwn.tbddev.test/dwn0',
'https://dwn.tbddev.test/dwn1',
'https://dwn.tbddev.test/dwn2',
'https://dwn.tbddev.test/dwn3',
'https://dwn.tbddev.test/dwn4',
'https://dwn.tbddev.test/dwn5',
'https://dwn.tbddev.test/dwn6'
];

fetchStub = sinon.stub(globalThis as any, 'fetch');

fetchStub.callsFake((url) => {
if (url === 'https://dwn.tbddev.org/.well-known/did.json') {
return Promise.resolve(tbdWellKnownOkResponse);
} else if (url.endsWith('/health')) {
return Promise.resolve(dwnServerHealthOkResponse);
}
});
});

afterEach(() => {
fetchStub.restore();
});

it('returns an array', async () => {
const dwnEndpoints = await Web5.getTechPreviewDwnEndpoints();

expect(dwnEndpoints).to.be.an('array');
});

it('returns valid DWN endpoint URLs', async () => {
const dwnEndpoints = await Web5.getTechPreviewDwnEndpoints();

// There must be at one URL to check or else this test always passes.
expect(dwnEndpoints).to.have.length.greaterThan(0);

dwnEndpoints.forEach(endpoint => {
expect(endpoint).to.be.a.url;
expect(mockDwnEndpoints).to.include(endpoint);
});
});

it('returns 2 DWN endpoints if at least 2 are healthy', async function() {
const promises = Array(50).fill(0).map(() => Web5.getTechPreviewDwnEndpoints());

const results = await Promise.all(promises);

results.forEach(result => {
expect(result).to.be.an('array').that.has.lengthOf(2);
});
});

it('returns 1 DWN endpoints if only 1 is healthy', async function() {
mockDwnEndpoints = [
'https://dwn.tbddev.test/dwn0'
];

const promises = Array(50).fill(0).map(() => Web5.getTechPreviewDwnEndpoints());

const results = await Promise.all(promises);

results.forEach(result => {
expect(result).to.be.an('array').that.has.lengthOf(1);
});
});

it('returns 0 DWN endpoints if none are healthy', async function() {
// Stub fetch to simulate dwn.tbddev.org responding but all of the hosted DWN Server reporting not healthy.
fetchStub.restore();
fetchStub = sinon.stub(globalThis as any, 'fetch');
fetchStub.callsFake((url) => {
if (url === 'https://dwn.tbddev.org/.well-known/did.json') {
return Promise.resolve(tbdWellKnownOkResponse);
} else if (url.endsWith('/health')) {
return Promise.resolve(dwnServerHealthBadResponse);
}
});

const dwnEndpoints = await Web5.getTechPreviewDwnEndpoints();

expect(dwnEndpoints).to.be.an('array').that.has.lengthOf(0);
});

it('returns 0 DWN endpoints if dwn.tbddev.org is not responding', async function() {
// Stub fetch to simulate dwn.tbddev.org responding but all of the hosted DWN Server reporting not healthy.
fetchStub.restore();
fetchStub = sinon.stub(globalThis as any, 'fetch');
fetchStub.callsFake((url) => {
if (url === 'https://dwn.tbddev.org/.well-known/did.json') {
return Promise.resolve(tbdWellKnownBadResponse);
}
});

const dwnEndpoints = await Web5.getTechPreviewDwnEndpoints();

expect(dwnEndpoints).to.be.an('array').that.has.lengthOf(0);
});
});
});
59 changes: 59 additions & 0 deletions packages/web5/tests/test-utils/chai-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Chai plugin for validating URLs.
*
* This function adds two types of URL validation methods to Chai:
* 1. For the BDD "expect" API: `expect(string).to.be.a.url;`
* 2. For the Assert API: `assert.isUrl(string);`
*
* @param {Chai.ChaiStatic} chai - The Chai library object.
* @param {Chai.ChaiUtils} utils - The Chai Utilities object.
*
* @example
* // BDD API example:
* import chai, { expect } from 'chai';
* import chaiUrl from './chai-plugins.js';
* chai.use(chaiUrl);
*
* describe('My Test Suite', () => {
* it('should validate the URL', () => {
* const url = 'https://example.org';
* expect(url).to.be.a.url;
* });
* });
*
* @example
* // Assert API example:
* import chai, { assert } from 'chai';
* import chaiUrl from './chai-plugins.js';
* chai.use(chaiUrl);
*
* describe('My Test Suite', () => {
* it('should validate the URL', () => {
* const url = 'https://example.org';
* assert.isUrl(url);
* });
* });
*/
export const chaiUrl: Chai.ChaiPlugin = function(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) {
const assert = chai.assert;
function isValidUrl() {
const obj = utils.flag(this, 'object') as string;
let isUrl = true;
try {
new URL(obj);
} catch (err) {
isUrl = false;
}
this.assert(
isUrl,
'expected #{this} to be a valid URL',
'expected #{this} not to be a valid URL'
);
}

// Add the property to the BDD "expect" API.
utils.addProperty(chai.Assertion.prototype, 'url', isValidUrl);

// Add the method to the Assert API.
assert.isUrl = (actual) => (new chai.Assertion(actual)).to.be.a.url;
};

0 comments on commit b259d24

Please sign in to comment.