gRPC mock tests

This commit is contained in:
Fedor Indutny
2026-04-03 16:29:20 -07:00
committed by GitHub
parent 0dcdbd9f23
commit 229ac20549
9 changed files with 90 additions and 62 deletions
+1 -1
View File
@@ -155,7 +155,7 @@
"@react-spring/web": "10.0.3",
"@react-types/shared": "3.33.1",
"@signalapp/minimask": "1.0.1",
"@signalapp/mock-server": "19.1.0",
"@signalapp/mock-server": "20.0.1",
"@signalapp/quill-cjs": "2.1.2",
"@storybook/addon-a11y": "8.4.4",
"@storybook/addon-actions": "8.4.4",
+16 -16
View File
@@ -185,8 +185,8 @@ importers:
specifier: 1.0.1
version: 1.0.1
'@signalapp/mock-server':
specifier: 19.1.0
version: 19.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
specifier: 20.0.1
version: 20.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
'@signalapp/quill-cjs':
specifier: 2.1.2
version: 2.1.2
@@ -1692,14 +1692,14 @@ packages:
peerDependencies:
prettier: ^3.0.0
'@indutny/protopiler@3.2.1':
resolution: {integrity: sha512-u3ZVLus/9wwkojVSO0es5tppdd+2f9zA6ogax3OytOwGueDWSj+eVU6sSWYiSY/zxMSgdnKxRik0D3D36u3iWw==}
hasBin: true
'@indutny/protopiler@3.2.4':
resolution: {integrity: sha512-uBz01wZxVz8+9DeGKN0QPvViDPq0Agcs5pAF8CkivULFP/sFGWpWaC9nPWIuVC9y44RrPQS+xpp1pFYlnX7BVg==}
hasBin: true
'@indutny/protopiler@4.0.0':
resolution: {integrity: sha512-QGVXaIBNXb0QwGHeWsbIdhwph72hYpYeE2M753Y91LGUTvBumFUMDEGBT8HQk1y4c2l80nvFAMGFFCWCCtk5gw==}
hasBin: true
'@indutny/range-finder@1.3.4':
resolution: {integrity: sha512-UWNRZZCp3f97NJuhP0IgBktob+kPDFsnyKx42PDwPQsAa/ZczyeLdhfz3tD8zK1wVC+OkOXAM6YkBAgEv9Xhaw==}
@@ -3787,8 +3787,8 @@ packages:
'@signalapp/minimask@1.0.1':
resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==}
'@signalapp/mock-server@19.1.0':
resolution: {integrity: sha512-NTOLGe/G+ptVd7xUOTJiWMn5CvxoLcalSlt5ePCFwtyDiJ4HFXR6O2d9f42Elm5PpLtbPULre5G1AeqijmyzUA==}
'@signalapp/mock-server@20.0.1':
resolution: {integrity: sha512-hpWuhfl0u1yX2KsJ9wa74tR/P3/eDDfZMRnlczeaty/q6EKxk8GP2HKkI13apj/redw0fJxq/fMdIg0qoBxM0Q==}
'@signalapp/parchment-cjs@3.0.1':
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
@@ -6874,8 +6874,8 @@ packages:
intl-tel-input@26.8.1:
resolution: {integrity: sha512-6UBRifs+nGtlS03SZGxxb9uVX7MTo1ePNJ4zIxO+qiwVwJ6fW7gYTcvYVFKjBn7jlbPw2ZqjGeNLaSUEwmDvIA==}
ioredis@5.10.0:
resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==}
ioredis@5.10.1:
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
engines: {node: '>=12.22.0'}
ip-address@9.0.5:
@@ -11293,10 +11293,10 @@ snapshots:
prettier: 3.8.1
rxjs: 6.6.7
'@indutny/protopiler@3.2.1': {}
'@indutny/protopiler@3.2.4': {}
'@indutny/protopiler@4.0.0': {}
'@indutny/range-finder@1.3.4':
dependencies:
readable-stream: 4.7.0
@@ -13851,10 +13851,10 @@ snapshots:
'@signalapp/minimask@1.0.1': {}
'@signalapp/mock-server@19.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
'@signalapp/mock-server@20.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
dependencies:
'@indutny/parallel-prettier': 3.0.0(prettier@3.8.1)
'@indutny/protopiler': 3.2.1
'@indutny/protopiler': 4.0.0
'@signalapp/libsignal-client': 0.89.2
'@tus/file-store': 1.5.1
'@tus/server': 1.10.2
@@ -14439,7 +14439,7 @@ snapshots:
lodash.throttle: 4.1.1
optionalDependencies:
'@redis/client': 1.6.1
ioredis: 5.10.0
ioredis: 5.10.1
transitivePeerDependencies:
- supports-color
@@ -17547,7 +17547,7 @@ snapshots:
intl-tel-input@26.8.1: {}
ioredis@5.10.0:
ioredis@5.10.1:
dependencies:
'@ioredis/commands': 1.5.1
cluster-key-slot: 1.1.2
+17 -14
View File
@@ -7,6 +7,7 @@ import {
} from '@signalapp/libsignal-client/dist/net.js';
import { isProduction } from './util/version.std.ts';
import { drop } from './util/drop.std.ts';
import * as RemoteConfig from './RemoteConfig.dom.ts';
import type { AddPrefix, ArrayValues } from './types/Util.std.ts';
import { createLogger } from './logging/log.std.ts';
@@ -21,13 +22,9 @@ function convertToDesktopRemoteConfigKey<
export function bindRemoteConfigToLibsignalNet(
libsignalNet: Net,
appVersion: string
appVersion: string,
reconnect: () => Promise<void>
): void {
// Calls setLibsignalRemoteConfig and is reset when any libsignal remote
// config key changes. Doing that asynchronously allows the callbacks for
// multiple keys that are triggered by the same config fetch from the server
// to be coalesced into a single timeout.
let reloadRemoteConfig: NodeJS.Immediate | undefined;
const libsignalBuildVariant = isProduction(appVersion)
? BuildVariant.Production
: BuildVariant.Beta;
@@ -46,18 +43,24 @@ export function bindRemoteConfigToLibsignalNet(
Object.fromEntries(remoteConfigs)
);
libsignalNet.setRemoteConfig(remoteConfigs, libsignalBuildVariant);
reloadRemoteConfig = undefined;
};
setLibsignalRemoteConfig();
KeysExpectedByLibsignalNet.map(convertToDesktopRemoteConfigKey).forEach(
key => {
RemoteConfig.onChange(key, () => {
if (reloadRemoteConfig === undefined) {
reloadRemoteConfig = setImmediate(setLibsignalRemoteConfig);
}
});
RemoteConfig.onChange(
KeysExpectedByLibsignalNet.map(convertToDesktopRemoteConfigKey),
() => {
setLibsignalRemoteConfig();
// When linking for the first time in mock tests we start with an empty
// remote config and fetch the latest values only when we connect the
// socket. However, new values won't be applied until we reconnect so
// we need to reconnect immediately to use remote config dependent
// features like gRPC.
if (window.SignalCI) {
log.info('Reconnecting socket after remote config change');
drop(reconnect());
}
}
);
}
+29 -19
View File
@@ -135,13 +135,13 @@ type ConfigValueType = {
export type ConfigMapType = {
[key in ConfigKeyType]?: ConfigValueType;
};
export type ConfigListenerType = (value: ConfigValueType) => unknown;
type ConfigListenersMapType = {
[key: string]: Array<ConfigListenerType>;
};
type ConfigListenerType = Readonly<{
keys: Set<ConfigKeyType>;
callback: () => unknown;
}>;
let config: ConfigMapType | undefined;
const listeners: ConfigListenersMapType = {};
const listeners = new Set<ConfigListenerType>();
export type OptionsType = Readonly<{
getConfig: typeof getConfig;
@@ -155,17 +155,17 @@ export function restoreRemoteConfigFromStorage({
}
export function onChange(
key: ConfigKeyType,
fn: ConfigListenerType
keys: Array<ConfigKeyType>,
callback: () => unknown
): () => void {
const keyListeners: Array<ConfigListenerType> = get(listeners, key, []);
keyListeners.push(fn);
listeners[key] = keyListeners;
const listener: ConfigListenerType = {
keys: new Set(keys),
callback,
};
listeners.add(listener);
return () => {
if (listeners[key]) {
listeners[key] = listeners[key].filter(l => l !== fn);
}
listeners.delete(listener);
};
}
@@ -210,6 +210,8 @@ export const _refreshRemoteConfig = async ({
newConfigValues.set(name, value);
}
const changedKeys = new Set<string>();
const oldConfig = config;
let semverError = false;
config = Array.from(newConfigValues.entries()).reduce(
@@ -253,13 +255,8 @@ export const _refreshRemoteConfig = async ({
semverError = true;
}
// If enablement changes at all, notify listeners
const currentListeners = listeners[name] || [];
if (hasChanged) {
log.info(`Remote Config: Flag ${name} has changed`);
currentListeners.forEach(listener => {
listener(configValue);
});
changedKeys.add(name);
}
// Return new configuration object
@@ -271,6 +268,19 @@ export const _refreshRemoteConfig = async ({
{}
);
if (changedKeys.size !== 0) {
log.info(
`Remote Config: Flags ${[...changedKeys].join(', ')} have changed`
);
// If enablement changes at all, notify listeners
for (const { keys, callback } of listeners) {
if (!keys.isDisjointFrom(changedKeys)) {
callback();
}
}
}
if (semverError && config['desktop.internalUser']?.enabled) {
window.reduxActions.toast.showToast({
toastType: ToastType.Error,
+7 -2
View File
@@ -248,6 +248,7 @@ import { encryptConversationAttachments } from './util/encryptConversationAttach
import { DataReader, DataWriter } from './sql/Client.preload.ts';
import {
restoreRemoteConfigFromStorage,
isEnabled as isRemoteConfigValueEnabled,
getValue as getRemoteConfigValue,
onChange as onRemoteConfigChange,
maybeRefreshRemoteConfig,
@@ -1557,8 +1558,12 @@ export async function startApp(): Promise<void> {
});
// Listen for changes to the `desktop.clientExpiration` remote flag
onRemoteConfigChange('desktop.clientExpiration', ({ enabled, value }) => {
if (!enabled) {
onRemoteConfigChange(['desktop.clientExpiration'], () => {
if (!isRemoteConfigValueEnabled('desktop.clientExpiration')) {
return;
}
const value = getRemoteConfigValue('desktop.clientExpiration');
if (value == null) {
return;
}
const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(value);
@@ -26,7 +26,7 @@ export const chatFolderCleanupService = createExpiringEntityCleanupService({
},
subscribeToTriggers: trigger => {
let prevMessageQueueTime = getMessageQueueTime();
return RemoteConfig.onChange('global.messageQueueTimeInSeconds', () => {
return RemoteConfig.onChange(['global.messageQueueTimeInSeconds'], () => {
const messageQueueTime = getMessageQueueTime();
if (messageQueueTime !== prevMessageQueueTime) {
trigger('messageQueueTime changed');
+4
View File
@@ -181,6 +181,10 @@ const DEFAULT_REMOTE_CONFIG = [
['global.backups.mediaTierFallbackCdnNumber', { enabled: true, value: '3' }],
['global.groupsv2.groupSizeHardLimit', { enabled: true, value: '64' }],
['global.groupsv2.maxGroupSize', { enabled: true, value: '32' }],
[
'desktop.libsignalNet.grpc.MessagesAnonymousSendMultiRecipientMessage.2',
{ enabled: true },
],
] as const;
//
+10 -8
View File
@@ -6,7 +6,7 @@ import * as sinon from 'sinon';
import lodash from 'lodash';
import { normalizeAci } from '../util/normalizeAci.std.ts';
import type { ConfigKeyType, ConfigListenerType } from '../RemoteConfig.dom.ts';
import type { ConfigKeyType } from '../RemoteConfig.dom.ts';
import {
COUNTRY_CODE_FALLBACK,
getCountryCodeValue,
@@ -187,8 +187,8 @@ describe('RemoteConfig', () => {
await updateRemoteConfig([]);
// oxlint-disable-next-line typescript/no-empty-function
const listener = sinon.spy<ConfigListenerType>(() => {});
onChange('desktop.internalUser', listener);
const listener = sinon.spy<() => void>(() => {});
onChange(['desktop.internalUser', 'desktop.clientExpiration'], listener);
await updateRemoteConfig([
{ name: 'desktop.internalUser', value: 'yes' },
@@ -198,14 +198,16 @@ describe('RemoteConfig', () => {
{ name: 'desktop.internalUser', value: 'yes' },
]);
// should not trigger callback
await updateRemoteConfig([
{ name: 'desktop.internalUser', value: 'yes' },
{ name: 'global.nicknames.max', value: '42' },
]);
const calls = listener
.getCalls()
.map(call => omit(call.firstArg, 'enabledAt'));
assert.deepEqual(calls, [
{ name: 'desktop.internalUser', value: 'yes', enabled: true },
{ name: 'desktop.internalUser', enabled: false },
{ name: 'desktop.internalUser', value: 'yes', enabled: true },
]);
assert.deepEqual(calls, [{}, {}, {}]);
});
});
});
+5 -1
View File
@@ -1803,7 +1803,11 @@ export async function connect({
hasStoriesDisabled,
hasBuildExpired,
}: WebAPIConnectOptionsType): Promise<void> {
bindRemoteConfigToLibsignalNet(getLibsignalNet(), window.getVersion());
bindRemoteConfigToLibsignalNet(
getLibsignalNet(),
window.getVersion(),
reconnect
);
username = initialUsername;
password = initialPassword;