1
0
Fork 0
toolkit/packages/artifact/__tests__/upload-artifact.test.ts

372 lines
10 KiB
TypeScript
Raw Permalink Normal View History

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'
import {noopLogs} from './common'
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-10-08 16:35:48 +00:00
import unzip from 'unzip-stream'
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: [
{name: 'file1.txt', content: 'test 1 file content'},
{name: 'file2.txt', content: 'test 2 file content'},
{name: 'file3.txt', content: 'test 3 file content'},
{
2024-10-08 16:35:48 +00:00
name: 'real.txt',
content: 'from a symlink'
},
{
name: 'relative.txt',
content: 'from a symlink',
symlink: 'real.txt',
relative: true
},
{
name: 'absolute.txt',
content: 'from a symlink',
2024-10-08 16:35:48 +00:00
symlink: 'real.txt',
relative: false
}
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(() => {
2024-10-08 16:35:48 +00:00
fs.mkdirSync(fixtures.uploadDirectory, {
recursive: true
})
2024-07-24 01:57:39 +00:00
for (const file of fixtures.files) {
if (file.symlink) {
2024-10-08 16:35:48 +00:00
let symlinkPath = file.symlink
if (!file.relative) {
symlinkPath = path.join(fixtures.uploadDirectory, file.symlink)
}
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 => ({
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')
.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,
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-07-24 01:57:39 +00:00
it('should successfully upload an artifact', async () => {
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-10-08 16:35:48 +00:00
let loadedBytes = 0
const uploadedZip = path.join(
fixtures.uploadDirectory,
'..',
'uploaded.zip'
)
2024-07-24 01:57:39 +00:00
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
2024-10-08 16:35:48 +00:00
const {onProgress} = options || {}
2024-07-24 01:57:39 +00:00
2024-10-08 16:35:48 +00:00
if (fs.existsSync(uploadedZip)) {
fs.unlinkSync(uploadedZip)
}
const uploadedZipStream = fs.createWriteStream(uploadedZip)
2024-07-24 01:57:39 +00:00
2024-10-08 16:35:48 +00:00
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
loadedBytes += chunk.length
uploadedZipStream.write(chunk)
onProgress?.({loadedBytes})
})
stream.on('end', () => {
onProgress?.({loadedBytes})
uploadedZipStream.end()
2024-07-24 01:57:39 +00:00
resolve({})
})
2024-10-08 16:35:48 +00:00
stream.on('error', err => {
reject(err)
})
2024-07-24 01:57:39 +00:00
})
}
)
2024-04-15 16:49:47 +00:00
2024-07-24 01:57:39 +00:00
const {id, size} = await uploadArtifact(
fixtures.inputs.artifactName,
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)
2024-10-08 16:35:48 +00:00
expect(size).toBe(loadedBytes)
const extractedDirectory = path.join(
fixtures.uploadDirectory,
'..',
'extracted'
)
if (fs.existsSync(extractedDirectory)) {
fs.rmdirSync(extractedDirectory, {recursive: true})
}
const extract = new Promise((resolve, reject) => {
fs.createReadStream(uploadedZip)
.pipe(unzip.Extract({path: extractedDirectory}))
.on('close', () => {
resolve(true)
})
.on('error', err => {
reject(err)
})
})
await expect(extract).resolves.toBe(true)
for (const file of fixtures.files) {
const filePath = path.join(extractedDirectory, file.name)
expect(fs.existsSync(filePath)).toBe(true)
expect(fs.readFileSync(filePath, 'utf8')).toBe(file.content)
}
2024-07-24 01:57:39 +00:00
})
it('should throw an error uploading blob chunks get delayed', async () => {
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
2024-07-24 01:57:39 +00:00
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
2024-07-24 01:57:39 +00:00
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
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-07-24 01:57:39 +00:00
})
}
)
const uploadResp = uploadArtifact(
2024-07-24 01:57:39 +00:00
fixtures.inputs.artifactName,
fixtures.inputs.files,
fixtures.inputs.rootDirectory
)
2024-07-24 01:57:39 +00:00
await expect(uploadResp).rejects.toThrow('Upload progress stalled.')
})
2023-08-23 20:54:31 +00:00
})