refine uploader, components and their tests (#23)
* feat(uploader, auth_pane): refine uploader and add tests * chore(package.json): remove unused deps * test(uploader, components): add tests for uploader and components
This commit is contained in:
parent
cc0b53eea7
commit
e40878f7be
13 changed files with 526 additions and 127 deletions
|
@ -90,5 +90,5 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="content"><div id="mount"></div></div>
|
<div id="content"><div id="mount"></div></div>
|
||||||
<script src="static/default-vendors-node_modules_axios_index_js-node_modules_css-loader_dist_runtime_api_js-node_-e9ca3b.bundle.js?910792307d026e16bec7"></script><script src="static/main.bundle.js?910792307d026e16bec7"></script></body>
|
<script src="static/default-vendors-node_modules_axios_index_js-node_modules_css-loader_dist_runtime_api_js-node_-e9ca3b.bundle.js?aa077616e258b24cc454"></script><script src="static/main.bundle.js?aa077616e258b24cc454"></script></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
1
src/client/web/jest.setup.js
Normal file
1
src/client/web/jest.setup.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
jest.setTimeout(15000);
|
|
@ -77,6 +77,9 @@
|
||||||
"ts",
|
"ts",
|
||||||
"tsx",
|
"tsx",
|
||||||
"js"
|
"js"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"./jest.setup.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"autoBump": {}
|
"autoBump": {}
|
||||||
|
|
137
src/client/web/src/client/__test__/uploader.test.tsx
Normal file
137
src/client/web/src/client/__test__/uploader.test.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import { FileUploader } from "../uploader";
|
||||||
|
import { FilesClient } from "../files_mock";
|
||||||
|
import { makePromise } from "../../test/helpers";
|
||||||
|
|
||||||
|
describe("Uploader", () => {
|
||||||
|
const content = ["123456"];
|
||||||
|
const filePath = "mock/file";
|
||||||
|
const blob = new Blob(content);
|
||||||
|
const fileSize = blob.size;
|
||||||
|
const file = new File(content, filePath);
|
||||||
|
|
||||||
|
const makeCreateResp = (status: number): Promise<any> => {
|
||||||
|
return makePromise({
|
||||||
|
status: status,
|
||||||
|
statusText: "",
|
||||||
|
data: {
|
||||||
|
path: filePath,
|
||||||
|
fileSize: fileSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeStatusResp = (status: number, uploaded: number): Promise<any> => {
|
||||||
|
return makePromise({
|
||||||
|
status: status,
|
||||||
|
statusText: "",
|
||||||
|
data: {
|
||||||
|
path: filePath,
|
||||||
|
isDir: false,
|
||||||
|
fileSize: fileSize,
|
||||||
|
uploaded: uploaded,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface statusResp {
|
||||||
|
status: number;
|
||||||
|
uploaded: number;
|
||||||
|
}
|
||||||
|
interface TestCase {
|
||||||
|
createResps: Array<number>;
|
||||||
|
uploadChunkResps: Array<any>;
|
||||||
|
uploadStatusResps: Array<any>;
|
||||||
|
result: boolean;
|
||||||
|
}
|
||||||
|
test("test start and upload method", async () => {
|
||||||
|
const testCases: Array<TestCase> = [
|
||||||
|
{
|
||||||
|
// fail to create file 4 times
|
||||||
|
createResps: [500, 500, 500, 500],
|
||||||
|
uploadChunkResps: [],
|
||||||
|
uploadStatusResps: [],
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// fail to get status
|
||||||
|
createResps: [200],
|
||||||
|
uploadChunkResps: [{ status: 500, uploaded: 0 }],
|
||||||
|
uploadStatusResps: [{ status: 600, uploaded: 0 }],
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// upload ok
|
||||||
|
createResps: [200],
|
||||||
|
uploadChunkResps: [
|
||||||
|
{ status: 200, uploaded: 0 },
|
||||||
|
{ status: 200, uploaded: 1 },
|
||||||
|
{ status: 200, uploaded: fileSize },
|
||||||
|
],
|
||||||
|
uploadStatusResps: [],
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// fail once
|
||||||
|
createResps: [200],
|
||||||
|
uploadChunkResps: [
|
||||||
|
{ status: 200, uploaded: 0 },
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
{ status: 200, uploaded: fileSize },
|
||||||
|
],
|
||||||
|
uploadStatusResps: [{ status: 200, uploaded: 1 }],
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// fail twice
|
||||||
|
createResps: [500, 500, 500, 200],
|
||||||
|
uploadChunkResps: [
|
||||||
|
{ status: 200, uploaded: 0 },
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
{ status: 200, uploaded: fileSize },
|
||||||
|
],
|
||||||
|
uploadStatusResps: [
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
],
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// other errors
|
||||||
|
createResps: [500, 200],
|
||||||
|
uploadChunkResps: [
|
||||||
|
{ status: 601, uploaded: 0 },
|
||||||
|
{ status: 408, uploaded: fileSize },
|
||||||
|
{ status: 200, uploaded: 1 },
|
||||||
|
{ status: 200, uploaded: fileSize },
|
||||||
|
],
|
||||||
|
uploadStatusResps: [
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
{ status: 500, uploaded: 1 },
|
||||||
|
],
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
|
const tc = testCases[i];
|
||||||
|
const uploader = new FileUploader(file, filePath);
|
||||||
|
const mockClient = new FilesClient("");
|
||||||
|
|
||||||
|
const createResps = tc.createResps.map((resp) => makeCreateResp(resp));
|
||||||
|
mockClient.createMock(createResps);
|
||||||
|
const uploadChunkResps = tc.uploadChunkResps.map((resp) =>
|
||||||
|
makeStatusResp(resp.status, resp.uploaded)
|
||||||
|
);
|
||||||
|
mockClient.uploadChunkMock(uploadChunkResps);
|
||||||
|
const uploadStatusResps = tc.uploadStatusResps.map((resp) =>
|
||||||
|
makeStatusResp(resp.status, resp.uploaded)
|
||||||
|
);
|
||||||
|
mockClient.uploadStatusMock(uploadStatusResps);
|
||||||
|
uploader.setClient(mockClient);
|
||||||
|
|
||||||
|
const ret = await uploader.start();
|
||||||
|
expect(ret).toEqual(tc.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,82 +1,102 @@
|
||||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
import {
|
||||||
|
Response,
|
||||||
import {MetadataResp, UploadStatusResp, ListResp} from "./";
|
UploadStatusResp,
|
||||||
|
ListResp,
|
||||||
|
} from "./";
|
||||||
|
|
||||||
export class FilesClient {
|
export class FilesClient {
|
||||||
private url: string;
|
private url: string;
|
||||||
private createMockResp: number;
|
|
||||||
private deleteMockResp: number;
|
private createMockRespID: number = 0;
|
||||||
private metadataMockResp: MetadataResp | null;
|
private createMockResps: Array<Promise<Response>>;
|
||||||
private mkdirMockResp: number | null;
|
private deleteMockResp: Promise<Response>;
|
||||||
private moveMockResp: number;
|
private metadataMockResp: Promise<Response>;
|
||||||
private uploadChunkMockResp: UploadStatusResp | null;
|
private mkdirMockResp: Promise<Response>;
|
||||||
private uploadStatusMockResp: UploadStatusResp | null;
|
private moveMockResp: Promise<Response>;
|
||||||
private listMockResp: ListResp | null;
|
private uploadChunkMockResps: Array<Promise<Response<UploadStatusResp>>>;
|
||||||
|
private uploadChunkMockRespID: number = 0;
|
||||||
|
private uploadStatusMockResps: Array<Promise<Response<UploadStatusResp>>>;
|
||||||
|
private uploadStatusMockRespID: number = 0;
|
||||||
|
private listMockResp: Promise<Response<ListResp>>;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
createMock = (resp: number) => {
|
createMock = (resps: Array<Promise<Response>>) => {
|
||||||
this.createMockResp = resp;
|
this.createMockResps = resps;
|
||||||
}
|
};
|
||||||
deleteMock = (resp: number) => {
|
|
||||||
|
deleteMock = (resp: Promise<Response>) => {
|
||||||
this.deleteMockResp = resp;
|
this.deleteMockResp = resp;
|
||||||
}
|
};
|
||||||
metadataMock = (resp: MetadataResp | null) => {
|
|
||||||
|
metadataMock = (resp: Promise<Response>) => {
|
||||||
this.metadataMockResp = resp;
|
this.metadataMockResp = resp;
|
||||||
}
|
};
|
||||||
mkdirMock = (resp: number | null) => {
|
|
||||||
|
mkdirMock = (resp: Promise<Response>) => {
|
||||||
this.mkdirMockResp = resp;
|
this.mkdirMockResp = resp;
|
||||||
}
|
};
|
||||||
moveMock = (resp: number) => {
|
|
||||||
|
moveMock = (resp: Promise<Response>) => {
|
||||||
this.moveMockResp = resp;
|
this.moveMockResp = resp;
|
||||||
}
|
};
|
||||||
uploadChunkMock = (resp: UploadStatusResp | null) => {
|
|
||||||
this.uploadChunkMockResp = resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadStatusMock = (resp: UploadStatusResp | null) => {
|
uploadChunkMock = (resps: Array<Promise<Response<UploadStatusResp>>>) => {
|
||||||
this.uploadStatusMockResp = resp;
|
this.uploadChunkMockResps = resps;
|
||||||
}
|
};
|
||||||
|
|
||||||
listMock = (resp: ListResp | null) => {
|
uploadStatusMock = (resps: Array<Promise<Response<UploadStatusResp>>>) => {
|
||||||
|
this.uploadStatusMockResps = resps;
|
||||||
|
};
|
||||||
|
|
||||||
|
listMock = (resp: Promise<Response<ListResp>>) => {
|
||||||
this.listMockResp = resp;
|
this.listMockResp = resp;
|
||||||
}
|
};
|
||||||
|
|
||||||
async create(filePath: string, fileSize: number): Promise<number> {
|
create = (filePath: string, fileSize: number): Promise<Response> => {
|
||||||
return this.createMockResp;
|
if (this.createMockRespID < this.createMockResps.length) {
|
||||||
}
|
return this.createMockResps[this.createMockRespID++];
|
||||||
|
}
|
||||||
|
throw new Error(`this.createMockRespID (${this.createMockRespID}) out of bound: ${this.createMockResps.length}`);
|
||||||
|
};
|
||||||
|
|
||||||
async delete(filePath: string): Promise<number> {
|
delete = (filePath: string): Promise<Response> => {
|
||||||
return this.deleteMockResp;
|
return this.deleteMockResp;
|
||||||
}
|
};
|
||||||
|
|
||||||
async metadata(filePath: string): Promise<MetadataResp | null> {
|
metadata = (filePath: string): Promise<Response> => {
|
||||||
return this.metadataMockResp;
|
return this.metadataMockResp;
|
||||||
}
|
};
|
||||||
|
|
||||||
async mkdir(dirpath: string): Promise<number | null> {
|
mkdir = (dirpath: string): Promise<Response> => {
|
||||||
return this.mkdirMockResp;
|
return this.mkdirMockResp;
|
||||||
}
|
};
|
||||||
|
|
||||||
async move(oldPath: string, newPath: string): Promise<number> {
|
move = (oldPath: string, newPath: string): Promise<Response> => {
|
||||||
return this.moveMockResp;
|
return this.moveMockResp;
|
||||||
}
|
};
|
||||||
|
|
||||||
async uploadChunk(
|
uploadChunk = (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
content: string | ArrayBuffer,
|
content: string | ArrayBuffer,
|
||||||
offset: number
|
offset: number
|
||||||
): Promise<UploadStatusResp | null> {
|
): Promise<Response<UploadStatusResp>> => {
|
||||||
return this.uploadChunkMockResp;
|
if (this.uploadChunkMockRespID < this.uploadChunkMockResps.length) {
|
||||||
}
|
return this.uploadChunkMockResps[this.uploadChunkMockRespID++];
|
||||||
|
}
|
||||||
|
throw new Error(`this.uploadChunkMockRespID (${this.uploadChunkMockRespID}) out of bound: ${this.uploadChunkMockResps.length}`);
|
||||||
|
};
|
||||||
|
|
||||||
async uploadStatus(filePath: string): Promise<UploadStatusResp | null> {
|
uploadStatus = (filePath: string): Promise<Response<UploadStatusResp>> => {
|
||||||
return this.uploadStatusMockResp;
|
if (this.uploadStatusMockRespID < this.uploadStatusMockResps.length) {
|
||||||
}
|
return this.uploadStatusMockResps[this.uploadStatusMockRespID++];
|
||||||
|
}
|
||||||
|
throw new Error(`this.uploadStatusMockRespID (${this.uploadStatusMockRespID}) out of bound: ${this.uploadStatusMockResps.length}`);
|
||||||
|
};
|
||||||
|
|
||||||
async list(dirPath: string): Promise<ListResp | null> {
|
list = (dirPath: string): Promise<Response<ListResp>> => {
|
||||||
return this.listMockResp;
|
return this.listMockResp;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ export class BaseClient {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!returned) {
|
if (!returned) {
|
||||||
src.cancel("request timeout");
|
src.cancel("request timeout");
|
||||||
// resolve(TimeoutResp);
|
resolve(TimeoutResp);
|
||||||
}
|
}
|
||||||
}, defaultTimeout);
|
}, defaultTimeout);
|
||||||
|
|
||||||
|
@ -95,11 +95,8 @@ export class BaseClient {
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const errMsg = e.toString();
|
const errMsg = e.toString();
|
||||||
console.log(e);
|
if (errMsg.includes("ERR_EMPTY")) {
|
||||||
|
// this means connection is eliminated by server because of timeout.
|
||||||
if (errMsg.includes("i/o timeput")) {
|
|
||||||
resolve(TimeoutResp);
|
|
||||||
} else if (errMsg.includes("ERR_EMPTY")) {
|
|
||||||
resolve(EmptyBodyResp);
|
resolve(EmptyBodyResp);
|
||||||
} else {
|
} else {
|
||||||
resolve(UnknownErrResp(errMsg));
|
resolve(UnknownErrResp(errMsg));
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
import { List } from "immutable";
|
import { IFilesClient } from "../client";
|
||||||
|
|
||||||
import { Response, UnknownErrResp, UploadStatusResp } from "./";
|
|
||||||
import { FilesClient } from "../client/files";
|
import { FilesClient } from "../client/files";
|
||||||
|
import { Response, UnknownErrResp, UploadStatusResp } from "./";
|
||||||
|
|
||||||
|
// TODO: get settings from server
|
||||||
const defaultChunkLen = 1024 * 1024 * 30; // 15MB/s
|
const defaultChunkLen = 1024 * 1024 * 30; // 15MB/s
|
||||||
const speedDownRatio = 0.5;
|
const speedDownRatio = 0.5;
|
||||||
const speedUpRatio = 1.1;
|
const speedUpRatio = 1.1;
|
||||||
|
const retryLimit = 4;
|
||||||
|
|
||||||
export class FileUploader {
|
export class FileUploader {
|
||||||
private reader = new FileReader();
|
private reader = new FileReader();
|
||||||
private client = new FilesClient("");
|
private client: IFilesClient = new FilesClient("");
|
||||||
private file: File;
|
|
||||||
private filePath: string;
|
|
||||||
private offset: number;
|
|
||||||
private chunkLen: number = defaultChunkLen;
|
private chunkLen: number = defaultChunkLen;
|
||||||
private progressCb: (filePath: string, progress: number) => void;
|
private file: File;
|
||||||
|
private offset: number;
|
||||||
|
private filePath: string;
|
||||||
private errMsg: string | null;
|
private errMsg: string | null;
|
||||||
|
private progressCb: (filePath: string, progress: number) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
file: File,
|
file: File,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
progressCb: (filePath: string, progress: number) => void
|
progressCb?: (filePath: string, progress: number) => void
|
||||||
) {
|
) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
|
@ -30,17 +31,41 @@ export class FileUploader {
|
||||||
|
|
||||||
err = (): string | null => {
|
err = (): string | null => {
|
||||||
return this.errMsg;
|
return this.errMsg;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
setClient = (client: IFilesClient) => {
|
||||||
|
this.client = client;
|
||||||
|
};
|
||||||
|
|
||||||
|
create = async (filePath: string, fileSize: number): Promise<Response> => {
|
||||||
|
return this.client.create(filePath, fileSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadChunk = async (
|
||||||
|
filePath: string,
|
||||||
|
base64Chunk: string,
|
||||||
|
offset: number
|
||||||
|
): Promise<Response<UploadStatusResp>> => {
|
||||||
|
return this.client.uploadChunk(filePath, base64Chunk, offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadStatus = async (
|
||||||
|
filePath: string
|
||||||
|
): Promise<Response<UploadStatusResp>> => {
|
||||||
|
return this.client.uploadStatus(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
start = async (): Promise<boolean> => {
|
start = async (): Promise<boolean> => {
|
||||||
const resp = await this.client.create(this.filePath, this.file.size);
|
let resp: Response;
|
||||||
switch (resp.status) {
|
for (let i = 0; i < retryLimit; i++) {
|
||||||
case 200:
|
resp = await this.create(this.filePath, this.file.size);
|
||||||
|
if (resp.status === 200) {
|
||||||
return await this.upload();
|
return await this.upload();
|
||||||
default:
|
}
|
||||||
this.errMsg = `failed to create ${this.filePath}: status=${resp.statusText}`;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.errMsg = `failed to create ${this.filePath}: status=${resp.statusText}`;
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
upload = async (): Promise<boolean> => {
|
upload = async (): Promise<boolean> => {
|
||||||
|
@ -49,7 +74,7 @@ export class FileUploader {
|
||||||
this.offset >= 0 &&
|
this.offset >= 0 &&
|
||||||
this.offset < this.file.size
|
this.offset < this.file.size
|
||||||
) {
|
) {
|
||||||
let uploadPromise = new Promise<Response<UploadStatusResp>>(
|
const uploadPromise = new Promise<Response<UploadStatusResp>>(
|
||||||
(resolve: (resp: Response<UploadStatusResp>) => void) => {
|
(resolve: (resp: Response<UploadStatusResp>) => void) => {
|
||||||
this.reader.onerror = (ev: ProgressEvent<FileReader>) => {
|
this.reader.onerror = (ev: ProgressEvent<FileReader>) => {
|
||||||
resolve(UnknownErrResp(this.reader.error.toString()));
|
resolve(UnknownErrResp(this.reader.error.toString()));
|
||||||
|
@ -58,8 +83,7 @@ export class FileUploader {
|
||||||
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => {
|
this.reader.onloadend = (ev: ProgressEvent<FileReader>) => {
|
||||||
const dataURL = ev.target.result as string; // readAsDataURL
|
const dataURL = ev.target.result as string; // readAsDataURL
|
||||||
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1);
|
const base64Chunk = dataURL.slice(dataURL.indexOf(",") + 1);
|
||||||
this.client
|
this.uploadChunk(this.filePath, base64Chunk, this.offset)
|
||||||
.uploadChunk(this.filePath, base64Chunk, this.offset)
|
|
||||||
.then((resp: Response<UploadStatusResp>) => {
|
.then((resp: Response<UploadStatusResp>) => {
|
||||||
resolve(resp);
|
resolve(resp);
|
||||||
})
|
})
|
||||||
|
@ -78,27 +102,28 @@ export class FileUploader {
|
||||||
this.reader.readAsDataURL(blob);
|
this.reader.readAsDataURL(blob);
|
||||||
|
|
||||||
const uploadResp = await uploadPromise;
|
const uploadResp = await uploadPromise;
|
||||||
|
|
||||||
if (uploadResp.status === 200 && uploadResp.data != null) {
|
if (uploadResp.status === 200 && uploadResp.data != null) {
|
||||||
this.offset = uploadResp.data.uploaded;
|
this.offset = uploadResp.data.uploaded;
|
||||||
this.chunkLen = Math.ceil(this.chunkLen * speedUpRatio);
|
this.chunkLen = Math.ceil(this.chunkLen * speedUpRatio);
|
||||||
} else {
|
} else {
|
||||||
this.errMsg = uploadResp.statusText;
|
this.errMsg = uploadResp.statusText;
|
||||||
this.chunkLen = Math.ceil(this.chunkLen * speedDownRatio);
|
this.chunkLen = Math.ceil(this.chunkLen * speedDownRatio);
|
||||||
const uploadStatusResp = await this.client.uploadStatus(this.filePath);
|
const uploadStatusResp = await this.uploadStatus(this.filePath);
|
||||||
|
|
||||||
console.log(uploadStatusResp.status);
|
|
||||||
|
|
||||||
if (uploadStatusResp.status === 200) {
|
if (uploadStatusResp.status === 200) {
|
||||||
this.offset = uploadStatusResp.data.uploaded;
|
this.offset = uploadStatusResp.data.uploaded;
|
||||||
} else if (uploadStatusResp.status === 600) {
|
} else if (uploadStatusResp.status === 600) {
|
||||||
this.errMsg = "unknown error";
|
this.errMsg = "unknown error";
|
||||||
break
|
break;
|
||||||
} else {
|
} else {
|
||||||
// do nothing and retry
|
// do nothing and retry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.progressCb(this.filePath, Math.ceil(this.offset / this.file.size));
|
if (this.progressCb != null) {
|
||||||
|
this.progressCb(this.filePath, Math.ceil(this.offset / this.file.size));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.chunkLen === 0) {
|
if (this.chunkLen === 0) {
|
||||||
|
|
|
@ -1,45 +1,44 @@
|
||||||
// import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
// TODO: replace this with jest mocks
|
// TODO: replace this with jest mocks
|
||||||
|
import { Response } from "./";
|
||||||
|
|
||||||
export class MockUsersClient {
|
export class MockUsersClient {
|
||||||
private url: string;
|
private url: string;
|
||||||
private loginResp: number;
|
private loginMockResp: Promise<Response>;
|
||||||
private logoutResp: number;
|
private logoutMockResp: Promise<Response>;
|
||||||
private isAuthedResp: number;
|
private isAuthedMockResp: Promise<Response>;
|
||||||
private setPwdResp: number;
|
private setPwdMockResp: Promise<Response>;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
mockLoginResp(status: number) {
|
loginMock = (resp: Promise<Response>) => {
|
||||||
this.loginResp = status;
|
this.loginMockResp = resp;
|
||||||
}
|
}
|
||||||
mocklogoutResp(status: number) {
|
logoutMock = (resp: Promise<Response>) => {
|
||||||
this.logoutResp = status;
|
this.logoutMockResp = resp;
|
||||||
}
|
}
|
||||||
mockisAuthedResp(status: number) {
|
isAuthedMock = (resp: Promise<Response>) => {
|
||||||
this.isAuthedResp = status;
|
this.isAuthedMockResp = resp;
|
||||||
}
|
}
|
||||||
mocksetPwdResp(status: number) {
|
setPwdMock = (resp: Promise<Response>) => {
|
||||||
this.setPwdResp = status;
|
this.setPwdMockResp = resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(user: string, pwd: string): Promise<number> {
|
login = (user: string, pwd: string): Promise<Response> => {
|
||||||
return this.loginResp
|
return this.loginMockResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// token cookie is set by browser
|
logout = (): Promise<Response> => {
|
||||||
async logout(): Promise<number> {
|
return this.logoutMockResp;
|
||||||
return this.logoutResp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async isAuthed(): Promise<number> {
|
isAuthed = (): Promise<Response> => {
|
||||||
return this.isAuthedResp
|
return this.isAuthedMockResp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// token cookie is set by browser
|
setPwd = (oldPwd: string, newPwd: string): Promise<Response> => {
|
||||||
async setPwd(oldPwd: string, newPwd: string): Promise<number> {
|
return this.setPwdMockResp;
|
||||||
return this.setPwdResp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,56 @@
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { init } from "../core_state";
|
import { init } from "../core_state";
|
||||||
import { Updater } from "../auth_pane";
|
import { Updater } from "../auth_pane";
|
||||||
import { MockUsersClient } from "../../client/users_mock";
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
|
import { Response } from "../../client";
|
||||||
|
|
||||||
describe("AuthPane", () => {
|
describe("AuthPane", () => {
|
||||||
test("Updater: initIsAuthed", () => {
|
const makePromise = (ret: any): Promise<any> => {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const makeNumberResponse = (status: number): Promise<Response> => {
|
||||||
|
return makePromise({
|
||||||
|
status: status,
|
||||||
|
statusText: "",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Updater-initIsAuthed", async () => {
|
||||||
const tests = [
|
const tests = [
|
||||||
{
|
{
|
||||||
loginStatus: 200,
|
loginStatus: 200,
|
||||||
|
logoutStatus: 200,
|
||||||
|
isAuthedStatus: 200,
|
||||||
|
setPwdStatus: 200,
|
||||||
isAuthed: true,
|
isAuthed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginStatus: 500,
|
loginStatus: 200,
|
||||||
|
logoutStatus: 200,
|
||||||
|
isAuthedStatus: 500,
|
||||||
|
setPwdStatus: 200,
|
||||||
isAuthed: false,
|
isAuthed: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const client = new MockUsersClient("foobarurl");
|
const client = new MockUsersClient("");
|
||||||
Updater.setClient(client);
|
for (let i = 0; i < tests.length; i++) {
|
||||||
const coreState = init();
|
const tc = tests[i];
|
||||||
|
|
||||||
tests.forEach(async (tc) => {
|
client.loginMock(makeNumberResponse(tc.loginStatus));
|
||||||
client.mockisAuthedResp(tc.loginStatus);
|
client.logoutMock(makeNumberResponse(tc.logoutStatus));
|
||||||
await Updater.initIsAuthed().then(() => {
|
client.isAuthedMock(makeNumberResponse(tc.isAuthedStatus));
|
||||||
const newState = Updater.setAuthPane(coreState);
|
client.setPwdMock(makeNumberResponse(tc.setPwdStatus));
|
||||||
expect(newState.panel.authPane.authed).toEqual(tc.isAuthed);
|
|
||||||
});
|
const coreState = init();
|
||||||
});
|
Updater.setClient(client);
|
||||||
|
Updater.init(coreState.panel.authPane);
|
||||||
|
await Updater.initIsAuthed();
|
||||||
|
const newState = Updater.setAuthPane(coreState);
|
||||||
|
|
||||||
|
expect(newState.panel.authPane.authed).toEqual(tc.isAuthed);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,188 @@
|
||||||
import * as React from "react";
|
import { List, Map } from "immutable";
|
||||||
|
|
||||||
import { init } from "../core_state";
|
import { init } from "../core_state";
|
||||||
|
import { makePromise, makeNumberResponse } from "../../test/helpers";
|
||||||
import { Updater } from "../browser";
|
import { Updater } from "../browser";
|
||||||
import { MockUsersClient } from "../../client/users_mock";
|
import { MockUsersClient } from "../../client/users_mock";
|
||||||
|
import { FilesClient } from "../../client/files_mock";
|
||||||
|
import { MetadataResp } from "../../client";
|
||||||
|
|
||||||
describe("Browser", () => {
|
describe("Browser", () => {
|
||||||
test("Updater: ", () => {
|
test("Updater: setPwd", async () => {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
listResp: {
|
||||||
|
status: 200,
|
||||||
|
statusText: "",
|
||||||
|
data: {
|
||||||
|
metadatas: [
|
||||||
|
{
|
||||||
|
name: "file",
|
||||||
|
size: 1,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "folder",
|
||||||
|
size: 0,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filePath: "path/file",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const usersClient = new MockUsersClient("");
|
||||||
|
const filesClient = new FilesClient("");
|
||||||
|
for (let i = 0; i < tests.length; i++) {
|
||||||
|
const tc = tests[i];
|
||||||
|
|
||||||
|
filesClient.listMock(makePromise(tc.listResp));
|
||||||
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
|
const coreState = init();
|
||||||
|
Updater.init(coreState.panel.browser);
|
||||||
|
await Updater.setItems(List<string>(tc.filePath.split("/")));
|
||||||
|
const newState = Updater.setBrowser(coreState);
|
||||||
|
|
||||||
|
newState.panel.browser.items.forEach((item, i) => {
|
||||||
|
expect(item.name).toEqual(tc.listResp.data.metadatas[i].name);
|
||||||
|
expect(item.size).toEqual(tc.listResp.data.metadatas[i].size);
|
||||||
|
expect(item.modTime).toEqual(tc.listResp.data.metadatas[i].modTime);
|
||||||
|
expect(item.isDir).toEqual(tc.listResp.data.metadatas[i].isDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Updater: delete", async () => {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
dirPath: "path/path2",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "file",
|
||||||
|
size: 1,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "folder",
|
||||||
|
size: 0,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selected: {
|
||||||
|
file: true,
|
||||||
|
},
|
||||||
|
listResp: {
|
||||||
|
status: 200,
|
||||||
|
statusText: "",
|
||||||
|
data: {
|
||||||
|
metadatas: [
|
||||||
|
{
|
||||||
|
name: "folder",
|
||||||
|
size: 0,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filePath: "path/file",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const usersClient = new MockUsersClient("");
|
||||||
|
const filesClient = new FilesClient("");
|
||||||
|
for (let i = 0; i < tests.length; i++) {
|
||||||
|
const tc = tests[i];
|
||||||
|
|
||||||
|
filesClient.listMock(makePromise(tc.listResp));
|
||||||
|
filesClient.deleteMock(makeNumberResponse(200));
|
||||||
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
|
const coreState = init();
|
||||||
|
Updater.init(coreState.panel.browser);
|
||||||
|
await Updater.delete(
|
||||||
|
List<string>(tc.dirPath.split("/")),
|
||||||
|
List<MetadataResp>(tc.items),
|
||||||
|
Map<boolean>(tc.selected)
|
||||||
|
);
|
||||||
|
const newState = Updater.setBrowser(coreState);
|
||||||
|
|
||||||
|
// TODO: check inputs of delete
|
||||||
|
|
||||||
|
newState.panel.browser.items.forEach((item, i) => {
|
||||||
|
expect(item.name).toEqual(tc.listResp.data.metadatas[i].name);
|
||||||
|
expect(item.size).toEqual(tc.listResp.data.metadatas[i].size);
|
||||||
|
expect(item.modTime).toEqual(tc.listResp.data.metadatas[i].modTime);
|
||||||
|
expect(item.isDir).toEqual(tc.listResp.data.metadatas[i].isDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Updater: moveHere", async () => {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
dirPath1: "path/path1",
|
||||||
|
dirPath2: "path/path2",
|
||||||
|
selected: {
|
||||||
|
file1: true,
|
||||||
|
file2: true,
|
||||||
|
},
|
||||||
|
listResp: {
|
||||||
|
status: 200,
|
||||||
|
statusText: "",
|
||||||
|
data: {
|
||||||
|
metadatas: [
|
||||||
|
{
|
||||||
|
name: "file1",
|
||||||
|
size: 1,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file2",
|
||||||
|
size: 2,
|
||||||
|
modTime: "1-1",
|
||||||
|
isDir: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const usersClient = new MockUsersClient("");
|
||||||
|
const filesClient = new FilesClient("");
|
||||||
|
for (let i = 0; i < tests.length; i++) {
|
||||||
|
const tc = tests[i];
|
||||||
|
|
||||||
|
filesClient.listMock(makePromise(tc.listResp));
|
||||||
|
filesClient.moveMock(makeNumberResponse(200));
|
||||||
|
Updater.setClients(usersClient, filesClient);
|
||||||
|
|
||||||
|
const coreState = init();
|
||||||
|
Updater.init(coreState.panel.browser);
|
||||||
|
await Updater.moveHere(
|
||||||
|
tc.dirPath1,
|
||||||
|
tc.dirPath2,
|
||||||
|
Map<boolean>(tc.selected)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: check inputs of move
|
||||||
|
|
||||||
|
const newState = Updater.setBrowser(coreState);
|
||||||
|
newState.panel.browser.items.forEach((item, i) => {
|
||||||
|
expect(item.name).toEqual(tc.listResp.data.metadatas[i].name);
|
||||||
|
expect(item.size).toEqual(tc.listResp.data.metadatas[i].size);
|
||||||
|
expect(item.modTime).toEqual(tc.listResp.data.metadatas[i].modTime);
|
||||||
|
expect(item.isDir).toEqual(tc.listResp.data.metadatas[i].isDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,6 +95,7 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
||||||
this.update(Updater.setAuthPane);
|
this.update(Updater.setAuthPane);
|
||||||
this.setState({ user: "", pwd: "" });
|
this.setState({ user: "", pwd: "" });
|
||||||
} else {
|
} else {
|
||||||
|
this.setState({ user: "", pwd: "" });
|
||||||
alert("Failed to login.");
|
alert("Failed to login.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -104,9 +105,8 @@ export class AuthPane extends React.Component<Props, State, {}> {
|
||||||
Updater.logout().then((ok: boolean) => {
|
Updater.logout().then((ok: boolean) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this.update(Updater.setAuthPane);
|
this.update(Updater.setAuthPane);
|
||||||
this.setState({ user: "", pwd: "" });
|
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to login.");
|
alert("Failed to logout.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface Item {
|
||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
dirPath: List<string>;
|
dirPath: List<string>;
|
||||||
items: List<MetadataResp>;
|
items: List<MetadataResp>;
|
||||||
|
@ -51,7 +52,7 @@ export class Updater {
|
||||||
|
|
||||||
Updater.props.dirPath = dirParts;
|
Updater.props.dirPath = dirParts;
|
||||||
Updater.props.items =
|
Updater.props.items =
|
||||||
listResp != null
|
listResp.status === 200
|
||||||
? List<MetadataResp>(listResp.data.metadatas)
|
? List<MetadataResp>(listResp.data.metadatas)
|
||||||
: Updater.props.items;
|
: Updater.props.items;
|
||||||
};
|
};
|
||||||
|
@ -98,7 +99,6 @@ export class Updater {
|
||||||
async (itemName: string): Promise<string> => {
|
async (itemName: string): Promise<string> => {
|
||||||
const oldPath = getItemPath(srcDir, itemName);
|
const oldPath = getItemPath(srcDir, itemName);
|
||||||
const newPath = getItemPath(dstDir, itemName);
|
const newPath = getItemPath(dstDir, itemName);
|
||||||
|
|
||||||
const resp = await Updater.filesClient.move(oldPath, newPath);
|
const resp = await Updater.filesClient.move(oldPath, newPath);
|
||||||
return resp.status === 200 ? "" : itemName;
|
return resp.status === 200 ? "" : itemName;
|
||||||
}
|
}
|
||||||
|
|
15
src/client/web/src/test/helpers.ts
Normal file
15
src/client/web/src/test/helpers.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Response } from "../client";
|
||||||
|
|
||||||
|
export const makePromise = (ret: any): Promise<any> => {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeNumberResponse = (status: number): Promise<Response> => {
|
||||||
|
return makePromise({
|
||||||
|
status: status,
|
||||||
|
statusText: "",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue