"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const locale_1 = require("../locale");
const ReportService_1 = require("./ReportService");
const test_utils_1 = require("../testing/test-utils");
const Folder_1 = require("../models/Folder");
const BaseItem_1 = require("../models/BaseItem");
const Note_1 = require("../models/Note");
const shim_1 = require("../shim");
const SyncTargetRegistry_1 = require("../SyncTargetRegistry");
const utils_1 = require("./e2ee/utils");
const Setting_1 = require("../models/Setting");
const BaseModel_1 = require("../BaseModel");
const firstSectionWithTitle = (report, title) => {
    const sections = report.filter(section => section.title === title);
    if (sections.length === 0)
        return null;
    return sections[0];
};
const getCannotSyncSection = (report) => {
    return firstSectionWithTitle(report, (0, locale_1._)('Items that cannot be synchronised'));
};
const getIgnoredSection = (report) => {
    return firstSectionWithTitle(report, (0, locale_1._)('Ignored items that cannot be synchronised'));
};
const getDecryptionErrorSection = (report) => {
    return firstSectionWithTitle(report, (0, locale_1._)('Items that cannot be decrypted'));
};
const sectionBodyToText = (section) => {
    return section.body.map(item => {
        if (typeof item === 'string') {
            return item;
        }
        return item.text;
    }).join('\n');
};
const getListItemsInBodyStartingWith = (section, keyPrefix) => {
    return section.body.filter(item => typeof item !== 'string' && item.type === 'openList' && item.key.startsWith(keyPrefix));
};
const addCannotDecryptNotes = async (corruptedNoteCount) => {
    await (0, test_utils_1.switchClient)(2);
    const notes = [];
    for (let i = 0; i < corruptedNoteCount; i++) {
        notes.push(await Note_1.default.save({ title: `Note ${i}` }));
    }
    await (0, test_utils_1.synchronizerStart)();
    await (0, test_utils_1.switchClient)(1);
    await (0, test_utils_1.synchronizerStart)();
    // First, simulate a broken note and check that the decryption worker
    // gives up decrypting after a number of tries. This is mainly relevant
    // for data that crashes the mobile application - we don't want to keep
    // decrypting these.
    for (const note of notes) {
        await Note_1.default.save({ id: note.id, encryption_cipher_text: 'bad' });
    }
    return notes.map(note => note.id);
};
const addRemoteNotes = async (noteCount) => {
    await (0, test_utils_1.switchClient)(2);
    const notes = [];
    for (let i = 0; i < noteCount; i++) {
        notes.push(await Note_1.default.save({ title: `Test Note ${i}` }));
    }
    await (0, test_utils_1.synchronizerStart)();
    await (0, test_utils_1.switchClient)(1);
    return notes.map(note => note.id);
};
const setUpLocalAndRemoteEncryption = async () => {
    await (0, test_utils_1.switchClient)(2);
    // Encryption setup
    const masterKey = await (0, test_utils_1.loadEncryptionMasterKey)();
    await (0, utils_1.setupAndEnableEncryption)((0, test_utils_1.encryptionService)(), masterKey, '123456');
    await (0, test_utils_1.synchronizerStart)();
    // Give both clients the same master key
    await (0, test_utils_1.switchClient)(1);
    await (0, test_utils_1.synchronizerStart)();
    Setting_1.default.setObjectValue('encryption.passwordCache', masterKey.id, '123456');
    await (0, utils_1.loadMasterKeysFromSettings)((0, test_utils_1.encryptionService)());
};
describe('ReportService', () => {
    beforeEach(async () => {
        await (0, test_utils_1.setupDatabaseAndSynchronizer)(1);
        await (0, test_utils_1.setupDatabaseAndSynchronizer)(2);
        await (0, test_utils_1.switchClient)(1);
    });
    it('should move sync errors to the "ignored" section after clicking "ignore"', async () => {
        var _a, _b;
        const folder = await Folder_1.default.save({ title: 'Test' });
        const noteCount = 5;
        const testNotes = await (0, test_utils_1.createNTestNotes)(noteCount, folder);
        await (0, test_utils_1.synchronizerStart)();
        const disabledReason = 'Test reason';
        for (const testNote of testNotes) {
            await BaseItem_1.default.saveSyncDisabled((0, test_utils_1.syncTargetId)(), testNote, disabledReason);
        }
        const service = new ReportService_1.default();
        let report = await service.status((0, test_utils_1.syncTargetId)());
        // Items should all initially be listed as "cannot be synchronized", but should be ignorable.
        const unsyncableSection = getCannotSyncSection(report);
        const ignorableItems = [];
        for (const item of unsyncableSection.body) {
            if (typeof item === 'object' && item.canIgnore) {
                ignorableItems.push(item);
            }
        }
        expect(ignorableItems).toHaveLength(noteCount);
        expect(sectionBodyToText(unsyncableSection)).toContain(disabledReason);
        // Ignore all
        expect(await BaseItem_1.default.syncDisabledItemsCount((0, test_utils_1.syncTargetId)())).toBe(noteCount);
        expect(await BaseItem_1.default.syncDisabledItemsCountIncludingIgnored((0, test_utils_1.syncTargetId)())).toBe(noteCount);
        for (const item of ignorableItems) {
            await item.ignoreHandler();
        }
        expect(await BaseItem_1.default.syncDisabledItemsCount((0, test_utils_1.syncTargetId)())).toBe(0);
        expect(await BaseItem_1.default.syncDisabledItemsCountIncludingIgnored((0, test_utils_1.syncTargetId)())).toBe(noteCount);
        await (0, test_utils_1.synchronizerStart)();
        report = await service.status((0, test_utils_1.syncTargetId)());
        // Should now be in the ignored section
        const ignoredSection = getIgnoredSection(report);
        expect(ignoredSection).toBeTruthy();
        expect(sectionBodyToText(unsyncableSection)).toContain(disabledReason);
        expect(sectionBodyToText(getCannotSyncSection(report))).not.toContain(disabledReason);
        // Should not be possible to re-ignore an item in the ignored section
        let ignoredItemCount = 0;
        for (const item of ignoredSection.body) {
            if (typeof item === 'object' && ((_a = item.text) === null || _a === void 0 ? void 0 : _a.includes(disabledReason))) {
                expect(item.canIgnore).toBeFalsy();
                expect(item.canRetry).toBe(true);
                ignoredItemCount++;
            }
        }
        // Should have the correct number of ignored items
        expect(await BaseItem_1.default.syncDisabledItemsCountIncludingIgnored((0, test_utils_1.syncTargetId)())).toBe(ignoredItemCount);
        expect(ignoredItemCount).toBe(noteCount);
        // Clicking "retry" should un-ignore
        for (const item of ignoredSection.body) {
            if (typeof item === 'object' && ((_b = item.text) === null || _b === void 0 ? void 0 : _b.includes(disabledReason))) {
                expect(item.canRetry).toBe(true);
                await item.retryHandler();
                break;
            }
        }
        expect(await BaseItem_1.default.syncDisabledItemsCountIncludingIgnored((0, test_utils_1.syncTargetId)())).toBe(noteCount - 1);
    });
    it('should support ignoring sync errors for resources that failed to download', async () => {
        const createAttachmentDownloadError = async () => {
            await (0, test_utils_1.switchClient)(2);
            const note1 = await Note_1.default.save({ title: 'note' });
            await shim_1.default.attachFileToNote(note1, `${test_utils_1.supportDir}/photo.jpg`);
            await (0, test_utils_1.synchronizerStart)();
            await (0, test_utils_1.switchClient)(1);
            const previousMax = (0, test_utils_1.synchronizer)().maxResourceSize_;
            (0, test_utils_1.synchronizer)().maxResourceSize_ = 1;
            await (0, test_utils_1.synchronizerStart)();
            (0, test_utils_1.synchronizer)().maxResourceSize_ = previousMax;
        };
        await createAttachmentDownloadError();
        const service = new ReportService_1.default();
        let report = await service.status((0, test_utils_1.syncTargetId)());
        const unsyncableSection = getCannotSyncSection(report);
        expect(unsyncableSection).not.toBeNull();
        expect(sectionBodyToText(unsyncableSection)).toContain('could not be downloaded');
        // Item for the download error should be ignorable
        const ignorableItems = [];
        for (const item of unsyncableSection.body) {
            if (typeof item === 'object' && item.canIgnore) {
                ignorableItems.push(item);
            }
        }
        expect(ignorableItems).toHaveLength(1);
        await ignorableItems[0].ignoreHandler();
        // Should now be ignored.
        report = await service.status((0, test_utils_1.syncTargetId)());
        const ignoredItem = getIgnoredSection(report).body.find(item => typeof item === 'object' && item.canRetry === true);
        expect(ignoredItem).not.toBeFalsy();
        // Type narrowing
        if (typeof ignoredItem === 'string')
            throw new Error('should be an object');
        // Should be possible to retry
        await ignoredItem.retryHandler();
        await (0, test_utils_1.synchronizerStart)();
        // Should be fixed after retrying
        report = await service.status((0, test_utils_1.syncTargetId)());
        expect(getIgnoredSection(report)).toBeNull();
        expect(getCannotSyncSection(report)).toBeNull();
    });
    it('should associate decryption failures with error message headers when errors are known', async () => {
        await setUpLocalAndRemoteEncryption();
        const service = new ReportService_1.default();
        const syncTargetId = SyncTargetRegistry_1.default.nameToId('joplinServer');
        let report = await service.status(syncTargetId);
        // Initially, should not have a "cannot be decrypted section"
        expect(getDecryptionErrorSection(report)).toBeNull();
        const corruptedNoteIds = await addCannotDecryptNotes(4);
        await addRemoteNotes(10);
        await (0, test_utils_1.synchronizerStart)();
        for (let i = 0; i < 3; i++) {
            report = await service.status(syncTargetId);
            expect(getDecryptionErrorSection(report)).toBeNull();
            // .start needs to be run multiple times for items to be disabled and thus
            // added to the report
            await (0, test_utils_1.decryptionWorker)().start();
        }
        // After adding corrupted notes, it should have such a section.
        report = await service.status(syncTargetId);
        const decryptionErrorsSection = getDecryptionErrorSection(report);
        expect(decryptionErrorsSection).not.toBeNull();
        // There should be a list of errors (all errors are known)
        const errorLists = getListItemsInBodyStartingWith(decryptionErrorsSection, 'itemsWithError');
        expect(errorLists).toHaveLength(1);
        // There should, however, be testIds.length ReportItems with the IDs of the notes.
        const decryptionErrorsText = sectionBodyToText(decryptionErrorsSection);
        for (const noteId of corruptedNoteIds) {
            expect(decryptionErrorsText).toContain(noteId);
        }
    });
    it('should not associate decryption failures with error message headers when errors are unknown', async () => {
        const decryption = (0, test_utils_1.decryptionWorker)();
        // Create decryption errors:
        const testIds = ['0123456789012345601234567890123456', '0123456789012345601234567890123457', '0123456789012345601234567890123458'];
        // Adds items to the decryption error list **without also adding the reason**. This matches
        // the format of older decryption errors.
        const addIdsToDecryptionErrorList = async (worker, ids) => {
            for (const id of ids) {
                // A value that is more than the maximum number of attempts:
                const numDecryptionAttempts = 3;
                // Add the failure manually so that the error message is unknown
                await worker.kvStore().setValue(`decrypt:${BaseModel_1.ModelType.Note}:${id}`, numDecryptionAttempts);
            }
        };
        await addIdsToDecryptionErrorList(decryption, testIds);
        const service = new ReportService_1.default();
        const syncTargetId = SyncTargetRegistry_1.default.nameToId('joplinServer');
        const report = await service.status(syncTargetId);
        // Report should have an "Items that cannot be decrypted" section
        const decryptionErrorSection = getDecryptionErrorSection(report);
        expect(decryptionErrorSection).not.toBeNull();
        // There should not be any lists of errors (no errors associated with the item).
        const errorLists = getListItemsInBodyStartingWith(decryptionErrorSection, 'itemsWithError');
        expect(errorLists).toHaveLength(0);
        // There should be items with the correct messages:
        const expectedMessages = testIds.map(id => `Note: ${id}`);
        const bodyText = sectionBodyToText(decryptionErrorSection);
        for (const message of expectedMessages) {
            expect(bodyText).toContain(message);
        }
    });
});
//# sourceMappingURL=ReportService.test.js.map