forked from Ivasoft/mattermost-mobile
[Gekidou] [MM-41524] Add tests to draft Upload Manager (#5990)
* Add tests to draft Upload Manager * Address feedback
This commit is contained in:
committed by
GitHub
parent
b27ebce2e0
commit
7e4b8b4dd9
439
app/init/draft_upload_manager/index.test.ts
Normal file
439
app/init/draft_upload_manager/index.test.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
||||
import {AppState, AppStateStatus} from 'react-native';
|
||||
|
||||
import {addFilesToDraft} from '@actions/local/draft';
|
||||
import {PROGRESS_TIME_TO_STORE} from '@constants/files';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import {queryDraft} from '@queries/servers/drafts';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {exportedForTesting} from '.';
|
||||
|
||||
const {DraftUploadManager} = exportedForTesting;
|
||||
|
||||
const url = 'baseHandler.test.com';
|
||||
const mockClient = TestHelper.createClient();
|
||||
|
||||
jest.mock('@init/network_manager', () => {
|
||||
const original = jest.requireActual('@init/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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const fileClientId = 'clientId';
|
||||
const fileServerId = 'serverId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]);
|
||||
|
||||
manager.prepareUpload(url, {clientId: fileClientId} 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}]}});
|
||||
|
||||
// Wait for other promises (on complete write) to finish
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
const draft = await queryDraft(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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const fileClientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]);
|
||||
|
||||
manager.prepareUpload(url, {clientId: fileClientId} 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 queryDraft(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 queryDraft(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 DraftUploadManager();
|
||||
|
||||
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 queryDraft(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 queryDraft(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 queryDraft(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 queryDraft(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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const fileClientId = 'clientId';
|
||||
const fileServerId = 'serverId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId: fileClientId} as FileInfo]);
|
||||
|
||||
manager.prepareUpload(url, {clientId: fileClientId} 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}]}});
|
||||
|
||||
// Wait for other promises (on complete write) to finish
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
const draft = await queryDraft(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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const clientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]);
|
||||
|
||||
manager.prepareUpload(url, {clientId} 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 queryDraft(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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const clientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]);
|
||||
|
||||
manager.prepareUpload(url, {clientId} 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 queryDraft(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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const clientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]);
|
||||
|
||||
const nullProgressHandler = jest.fn();
|
||||
let cancelProgressHandler = manager.registerProgressHandler(clientId, nullProgressHandler);
|
||||
expect(cancelProgressHandler).toBeNull();
|
||||
|
||||
manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0);
|
||||
expect(manager.isUploading(clientId)).toBe(true);
|
||||
|
||||
const progressHandler = jest.fn();
|
||||
cancelProgressHandler = manager.registerProgressHandler(clientId, progressHandler);
|
||||
expect(cancelProgressHandler).not.toBeNull();
|
||||
|
||||
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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const clientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]);
|
||||
|
||||
const nullErrorHandler = jest.fn();
|
||||
let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler);
|
||||
expect(cancelErrorHandler).toBeNull();
|
||||
|
||||
manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0);
|
||||
expect(manager.isUploading(clientId)).toBe(true);
|
||||
|
||||
const errorHandler = jest.fn();
|
||||
cancelErrorHandler = manager.registerErrorHandler(clientId, errorHandler);
|
||||
expect(cancelErrorHandler).not.toBeNull();
|
||||
|
||||
uploadMocks.rejectPromise!({message: 'error'});
|
||||
|
||||
// Wait for other promises (on complete write) to finish
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(errorHandler).toHaveBeenCalledWith('error');
|
||||
|
||||
// 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 DraftUploadManager();
|
||||
const uploadMocks = mockUpload();
|
||||
|
||||
const clientId = 'clientId';
|
||||
await addFilesToDraft(url, channelId, rootId, [{clientId} as FileInfo]);
|
||||
|
||||
const nullErrorHandler = jest.fn();
|
||||
let cancelErrorHandler = manager.registerProgressHandler(clientId, nullErrorHandler);
|
||||
expect(cancelErrorHandler).toBeNull();
|
||||
|
||||
manager.prepareUpload(url, {clientId} as FileInfo, channelId, rootId, 0);
|
||||
expect(manager.isUploading(clientId)).toBe(true);
|
||||
|
||||
const errorHandler = jest.fn();
|
||||
cancelErrorHandler = manager.registerErrorHandler(clientId, errorHandler);
|
||||
expect(cancelErrorHandler).not.toBeNull();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -168,17 +168,23 @@ class DraftUploadManager {
|
||||
|
||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||
if (appState !== 'active' && this.previousAppState === 'active') {
|
||||
this.storeProgress();
|
||||
await this.storeProgress();
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
};
|
||||
|
||||
private storeProgress = () => {
|
||||
private storeProgress = async () => {
|
||||
for (const h of Object.values(this.handlers)) {
|
||||
updateDraftFile(h.serverUrl, h.channelId, h.rootId, h.fileInfo);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateDraftFile(h.serverUrl, h.channelId, h.rootId, h.fileInfo);
|
||||
h.lastTimeStored = Date.now();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new DraftUploadManager();
|
||||
|
||||
export const exportedForTesting = {
|
||||
DraftUploadManager,
|
||||
};
|
||||
@@ -116,6 +116,7 @@ class TestHelper {
|
||||
patch: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
upload: jest.fn(),
|
||||
};
|
||||
|
||||
return new Client(mockApiClient, mockApiClient.baseUrl);
|
||||
|
||||
Reference in New Issue
Block a user