import { Sitefinity, TypeOptions } from "../Sitefinity"; import { ChunkUploadInfoDTO, ChunkUploadResult, ChunkUploadSdk, CleanUpChunksRequestOptions, CommitChunksRequestOptions, GetChunkUploadInfoRequestOptions, UploadChunkRequestOptions } from "./chunk-upload.sdk"; import { FileUploadChunk } from "./chunked-file-upload.utilities"; const baseUrl = "http://www.mysite.com/api/default/"; const chunkSize10MegaBytes = 10000000; const typeOptions: TypeOptions = { entitySet: "images" }; const mimeType = "image/jpeg"; const libraryId = "4dff7037-e3e4-4980-8cd2-333333333333"; const chunkedFileUploadSessionId = "4dff7037-e3e4-4980-8cd2-666666666666"; const blobStorageProviderName = "Database"; const itemId = "4dff7037-e3e4-4980-8cd2-999999999999"; const fileName = "fakeImage.jpeg"; const uploadProperties = { "Title": "asd", "UrlName": "asd" }; describe("Chunk upload tests", () => { let sitefinity: Sitefinity; let chunkUploadSdk: ChunkUploadSdk; beforeAll(() => { sitefinity = new Sitefinity({ baseUrl }); chunkUploadSdk = sitefinity.data(typeOptions).chunkUpload; }); beforeEach(function () { jasmine.Ajax.install(); }); afterEach(function () { jasmine.Ajax.uninstall(); }); it("chunkFile should take base64 string input and convert it successfully to a blob and produce expected chunks result", () => { const result = chunkUploadSdk.chunkFile(getBase64Image713BytesInSize(), chunkSize10MegaBytes, mimeType); // a bit of magic, but this is the size of the base64 expect(result.fileTotalSize).toEqual(713); // there should be only 1 chunk since the chunk size is 10MB expect(result.totalChunksCount).toBe(1); expect(result.chunks[0].content instanceof Blob).toBeTrue(); expect(result.chunks[0].content.size).toEqual(713); expect(result.chunks[0].content.type).toEqual(mimeType); expect(result.chunks[0].ordinal).toEqual(0); }); it("chunkFile should generate correct number of chunks with correct size when the chunk size divides the files size with a remainder.", () => { // if we have a file that is 713 bytes in size and we chunk it in parts with the size of // 2 bytes, we should get 357 chunks, (713/2 = 356.5), thus there should be 356 chunks with // size of 2 bytes and the 357th chunk should have a size of 1 byte. const testFileSize = 713; const chunkSize2Bytes = 2; const expectedSizeOfLastChunk = testFileSize % chunkSize2Bytes; const expectNumberOfChunks = Math.ceil(testFileSize / chunkSize2Bytes); const result = chunkUploadSdk.chunkFile(getBase64Image713BytesInSize(), chunkSize2Bytes, mimeType); expect(result.fileTotalSize).toEqual(testFileSize); expect(result.totalChunksCount).toEqual(expectNumberOfChunks); result.chunks.forEach((chunk, i) => { expect(chunk.ordinal).toEqual(i); expect(chunk.content.type).toEqual(mimeType); if (i === result.totalChunksCount - 1) { expect(chunk.content.size).toEqual(expectedSizeOfLastChunk); } else { expect(chunk.content.size).toEqual(chunkSize2Bytes); } }); }); it("getChunkUploadInfo should set correct action name and parameter and be a GET request", () => { const requestOptions: GetChunkUploadInfoRequestOptions = { data: { libraryId } }; chunkUploadSdk.getChunkUploadInfo(requestOptions); const getChunkUploadInfoRequest = jasmine.Ajax.requests.mostRecent(); assertGetChunkUploadInfoRequest(getChunkUploadInfoRequest); }); it("cleanUpChunks should set correct action name and parameters and be a POST request", () => { const cleanUpChunksRequestOptions: CleanUpChunksRequestOptions = { data: { chunkedFileUploadSessionId, blobStorageProviderName } }; chunkUploadSdk.cleanUpChunks(cleanUpChunksRequestOptions); const cleanUpChunksRequest = jasmine.Ajax.requests.mostRecent(); expect(cleanUpChunksRequest.url).toContain("Default.CleanUpChunks"); expect(cleanUpChunksRequest.params).toBe(JSON.stringify(cleanUpChunksRequestOptions.data)); expect(cleanUpChunksRequest.url).toContain(typeOptions.entitySet); expect(cleanUpChunksRequest.method).toBe("POST"); }); it("commitChunks should set correct action name and parameters and be a POST request", () => { const fileTotalSize = 1000; const numberOfChunks = Math.ceil(fileTotalSize / chunkSize10MegaBytes); const commitChunksRequestOptions: CommitChunksRequestOptions = { data: { chunkedFileUploadSessionId, contentType: mimeType, fileTotalSize, numberOfChunks, name: fileName, blobStorageProviderName, itemId, uploadProperties } }; chunkUploadSdk.commitChunks(commitChunksRequestOptions); const commitChunksRequest = jasmine.Ajax.requests.mostRecent(); assertCommitChunksRequest(commitChunksRequest, commitChunksRequestOptions); }); it("uploadChunk should set correct url, headers, body to be a Blob and be a POST request", () => { const result = chunkUploadSdk.chunkFile(getBase64Image713BytesInSize(), chunkSize10MegaBytes, mimeType); const expectedChunksCount = Math.ceil(713 / chunkSize10MegaBytes); expect(result.totalChunksCount).toEqual(expectedChunksCount); result.chunks.forEach(chunk => { const uploadChunkRequestOptions: UploadChunkRequestOptions = { data: { fileName: fileName, fileMimeType: mimeType, chunkedFileUploadSessionId, chunkSize: chunkSize10MegaBytes, chunkOrdinal: chunk.ordinal, numberOfChunks: result.totalChunksCount, positionOfChunkInQueue: chunk.ordinal, chunkBlob: chunk.content, blobStorageProviderName: blobStorageProviderName } }; chunkUploadSdk.uploadChunk(uploadChunkRequestOptions); const chunkUploadRequest = jasmine.Ajax.requests.mostRecent(); assertChunkUploadRequest(chunkUploadRequest, chunk); }); }); it("partially simulate chunk upload", () => { const GET_CHUNK_UPLOAD_INFO_RESPONSE_MOCK = { "@odata.context": "http://localhost:1337/sf/system/$metadata#Telerik.Sitefinity.Web.Api.OData.Operations.Media.Models.ChunkUploadInfo", "ProviderSupportsChunkUpload": true, "BlobStorageProviderName": "Database", "ChunkedFileUploadSessionId": chunkedFileUploadSessionId, "ChunkSize": chunkSize10MegaBytes, "MaxRetryCountForChunk": 5, "MaxSimultaneousChunkUploadRequestsPerSession": 10 }; const CHUNK_UPLOAD_RESPONSE_MOCK = { "SuccessfullyUploaded": true, "ChunkedFileUploadSessionId": chunkedFileUploadSessionId }; const COMMIT_CHUNKS_RESPONSE_MOCK = { "@odata.context": "http://localhost:1337/sf/system/$metadata#images/$entity", "Id": "e3dfab01-3e0b-4e60-9a38-934c640c937d", "LastModified": "2022-09-07T08:23:36Z", "PublicationDate": "2022-09-07T08:23:34Z", "Title": "christmas_pack", "Description": "", "DateCreated": "2022-09-07T08:23:34Z", "IncludeInSitemap": true, "SystemSourceKey": null, "Ordinal": -1, "UrlName": "christmas_pack", "ItemDefaultUrl": "/images/default-source/default-album/christmas_pack", "Author": "", "Extension": ".jpg", "MimeType": "image/jpeg", "TotalSize": 141429, "Tags": [], "Category": [], "Width": 600, "Height": 400, "AlternativeText": "", "FolderId": null, "ParentId": "4ba7ad46-f29b-4e65-be17-9bf7ce5ba1fb", "Urls": [], "MediaFileUrlName": "christmas_pack", "MediaFileUrls": [], "Provider": "OpenAccessDataProvider", "Url": "http://localhost:1337/images/default-source/default-album/christmas_pack.jpg?Status=Master&sfvrsn=4ed4e51c_1", "ThumbnailUrl": "http://localhost:1337/images/default-source/default-album/christmas_pack.tmb-0.jpg?Status=Master&Culture=en&sfvrsn=4ed4e51c_1", "IsDamMedia": false, "Breadcrumb": ["Default Library"], "CreatedBy": "stefan koserkov", "LastModifiedBy": "stefan koserkov", "AvailableLanguages": ["en"], "LibraryTitle": "Default Library", "Thumbnails": [{ "Title": "0", "Url": "http://localhost:1337/images/default-source/default-album/christmas_pack.tmb-0.jpg?Status=Master&Culture=en&sfvrsn=4ed4e51c_1", "Width": 240, "Height": 160, "MimeType": "image/jpeg" }], "LockedStatus": { "Date": "2022-09-07T08:23:34.503Z", "User": "User Not Found", "Id": "User Not Found" }, "DisplayStatus": [{ "Name": "Draft", "Source": "Lifecycle", "Label": "Draft", "DetailedLabel": "Saved as draft", "PublicationDate": null, "ExpirationDate": null, "Date": "2022-09-07T08:23:36.44Z", "User": "stefan koserkov", "Id": null, "Message": { "Title": null, "Description": null, "Operations": [] } }], "FirstPublished": null, "LastPublished": null }; const expectedChunksCount = Math.ceil(713 / GET_CHUNK_UPLOAD_INFO_RESPONSE_MOCK.ChunkSize); let successfullyUploadedChunks = 0; const getChunkUploadInfoOptions: GetChunkUploadInfoRequestOptions = { data: { libraryId }, successCb: (dto: ChunkUploadInfoDTO) => { const result = chunkUploadSdk.chunkFile(getBase64Image713BytesInSize(), chunkSize10MegaBytes, mimeType); expect(result.totalChunksCount).toEqual(expectedChunksCount); result.chunks.forEach(chunk => { const uploadChunkRequestOptions: UploadChunkRequestOptions = { data: { fileName: fileName, fileMimeType: mimeType, chunkedFileUploadSessionId: dto.ChunkedFileUploadSessionId, chunkSize: dto.ChunkSize, chunkOrdinal: chunk.ordinal, numberOfChunks: result.totalChunksCount, positionOfChunkInQueue: chunk.ordinal, chunkBlob: chunk.content, blobStorageProviderName: dto.BlobStorageProviderName }, successCb: (chunkResult: ChunkUploadResult) => { if (chunkResult.SuccessfullyUploaded) { successfullyUploadedChunks++; } if (successfullyUploadedChunks === result.totalChunksCount) { const commitChunksRequestOptions: CommitChunksRequestOptions = { data: { chunkedFileUploadSessionId, contentType: mimeType, fileTotalSize: result.fileTotalSize, numberOfChunks: result.totalChunksCount, name: fileName, blobStorageProviderName: dto.BlobStorageProviderName, uploadProperties, itemId: null }, successCb: commitChunksResponse => { expect(commitChunksResponse).toEqual(COMMIT_CHUNKS_RESPONSE_MOCK); } }; chunkUploadSdk.commitChunks(commitChunksRequestOptions); // responds to chunkUploadSdk.commitChunks const commitChunksRequest = jasmine.Ajax.requests.mostRecent(); assertCommitChunksRequest(commitChunksRequest, commitChunksRequestOptions); commitChunksRequest.respondWith({ status: 200, responseText: JSON.stringify(COMMIT_CHUNKS_RESPONSE_MOCK) }); } } }; chunkUploadSdk.uploadChunk(uploadChunkRequestOptions); // responds to chunkUploadSdk.uploadChunk const chunkUploadRequest = jasmine.Ajax.requests.mostRecent(); assertChunkUploadRequest(chunkUploadRequest, chunk); chunkUploadRequest.respondWith({ status: 200, responseText: JSON.stringify(CHUNK_UPLOAD_RESPONSE_MOCK) }); }); } }; chunkUploadSdk.getChunkUploadInfo(getChunkUploadInfoOptions); // responds to chunkUploadSdk.getChunkUploadInfo const getChunkUploadInfoRequest = jasmine.Ajax.requests.mostRecent(); assertGetChunkUploadInfoRequest(getChunkUploadInfoRequest); getChunkUploadInfoRequest.respondWith({ status: 200, responseText: JSON.stringify(GET_CHUNK_UPLOAD_INFO_RESPONSE_MOCK) }); }); }); function assertCommitChunksRequest(commitChunksRequest: JasmineAjaxRequest, commitChunksRequestOptions: CommitChunksRequestOptions) { const expectedPayload = { commitChunksDto: { chunkedFileUploadSessionId: commitChunksRequestOptions.data.chunkedFileUploadSessionId, contentType: commitChunksRequestOptions.data.contentType, fileTotalSize: commitChunksRequestOptions.data.fileTotalSize, numberOfChunks: commitChunksRequestOptions.data.numberOfChunks, fileName: commitChunksRequestOptions.data.name, blobStorageProviderName: commitChunksRequestOptions.data.blobStorageProviderName, itemId: commitChunksRequestOptions.data.itemId, properties: commitChunksRequestOptions.data.uploadProperties } }; expect(commitChunksRequest.url).toContain("Default.CommitChunks"); expect(commitChunksRequest.params).toEqual(JSON.stringify(expectedPayload)); expect(commitChunksRequest.url).toContain(typeOptions.entitySet); expect(commitChunksRequest.method).toEqual("POST"); } function assertChunkUploadRequest(chunkUploadRequest: JasmineAjaxRequest, chunk: FileUploadChunk) { expect(chunkUploadRequest.url) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should contain ${baseUrl}${typeOptions.entitySet}`) .toEqual(`${baseUrl}${typeOptions.entitySet}`); expect(chunkUploadRequest.method) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should be a POST request`) .toEqual("POST"); expect(chunkUploadRequest.params instanceof Blob) .withContext(`Chunk request for chunk number ${chunk.ordinal} body must be of type Blob`) .toBeTrue(); expect((chunkUploadRequest.params as Blob).size) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have size of ${chunk.content.size}`) .toBe(chunk.content.size); expect((chunkUploadRequest.params as Blob).type) .withContext(`Chunk request for chunk number ${chunk.ordinal} body param should have a mime type of ${mimeType}`) .toBe(mimeType); expect(chunkUploadRequest.requestHeaders["Content-Encoding"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "Content-Encoding" with value identity`) .toEqual("identity"); expect(chunkUploadRequest.requestHeaders["Content-Type"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "Content-type" with value ${mimeType}`) .toEqual(mimeType); expect(chunkUploadRequest.requestHeaders["X-SF-Blob-Storage-Provider-Name"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "X-SF-Blob-Storage-Provider-Name" with value ${blobStorageProviderName}`) .toEqual(blobStorageProviderName); expect(chunkUploadRequest.requestHeaders["X-SF-Chunk-Ordinal"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "X-SF-Chunk-Ordinal" with value ${chunk.ordinal}`) .toEqual(chunk.ordinal.toString()); expect(chunkUploadRequest.requestHeaders["X-SF-Chunk-Size"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "X-SF-Chunk-Size" with value ${chunkSize10MegaBytes}`) .toEqual(chunkSize10MegaBytes.toString()); expect(chunkUploadRequest.requestHeaders["X-SF-Chunked-File-Upload-Session-Id"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "X-SF-Chunked-File-Upload-Session-Id" with value ${chunkedFileUploadSessionId}`) .toEqual(chunkedFileUploadSessionId); expect(chunkUploadRequest.requestHeaders["X-SF-Service-Request"]) .withContext(`Chunk request for chunk number ${chunk.ordinal}, should have header "X-SF-Service-Request" with value true`) .toEqual("true"); } function assertGetChunkUploadInfoRequest(getChunkUploadInfoRequest: JasmineAjaxRequest) { expect(getChunkUploadInfoRequest.url).toContain(`Default.GetChunkUploadInfo(libraryId=${libraryId})`); expect(getChunkUploadInfoRequest.url).toContain(typeOptions.entitySet); expect(getChunkUploadInfoRequest.method).toBe("GET"); } function getBase64Image713BytesInSize() { // ! 713 Bytes return "/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAOAA4DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD91PFnxd8M+A/HXhXwxqusWun6944nubbQbKXPmanLb273MyR8YykMbuQSPlQ46V1FFFAH/9k="; }