mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 19:00:14 +00:00
* MM-68219 - Add redacted files placeholder and related functionality * add feature flag control and e2e tests * adjust placeholder design, polish code, fix permalink preview issue * polish code and add missing test * use import path with right path * gate post refetch behind the feature flag, refetch the open thread and avoid observeCount * restore filesInfo for the permalink files prop added upstream * fix stale redaction on background servers, consume the flag on every fetch path and share the viewport emitter * replace the in-memory redaction flag with a persisted per-post epoch so ABAC file decisions survive restarts and stale posts stay hidden * raise the redaction epoch on every resync, capture the epoch counter, only invalidate channels on role changes, and store the newer epoch when a re-verified post is otherwise unchanged * store superseded responses unverified instead of dropping them, keep the since-fetch when re-verifying, verify permalink embeds against the linked channel, and capture the websocket epoch on arrival * nvalidate file decisions when a user attribute field changes or all its values are cleared * batch channel invalidations, close file viewers only when affected, reset the epoch on unstamped redaction changes, and only re-verify the newest page's attachment posts * re-check older channel posts a page at a time instead of one request per post * skip the unstamped epoch lookup for posts with no files, redacted count or permalink * keep the attribute-view retry after later invalidations, retry linked posts unless refused for the current epoch, and stop older payloads from rolling back edits via the redaction check * MM-70905 - disable and grey out the attachment button when an ABAC pe… (#10195) * MM-70905 - disable and grey out the attachment button when an ABAC permission policy denies uploads in the channel * only back off render permission fetches when the server request fails, and log other errors without retrying * remove the disableServer helper and have removeServer do both steps * split defaultAllowed into allowWhenUnenforced and allowWhenNotEvaluated, and treat render permissions as unenforced on servers without the decisions API * MM-69627: Add playbook task requirements support on mobile (#10035) * MM-69627: Add playbook task requirements support on mobile Allow filling required task fields when checking off checklist items during a run, gated by the same Playbooks beta setting used on web. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address PR review feedback for task requirements Propagate settings write failures, harden fill-requirements save/title handling, and add missing unit coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix TypeScript errors in new unit tests Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Use defineMessages for fill requirements titles Keep i18n extraction able to find the header titles after moving them to the route. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address review feedback for task requirements Update checklist items locally when saving requirement values instead of refetching the run, add missing query/navigation tests, and apply small callback/reduce cleanups from review. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix task requirements review follow-ups Propagate local checklist write failures, clear a stale task-requirements flag when playbooks is unsupported, and cap fill values at the desktop/ server max length. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Move requirement field into its own component Keep one component per file on the fill-requirements screen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69122: Add file element support to interactive dialogs (#9898) * Implement Interactive Dialog - file upload * update demo plugin version for e2etests * fix and add e2etests * review fixes * Fix version bump checkout when a tag shares the branch name (#10229) * Fix Android crashes caused by animated custom emoji (#10030) * Route GIF decoding through Glide - Keep APNG4Android for APNG, animated WebP, and animated AVIF support. - Prepend Glide's GIF decoders to avoid APNG4Android's native GIF overflow. * Clear Expo image resources with Glide targets - Detach drawables when Glide clears their custom target. - Prevent recycled GIF frame resources from remaining attached to Expo image views. * Preserve foreground image during GIF target recycling --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> * detox - fix clobbering of feature flags that cause failures in other suites. Also small fix of a incorrectly set test-id --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Co-authored-by: Nate Schlossberg <nate.schlossberg@mattermost.com>
513 lines
21 KiB
TypeScript
513 lines
21 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AppState, type AppStateStatus} from 'react-native';
|
|
|
|
import {addFilesToDraft} from '@actions/local/draft';
|
|
import {PROGRESS_TIME_TO_STORE} from '@constants/files';
|
|
import DatabaseManager from '@database/manager';
|
|
import {getDraft} from '@queries/servers/drafts';
|
|
import RenderPermissionsStore from '@store/render_permissions_store';
|
|
import TestHelper from '@test/test_helper';
|
|
|
|
import {exportedForTesting} from './index';
|
|
|
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
|
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
|
|
|
const {DraftEditPostUploadManagerSingleton} = exportedForTesting;
|
|
|
|
const url = 'baseHandler.test.com';
|
|
const mockClient = TestHelper.createClient();
|
|
|
|
jest.mock('@managers/network_manager', () => {
|
|
const original = jest.requireActual('@managers/network_manager');
|
|
return {
|
|
...original,
|
|
getClient: (serverUrl: string) => {
|
|
if (serverUrl === url) {
|
|
return mockClient;
|
|
}
|
|
|
|
throw new Error('client not found');
|
|
},
|
|
};
|
|
});
|
|
|
|
const now = new Date('2020-01-01').getTime();
|
|
const timeNotStore = now + (PROGRESS_TIME_TO_STORE - 1);
|
|
const timeStore = now + PROGRESS_TIME_TO_STORE + 1;
|
|
|
|
const mockUpload = () => {
|
|
const returnValue: {
|
|
resolvePromise: ((value: ClientResponse | PromiseLike<ClientResponse>) => void) | null;
|
|
rejectPromise: ((reason?: any) => void) | null;
|
|
progressFunc: ((fractionCompleted: number, bytesRead?: number | null | undefined) => void) | null;
|
|
} = {
|
|
resolvePromise: null,
|
|
rejectPromise: null,
|
|
progressFunc: null,
|
|
};
|
|
(mockClient.apiClient.upload as jest.Mock).mockImplementationOnce(() => {
|
|
const promise = (new Promise<ClientResponse>((resolve, reject) => {
|
|
returnValue.resolvePromise = resolve;
|
|
returnValue.rejectPromise = reject;
|
|
}) as ProgressPromise<ClientResponse>);
|
|
promise.progress = (f) => {
|
|
returnValue.progressFunc = f;
|
|
return promise;
|
|
};
|
|
promise.cancel = jest.fn();
|
|
return promise;
|
|
});
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
describe('draft upload manager', () => {
|
|
let operator: ServerDataOperator;
|
|
const channelId = 'cid';
|
|
const rootId = 'rid';
|
|
|
|
beforeEach(async () => {
|
|
await DatabaseManager.init([url]);
|
|
operator = DatabaseManager.serverDatabases[url]!.operator;
|
|
AppState.currentState = 'active';
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await DatabaseManager.destroyServerDatabase(url);
|
|
});
|
|
|
|
it('File is uploaded and stored', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const fileClientId = 'clientId';
|
|
const fileServerId = 'serverId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(fileClientId)).toBe(true);
|
|
|
|
expect(uploadMocks.resolvePromise).not.toBeNull();
|
|
uploadMocks.resolvePromise!({ok: true, code: 201, data: {file_infos: [{clientId: fileClientId, id: fileServerId, localPath: 'path1'}]}});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
const draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].id).toBe(fileServerId);
|
|
|
|
expect(manager.isUploading(fileClientId)).toBe(false);
|
|
});
|
|
|
|
it('Progress is not stored on progress, but stored on fail', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const fileClientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path2'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(fileClientId)).toBe(true);
|
|
|
|
// Wait for other promises to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
const bytesRead = 200;
|
|
uploadMocks.progressFunc!(0.1, bytesRead);
|
|
|
|
// Wait for other promises to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
// There has been progress, but we are not storing in to the database since the app is still active.
|
|
let draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBeUndefined();
|
|
|
|
uploadMocks.rejectPromise!('error');
|
|
|
|
// Wait for other promises to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
// After a failure, we store the progress on the database, so we can resume from the point before failure.
|
|
draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBe(bytesRead);
|
|
expect(draft?.files[0].failed).toBe(true);
|
|
|
|
expect(manager.isUploading(fileClientId)).toBe(false);
|
|
});
|
|
|
|
it('Progress is stored on AppState change to background for all files, and then only after certain time', async () => {
|
|
const channelIds = ['cid1', 'cid2', 'cid3'];
|
|
const rootIds = ['rid1', '', 'rid3'];
|
|
const clientIds = ['client1', 'client2', 'client3'];
|
|
const fileUrls = ['url1', 'url2', 'url3'];
|
|
const bytesReads = [200, 300, 400];
|
|
const bytesReadsNotStore = [200, 300, 400];
|
|
const bytesReadsStore = [400, 1000, 450];
|
|
const bytesReadBeforeActive = [500, 1200, 650];
|
|
const appStateSpy = jest.spyOn(AppState, 'addEventListener');
|
|
let eventListener: (state: AppStateStatus) => void;
|
|
appStateSpy.mockImplementationOnce((name, f) => {
|
|
eventListener = f;
|
|
return {} as any;
|
|
});
|
|
const spyNow = jest.spyOn(Date, 'now');
|
|
spyNow.mockImplementation(() => now);
|
|
AppState.currentState = 'active';
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
|
|
const progressFunc: {[fileUrl: string]: ((fractionCompleted: number, bytesRead?: number | null | undefined) => void)} = {};
|
|
const cancel = jest.fn();
|
|
|
|
let promise: ProgressPromise<ClientResponse>;
|
|
(mockClient.apiClient.upload as jest.Mock).mockImplementation((endpoint, fileUrl) => {
|
|
promise = (new Promise<ClientResponse>(() => {
|
|
// Do nothing
|
|
}) as ProgressPromise<ClientResponse>);
|
|
promise.progress = (f) => {
|
|
progressFunc[fileUrl] = f;
|
|
return promise;
|
|
};
|
|
promise.cancel = cancel;
|
|
return promise;
|
|
});
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const file = {clientId: clientIds[i], localPath: fileUrls[i]} as FileInfo;
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await addFilesToDraft(url, channelIds[i], rootIds[i], [file]);
|
|
manager.prepareUpload(url, file, channelIds[i], rootIds[i], 0);
|
|
}
|
|
|
|
(mockClient.apiClient.upload as jest.Mock).mockRestore();
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
progressFunc[fileUrls[i]](0.1, bytesReads[i]);
|
|
}
|
|
|
|
AppState.currentState = 'background';
|
|
eventListener!('background');
|
|
|
|
// Wait for other promises to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const draft = await getDraft(operator.database, channelIds[i], rootIds[i]);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBe(bytesReads[i]);
|
|
}
|
|
|
|
// Add progress inside the time window where it should not store in background
|
|
spyNow.mockImplementation(() => timeNotStore);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
progressFunc[fileUrls[i]](0.1, bytesReadsNotStore[i]);
|
|
}
|
|
|
|
// Wait for other promises to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const draft = await getDraft(operator.database, channelIds[i], rootIds[i]);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBe(bytesReads[i]);
|
|
}
|
|
|
|
// Add progress inside the time window where it should store in background
|
|
spyNow.mockImplementation(() => timeStore);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
progressFunc[fileUrls[i]](0.1, bytesReadsStore[i]);
|
|
|
|
// Wait for other promises to finish (if not, watermelondb complains)
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await new Promise(process.nextTick);
|
|
}
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const draft = await getDraft(operator.database, channelIds[i], rootIds[i]);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBe(bytesReadsStore[i]);
|
|
}
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
progressFunc[fileUrls[i]](0.1, bytesReadBeforeActive[i]);
|
|
}
|
|
|
|
AppState.currentState = 'active';
|
|
eventListener!('active');
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const draft = await getDraft(operator.database, channelIds[i], rootIds[i]);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].bytesRead).toBe(bytesReadsStore[i]);
|
|
}
|
|
|
|
spyNow.mockClear();
|
|
});
|
|
|
|
it('Error on complete: Received wrong response code', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const fileClientId = 'clientId';
|
|
const fileServerId = 'serverId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
manager.prepareUpload(url, {clientId: fileClientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(fileClientId)).toBe(true);
|
|
|
|
expect(uploadMocks.resolvePromise).not.toBeNull();
|
|
uploadMocks.resolvePromise!({ok: true, code: 500, data: {file_infos: [{clientId: fileClientId, id: fileServerId, localPath: 'path1'}]}});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
const draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].id).toBeUndefined();
|
|
expect(draft?.files[0].failed).toBe(true);
|
|
|
|
expect(manager.isUploading(fileClientId)).toBe(false);
|
|
});
|
|
|
|
it('Error on complete: Received no data', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
|
|
expect(uploadMocks.resolvePromise).not.toBeNull();
|
|
uploadMocks.resolvePromise!({ok: true, code: 201});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
const draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].id).toBeUndefined();
|
|
expect(draft?.files[0].failed).toBe(true);
|
|
|
|
expect(manager.isUploading(clientId)).toBe(false);
|
|
});
|
|
|
|
it('Error on complete: Received no file info', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
|
|
expect(uploadMocks.resolvePromise).not.toBeNull();
|
|
uploadMocks.resolvePromise!({ok: true, code: 201, data: {}});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
const draft = await getDraft(operator.database, channelId, rootId);
|
|
expect(draft?.files.length).toBe(1);
|
|
expect(draft?.files[0].id).toBeUndefined();
|
|
expect(draft?.files[0].failed).toBe(true);
|
|
|
|
expect(manager.isUploading(clientId)).toBe(false);
|
|
});
|
|
|
|
it('Progress handler', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
const nullProgressHandler = jest.fn();
|
|
let cancelProgressHandler = manager.registerProgressHandler(clientId, nullProgressHandler);
|
|
expect(cancelProgressHandler).toBeUndefined();
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
|
|
const progressHandler = jest.fn();
|
|
cancelProgressHandler = manager.registerProgressHandler(clientId, progressHandler);
|
|
expect(cancelProgressHandler).not.toBeUndefined();
|
|
|
|
let bytesRead = 200;
|
|
uploadMocks.progressFunc!(0.1, bytesRead);
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(progressHandler).toHaveBeenCalledWith(0.1, bytesRead);
|
|
|
|
cancelProgressHandler!();
|
|
bytesRead = 400;
|
|
uploadMocks.progressFunc!(0.1, bytesRead);
|
|
|
|
// Make sure calling several times the cancel does not create any problem.
|
|
cancelProgressHandler!();
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
expect(progressHandler).toHaveBeenCalledTimes(1);
|
|
expect(nullProgressHandler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Error handler: normal error', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
const nullErrorHandler = jest.fn();
|
|
let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler);
|
|
expect(cancelErrorHandler).toBeUndefined();
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
|
|
const errorHandler = jest.fn();
|
|
cancelErrorHandler = manager.registerErrorHandler(clientId, errorHandler);
|
|
expect(cancelErrorHandler).not.toBeUndefined();
|
|
|
|
uploadMocks.rejectPromise!({message: 'error'});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(errorHandler).toHaveBeenCalledWith('error', undefined);
|
|
|
|
// Make sure cancelling after error does not create any problem.
|
|
cancelErrorHandler!();
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(manager.isUploading(clientId)).toBe(false);
|
|
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
expect(nullErrorHandler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Error handler: complete error', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
|
|
const nullErrorHandler = jest.fn();
|
|
let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler);
|
|
expect(cancelErrorHandler).toBeUndefined();
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
|
|
const errorHandler = jest.fn();
|
|
cancelErrorHandler = manager.registerErrorHandler(clientId, errorHandler);
|
|
expect(cancelErrorHandler).not.toBeUndefined();
|
|
|
|
uploadMocks.resolvePromise!({ok: true, code: 500});
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
// Make sure cancelling after error does not create any problem.
|
|
cancelErrorHandler!();
|
|
|
|
// Wait for other promises (on complete write) to finish
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(manager.isUploading(clientId)).toBe(false);
|
|
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
expect(nullErrorHandler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should pass the server error id to error handlers when the upload is refused', async () => {
|
|
// HTTP errors resolve rather than reject, so the id is only available on complete.
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
|
|
const errorHandler = jest.fn();
|
|
manager.registerErrorHandler(clientId, errorHandler);
|
|
|
|
uploadMocks.resolvePromise!({ok: false, code: 403, data: {id: 'api.file.upload_file.abac_denied.app_error', message: 'denied'}});
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(errorHandler).toHaveBeenCalledWith('denied', 'api.file.upload_file.abac_denied.app_error');
|
|
});
|
|
|
|
it('should revalidate the channel upload decision when the server refuses the upload by policy', async () => {
|
|
// The stored decision allowed an upload the server refused.
|
|
const expireEntry = jest.spyOn(RenderPermissionsStore, 'expireEntry');
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
|
|
uploadMocks.resolvePromise!({ok: false, code: 403, data: {id: 'api.file.upload_file.abac_denied.app_error', message: 'denied'}});
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(expireEntry).toHaveBeenCalledTimes(1);
|
|
expect(expireEntry).toHaveBeenCalledWith(url, channelId);
|
|
expireEntry.mockRestore();
|
|
});
|
|
|
|
it('should keep the channel upload decision when the upload fails for another reason', async () => {
|
|
const expireEntry = jest.spyOn(RenderPermissionsStore, 'expireEntry');
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
|
|
const clientId = 'clientId';
|
|
await addFilesToDraft(url, channelId, rootId, [{clientId, localPath: 'path1'} as FileInfo]);
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0);
|
|
|
|
uploadMocks.resolvePromise!({ok: false, code: 413, data: {id: 'api.file.upload_file.too_large.app_error', message: 'too large'}});
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(expireEntry).not.toHaveBeenCalled();
|
|
expireEntry.mockRestore();
|
|
});
|
|
|
|
it('should call updateFileCallback when isEditPost is true', async () => {
|
|
const manager = new DraftEditPostUploadManagerSingleton();
|
|
const uploadMocks = mockUpload();
|
|
const updateFileCallback = jest.fn();
|
|
const updateDraftFileSpy = jest.spyOn(require('@actions/local/draft'), 'updateDraftFile');
|
|
const clientId = 'clientId';
|
|
|
|
manager.prepareUpload(url, {clientId, localPath: 'path1'} as FileInfo, channelId, rootId, 0, true, updateFileCallback);
|
|
expect(manager.isUploading(clientId)).toBe(true);
|
|
uploadMocks.rejectPromise!('Upload failed');
|
|
await new Promise(process.nextTick);
|
|
|
|
expect(updateFileCallback).toHaveBeenCalledWith({clientId, localPath: 'path1', failed: true});
|
|
expect(updateDraftFileSpy).not.toHaveBeenCalled();
|
|
expect(manager.isUploading(clientId)).toBe(false);
|
|
|
|
updateDraftFileSpy.mockRestore();
|
|
});
|
|
});
|