2023-08-23 20:54:31 +00:00
|
|
|
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
|
|
|
|
import * as zip from '../src/internal/upload/zip'
|
|
|
|
import * as util from '../src/internal/shared/util'
|
|
|
|
import * as config from '../src/internal/shared/config'
|
2024-07-24 01:57:39 +00:00
|
|
|
import {ArtifactServiceClientJSON} from '../src/generated'
|
2023-08-23 20:54:31 +00:00
|
|
|
import * as blobUpload from '../src/internal/upload/blob-upload'
|
|
|
|
import {uploadArtifact} from '../src/internal/upload/upload-artifact'
|
2023-12-01 01:32:45 +00:00
|
|
|
import {noopLogs} from './common'
|
2023-12-05 17:35:46 +00:00
|
|
|
import {FilesNotFoundError} from '../src/internal/shared/errors'
|
2024-07-24 01:57:39 +00:00
|
|
|
import {BlockBlobUploadStreamOptions} from '@azure/storage-blob'
|
2024-04-15 15:24:57 +00:00
|
|
|
import * as fs from 'fs'
|
|
|
|
import * as path from 'path'
|
2024-04-09 18:52:19 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
const uploadStreamMock = jest.fn()
|
|
|
|
const blockBlobClientMock = jest.fn().mockImplementation(() => ({
|
|
|
|
uploadStream: uploadStreamMock
|
|
|
|
}))
|
|
|
|
|
|
|
|
jest.mock('@azure/storage-blob', () => ({
|
|
|
|
BlobClient: jest.fn().mockImplementation(() => {
|
|
|
|
return {
|
|
|
|
getBlockBlobClient: blockBlobClientMock
|
|
|
|
}
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
2024-07-24 01:57:39 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
const fixtures = {
|
|
|
|
uploadDirectory: path.join(__dirname, '_temp', 'plz-upload'),
|
|
|
|
files: [
|
2024-09-20 21:23:43 +00:00
|
|
|
{name: 'file1.txt', content: 'test 1 file content'},
|
|
|
|
{name: 'file2.txt', content: 'test 2 file content'},
|
|
|
|
{name: 'file3.txt', content: 'test 3 file content'},
|
|
|
|
{
|
|
|
|
name: 'from_symlink.txt',
|
|
|
|
content: 'from a symlink',
|
|
|
|
symlink: '../symlinked.txt'
|
|
|
|
}
|
2024-07-24 01:57:39 +00:00
|
|
|
],
|
|
|
|
backendIDs: {
|
|
|
|
workflowRunBackendId: '67dbcc20-e851-4452-a7c3-2cc0d2e0ec67',
|
|
|
|
workflowJobRunBackendId: '5f49179d-3386-4c38-85f7-00f8138facd0'
|
|
|
|
},
|
|
|
|
runtimeToken: 'test-token',
|
|
|
|
resultsServiceURL: 'http://results.local',
|
|
|
|
inputs: {
|
|
|
|
artifactName: 'test-artifact',
|
|
|
|
files: [
|
|
|
|
'/home/user/files/plz-upload/file1.txt',
|
|
|
|
'/home/user/files/plz-upload/file2.txt',
|
|
|
|
'/home/user/files/plz-upload/dir/file3.txt'
|
|
|
|
],
|
|
|
|
rootDirectory: '/home/user/files/plz-upload'
|
|
|
|
}
|
|
|
|
}
|
2023-08-23 20:54:31 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
describe('upload-artifact', () => {
|
|
|
|
beforeAll(() => {
|
|
|
|
if (!fs.existsSync(fixtures.uploadDirectory)) {
|
|
|
|
fs.mkdirSync(fixtures.uploadDirectory, {recursive: true})
|
|
|
|
}
|
|
|
|
|
2024-09-20 21:23:43 +00:00
|
|
|
for (const file of fixtures.files) {
|
|
|
|
if (file.symlink) {
|
|
|
|
const symlinkPath = path.join(fixtures.uploadDirectory, file.symlink)
|
|
|
|
fs.writeFileSync(symlinkPath, file.content)
|
|
|
|
if (!fs.existsSync(path.join(fixtures.uploadDirectory, file.name))) {
|
|
|
|
fs.symlinkSync(
|
|
|
|
symlinkPath,
|
|
|
|
path.join(fixtures.uploadDirectory, file.name),
|
|
|
|
'file'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fs.writeFileSync(
|
|
|
|
path.join(fixtures.uploadDirectory, file.name),
|
|
|
|
file.content
|
|
|
|
)
|
|
|
|
}
|
2024-07-24 01:57:39 +00:00
|
|
|
}
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
beforeEach(() => {
|
|
|
|
noopLogs()
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
|
|
|
.mockReturnValue()
|
|
|
|
jest
|
2024-07-24 01:57:39 +00:00
|
|
|
.spyOn(util, 'getBackendIdsFromToken')
|
|
|
|
.mockReturnValue(fixtures.backendIDs)
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
2024-07-24 01:57:39 +00:00
|
|
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
2023-08-23 20:54:31 +00:00
|
|
|
.mockReturnValue(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.files.map(file => ({
|
2024-09-20 21:23:43 +00:00
|
|
|
sourcePath: path.join(fixtures.uploadDirectory, file.name),
|
|
|
|
destinationPath: file.name,
|
|
|
|
stats: new fs.Stats()
|
2024-07-24 01:57:39 +00:00
|
|
|
}))
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
2024-07-24 01:57:39 +00:00
|
|
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue(fixtures.runtimeToken)
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(config, 'getResultsServiceUrl')
|
2024-07-24 01:57:39 +00:00
|
|
|
.mockReturnValue(fixtures.resultsServiceURL)
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
afterEach(() => {
|
|
|
|
jest.restoreAllMocks()
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should reject if there are no files to upload', async () => {
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
2024-07-24 01:57:39 +00:00
|
|
|
.mockClear()
|
2023-08-23 20:54:31 +00:00
|
|
|
.mockReturnValue([])
|
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrowError(FilesNotFoundError)
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should reject if no backend IDs are found', async () => {
|
|
|
|
jest.spyOn(util, 'getBackendIdsFromToken').mockRestore()
|
2023-08-23 20:54:31 +00:00
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrow()
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should return false if the creation request fails', async () => {
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(zip, 'createZipUploadStream')
|
|
|
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
|
|
|
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
|
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrow()
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should return false if blob storage upload is unsuccessful', async () => {
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(zip, 'createZipUploadStream')
|
|
|
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
|
|
|
signedUploadUrl: 'https://signed-upload-url.com'
|
|
|
|
})
|
|
|
|
)
|
|
|
|
jest
|
|
|
|
.spyOn(blobUpload, 'uploadZipToBlobStorage')
|
2023-12-05 17:35:46 +00:00
|
|
|
.mockReturnValue(Promise.reject(new Error('boom')))
|
2023-08-23 20:54:31 +00:00
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrow()
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should reject if finalize artifact fails', async () => {
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(zip, 'createZipUploadStream')
|
|
|
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
|
|
|
signedUploadUrl: 'https://signed-upload-url.com'
|
|
|
|
})
|
|
|
|
)
|
2023-08-23 20:55:26 +00:00
|
|
|
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
uploadSize: 1234,
|
2023-10-16 16:20:24 +00:00
|
|
|
sha256Hash: 'test-sha256-hash'
|
2023-08-23 20:55:26 +00:00
|
|
|
})
|
|
|
|
)
|
2023-08-23 20:54:31 +00:00
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
|
|
|
.mockReturnValue(Promise.resolve({ok: false, artifactId: ''}))
|
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2023-08-23 20:54:31 +00:00
|
|
|
)
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrow()
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|
2024-04-09 18:02:48 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
it('should successfully upload an artifact', async () => {
|
2024-09-20 21:23:43 +00:00
|
|
|
jest
|
|
|
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
|
|
|
.mockRestore()
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
|
|
|
signedUploadUrl: 'https://signed-upload-url.local'
|
|
|
|
})
|
|
|
|
)
|
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
|
|
|
artifactId: '1'
|
|
|
|
})
|
|
|
|
)
|
2024-04-15 16:49:47 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
uploadStreamMock.mockImplementation(
|
|
|
|
async (
|
|
|
|
stream: NodeJS.ReadableStream,
|
|
|
|
bufferSize?: number,
|
|
|
|
maxConcurrency?: number,
|
|
|
|
options?: BlockBlobUploadStreamOptions
|
|
|
|
) => {
|
|
|
|
const {onProgress, abortSignal} = options || {}
|
|
|
|
|
|
|
|
onProgress?.({loadedBytes: 0})
|
|
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
const timerId = setTimeout(() => {
|
|
|
|
onProgress?.({loadedBytes: 256})
|
|
|
|
resolve({})
|
|
|
|
}, 1_000)
|
|
|
|
abortSignal?.addEventListener('abort', () => {
|
|
|
|
clearTimeout(timerId)
|
|
|
|
resolve({})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
2024-04-15 16:49:47 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
const {id, size} = await uploadArtifact(
|
|
|
|
fixtures.inputs.artifactName,
|
2024-09-20 21:23:43 +00:00
|
|
|
fixtures.files.map(file =>
|
|
|
|
path.join(fixtures.uploadDirectory, file.name)
|
|
|
|
),
|
|
|
|
fixtures.uploadDirectory
|
2024-07-24 01:57:39 +00:00
|
|
|
)
|
2024-04-09 18:52:19 +00:00
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
expect(id).toBe(1)
|
|
|
|
expect(size).toBe(256)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should throw an error uploading blob chunks get delayed', async () => {
|
2024-04-09 18:02:48 +00:00
|
|
|
jest
|
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
2024-07-24 01:57:39 +00:00
|
|
|
signedUploadUrl: 'https://signed-upload-url.local'
|
2024-04-09 18:02:48 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
jest
|
2024-07-24 01:57:39 +00:00
|
|
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
|
|
|
.mockReturnValue(
|
|
|
|
Promise.resolve({
|
|
|
|
ok: true,
|
|
|
|
artifactId: '1'
|
|
|
|
})
|
|
|
|
)
|
2024-04-09 18:02:48 +00:00
|
|
|
jest
|
|
|
|
.spyOn(config, 'getResultsServiceUrl')
|
2024-07-24 01:57:39 +00:00
|
|
|
.mockReturnValue('https://results.local')
|
|
|
|
|
|
|
|
jest.spyOn(config, 'getUploadChunkTimeout').mockReturnValue(2_000)
|
|
|
|
|
|
|
|
uploadStreamMock.mockImplementation(
|
|
|
|
async (
|
|
|
|
stream: NodeJS.ReadableStream,
|
|
|
|
bufferSize?: number,
|
|
|
|
maxConcurrency?: number,
|
|
|
|
options?: BlockBlobUploadStreamOptions
|
|
|
|
) => {
|
|
|
|
const {onProgress, abortSignal} = options || {}
|
|
|
|
onProgress?.({loadedBytes: 0})
|
|
|
|
return new Promise(resolve => {
|
|
|
|
abortSignal?.addEventListener('abort', () => {
|
|
|
|
resolve({})
|
2024-04-09 18:02:48 +00:00
|
|
|
})
|
2024-07-24 01:57:39 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
2024-04-09 18:02:48 +00:00
|
|
|
|
|
|
|
const uploadResp = uploadArtifact(
|
2024-07-24 01:57:39 +00:00
|
|
|
fixtures.inputs.artifactName,
|
|
|
|
fixtures.inputs.files,
|
|
|
|
fixtures.inputs.rootDirectory
|
2024-04-09 18:02:48 +00:00
|
|
|
)
|
|
|
|
|
2024-07-24 01:57:39 +00:00
|
|
|
await expect(uploadResp).rejects.toThrow('Upload progress stalled.')
|
2024-04-09 18:02:48 +00:00
|
|
|
})
|
2023-08-23 20:54:31 +00:00
|
|
|
})
|