@river-build/sdk
Version:
For more details, visit the following resources:
786 lines • 40.9 kB
JavaScript
/**
* @group main
*/
import { dlog, check } from '@river-build/dlog';
import { isDefined } from '../../check';
import { DecryptionStatus, GroupEncryptionAlgorithmId } from '@river-build/encryption';
import { makeUserStreamId, makeUserSettingsStreamId, makeUserMetadataStreamId, makeUserInboxStreamId, makeUniqueChannelStreamId, addressFromUserId, makeUniqueMediaStreamId, } from '../../id';
import { makeDonePromise, makeTestClient, makeUniqueSpaceStreamId, waitFor, getChannelMessagePayload, makeRandomUserAddress, cloneTestClient, } from '../testUtils';
import { ChannelMessageSchema, SyncOp, SyncStreamsResponseSchema, CancelSyncResponseSchema, MediaInfoSchema, } from '@river-build/proto';
import { create, toBinary } from '@bufbuild/protobuf';
import { vi } from 'vitest';
import { make_ChannelPayload_Message, make_MemberPayload_KeyFulfillment, make_MemberPayload_KeySolicitation, } from '../../types';
import { deriveKeyAndIV } from '../../crypto_utils';
import { nanoid } from 'nanoid';
const log = dlog('csb:test');
const createMockSyncGenerator = (shouldFail, updateEmitted) => {
let syncCanceled = false;
let syncStarted = false;
const generatorFunction = () => {
if (shouldFail()) {
updateEmitted?.();
syncStarted = false;
syncCanceled = false;
throw new TypeError('fetch failed');
}
if (syncCanceled) {
log('emitting close');
return Promise.resolve(create(SyncStreamsResponseSchema, {
syncId: 'mockSyncId',
syncOp: SyncOp.SYNC_CLOSE,
}));
}
if (!syncStarted) {
syncStarted = true;
log('emitting new');
return Promise.resolve(create(SyncStreamsResponseSchema, {
syncId: 'mockSyncId',
syncOp: SyncOp.SYNC_NEW,
}));
}
else {
log('emitting junk');
updateEmitted?.();
return Promise.resolve(create(SyncStreamsResponseSchema, {
syncId: 'mockSyncId',
syncOp: SyncOp.SYNC_UPDATE,
stream: { events: [], nextSyncCookie: {} },
}));
}
};
generatorFunction.setSyncCancelled = () => {
syncCanceled = true;
};
return generatorFunction;
};
function makeMockSyncGenerator(generator) {
const obj = {
[Symbol.asyncIterator]: async function* asyncGenerator() {
while (true) {
yield generator();
}
},
};
return obj;
}
describe('clientTest', () => {
let bobsClient;
let alicesClient;
beforeEach(async () => {
bobsClient = await makeTestClient();
alicesClient = await makeTestClient();
});
afterEach(async () => {
await bobsClient.stop();
await alicesClient.stop();
});
test('bobTalksToHimself-noflush', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
const channelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, channelId)).resolves.not.toThrow();
const stream = await bobsClient.waitForStream(channelId);
await bobsClient.sendMessage(channelId, 'Hello, world!');
await waitFor(() => {
const event = stream.view.timeline.find((e) => getChannelMessagePayload(e.localEvent?.channelMessage) === 'Hello, world!');
expect(event).toBeDefined();
expect(event?.remoteEvent).toBeDefined();
});
await bobsClient.stopSync();
log('pass1 done');
await expect(bobCanReconnect(bobsClient.signerContext)).resolves.not.toThrow();
log('pass2 done');
});
test('bobSendsBadPrevMiniblockHashShouldResolve', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
const channelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, channelId)).resolves.not.toThrow();
await bobsClient.waitForStream(channelId);
// hand construct a message, (don't do this normally! just use sendMessage(..))
const algorithm = GroupEncryptionAlgorithmId.GroupEncryption; // algorithm doesn't matter here, don't copy paste
const channelMessage = create(ChannelMessageSchema, {
payload: {
case: 'post',
value: {
content: {
case: 'text',
value: { body: 'Hello world', mentions: [], attachments: [] },
},
},
},
});
const encrypted = await bobsClient.encryptGroupEvent(toBinary(ChannelMessageSchema, channelMessage), channelId, algorithm);
check(isDefined(encrypted), 'encrypted should be defined');
const message = make_ChannelPayload_Message(encrypted);
await expect(bobsClient.makeEventWithHashAndAddToStream(channelId, message, Uint8Array.from(Array(32).fill(0)))).resolves.not.toThrow();
});
test('clientsCanBeClosedNoSync', async () => { });
test('clientsRetryOnSyncErrorDuringStart', async () => {
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
const done = makeDonePromise();
let syncOpCount = 0;
const generator = createMockSyncGenerator(() => syncOpCount++ < 2);
const spy = vi
.spyOn(alicesClient.rpcClient, 'syncStreams')
.mockImplementation((_request, _options) => {
return makeMockSyncGenerator(generator);
});
alicesClient.on('streamSyncActive', (active) => {
if (active) {
done.done();
}
});
alicesClient.startSync();
await expect(done.expectToSucceed()).resolves.not.toThrow();
const cancelSyncSpy = vi
.spyOn(alicesClient.rpcClient, 'cancelSync')
.mockImplementation((request, _options) => {
log('mocked cancelSync', request);
generator.setSyncCancelled();
return Promise.resolve(create(CancelSyncResponseSchema, {}));
});
await alicesClient.stopSync();
spy.mockRestore();
cancelSyncSpy.mockRestore();
});
test('clientsResetsRetryCountAfterSyncSuccess', async () => {
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
const done = makeDonePromise();
let syncOpCount = 0;
const generator = createMockSyncGenerator(() => syncOpCount > 2 && syncOpCount < 4, () => syncOpCount++);
const spy = vi
.spyOn(alicesClient.rpcClient, 'syncStreams')
.mockImplementation((_request, _options) => {
return makeMockSyncGenerator(generator);
});
alicesClient.on('streamSyncActive', (active) => {
if (syncOpCount > 3 && active) {
done.done();
}
});
alicesClient.startSync();
await expect(done.expectToSucceed()).resolves.not.toThrow();
const cancelSyncSpy = vi
.spyOn(alicesClient.rpcClient, 'cancelSync')
.mockImplementation((request, _options) => {
log('mocked cancelSync', request);
generator.setSyncCancelled();
return Promise.resolve(create(CancelSyncResponseSchema, {}));
});
await alicesClient.stopSync();
spy.mockRestore();
cancelSyncSpy.mockRestore();
});
test('clientCreatesStreamsForNewUser', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
expect(bobsClient.streams.size()).toEqual(4);
expect(bobsClient.streams.get(makeUserSettingsStreamId(bobsClient.userId))).toBeDefined();
expect(bobsClient.streams.get(makeUserStreamId(bobsClient.userId))).toBeDefined();
expect(bobsClient.streams.get(makeUserInboxStreamId(bobsClient.userId))).toBeDefined();
expect(bobsClient.streams.get(makeUserMetadataStreamId(bobsClient.userId))).toBeDefined();
});
test('clientCreatesStreamsForExistingUser', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
const bobsAnotherClient = await cloneTestClient(bobsClient);
await expect(bobsAnotherClient.initializeUser()).resolves.not.toThrow();
expect(bobsAnotherClient.streams.size()).toEqual(4);
expect(bobsAnotherClient.streams.get(makeUserSettingsStreamId(bobsClient.userId))).toBeDefined();
expect(bobsAnotherClient.streams.get(makeUserStreamId(bobsClient.userId))).toBeDefined();
expect(bobsAnotherClient.streams.get(makeUserMetadataStreamId(bobsClient.userId))).toBeDefined();
});
test('bobCanSendMemberPayload', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
expect(bobsClient.userSettingsStreamId).toBeDefined();
// fulfillment without matching solicitation should fail
let payload = make_MemberPayload_KeyFulfillment({
deviceKey: 'foo',
userAddress: makeRandomUserAddress(),
sessionIds: ['bar'],
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).rejects.toThrow('INVALID_ARGUMENT');
// solicitation with no keys should fail
payload = make_MemberPayload_KeySolicitation({
deviceKey: 'foo',
sessionIds: [],
fallbackKey: 'baz',
isNewDevice: false,
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).rejects.toThrow('INVALID_ARGUMENT');
// solicitation with empty key should fail
payload = make_MemberPayload_KeySolicitation({
deviceKey: 'foo',
sessionIds: [''],
fallbackKey: 'baz',
isNewDevice: false,
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).rejects.toThrow('INVALID_ARGUMENT');
// solicitation for isNewDevice should resolve
payload = make_MemberPayload_KeySolicitation({
deviceKey: 'foo',
sessionIds: [],
fallbackKey: 'baz',
isNewDevice: true,
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).resolves.not.toThrow();
// fulfillment should resolve
payload = make_MemberPayload_KeyFulfillment({
deviceKey: 'foo',
userAddress: addressFromUserId(bobsClient.userId),
sessionIds: [],
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).resolves.not.toThrow();
await waitFor(() => {
const lastEvent = bobsClient.streams
.get(bobsClient.userSettingsStreamId)
?.view.timeline.filter((x) => x.remoteEvent?.event.payload.case === 'memberPayload')
.at(-1);
expect(lastEvent).toBeDefined();
check(lastEvent?.remoteEvent?.event.payload.case === 'memberPayload', '??');
check(lastEvent?.remoteEvent?.event.payload.value.content.case === 'keyFulfillment', '??');
expect(lastEvent?.remoteEvent?.event.payload.value.content.value.deviceKey).toBe('foo');
});
// fulfillment with empty session ids should now fail
payload = make_MemberPayload_KeyFulfillment({
deviceKey: 'foo',
userAddress: addressFromUserId(bobsClient.userId),
sessionIds: [],
});
await expect(bobsClient.makeEventAndAddToStream(bobsClient.userSettingsStreamId, payload)).rejects.toThrow('PERMISSION_DENIED');
});
test('bobCreatesUnamedSpaceAndStream', async () => {
log('bobCreatesUnamedSpace');
// Bob gets created, creates a space without providing an ID, and a channel without providing an ID.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const spaceId = makeUniqueSpaceStreamId();
const spacePromise = bobsClient.createSpace(spaceId);
await expect(spacePromise).resolves.not.toThrow();
const channelName = 'Bobs channel';
const channelTopic = 'Bobs channel topic';
const channelId = makeUniqueChannelStreamId(spaceId);
await expect(bobsClient.createChannel(spaceId, channelName, channelTopic, channelId)).resolves.not.toThrow();
await expect(bobsClient.stopSync()).resolves.not.toThrow();
});
const bobCanReconnect = async (signer) => {
const bobsAnotherClient = await makeTestClient({ context: signer, deviceId: 'd2' });
const bobsOneMoreAnotherClient = await makeTestClient({ context: signer, deviceId: 'd3' });
const eventDecryptedPromise = makeDonePromise();
const streamInitializedPromise = makeDonePromise();
let channelWithContentId;
const onEventDecrypted = (streamId, contentKind, event) => {
try {
log(event);
const clearEvent = event.decryptedContent;
check(clearEvent.kind === 'channelMessage');
if (clearEvent?.content.payload?.case === 'post' &&
clearEvent?.content.payload?.value?.content?.case === 'text') {
expect(clearEvent?.content.payload?.value?.content.value?.body).toContain('Hello, again!');
expect(streamId).toBe(channelWithContentId);
//This done should be inside of the if statement to be sure that check happened.
eventDecryptedPromise.done();
}
}
catch (e) {
log('onEventDecrypted error', e);
eventDecryptedPromise.reject(e);
}
};
const channelWithContentIdPromise = makeDonePromise();
const onStreamInitialized = (streamId, streamKind) => {
log('streamInitialized', streamId, streamKind);
try {
if (streamKind === 'channelContent') {
channelWithContentId = streamId;
channelWithContentIdPromise.done();
const channel = bobsAnotherClient.stream(streamId);
log('!!!channel content');
log(channel.view);
channel.view.timeline.forEach((x) => {
log('@@@', {
c1: x.remoteEvent?.event.payload.case,
v1: x.remoteEvent?.event.payload.value,
c2: x.remoteEvent?.event.payload.value?.content.case,
b2: x.remoteEvent?.event.payload.value?.content.value,
});
});
const messages = channel.view.timeline.filter((x) => x.remoteEvent?.event.payload.case === 'channelPayload' &&
x.remoteEvent?.event.payload.value.content.case === 'message');
expect(messages).toHaveLength(1);
//This done should be inside of the if statement to be sure that check happened.
streamInitializedPromise.done();
}
}
catch (e) {
log('onStreamInitialized error', e);
streamInitializedPromise.reject(e);
}
};
bobsAnotherClient.on('streamInitialized', onStreamInitialized);
await expect(bobsAnotherClient.initializeUser()).resolves.not.toThrow();
bobsAnotherClient.startSync();
bobsOneMoreAnotherClient.on('eventDecrypted', onEventDecrypted);
await expect(bobsOneMoreAnotherClient.initializeUser()).resolves.not.toThrow();
bobsOneMoreAnotherClient.startSync();
await channelWithContentIdPromise.expectToSucceed();
expect(channelWithContentId).toBeDefined();
await bobsAnotherClient.sendMessage(channelWithContentId, 'Hello, again!');
await streamInitializedPromise.expectToSucceed();
await eventDecryptedPromise.expectToSucceed();
await bobsAnotherClient.stopSync();
return 'done';
};
test('bobSendsSingleMessage', async () => {
log('bobSendsSingleMessage');
// Bob gets created, creates a space, and creates a channel.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
const bobsChannelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, bobsChannelId)).resolves.not.toThrow();
// Bob can send a message.
const stream = await bobsClient.waitForStream(bobsChannelId);
await expect(bobsClient.sendMessage(bobsChannelId, 'Hello, world from Bob!')).resolves.not.toThrow();
await waitFor(() => {
const event = stream.view.timeline.find((e) => getChannelMessagePayload(e.localEvent?.channelMessage) ===
'Hello, world from Bob!');
expect(event).toBeDefined();
expect(event?.remoteEvent).toBeDefined();
});
log('bobSendsSingleMessage done');
});
test('bobPinsAMessage', async () => {
log('bobPinsAMessage');
// Bob gets created, creates a space, and creates a channel.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
const bobsChannelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, bobsChannelId)).resolves.not.toThrow();
// Bob can send a message.
const channelStream = await bobsClient.waitForStream(bobsChannelId);
const { eventId: eventId_1 } = await bobsClient.sendMessage(bobsChannelId, 'Hello, world from Bob!');
const { eventId: eventId_2 } = await bobsClient.sendMessage(bobsChannelId, 'event 2');
const { eventId: eventId_3 } = await bobsClient.sendMessage(bobsChannelId, 'event 3');
await bobsClient.pin(bobsChannelId, eventId_1);
await waitFor(() => {
const pin = channelStream.view.membershipContent.pins.find((e) => e.event.hashStr === eventId_1);
expect(pin).toBeDefined();
expect(pin?.event.decryptedContent?.kind).toBe('channelMessage');
if (pin?.event.decryptedContent?.kind === 'channelMessage') {
expect(getChannelMessagePayload(pin?.event.decryptedContent?.content)).toBe('Hello, world from Bob!');
}
});
await expect(bobsClient.pin(bobsChannelId, eventId_1)).rejects.toThrow('message is already pinned');
await bobsClient.unpin(bobsChannelId, eventId_1);
await waitFor(() => {
expect(channelStream.view.membershipContent.pins.length).toBe(0);
});
await bobsClient.pin(bobsChannelId, eventId_1);
await bobsClient.pin(bobsChannelId, eventId_2);
await bobsClient.pin(bobsChannelId, eventId_3);
await bobsClient.debugForceMakeMiniblock(bobsChannelId, { forceSnapshot: true });
await waitFor(() => {
const pin = channelStream.view.membershipContent.pins.find((e) => e.event.hashStr === eventId_1);
expect(pin).toBeDefined();
});
await waitFor(() => {
const pin = channelStream.view.membershipContent.pins.find((e) => e.event.hashStr === eventId_2);
expect(pin).toBeDefined();
});
await waitFor(() => {
const pin = channelStream.view.membershipContent.pins.find((e) => e.event.hashStr === eventId_3);
expect(pin).toBeDefined();
});
await bobsClient.unpin(bobsChannelId, eventId_1);
await bobsClient.unpin(bobsChannelId, eventId_2);
await bobsClient.debugForceMakeMiniblock(bobsChannelId, { forceSnapshot: true });
const rawStream = await bobsClient.getStream(bobsChannelId);
expect(rawStream).toBeDefined();
expect(rawStream?.membershipContent.pins.length).toBe(1);
expect(rawStream?.membershipContent.pins[0].event.hashStr).toBe(eventId_3);
log('bobSendsSingleMessage done');
});
test('bobAndAliceConverse', async () => {
log('bobAndAliceConverse');
// Bob gets created, creates a space, and creates a channel.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
const bobsChannelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, bobsChannelId)).resolves.not.toThrow();
await expect(bobsClient.waitForStream(bobsChannelId)).resolves.not.toThrow();
// Alice gest created.
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
alicesClient.startSync();
// Alice can't sent a message to Bob's channel.
// TODO: since Alice doesn't sync Bob's channel, this fails fast (i.e. stream is unknown to Alice's client).
// It would be interesting for Alice to sync this channel, and then try to send a message.
await expect(alicesClient.sendMessage(bobsChannelId, 'Hello, world from Alice!')).rejects.toThrow();
// Alice waits for invite to Bob's channel.
const aliceJoined = makeDonePromise();
alicesClient.on('userInvitedToStream', (streamId) => {
void (async () => {
try {
expect(streamId).toBe(bobsChannelId);
await expect(alicesClient.joinStream(streamId)).resolves.not.toThrow();
aliceJoined.done();
}
catch (e) {
aliceJoined.reject(e);
}
})();
});
// Bob invites Alice to his channel.
await bobsClient.inviteUser(bobsChannelId, alicesClient.userId);
await aliceJoined.expectToSucceed();
const aliceGetsMessage = makeDonePromise();
const bobGetsMessage = makeDonePromise();
const conversation = [
'Hello, world from Bob!',
'Hello, Alice!',
'Hello, Bob!',
'Weather nice?',
'Sun and rain!',
'Coffee or tea?',
'Both!',
];
alicesClient.on('eventDecrypted', (streamId, contentKind, event) => {
const channelId = streamId;
const content = event.decryptedContent.content;
expect(content).toBeDefined();
log('eventDecrypted', 'Alice', channelId);
void (async () => {
try {
expect(channelId).toBe(bobsChannelId);
const clearEvent = event.decryptedContent;
check(clearEvent.kind === 'channelMessage');
if (clearEvent.content.payload?.case === 'post' &&
clearEvent.content.payload?.value?.content?.case === 'text') {
const body = clearEvent.content.payload?.value?.content.value?.body;
// @ts-ignore
expect(conversation).toContain(body);
if (body === 'Hello, Alice!') {
await alicesClient.sendMessage(channelId, 'Hello, Bob!');
}
else if (body === 'Weather nice?') {
await alicesClient.sendMessage(channelId, 'Sun and rain!');
}
else if (body === 'Coffee or tea?') {
await alicesClient.sendMessage(channelId, 'Both!');
aliceGetsMessage.done();
}
}
}
catch (e) {
log('streamInitialized error', e);
aliceGetsMessage.reject(e);
}
})();
});
bobsClient.on('eventDecrypted', (streamId, contentKind, event) => {
const channelId = streamId;
const content = event.decryptedContent.content;
expect(content).toBeDefined();
log('eventDecrypted', 'Bob', channelId);
void (async () => {
try {
expect(channelId).toBe(bobsChannelId);
const clearEvent = event.decryptedContent;
check(clearEvent.kind === 'channelMessage');
if (clearEvent.content?.payload?.case === 'post' &&
clearEvent.content?.payload?.value?.content?.case === 'text') {
const body = clearEvent.content?.payload?.value?.content.value?.body;
// @ts-ignore
expect(conversation).toContain(body);
if (body === 'Hello, Bob!') {
await bobsClient.sendMessage(channelId, 'Weather nice?');
}
else if (body === 'Sun and rain!') {
await bobsClient.sendMessage(channelId, 'Coffee or tea?');
}
else if (body === 'Both!') {
bobGetsMessage.done();
}
}
}
catch (e) {
log('streamInitialized error', e);
bobGetsMessage.reject(e);
}
})();
});
await expect(bobsClient.sendMessage(bobsChannelId, 'Hello, world from Bob!')).resolves.not.toThrow();
await expect(bobsClient.sendMessage(bobsChannelId, 'Hello, Alice!')).resolves.not.toThrow();
log('Waiting for Alice to get messages...');
await aliceGetsMessage.expectToSucceed();
log('Waiting for Bob to get messages...');
await bobGetsMessage.expectToSucceed();
log('bobAndAliceConverse All done!');
});
test('bobUploadsDeviceKeys', async () => {
log('bobUploadsDeviceKeys');
// Bob gets created, starts syncing, and uploads his device keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsUserId = bobsClient.userId;
const bobSelfInbox = makeDonePromise();
bobsClient.once('userDeviceKeyMessage', (streamId, userId, userDevice) => {
log('userDeviceKeyMessage for Bob', streamId, userId, userDevice);
bobSelfInbox.runAndDone(() => {
expect(streamId).toBe(bobUserMetadataStreamId);
expect(userId).toBe(bobsUserId);
expect(userDevice.deviceKey).toBeDefined();
});
});
const bobUserMetadataStreamId = bobsClient.userMetadataStreamId;
await bobSelfInbox.expectToSucceed();
});
test('bobDownloadsOwnDeviceKeys', async () => {
log('bobDownloadsOwnDeviceKeys');
// Bob gets created, starts syncing, and uploads his device keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsUserId = bobsClient.userId;
const bobSelfInbox = makeDonePromise();
bobsClient.once('userDeviceKeyMessage', (streamId, userId, deviceKeys) => {
log('userDeviceKeyMessage for Bob', streamId, userId, deviceKeys);
bobSelfInbox.runAndDone(() => {
expect(streamId).toBe(bobUserMetadataStreamId);
expect(userId).toBe(bobsUserId);
expect(deviceKeys.deviceKey).toBeDefined();
});
});
const bobUserMetadataStreamId = bobsClient.userMetadataStreamId;
await bobSelfInbox.expectToSucceed();
const deviceKeys = await bobsClient.downloadUserDeviceInfo([bobsUserId]);
expect(deviceKeys[bobsUserId]).toBeDefined();
});
test('bobDownloadsAlicesDeviceKeys', async () => {
log('bobDownloadsAlicesDeviceKeys');
// Bob gets created, starts syncing, and uploads his device keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
alicesClient.startSync();
const alicesUserId = alicesClient.userId;
const alicesSelfInbox = makeDonePromise();
alicesClient.once('userDeviceKeyMessage', (streamId, userId, deviceKeys) => {
log('userDeviceKeyMessage for Alice', streamId, userId, deviceKeys);
alicesSelfInbox.runAndDone(() => {
expect(streamId).toBe(aliceUserMetadataStreamId);
expect(userId).toBe(alicesUserId);
expect(deviceKeys.deviceKey).toBeDefined();
});
});
const aliceUserMetadataStreamId = alicesClient.userMetadataStreamId;
const deviceKeys = await bobsClient.downloadUserDeviceInfo([alicesUserId]);
expect(deviceKeys[alicesUserId]).toBeDefined();
});
test('bobDownloadsAlicesAndOwnDeviceKeys', async () => {
log('bobDownloadsAlicesAndOwnDeviceKeys');
// Bob, Alice get created, starts syncing, and uploads respective device keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
alicesClient.startSync();
const bobsUserId = bobsClient.userId;
const alicesUserId = alicesClient.userId;
const bobSelfInbox = makeDonePromise();
// bobs client should sync userDeviceKeyMessage twice (once for alice, once for bob)
bobsClient.on('userDeviceKeyMessage', (streamId, userId, deviceKeys) => {
log('userDeviceKeyMessage', streamId, userId, deviceKeys);
bobSelfInbox.runAndDone(() => {
expect([bobUserMetadataStreamId, aliceUserMetadataStreamId]).toContain(streamId);
expect([bobsUserId, alicesUserId]).toContain(userId);
expect(deviceKeys.deviceKey).toBeDefined();
});
});
const aliceUserMetadataStreamId = alicesClient.userMetadataStreamId;
const bobUserMetadataStreamId = bobsClient.userMetadataStreamId;
const deviceKeys = await bobsClient.downloadUserDeviceInfo([alicesUserId, bobsUserId]);
expect(Object.keys(deviceKeys).length).toEqual(2);
expect(deviceKeys[alicesUserId]).toBeDefined();
expect(deviceKeys[bobsUserId]).toBeDefined();
});
test('bobDownloadsAlicesAndOwnFallbackKeys', async () => {
log('bobDownloadsAlicesAndOwnFallbackKeys');
// Bob, Alice get created, starts syncing, and uploads respective device keys, including
// fallback keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
alicesClient.startSync();
const bobsUserId = bobsClient.userId;
const alicesUserId = alicesClient.userId;
const bobSelfInbox = makeDonePromise();
// bobs client should sync userDeviceKeyMessage twice (once for alice, once for bob)
bobsClient.on('userDeviceKeyMessage', (streamId, userId, deviceKeys) => {
log('userDeviceKeyMessage', streamId, userId, deviceKeys);
bobSelfInbox.runAndDone(() => {
expect([bobUserMetadataStreamId, aliceUserMetadataStreamId]).toContain(streamId);
expect([bobsUserId, alicesUserId]).toContain(userId);
expect(deviceKeys.deviceKey).toBeDefined();
});
});
const aliceUserMetadataStreamId = alicesClient.userMetadataStreamId;
const bobUserMetadataStreamId = bobsClient.userMetadataStreamId;
const fallbackKeys = await bobsClient.downloadUserDeviceInfo([alicesUserId, bobsUserId]);
expect(fallbackKeys).toBeDefined();
expect(Object.keys(fallbackKeys).length).toEqual(2);
});
test('bobDownloadsAlicesFallbackKeys', async () => {
log('bobDownloadsAlicesFallbackKeys');
// Bob, Alice get created, starts syncing, and uploads respective device keys, including
// fallback keys.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
alicesClient.startSync();
await waitFor(() => {
// @ts-ignore
expect(alicesClient.decryptionExtensions?.status).toEqual(DecryptionStatus.done);
});
const alicesUserId = alicesClient.userId;
const fallbackKeys = await bobsClient.downloadUserDeviceInfo([alicesUserId]);
expect(Object.keys(fallbackKeys)).toContain(alicesUserId);
expect(Object.keys(fallbackKeys).length).toEqual(1);
expect(fallbackKeys[alicesUserId].map((k) => k.fallbackKey)).toContain(alicesClient.userDeviceKey().fallbackKey);
});
test('aliceLeavesChannelsWhenLeavingSpace', async () => {
log('aliceLeavesChannelsWhenLeavingSpace');
// Bob gets created, creates a space, and creates a channel.
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const bobsSpaceId = makeUniqueSpaceStreamId();
await expect(bobsClient.createSpace(bobsSpaceId)).resolves.not.toThrow();
const bobsChannelId = makeUniqueChannelStreamId(bobsSpaceId);
const bobsChannelName = 'Bobs channel';
const bobsChannelTopic = 'Bobs channel topic';
await expect(bobsClient.createChannel(bobsSpaceId, bobsChannelName, bobsChannelTopic, bobsChannelId)).resolves.not.toThrow();
await expect(bobsClient.waitForStream(bobsChannelId)).resolves.not.toThrow();
// Alice gest created.
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
alicesClient.startSync();
await expect(alicesClient.joinStream(bobsSpaceId)).resolves.not.toThrow();
await expect(alicesClient.joinStream(bobsChannelId)).resolves.not.toThrow();
const channelStream = bobsClient.stream(bobsChannelId);
expect(channelStream).toBeDefined();
await waitFor(() => {
expect(channelStream?.view.getMembers().membership.joinedUsers).toContain(alicesClient.userId);
});
// leave the space
await expect(alicesClient.leaveStream(bobsSpaceId)).resolves.not.toThrow();
// the channel should be left as well
await waitFor(() => {
expect(channelStream?.view.getMembers().membership.joinedUsers).not.toContain(alicesClient.userId);
});
await alicesClient.stopSync();
});
test('clientReturnsKnownDevicesForUserId', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
await expect(alicesClient.initializeUser()).resolves.not.toThrow();
alicesClient.startSync();
await waitFor(() => {
// @ts-ignore
expect(alicesClient.decryptionExtensions?.status).toEqual(DecryptionStatus.done);
});
await expect(bobsClient.downloadUserDeviceInfo([alicesClient.userId])).resolves.not.toThrow();
const knownDevices = await bobsClient.knownDevicesForUserId(alicesClient.userId);
expect(knownDevices.length).toBe(1);
expect(knownDevices[0].fallbackKey).toBe(alicesClient.userDeviceKey().fallbackKey);
});
// Make sure that the client only uploads device keys
// if this exact device key does not exist.
test('clientOnlyUploadsDeviceKeysOnce', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const stream = bobsClient.stream(bobsClient.userMetadataStreamId);
const waitForInitialUpload = makeDonePromise();
stream.on('userDeviceKeyMessage', () => {
waitForInitialUpload.done();
});
await waitForInitialUpload.expectToSucceed();
for (let i = 0; i < 5; i++) {
await bobsClient.uploadDeviceKeys();
}
const keys = stream.view.userMetadataContent.deviceKeys;
expect(keys).toHaveLength(1);
});
test('setUserProfilePicture', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const streamId = bobsClient.userMetadataStreamId;
const userMetadataStream = await bobsClient.waitForStream(streamId);
// assert assumptionsP
expect(userMetadataStream).toBeDefined();
// make a space image event
const mediaStreamId = makeUniqueMediaStreamId();
const image = create(MediaInfoSchema, {
mimetype: 'image/png',
filename: 'bob-1.png',
});
const { key, iv } = await deriveKeyAndIV(nanoid(128)); // if in browser please use window.crypto.subtle.generateKey
const chunkedMediaInfo = {
info: image,
streamId: mediaStreamId,
encryption: {
case: 'aesgcm',
value: { secretKey: key, iv },
},
thumbnail: undefined,
};
const { eventId } = await bobsClient.setUserProfileImage(chunkedMediaInfo);
await waitFor(() => expect(userMetadataStream.view.events.has(eventId)).toBe(true));
const decrypted = await bobsClient.getUserProfileImage(bobsClient.userId);
expect(decrypted).toBeDefined();
expect(decrypted?.info?.mimetype).toBe(image.mimetype);
expect(decrypted?.info?.filename).toBe(image.filename);
expect(decrypted?.encryption.case).toBe(chunkedMediaInfo.encryption.case);
expect(decrypted?.encryption.value?.secretKey).toBeDefined();
});
test('setUserBio', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const streamId = bobsClient.userMetadataStreamId;
const userMetadataStream = await bobsClient.waitForStream(streamId);
expect(userMetadataStream).toBeDefined();
const bio = { bio: 'Hello, world!' };
const { eventId } = await bobsClient.setUserBio(bio);
await waitFor(() => expect(userMetadataStream.view.events.has(eventId)).toBe(true));
const decrypted = await bobsClient.getUserBio(bobsClient.userId);
expect(decrypted?.bio).toStrictEqual(bio.bio);
});
test('setUserBio empty', async () => {
await expect(bobsClient.initializeUser()).resolves.not.toThrow();
bobsClient.startSync();
const streamId = bobsClient.userMetadataStreamId;
const userMetadataStream = await bobsClient.waitForStream(streamId);
expect(userMetadataStream).toBeDefined();
const bio = { bio: '' };
const { eventId } = await bobsClient.setUserBio(bio);
await waitFor(() => expect(userMetadataStream.view.events.has(eventId)).toBe(true));
const decrypted = await bobsClient.getUserBio(bobsClient.userId);
expect(decrypted?.bio).toStrictEqual(bio.bio);
});
});
//# sourceMappingURL=client.test.js.map