test: add test ofr SnodeAPI.buildRetrieveRequests

pull/2963/head
Audric Ackermann 1 year ago
parent a53299377a
commit 665f6df57e

@ -42,6 +42,8 @@ async function retrieveRequestForUs({
});
}
type NamespaceAndLastHash = { lastHash: string | null; namespace: SnodeNamespaces };
/**
* Retrieve for legacy groups are not authenticated so no need to sign the request
*/
@ -108,23 +110,31 @@ type RetrieveSubRequestType =
| UpdateExpiryOnNodeUserSubRequest
| UpdateExpiryOnNodeGroupSubRequest;
/**
* build the Array of retrieveRequests to do on the next poll, given the specified namespaces, lastHash, pubkey and hashes to bump (expiry)
* Note: exported only for testing purposes
* @param namespacesAndLastHashes
* @param pubkey
* @param ourPubkey
* @param configHashesToBump
* @returns
*/
async function buildRetrieveRequest(
lastHashes: Array<string>,
namespacesAndLastHashes: Array<NamespaceAndLastHash>,
pubkey: string,
namespaces: Array<SnodeNamespaces>,
ourPubkey: string,
configHashesToBump: Array<string> | null
) {
const isUs = pubkey === ourPubkey;
const maxSizeMap = SnodeNamespace.maxSizeMap(namespaces);
const maxSizeMap = SnodeNamespace.maxSizeMap(namespacesAndLastHashes.map(m => m.namespace));
const now = GetNetworkTime.now();
const retrieveRequestsParams: Array<RetrieveSubRequestType> = await Promise.all(
namespaces.map(async (namespace, index) => {
namespacesAndLastHashes.map(async ({ lastHash, namespace }) => {
const foundMaxSize = maxSizeMap.find(m => m.namespace === namespace)?.maxSize;
const retrieveParam = {
pubkey,
last_hash: lastHashes.at(index) || '',
last_hash: lastHash || '',
timestamp: now,
max_size: foundMaxSize,
};
@ -185,20 +195,14 @@ async function buildRetrieveRequest(
async function retrieveNextMessages(
targetNode: Snode,
lastHashes: Array<string>,
associatedWith: string,
namespaces: Array<SnodeNamespaces>,
namespacesAndLastHashes: Array<NamespaceAndLastHash>,
ourPubkey: string,
configHashesToBump: Array<string> | null
): Promise<RetrieveMessagesResultsBatched> {
if (namespaces.length !== lastHashes.length) {
throw new Error('namespaces and lasthashes does not match');
}
const rawRequests = await buildRetrieveRequest(
lastHashes,
namespacesAndLastHashes,
associatedWith,
namespaces,
ourPubkey,
configHashesToBump
);
@ -222,9 +226,12 @@ async function retrieveNextMessages(
}
// the +1 is to take care of the extra `expire` method added once user config is released
if (results.length !== namespaces.length && results.length !== namespaces.length + 1) {
if (
results.length !== namespacesAndLastHashes.length &&
results.length !== namespacesAndLastHashes.length + 1
) {
throw new Error(
`We asked for updates about ${namespaces.length} messages but got results of length ${results.length}`
`We asked for updates about ${namespacesAndLastHashes.length} messages but got results of length ${results.length}`
);
}
@ -250,7 +257,7 @@ async function retrieveNextMessages(
return results.map((result, index) => ({
code: result.code,
messages: result.body as RetrieveMessagesResultsContent,
namespace: namespaces[index],
namespace: namespacesAndLastHashes[index].namespace,
}));
} catch (e) {
window?.log?.warn('exception while parsing json of nextMessage:', e);
@ -261,4 +268,4 @@ async function retrieveNextMessages(
}
}
export const SnodeAPIRetrieve = { retrieveNextMessages };
export const SnodeAPIRetrieve = { retrieveNextMessages, buildRetrieveRequest };

@ -511,16 +511,18 @@ export class SwarmPolling {
const snodeEdkey = node.pubkey_ed25519;
try {
const prevHashes = await Promise.all(
namespaces.map(namespace => this.getLastHash(snodeEdkey, pubkey, namespace))
);
const configHashesToBump = await this.getHashesToBump(type, pubkey);
const namespacesAndLastHashes = await Promise.all(
namespaces.map(async namespace => {
const lastHash = await this.getLastHash(snodeEdkey, pubkey, namespace);
return { namespace, lastHash };
})
);
let results = await SnodeAPIRetrieve.retrieveNextMessages(
node,
prevHashes,
pubkey,
namespaces,
namespacesAndLastHashes,
UserUtils.getOurPubKeyStrFromCache(),
configHashesToBump
);

@ -0,0 +1,442 @@
import chai from 'chai';
import { beforeEach, describe } from 'mocha';
import Sinon from 'sinon';
import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import {
RetrieveGroupSubRequest,
RetrieveLegacyClosedGroupSubRequest,
RetrieveUserSubRequest,
UpdateExpiryOnNodeGroupSubRequest,
UpdateExpiryOnNodeUserSubRequest,
} from '../../../../session/apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../../session/apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces';
import { SnodeAPIRetrieve } from '../../../../session/apis/snode_api/retrieveRequest';
import { WithShortenOrExtend } from '../../../../session/apis/snode_api/types';
import { TestUtils } from '../../../test-utils';
import { expectAsyncToThrow, stubLibSessionWorker } from '../../../test-utils/utils';
const { expect } = chai;
function expectRetrieveWith({
request,
namespace,
lastHash,
maxSize,
}: {
request: RetrieveLegacyClosedGroupSubRequest | RetrieveUserSubRequest | RetrieveGroupSubRequest;
namespace: SnodeNamespaces;
lastHash: string | null;
maxSize: number;
}) {
expect(request.namespace).to.be.eq(namespace);
expect(request.last_hash).to.be.eq(lastHash);
expect(request.max_size).to.be.eq(maxSize);
}
function expectExpireWith({
request,
hashes,
shortenOrExtend,
}: {
request: UpdateExpiryOnNodeUserSubRequest | UpdateExpiryOnNodeGroupSubRequest;
hashes: Array<string>;
} & WithShortenOrExtend) {
expect(request.messageHashes).to.be.deep.eq(hashes);
expect(request.shortenOrExtend).to.be.eq(shortenOrExtend);
expect(request.expiryMs).to.be.above(GetNetworkTime.now() + 14 * 24 * 3600 * 1000 - 100);
expect(request.expiryMs).to.be.above(GetNetworkTime.now() + 14 * 24 * 3600 * 1000 + 100);
}
describe('SnodeAPI:buildRetrieveRequest', () => {
let us: PubkeyType;
beforeEach(async () => {
TestUtils.stubWindowLog();
us = TestUtils.generateFakePubKeyStr();
});
afterEach(() => {
Sinon.restore();
});
describe('us', () => {
it('with single namespace and lasthash, no hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[{ lastHash: 'lasthash', namespace: SnodeNamespaces.Default }],
us,
us,
null
);
expect(requests.length).to.be.eq(1);
const req = requests[0];
if (req.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
expectRetrieveWith({
request: req,
lastHash: 'lasthash',
maxSize: -1,
namespace: SnodeNamespaces.Default,
});
});
it('with two namespace and lasthashes, no hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.Default },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.UserContacts },
],
us,
us,
null
);
expect(requests.length).to.be.eq(2);
const req1 = requests[0];
const req2 = requests[1];
if (req1.method !== 'retrieve' || req2.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
expectRetrieveWith({
request: req1,
lastHash: 'lasthash1',
maxSize: -2,
namespace: SnodeNamespaces.Default,
});
expectRetrieveWith({
request: req2,
lastHash: 'lasthash2',
maxSize: -2,
namespace: SnodeNamespaces.UserContacts,
});
});
it('with two namespace and lasthashes, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.Default },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.UserContacts },
],
us,
us,
['hashbump1', 'hashbump2']
);
expect(requests.length).to.be.eq(3);
const req1 = requests[0];
const req2 = requests[1];
const req3 = requests[2];
if (req1.method !== 'retrieve' || req2.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
if (req3.method !== 'expire') {
throw new Error('expected expire method');
}
expectRetrieveWith({
request: req1,
lastHash: 'lasthash1',
maxSize: -2,
namespace: SnodeNamespaces.Default,
});
expectRetrieveWith({
request: req2,
lastHash: 'lasthash2',
maxSize: -2,
namespace: SnodeNamespaces.UserContacts,
});
expectExpireWith({
request: req3,
hashes: ['hashbump1', 'hashbump2'],
shortenOrExtend: '',
});
});
it('with 0 namespaces, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], us, us, [
'hashbump1',
'hashbump2',
]);
expect(requests.length).to.be.eq(1);
const req1 = requests[0];
if (req1.method !== 'expire') {
throw new Error('expected expire method');
}
expectExpireWith({
request: req1,
hashes: ['hashbump1', 'hashbump2'],
shortenOrExtend: '',
});
});
it('with 0 namespaces, 0 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], us, us, []);
expect(requests.length).to.be.eq(0);
});
it('with 0 namespaces, null hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], us, us, null);
expect(requests.length).to.be.eq(0);
});
it('throws if given an invalid user namespace to retrieve from ', async () => {
const pr = async () =>
SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.ClosedGroupKeys },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.UserContacts },
],
us,
us,
['hashbump1', 'hashbump2']
);
await expectAsyncToThrow(
pr,
`retrieveRequestForUs not a valid namespace to retrieve as us:${SnodeNamespaces.ClosedGroupKeys}`
);
});
});
describe('legacy group', () => {
let groupPk: PubkeyType;
beforeEach(() => {
groupPk = TestUtils.generateFakePubKeyStr();
});
it('with single namespace and lasthash, no hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[{ lastHash: 'lasthash', namespace: SnodeNamespaces.LegacyClosedGroup }],
groupPk,
us,
null
);
expect(requests.length).to.be.eq(1);
const req = requests[0];
if (req.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
expectRetrieveWith({
request: req,
lastHash: 'lasthash',
maxSize: -1,
namespace: SnodeNamespaces.LegacyClosedGroup,
});
});
it('with 1 namespace and lasthashes, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[{ lastHash: 'lasthash1', namespace: SnodeNamespaces.LegacyClosedGroup }],
groupPk,
us,
['hashbump1', 'hashbump2'] // legacy groups have not the possibility to bump the expire of messages
);
expect(requests.length).to.be.eq(1);
const req1 = requests[0];
if (req1.method !== 'retrieve') {
throw new Error('expected retrieve/expire method');
}
expectRetrieveWith({
request: req1,
lastHash: 'lasthash1',
maxSize: -1,
namespace: SnodeNamespaces.LegacyClosedGroup,
});
});
it('with 0 namespaces, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, [
'hashbump1',
'hashbump2',
]);
expect(requests.length).to.be.eq(0); // legacy groups have not possibility to bump expire of messages
});
it('with 0 namespaces, 0 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, []);
expect(requests.length).to.be.eq(0);
});
it('with 0 namespaces, null hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, null);
expect(requests.length).to.be.eq(0);
});
it('throws if given an invalid legacy group namespace to retrieve from ', async () => {
const pr = async () =>
SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.ClosedGroupKeys },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.UserContacts },
],
groupPk,
us,
['hashbump1', 'hashbump2']
);
await expectAsyncToThrow(
pr,
`retrieveRequestForUs not a valid namespace to retrieve as us:${SnodeNamespaces.ClosedGroupKeys}`
);
});
});
describe('group v2', () => {
let groupPk: GroupPubkeyType;
beforeEach(() => {
groupPk = TestUtils.generateFakeClosedGroupV2PkStr();
stubLibSessionWorker({});
});
it('with single namespace and lasthash, no hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[{ lastHash: 'lasthash', namespace: SnodeNamespaces.ClosedGroupInfo }],
groupPk,
us,
null
);
expect(requests.length).to.be.eq(1);
const req = requests[0];
if (req.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
expectRetrieveWith({
request: req,
lastHash: 'lasthash',
maxSize: -1,
namespace: SnodeNamespaces.ClosedGroupInfo,
});
});
it('with two namespace and lasthashes, no hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.ClosedGroupInfo },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.ClosedGroupMessages },
],
groupPk,
us,
null
);
expect(requests.length).to.be.eq(2);
const req1 = requests[0];
const req2 = requests[1];
if (req1.method !== 'retrieve' || req2.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
expectRetrieveWith({
request: req1,
lastHash: 'lasthash1',
maxSize: -2,
namespace: SnodeNamespaces.ClosedGroupInfo,
});
expectRetrieveWith({
request: req2,
lastHash: 'lasthash2',
maxSize: -2,
namespace: SnodeNamespaces.ClosedGroupMessages,
});
});
it('with two namespace and lasthashes, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.ClosedGroupInfo },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.ClosedGroupKeys },
],
groupPk,
us,
['hashbump1', 'hashbump2']
);
expect(requests.length).to.be.eq(3);
const req1 = requests[0];
const req2 = requests[1];
const req3 = requests[2];
if (req1.method !== 'retrieve' || req2.method !== 'retrieve') {
throw new Error('expected retrieve method');
}
if (req3.method !== 'expire') {
throw new Error('expected expire method');
}
expectRetrieveWith({
request: req1,
lastHash: 'lasthash1',
maxSize: -2,
namespace: SnodeNamespaces.ClosedGroupInfo,
});
expectRetrieveWith({
request: req2,
lastHash: 'lasthash2',
maxSize: -2,
namespace: SnodeNamespaces.ClosedGroupKeys,
});
expectExpireWith({
request: req3,
hashes: ['hashbump1', 'hashbump2'],
shortenOrExtend: '',
});
});
it('with 0 namespaces, 2 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, [
'hashbump1',
'hashbump2',
]);
expect(requests.length).to.be.eq(1);
const req1 = requests[0];
if (req1.method !== 'expire') {
throw new Error('expected expire method');
}
expectExpireWith({
request: req1,
hashes: ['hashbump1', 'hashbump2'],
shortenOrExtend: '',
});
});
it('with 0 namespaces, 0 hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, []);
expect(requests.length).to.be.eq(0);
});
it('with 0 namespaces, null hashesToBump ', async () => {
const requests = await SnodeAPIRetrieve.buildRetrieveRequest([], groupPk, us, null);
expect(requests.length).to.be.eq(0);
});
it('throws if given an invalid group namespace to retrieve from ', async () => {
const pr = async () =>
SnodeAPIRetrieve.buildRetrieveRequest(
[
{ lastHash: 'lasthash1', namespace: SnodeNamespaces.ClosedGroupKeys },
{ lastHash: 'lasthash2', namespace: SnodeNamespaces.UserContacts },
],
groupPk,
us,
['hashbump1', 'hashbump2']
);
await expectAsyncToThrow(
pr,
`tried to poll from a non 03 group namespace ${SnodeNamespaces.UserContacts}`
);
});
});
});
Loading…
Cancel
Save