Skip to content

Commit

Permalink
test coverage and lint
Browse files Browse the repository at this point in the history
  • Loading branch information
LiranCohen committed Mar 1, 2024
1 parent 333dc22 commit f9c65c1
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 75 deletions.
4 changes: 2 additions & 2 deletions packages/agent/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ export class AgentDwnApi {
const info = await this.agent.rpc.getServerInfo(dwnUrl);
if (isSubscribeMessage && !info.webSocketSupport) {
errorMessages.push({
url: dwnUrl,
message: 'web sockets not supported'
url : dwnUrl,
message : 'web sockets not supported'
});
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ export class DwnServerInfoCacheMemory implements DwnServerInfoCache {
* @returns The cached DWN ServerInfo entry or undefined if not found or expired.
*/
public async get(dwnUrl: string): Promise<ServerInfo| void> {
if (!dwnUrl) {
throw new Error('Key cannot be null or undefined');
}

return this.cache.get(dwnUrl);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import { DwnServerInfoCache, ServerInfo } from './web5-rpc-types.js';

export class DwnServerInfoCacheNoOp implements DwnServerInfoCache {

public async get(_dwnUrl: string): Promise<ServerInfo| void> {}

public async set(_dwnUrl: string, _value: ServerInfo): Promise<void> {}

public async delete(_dwnUrl: string): Promise<void> {}

public async clear(): Promise<void> {}

public async close(): Promise<void> {}
}
4 changes: 2 additions & 2 deletions packages/agent/src/prototyping/clients/http-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,10 @@ export class HttpWeb5RpcClient extends HttpDwnRpcClient implements Web5Rpc {
registrationRequirements : results.registrationRequirements,
maxFileSize : results.maxFileSize,
webSocketSupport : results.webSocketSupport,
}
};
this.serverInfoCache.set(dwnUrl, serverInfo);

return serverInfo;
return serverInfo;
} else {
throw new Error(`HTTP (${response.status}) - ${response.statusText}`);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/prototyping/clients/web5-rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ export type RpcStatus = {
export type ServerInfo = {
/** the maximum file size the user can request to store */
maxFileSize: number,
/**
/**
* an array of strings representing the server's registration requirements.
*
* ie. ['proof-of-work-sha256-v0', 'terms-of-service']
* */
registrationRequirements: string[],
/** whether web socket support is enabled on this server */
webSocketSupport: boolean,
webSocketSupport: boolean,
}

export interface DwnServerInfoCache extends KeyValueStore<string, ServerInfo| void> {}
Expand Down
155 changes: 155 additions & 0 deletions packages/agent/tests/prototyping/clients/dwn-server-info-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import sinon from 'sinon';

import chaiAsPromised from 'chai-as-promised';
import chai, { expect } from 'chai';

import { DwnServerInfoCache, ServerInfo } from '../../../src/prototyping/clients/web5-rpc-types.js';
import { DwnServerInfoCacheMemory } from '../../../src/prototyping/clients/dwn-server-info-cache-memory.js';
import { DwnServerInfoCacheLevel } from '../../../src/prototyping/clients/dwn-server-info-cache-level.js';
import { DwnServerInfoCacheNoOp } from '../../../src/prototyping/clients/dwn-server-info-cache-no-op.js';
import { AbstractLevel } from 'abstract-level';

chai.use(chaiAsPromised);

describe('DwnServerInfoCache', () => {

const cacheImplementations = [ DwnServerInfoCacheMemory, DwnServerInfoCacheLevel ];

// basic cache tests for all caching interface implementations
for (const Cache of cacheImplementations) {
describe(`interface ${Cache.name}`, () => {
let cache: DwnServerInfoCache;
let clock: sinon.SinonFakeTimers;
const exampleInfo:ServerInfo = {
maxFileSize : 100,
webSocketSupport : true,
registrationRequirements : []
};

after(() => {
sinon.restore();
});

beforeEach(() => {
clock = sinon.useFakeTimers();
cache = new Cache();
});

afterEach(async () => {
await cache.clear();
await cache.close();
clock.restore();
});

it('sets server info in cache', async () => {
const key1 = 'some-key1';
const key2 = 'some-key2';
await cache.set(key1, { ...exampleInfo });
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false

const result1 = await cache.get(key1);
expect(result1!.webSocketSupport).to.deep.equal(true);
expect(result1).to.deep.equal(exampleInfo);

const result2 = await cache.get(key2);
expect(result2!.webSocketSupport).to.deep.equal(false);
});

it('deletes from cache', async () => {
const key1 = 'some-key1';
const key2 = 'some-key2';
await cache.set(key1, { ...exampleInfo });
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false

const result1 = await cache.get(key1);
expect(result1!.webSocketSupport).to.deep.equal(true);
expect(result1).to.deep.equal(exampleInfo);

const result2 = await cache.get(key2);
expect(result2!.webSocketSupport).to.deep.equal(false);

// delete one of the keys
await cache.delete(key1);

// check results after delete
const resultAfterDelete = await cache.get(key1);
expect(resultAfterDelete).to.equal(undefined);

// key 2 still exists
const result2AfterDelete = await cache.get(key2);
expect(result2AfterDelete!.webSocketSupport).to.equal(false);
});

it('clears cache', async () => {
const key1 = 'some-key1';
const key2 = 'some-key2';
await cache.set(key1, { ...exampleInfo });
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false

const result1 = await cache.get(key1);
expect(result1!.webSocketSupport).to.deep.equal(true);
expect(result1).to.deep.equal(exampleInfo);

const result2 = await cache.get(key2);
expect(result2!.webSocketSupport).to.deep.equal(false);

// delete one of the keys
await cache.clear();

// check results after delete
const resultAfterDelete = await cache.get(key1);
expect(resultAfterDelete).to.equal(undefined);
const result2AfterDelete = await cache.get(key2);
expect(result2AfterDelete).to.equal(undefined);
});

it('returns undefined after ttl', async () => {
const key = 'some-key1';
await cache.set(key, { ...exampleInfo });

const result = await cache.get(key);
expect(result!.webSocketSupport).to.deep.equal(true);
expect(result).to.deep.equal(exampleInfo);

// wait until 15m default ttl is up
await clock.tickAsync('15:01');

const resultAfter = await cache.get(key);
expect(resultAfter).to.be.undefined;
});
});
}

describe('DwnServerInfoCacheLevel', () => {
it('should throw on unknown level error', async () => {
const mockLevel = sinon.createStubInstance(AbstractLevel);
mockLevel.get.throws('test error');
const cache = new DwnServerInfoCacheLevel({ db: mockLevel });

const getPromise = cache.get('key');
await expect(getPromise).to.eventually.be.rejectedWith('test error');
});
});

describe('DwnServerInfoCacheNoOp', () => {
// for test coverage
const cache = new DwnServerInfoCacheNoOp();

it('sets', async () => {
await cache.set('test', {
webSocketSupport : true,
maxFileSize : 100,
registrationRequirements : []
});
});
it('gets', async () => {
await cache.get('test');
});
it('delete', async () => {
await cache.delete('test');
});
it('clear', async () => {
await cache.clear();
});
});
});
145 changes: 80 additions & 65 deletions packages/agent/tests/prototyping/clients/json-rpc-socket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,51 +107,6 @@ describe('JsonRpcSocket', () => {
}
});

xit('closes subscription upon receiving a JsonRpc Error for a long running subscription', async () => {

const client = await JsonRpcSocket.connect(socketDwnUrl, { responseTimeout: 5 });
const { message } = await TestDataGenerator.generateRecordsSubscribe({ author: alice });

const requestId = cryptoUtils.randomUuid();
const subscriptionId = cryptoUtils.randomUuid();
const request = createJsonRpcSubscriptionRequest(
requestId,
'dwn.processMessage',
subscriptionId,
{ target: alice.did, message }
);

let errorCounter = 0;
let responseCounter = 0;
const responseListener = (response: JsonRpcResponse): void => {
expect(response.id).to.equal(subscriptionId);
if (response.error) {
errorCounter++;
}

if (response.result) {
responseCounter++;
}
};

const subscription = await client.subscribe(request, responseListener);
expect(subscription.response.error).to.be.undefined;
// wait for the messages to arrive

// induce positive result
const jsonResponse = createJsonRpcSuccessResponse(subscriptionId, { reply: {} });
client['socket'].emit('message', JSON.stringify(jsonResponse));

// induce error message
const errorResponse = createJsonRpcErrorResponse(subscriptionId, JsonRpcErrorCodes.InternalError, 'message');
client['socket'].emit('message', JSON.stringify(errorResponse));

await new Promise((resolve) => setTimeout(resolve, 5));
// the original response
expect(responseCounter).to.equal(1, 'response');
expect(errorCounter).to.equal(1, 'error');
});

it('only JSON RPC Methods prefixed with `rpc.subscribe.` are accepted for a subscription', async () => {
const client = await JsonRpcSocket.connect(socketDwnUrl);
const requestId = cryptoUtils.randomUuid();
Expand Down Expand Up @@ -191,26 +146,86 @@ describe('JsonRpcSocket', () => {
expect(logMessage).to.equal(`JSON RPC Socket close ${socketDwnUrl}`);
});

xit('calls onerror handler', async () => {
// test injected handler
const onErrorHandler = { onerror: ():void => {} };
const onErrorSpy = sinon.spy(onErrorHandler, 'onerror');
const client = await JsonRpcSocket.connect(socketDwnUrl, { onerror: onErrorHandler.onerror });
client['socket'].emit('error', 'some error');

await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
expect(onErrorSpy.callCount).to.equal(1, 'error');

// test default logger
const logInfoSpy = sinon.stub(console, 'error');
const defaultClient = await JsonRpcSocket.connect(socketDwnUrl);
defaultClient['socket'].emit('error', 'some error');

await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
expect(logInfoSpy.callCount).to.equal(1, 'log');
// NOTE: Temporary in lieu of a better mock of isomorphic-ws
// tests reply on Node's use of event listeners to emit an error or message over the socket.
describe('browser', () => {
if (typeof window !== 'undefined') {
xit('calls onerror handler', async () => {
});
xit('closes subscription upon receiving a JsonRpc Error for a long running subscription', async () => {
});
}
});

// extract log message from argument
const logMessage:string = logInfoSpy.args[0][0]!;
expect(logMessage).to.equal(`JSON RPC Socket error ${socketDwnUrl}`);
describe('NodeJS', function () {
if (typeof process !== 'undefined' && (process as any).browser !== true) {
it('calls onerror handler', async () => {
// test injected handler
const onErrorHandler = { onerror: ():void => {} };
const onErrorSpy = sinon.spy(onErrorHandler, 'onerror');
const client = await JsonRpcSocket.connect(socketDwnUrl, { onerror: onErrorHandler.onerror });
client['socket'].emit('error', 'some error');

await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
expect(onErrorSpy.callCount).to.equal(1, 'error');

// test default logger
const logInfoSpy = sinon.stub(console, 'error');
const defaultClient = await JsonRpcSocket.connect(socketDwnUrl);
defaultClient['socket'].emit('error', 'some error');

await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
expect(logInfoSpy.callCount).to.equal(1, 'log');

// extract log message from argument
const logMessage:string = logInfoSpy.args[0][0]!;
expect(logMessage).to.equal(`JSON RPC Socket error ${socketDwnUrl}`);
});

it('closes subscription upon receiving a JsonRpc Error for a long running subscription', async () => {

const client = await JsonRpcSocket.connect(socketDwnUrl, { responseTimeout: 5 });
const { message } = await TestDataGenerator.generateRecordsSubscribe({ author: alice });

const requestId = cryptoUtils.randomUuid();
const subscriptionId = cryptoUtils.randomUuid();
const request = createJsonRpcSubscriptionRequest(
requestId,
'dwn.processMessage',
subscriptionId,
{ target: alice.did, message }
);

let errorCounter = 0;
let responseCounter = 0;
const responseListener = (response: JsonRpcResponse): void => {
expect(response.id).to.equal(subscriptionId);
if (response.error) {
errorCounter++;
}

if (response.result) {
responseCounter++;
}
};

const subscription = await client.subscribe(request, responseListener);
expect(subscription.response.error).to.be.undefined;
// wait for the messages to arrive

// induce positive result
const jsonResponse = createJsonRpcSuccessResponse(subscriptionId, { reply: {} });
client['socket'].emit('message', JSON.stringify(jsonResponse));

// induce error message
const errorResponse = createJsonRpcErrorResponse(subscriptionId, JsonRpcErrorCodes.InternalError, 'message');
client['socket'].emit('message', JSON.stringify(errorResponse));

await new Promise((resolve) => setTimeout(resolve, 5));
// the original response
expect(responseCounter).to.equal(1, 'response');
expect(errorCounter).to.equal(1, 'error');
});
}
});
});

0 comments on commit f9c65c1

Please sign in to comment.