Better backup integration test harness
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
38d181a0ee
commit
d73619d09e
17 changed files with 147 additions and 198 deletions
9
ts/CI.ts
9
ts/CI.ts
|
@ -20,7 +20,6 @@ type ResolveType = (data: unknown) => void;
|
||||||
export type CIType = {
|
export type CIType = {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
backupData?: Uint8Array;
|
backupData?: Uint8Array;
|
||||||
isBackupIntegration?: boolean;
|
|
||||||
getConversationId: (address: string | null) => string | null;
|
getConversationId: (address: string | null) => string | null;
|
||||||
getMessagesBySentAt(
|
getMessagesBySentAt(
|
||||||
sentAt: number
|
sentAt: number
|
||||||
|
@ -46,14 +45,9 @@ export type CIType = {
|
||||||
export type GetCIOptionsType = Readonly<{
|
export type GetCIOptionsType = Readonly<{
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
backupData?: Uint8Array;
|
backupData?: Uint8Array;
|
||||||
isBackupIntegration?: boolean;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function getCI({
|
export function getCI({ deviceName, backupData }: GetCIOptionsType): CIType {
|
||||||
deviceName,
|
|
||||||
backupData,
|
|
||||||
isBackupIntegration,
|
|
||||||
}: GetCIOptionsType): CIType {
|
|
||||||
const eventListeners = new Map<string, Array<ResolveType>>();
|
const eventListeners = new Map<string, Array<ResolveType>>();
|
||||||
const completedEvents = new Map<string, Array<unknown>>();
|
const completedEvents = new Map<string, Array<unknown>>();
|
||||||
|
|
||||||
|
@ -199,7 +193,6 @@ export function getCI({
|
||||||
return {
|
return {
|
||||||
deviceName,
|
deviceName,
|
||||||
backupData,
|
backupData,
|
||||||
isBackupIntegration,
|
|
||||||
getConversationId,
|
getConversationId,
|
||||||
getMessagesBySentAt,
|
getMessagesBySentAt,
|
||||||
handleEvent,
|
handleEvent,
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { getTitleNoDefault } from './util/getTitle';
|
||||||
import * as StorageService from './services/storage';
|
import * as StorageService from './services/storage';
|
||||||
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats';
|
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats';
|
||||||
import { countAllConversationsUnreadStats } from './util/countUnreadStats';
|
import { countAllConversationsUnreadStats } from './util/countUnreadStats';
|
||||||
|
import { isTestOrMockEnvironment } from './environment';
|
||||||
|
|
||||||
type ConvoMatchType =
|
type ConvoMatchType =
|
||||||
| {
|
| {
|
||||||
|
@ -183,7 +184,7 @@ export class ConversationController {
|
||||||
// then we reset the state right away.
|
// then we reset the state right away.
|
||||||
this._conversations.on('add', (model: ConversationModel): void => {
|
this._conversations.on('add', (model: ConversationModel): void => {
|
||||||
// Don't modify conversations in backup integration testing
|
// Don't modify conversations in backup integration testing
|
||||||
if (window.SignalCI?.isBackupIntegration) {
|
if (isTestOrMockEnvironment()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
model.startMuteTimer();
|
model.startMuteTimer();
|
||||||
|
|
|
@ -2097,9 +2097,7 @@ export async function startApp(): Promise<void> {
|
||||||
storage,
|
storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!window.SignalCI?.isBackupIntegration) {
|
void routineProfileRefresher.start();
|
||||||
void routineProfileRefresher.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(usernameIntegrity.start());
|
drop(usernameIntegrity.start());
|
||||||
|
@ -2135,10 +2133,6 @@ export async function startApp(): Promise<void> {
|
||||||
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
|
|
||||||
if (window.SignalCI?.isBackupIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { configuration } = ev;
|
const { configuration } = ev;
|
||||||
const {
|
const {
|
||||||
readReceipts,
|
readReceipts,
|
||||||
|
@ -3204,10 +3198,6 @@ export async function startApp(): Promise<void> {
|
||||||
async function onKeysSync(ev: KeysEvent) {
|
async function onKeysSync(ev: KeysEvent) {
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
|
|
||||||
if (window.SignalCI?.isBackupIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { masterKey } = ev;
|
const { masterKey } = ev;
|
||||||
let { storageServiceKey } = ev;
|
let { storageServiceKey } = ev;
|
||||||
|
|
||||||
|
|
|
@ -219,9 +219,6 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||||
}
|
}
|
||||||
|
|
||||||
static async start(): Promise<void> {
|
static async start(): Promise<void> {
|
||||||
if (window.SignalCI?.isBackupIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await AttachmentDownloadManager.instance.start();
|
await AttachmentDownloadManager.instance.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,7 @@ import {
|
||||||
import { isAciString } from '../../util/isAciString';
|
import { isAciString } from '../../util/isAciString';
|
||||||
import { hslToRGB } from '../../util/hslToRGB';
|
import { hslToRGB } from '../../util/hslToRGB';
|
||||||
import type { AboutMe, LocalChatStyle } from './types';
|
import type { AboutMe, LocalChatStyle } from './types';
|
||||||
|
import { BackupType } from './types';
|
||||||
import { messageHasPaymentEvent } from '../../messages/helpers';
|
import { messageHasPaymentEvent } from '../../messages/helpers';
|
||||||
import {
|
import {
|
||||||
numberToAddressType,
|
numberToAddressType,
|
||||||
|
@ -209,6 +210,10 @@ export class BackupExportStream extends Readable {
|
||||||
// array.
|
// array.
|
||||||
private customColorIdByUuid = new Map<string, Long>();
|
private customColorIdByUuid = new Map<string, Long>();
|
||||||
|
|
||||||
|
constructor(private readonly backupType: BackupType) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public run(backupLevel: BackupLevel): void {
|
public run(backupLevel: BackupLevel): void {
|
||||||
drop(
|
drop(
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -224,7 +229,7 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
// TODO (DESKTOP-7344): Clear & add backup jobs in a single transaction
|
// TODO (DESKTOP-7344): Clear & add backup jobs in a single transaction
|
||||||
await DataWriter.clearAllAttachmentBackupJobs();
|
await DataWriter.clearAllAttachmentBackupJobs();
|
||||||
if (!window.SignalCI?.isBackupIntegration) {
|
if (this.backupType !== BackupType.TestOnlyPlaintext) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.attachmentBackupJobs.map(job =>
|
this.attachmentBackupJobs.map(job =>
|
||||||
AttachmentBackupManager.addJobAndMaybeThumbnailJob(job)
|
AttachmentBackupManager.addJobAndMaybeThumbnailJob(job)
|
||||||
|
@ -2180,7 +2185,7 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
// We don't download attachments during integration tests and thus have no
|
// We don't download attachments during integration tests and thus have no
|
||||||
// "iv" for an attachment and can't create a job
|
// "iv" for an attachment and can't create a job
|
||||||
if (!window.SignalCI?.isBackupIntegration) {
|
if (this.backupType !== BackupType.TestOnlyPlaintext) {
|
||||||
const backupJob = await maybeGetBackupJobForAttachmentAndFilePointer({
|
const backupJob = await maybeGetBackupJobForAttachmentAndFilePointer({
|
||||||
attachment: updatedAttachment ?? attachment,
|
attachment: updatedAttachment ?? attachment,
|
||||||
filePointer,
|
filePointer,
|
||||||
|
|
|
@ -76,6 +76,7 @@ import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
import { BACKUP_VERSION, WALLPAPER_TO_BUBBLE_COLOR } from './constants';
|
import { BACKUP_VERSION, WALLPAPER_TO_BUBBLE_COLOR } from './constants';
|
||||||
import type { AboutMe, LocalChatStyle } from './types';
|
import type { AboutMe, LocalChatStyle } from './types';
|
||||||
|
import { BackupType } from './types';
|
||||||
import type { GroupV2ChangeDetailType } from '../../groups';
|
import type { GroupV2ChangeDetailType } from '../../groups';
|
||||||
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
||||||
import { drop } from '../../util/drop';
|
import { drop } from '../../util/drop';
|
||||||
|
@ -298,17 +299,19 @@ export class BackupImportStream extends Writable {
|
||||||
flush: () => this.saveMessageBatcher.flushAndWait(),
|
flush: () => this.saveMessageBatcher.flushAndWait(),
|
||||||
});
|
});
|
||||||
|
|
||||||
private constructor() {
|
private constructor(private readonly backupType: BackupType) {
|
||||||
super({ objectMode: true });
|
super({ objectMode: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async create(): Promise<BackupImportStream> {
|
public static async create(
|
||||||
|
backupType = BackupType.Ciphertext
|
||||||
|
): Promise<BackupImportStream> {
|
||||||
await AttachmentDownloadManager.stop();
|
await AttachmentDownloadManager.stop();
|
||||||
await DataWriter.removeAllBackupAttachmentDownloadJobs();
|
await DataWriter.removeAllBackupAttachmentDownloadJobs();
|
||||||
await window.storage.put('backupMediaDownloadCompletedBytes', 0);
|
await window.storage.put('backupMediaDownloadCompletedBytes', 0);
|
||||||
await window.storage.put('backupMediaDownloadTotalBytes', 0);
|
await window.storage.put('backupMediaDownloadTotalBytes', 0);
|
||||||
|
|
||||||
return new BackupImportStream();
|
return new BackupImportStream(backupType);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async _write(
|
override async _write(
|
||||||
|
@ -387,9 +390,10 @@ export class BackupImportStream extends Writable {
|
||||||
await pMap(
|
await pMap(
|
||||||
[...this.pendingGroupAvatars.entries()],
|
[...this.pendingGroupAvatars.entries()],
|
||||||
async ([conversationId, newAvatarUrl]) => {
|
async ([conversationId, newAvatarUrl]) => {
|
||||||
if (!window.SignalCI?.isBackupIntegration) {
|
if (this.backupType === BackupType.TestOnlyPlaintext) {
|
||||||
await groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
|
return;
|
||||||
}
|
}
|
||||||
|
await groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
|
||||||
},
|
},
|
||||||
{ concurrency: MAX_CONCURRENCY }
|
{ concurrency: MAX_CONCURRENCY }
|
||||||
);
|
);
|
||||||
|
@ -411,7 +415,7 @@ export class BackupImportStream extends Writable {
|
||||||
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
|
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!window.SignalCI?.isBackupIntegration) {
|
if (this.backupType !== BackupType.TestOnlyPlaintext) {
|
||||||
await AttachmentDownloadManager.start();
|
await AttachmentDownloadManager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,16 +38,14 @@ import { getKeyMaterial } from './crypto';
|
||||||
import { BackupCredentials } from './credentials';
|
import { BackupCredentials } from './credentials';
|
||||||
import { BackupAPI, type DownloadOptionsType } from './api';
|
import { BackupAPI, type DownloadOptionsType } from './api';
|
||||||
import { validateBackup } from './validator';
|
import { validateBackup } from './validator';
|
||||||
|
import { BackupType } from './types';
|
||||||
|
|
||||||
|
export { BackupType };
|
||||||
|
|
||||||
const IV_LENGTH = 16;
|
const IV_LENGTH = 16;
|
||||||
|
|
||||||
const BACKUP_REFRESH_INTERVAL = 24 * HOUR;
|
const BACKUP_REFRESH_INTERVAL = 24 * HOUR;
|
||||||
|
|
||||||
export enum BackupType {
|
|
||||||
Ciphertext = 'Ciphertext',
|
|
||||||
TestOnlyPlaintext = 'TestOnlyPlaintext',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BackupsService {
|
export class BackupsService {
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
|
@ -100,13 +98,14 @@ export class BackupsService {
|
||||||
|
|
||||||
// Test harness
|
// Test harness
|
||||||
public async exportBackupData(
|
public async exportBackupData(
|
||||||
backupLevel: BackupLevel = BackupLevel.Messages
|
backupLevel: BackupLevel = BackupLevel.Messages,
|
||||||
|
backupType = BackupType.Ciphertext
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const sink = new PassThrough();
|
const sink = new PassThrough();
|
||||||
|
|
||||||
const chunks = new Array<Uint8Array>();
|
const chunks = new Array<Uint8Array>();
|
||||||
sink.on('data', chunk => chunks.push(chunk));
|
sink.on('data', chunk => chunks.push(chunk));
|
||||||
await this.exportBackup(sink, backupLevel);
|
await this.exportBackup(sink, backupLevel, backupType);
|
||||||
|
|
||||||
return Bytes.concatenate(chunks);
|
return Bytes.concatenate(chunks);
|
||||||
}
|
}
|
||||||
|
@ -246,7 +245,7 @@ export class BackupsService {
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const importStream = await BackupImportStream.create();
|
const importStream = await BackupImportStream.create(backupType);
|
||||||
if (backupType === BackupType.Ciphertext) {
|
if (backupType === BackupType.Ciphertext) {
|
||||||
const { aesKey, macKey } = getKeyMaterial();
|
const { aesKey, macKey } = getKeyMaterial();
|
||||||
|
|
||||||
|
@ -370,7 +369,12 @@ export class BackupsService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO (DESKTOP-7168): Update mock-server to support this endpoint
|
// TODO (DESKTOP-7168): Update mock-server to support this endpoint
|
||||||
if (!window.SignalCI) {
|
if (window.SignalCI || backupType === BackupType.TestOnlyPlaintext) {
|
||||||
|
strictAssert(
|
||||||
|
isTestOrMockEnvironment(),
|
||||||
|
'Plaintext backups can be exported only in test harness'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
// We first fetch the latest info on what's on the CDN, since this affects the
|
// We first fetch the latest info on what's on the CDN, since this affects the
|
||||||
// filePointers we will generate during export
|
// filePointers we will generate during export
|
||||||
log.info('Fetching latest backup CDN metadata');
|
log.info('Fetching latest backup CDN metadata');
|
||||||
|
@ -378,7 +382,7 @@ export class BackupsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { aesKey, macKey } = getKeyMaterial();
|
const { aesKey, macKey } = getKeyMaterial();
|
||||||
const recordStream = new BackupExportStream();
|
const recordStream = new BackupExportStream(backupType);
|
||||||
|
|
||||||
recordStream.run(backupLevel);
|
recordStream.run(backupLevel);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@ export type AboutMe = {
|
||||||
pni?: PniString;
|
pni?: PniString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum BackupType {
|
||||||
|
Ciphertext = 'Ciphertext',
|
||||||
|
TestOnlyPlaintext = 'TestOnlyPlaintext',
|
||||||
|
}
|
||||||
|
|
||||||
export type LocalChatStyle = Readonly<{
|
export type LocalChatStyle = Readonly<{
|
||||||
wallpaperPhotoPointer: Uint8Array | undefined;
|
wallpaperPhotoPointer: Uint8Array | undefined;
|
||||||
wallpaperPreset: number | undefined;
|
wallpaperPreset: number | undefined;
|
|
@ -20,13 +20,6 @@ export class FileStream extends InputStream {
|
||||||
await this.file?.close();
|
await this.file?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only for comparator tests
|
|
||||||
public async size(): Promise<number> {
|
|
||||||
const file = await this.lazyOpen();
|
|
||||||
const { size } = await file.stat();
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(amount: number): Promise<Buffer> {
|
async read(amount: number): Promise<Buffer> {
|
||||||
const file = await this.lazyOpen();
|
const file = await this.lazyOpen();
|
||||||
if (this.buffer.length < amount) {
|
if (this.buffer.length < amount) {
|
||||||
|
|
|
@ -229,10 +229,6 @@ async function doContactSync({
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onContactSync(ev: ContactSyncEvent): Promise<void> {
|
export async function onContactSync(ev: ContactSyncEvent): Promise<void> {
|
||||||
if (window.SignalCI?.isBackupIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`onContactSync(sent=${ev.sentAt}, receivedAt=${ev.receivedAtCounter}): queueing sync`
|
`onContactSync(sent=${ev.sentAt}, receivedAt=${ev.receivedAtCounter}): queueing sync`
|
||||||
);
|
);
|
||||||
|
|
|
@ -308,7 +308,6 @@ function startInstaller(): ThunkAction<
|
||||||
finishInstall({
|
finishInstall({
|
||||||
deviceName: SignalCI.deviceName,
|
deviceName: SignalCI.deviceName,
|
||||||
backupFile: SignalCI.backupData,
|
backupFile: SignalCI.backupData,
|
||||||
isBackupIntegration: SignalCI.isBackupIntegration,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,9 +233,9 @@ export async function asymmetricRoundtripHarness(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearData() {
|
export async function clearData(): Promise<void> {
|
||||||
await DataWriter.removeAll();
|
await DataWriter.removeAll();
|
||||||
window.storage.reset();
|
await window.storage.fetch();
|
||||||
window.ConversationController.reset();
|
window.ConversationController.reset();
|
||||||
|
|
||||||
await setupBasics();
|
await setupBasics();
|
||||||
|
@ -255,7 +255,8 @@ export async function setupBasics(): Promise<void> {
|
||||||
|
|
||||||
window.Events = {
|
window.Events = {
|
||||||
...window.Events,
|
...window.Events,
|
||||||
getTypingIndicatorSetting: () => false,
|
getTypingIndicatorSetting: () =>
|
||||||
getLinkPreviewSetting: () => false,
|
window.storage.get('typingIndicators', false),
|
||||||
|
getLinkPreviewSetting: () => window.storage.get('linkPreviews', false),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
99
ts/test-electron/backup/integration_test.ts
Normal file
99
ts/test-electron/backup/integration_test.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { readdirSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||||
|
import { InputStream } from '@signalapp/libsignal-client/dist/io';
|
||||||
|
import {
|
||||||
|
ComparableBackup,
|
||||||
|
Purpose,
|
||||||
|
} from '@signalapp/libsignal-client/dist/MessageBackup';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { clearData } from './helpers';
|
||||||
|
import { loadAll } from '../../services/allLoaders';
|
||||||
|
import { backupsService, BackupType } from '../../services/backups';
|
||||||
|
import { DataWriter } from '../../sql/Client';
|
||||||
|
|
||||||
|
const { BACKUP_INTEGRATION_DIR } = process.env;
|
||||||
|
|
||||||
|
class MemoryStream extends InputStream {
|
||||||
|
private offset = 0;
|
||||||
|
|
||||||
|
constructor(private readonly buffer: Buffer) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async read(amount: number): Promise<Buffer> {
|
||||||
|
const result = this.buffer.slice(this.offset, this.offset + amount);
|
||||||
|
this.offset += amount;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async skip(amount: number): Promise<void> {
|
||||||
|
this.offset += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('backup/integration', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clearData();
|
||||||
|
await loadAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!BACKUP_INTEGRATION_DIR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(BACKUP_INTEGRATION_DIR)
|
||||||
|
.filter(file => file.endsWith('.binproto'))
|
||||||
|
.map(file => join(BACKUP_INTEGRATION_DIR, file));
|
||||||
|
|
||||||
|
for (const fullPath of files) {
|
||||||
|
it(basename(fullPath), async () => {
|
||||||
|
const expectedBuffer = await readFile(fullPath);
|
||||||
|
|
||||||
|
await backupsService.importBackup(
|
||||||
|
() => Readable.from([expectedBuffer]),
|
||||||
|
BackupType.TestOnlyPlaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
const exported = await backupsService.exportBackupData(
|
||||||
|
BackupLevel.Media,
|
||||||
|
BackupType.TestOnlyPlaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualStream = new MemoryStream(Buffer.from(exported));
|
||||||
|
const expectedStream = new MemoryStream(expectedBuffer);
|
||||||
|
|
||||||
|
const actual = await ComparableBackup.fromUnencrypted(
|
||||||
|
Purpose.RemoteBackup,
|
||||||
|
actualStream,
|
||||||
|
BigInt(exported.byteLength)
|
||||||
|
);
|
||||||
|
const expected = await ComparableBackup.fromUnencrypted(
|
||||||
|
Purpose.RemoteBackup,
|
||||||
|
expectedStream,
|
||||||
|
BigInt(expectedBuffer.byteLength)
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualString = actual.comparableString();
|
||||||
|
const expectedString = expected.comparableString();
|
||||||
|
|
||||||
|
if (expectedString.includes('ReleaseChannelDonationRequest')) {
|
||||||
|
// Skip the unsupported tests
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need "deep*" for fancy diffs
|
||||||
|
assert.deepStrictEqual(actualString, expectedString);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,125 +0,0 @@
|
||||||
// Copyright 2023 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import { cpus } from 'node:os';
|
|
||||||
import { inspect } from 'node:util';
|
|
||||||
import { basename } from 'node:path';
|
|
||||||
import { reporters } from 'mocha';
|
|
||||||
import pMap from 'p-map';
|
|
||||||
import logSymbols from 'log-symbols';
|
|
||||||
import {
|
|
||||||
ComparableBackup,
|
|
||||||
Purpose,
|
|
||||||
} from '@signalapp/libsignal-client/dist/MessageBackup';
|
|
||||||
|
|
||||||
import { FileStream } from '../../services/backups/util/FileStream';
|
|
||||||
import { drop } from '../../util/drop';
|
|
||||||
import type { App } from '../playwright';
|
|
||||||
import { Bootstrap } from '../bootstrap';
|
|
||||||
|
|
||||||
const WORKER_COUNT = process.env.WORKER_COUNT
|
|
||||||
? parseInt(process.env.WORKER_COUNT, 10)
|
|
||||||
: Math.min(8, cpus().length);
|
|
||||||
|
|
||||||
(reporters.base as unknown as { maxDiffSize: number }).maxDiffSize = Infinity;
|
|
||||||
|
|
||||||
const testFiles = process.argv.slice(2);
|
|
||||||
let total = 0;
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function pass(): void {
|
|
||||||
process.stdout.write(`${logSymbols.success}`);
|
|
||||||
total += 1;
|
|
||||||
passed += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fail(filePath: string, error: string): void {
|
|
||||||
total += 1;
|
|
||||||
failed += 1;
|
|
||||||
console.log(`\n${logSymbols.error} ${basename(filePath)}`);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runOne(filePath: string): Promise<void> {
|
|
||||||
const bootstrap = new Bootstrap({ contactCount: 0 });
|
|
||||||
let app: App | undefined;
|
|
||||||
try {
|
|
||||||
await bootstrap.init();
|
|
||||||
|
|
||||||
app = await bootstrap.link({
|
|
||||||
ciBackupPath: filePath,
|
|
||||||
ciIsBackupIntegration: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupPath = bootstrap.getBackupPath('backup.bin');
|
|
||||||
await app.exportPlaintextBackupToDisk(backupPath);
|
|
||||||
|
|
||||||
await app.close();
|
|
||||||
app = undefined;
|
|
||||||
|
|
||||||
const actualStream = new FileStream(backupPath);
|
|
||||||
const expectedStream = new FileStream(filePath);
|
|
||||||
try {
|
|
||||||
const actual = await ComparableBackup.fromUnencrypted(
|
|
||||||
Purpose.RemoteBackup,
|
|
||||||
actualStream,
|
|
||||||
BigInt(await actualStream.size())
|
|
||||||
);
|
|
||||||
const expected = await ComparableBackup.fromUnencrypted(
|
|
||||||
Purpose.RemoteBackup,
|
|
||||||
expectedStream,
|
|
||||||
BigInt(await expectedStream.size())
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualString = actual.comparableString();
|
|
||||||
const expectedString = expected.comparableString();
|
|
||||||
|
|
||||||
if (actualString === expectedString) {
|
|
||||||
pass();
|
|
||||||
} else {
|
|
||||||
fail(
|
|
||||||
filePath,
|
|
||||||
reporters.base.generateDiff(
|
|
||||||
inspect(actualString, { depth: Infinity, sorted: true }),
|
|
||||||
inspect(expectedString, { depth: Infinity, sorted: true })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await bootstrap.saveLogs(app, basename(filePath));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await actualStream.close();
|
|
||||||
await expectedStream.close();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await bootstrap.saveLogs(app, basename(filePath));
|
|
||||||
fail(filePath, error.stack);
|
|
||||||
} finally {
|
|
||||||
// No need to block on this
|
|
||||||
drop(
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await bootstrap.teardown();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to teardown ${basename(filePath)}`, error);
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
|
||||||
await pMap(testFiles, runOne, { concurrency: WORKER_COUNT });
|
|
||||||
|
|
||||||
console.log(`${passed}/${total} (${failed} failures)`);
|
|
||||||
if (failed !== 0) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
|
@ -25,7 +25,7 @@ import createTaskWithTimeout from './TaskWithTimeout';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import { senderCertificateService } from '../services/senderCertificate';
|
import { senderCertificateService } from '../services/senderCertificate';
|
||||||
import { backupsService, BackupType } from '../services/backups';
|
import { backupsService } from '../services/backups';
|
||||||
import {
|
import {
|
||||||
decryptDeviceName,
|
decryptDeviceName,
|
||||||
deriveAccessKey,
|
deriveAccessKey,
|
||||||
|
@ -126,7 +126,6 @@ type CreateAccountSharedOptionsType = Readonly<{
|
||||||
|
|
||||||
// Test-only
|
// Test-only
|
||||||
backupFile?: Uint8Array;
|
backupFile?: Uint8Array;
|
||||||
isBackupIntegration?: boolean;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type CreatePrimaryDeviceOptionsType = Readonly<{
|
type CreatePrimaryDeviceOptionsType = Readonly<{
|
||||||
|
@ -220,7 +219,6 @@ function signedPreKeyToUploadSignedPreKey({
|
||||||
export type ConfirmNumberResultType = Readonly<{
|
export type ConfirmNumberResultType = Readonly<{
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
backupFile: Uint8Array | undefined;
|
backupFile: Uint8Array | undefined;
|
||||||
isBackupIntegration: boolean;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default class AccountManager extends EventTarget {
|
export default class AccountManager extends EventTarget {
|
||||||
|
@ -923,7 +921,6 @@ export default class AccountManager extends EventTarget {
|
||||||
readReceipts,
|
readReceipts,
|
||||||
userAgent,
|
userAgent,
|
||||||
backupFile,
|
backupFile,
|
||||||
isBackupIntegration,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const { storage } = window.textsecure;
|
const { storage } = window.textsecure;
|
||||||
|
@ -968,8 +965,7 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
if (backupFile !== undefined) {
|
if (backupFile !== undefined) {
|
||||||
log.warn(
|
log.warn(
|
||||||
'createAccount: Restoring from ' +
|
'createAccount: Restoring from backup; ' +
|
||||||
`${isBackupIntegration ? 'plaintext' : 'ciphertext'} backup; ` +
|
|
||||||
'deleting all previous data'
|
'deleting all previous data'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1229,12 +1225,7 @@ export default class AccountManager extends EventTarget {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (backupFile !== undefined) {
|
if (backupFile !== undefined) {
|
||||||
await backupsService.importBackup(
|
await backupsService.importBackup(() => Readable.from([backupFile]));
|
||||||
() => Readable.from([backupFile]),
|
|
||||||
isBackupIntegration
|
|
||||||
? BackupType.TestOnlyPlaintext
|
|
||||||
: BackupType.Ciphertext
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,6 @@ type StateType = Readonly<
|
||||||
export type PrepareLinkDataOptionsType = Readonly<{
|
export type PrepareLinkDataOptionsType = Readonly<{
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
backupFile?: Uint8Array;
|
backupFile?: Uint8Array;
|
||||||
isBackupIntegration?: boolean;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export class Provisioner {
|
export class Provisioner {
|
||||||
|
@ -153,7 +152,6 @@ export class Provisioner {
|
||||||
public prepareLinkData({
|
public prepareLinkData({
|
||||||
deviceName,
|
deviceName,
|
||||||
backupFile,
|
backupFile,
|
||||||
isBackupIntegration,
|
|
||||||
}: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType {
|
}: PrepareLinkDataOptionsType): CreateLinkedDeviceOptionsType {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
this.state.step === Step.ReadyToLink,
|
this.state.step === Step.ReadyToLink,
|
||||||
|
@ -211,7 +209,6 @@ export class Provisioner {
|
||||||
MAX_DEVICE_NAME_LENGTH
|
MAX_DEVICE_NAME_LENGTH
|
||||||
),
|
),
|
||||||
backupFile,
|
backupFile,
|
||||||
isBackupIntegration,
|
|
||||||
userAgent,
|
userAgent,
|
||||||
ourAci,
|
ourAci,
|
||||||
ourPni,
|
ourPni,
|
||||||
|
|
|
@ -25,6 +25,5 @@ if (config.ciMode) {
|
||||||
backupData: config.ciBackupPath
|
backupData: config.ciBackupPath
|
||||||
? fs.readFileSync(config.ciBackupPath)
|
? fs.readFileSync(config.ciBackupPath)
|
||||||
: undefined,
|
: undefined,
|
||||||
isBackupIntegration: config.ciIsBackupIntegration === true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue