Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 276 additions & 5 deletions packages/artifact/__tests__/upload-artifact.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
import * as zip from '../src/internal/upload/zip'
import * as stream from '../src/internal/upload/stream'
import * as util from '../src/internal/shared/util'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON} from '../src/generated'
Expand Down Expand Up @@ -150,7 +151,7 @@
it('should return false if the creation request fails', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
Expand All @@ -167,7 +168,7 @@
it('should return false if blob storage upload is unsuccessful', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Expand All @@ -177,7 +178,7 @@
})
)
jest
.spyOn(blobUpload, 'uploadZipToBlobStorage')
.spyOn(blobUpload, 'uploadToBlobStorage')
.mockReturnValue(Promise.reject(new Error('boom')))

const uploadResp = uploadArtifact(
Expand All @@ -192,7 +193,7 @@
it('should reject if finalize artifact fails', async () => {
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
.mockReturnValue(Promise.resolve(new stream.WaterMarkedUploadStream(1)))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Expand All @@ -201,7 +202,7 @@
signedUploadUrl: 'https://signed-upload-url.com'
})
)
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
jest.spyOn(blobUpload, 'uploadToBlobStorage').mockReturnValue(
Promise.resolve({
uploadSize: 1234,
sha256Hash: 'test-sha256-hash'
Expand Down Expand Up @@ -370,4 +371,274 @@

await expect(uploadResp).rejects.toThrow('Upload progress stalled.')
})

describe('skipArchive option', () => {
it('should throw an error if skipArchive is true and multiple files are provided', async () => {
const uploadResp = uploadArtifact(
fixtures.inputs.artifactName,
fixtures.inputs.files,
fixtures.inputs.rootDirectory,
{skipArchive: true}
)

await expect(uploadResp).rejects.toThrow(
'skipArchive option is only supported when uploading a single file'
)
})
Comment on lines +376 to +387
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the edge case where skipArchive is true but the files array is empty (files.length === 0). This scenario would lead to accessing files[0] which is undefined, causing unexpected behavior. Consider adding a test case for this scenario.

Copilot uses AI. Check for mistakes.

it('should upload a single file without archiving when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()

const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')
const expectedFileName = 'file1.txt'

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 24.x)

'expectedFileName' is assigned a value but never used

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 20.x)

'expectedFileName' is assigned a value but never used

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (macos-latest-large, 24.x)

'expectedFileName' is assigned a value but never used

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (macos-latest-large, 20.x)

'expectedFileName' is assigned a value but never used

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (windows-latest, 24.x)

'expectedFileName' is assigned a value but never used

Check failure on line 395 in packages/artifact/__tests__/upload-artifact.test.ts

View workflow job for this annotation

GitHub Actions / Build (windows-latest, 20.x)

'expectedFileName' is assigned a value but never used
const expectedContent = 'test 1 file content'

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'
})
)

let uploadedContent = ''
let loadedBytes = 0
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}

onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
loadedBytes += chunk.length
uploadedContent += chunk.toString()
onProgress?.({loadedBytes})
})
stream.on('end', () => {
onProgress?.({loadedBytes})
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)

const {id, size, digest} = await uploadArtifact(
fixtures.inputs.artifactName,
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)

expect(id).toBe(1)
expect(size).toBe(loadedBytes)
expect(digest).toBeDefined()
expect(digest).toHaveLength(64)
// Verify the uploaded content is the raw file, not a zip
expect(uploadedContent).toBe(expectedContent)
})

it('should use the correct MIME type when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()

const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')

const createArtifactSpy = 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'
})
)

uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)

await uploadArtifact(
fixtures.inputs.artifactName,
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)

// Verify CreateArtifact was called with the correct MIME type for .txt file
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
mimeType: expect.objectContaining({value: 'text/plain'})
})
)
})

it('should use application/zip MIME type when skipArchive is false', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()

const createArtifactSpy = 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'
})
)

uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)

await uploadArtifact(
fixtures.inputs.artifactName,
fixtures.files.map(file =>
path.join(fixtures.uploadDirectory, file.name)
),
fixtures.uploadDirectory
)

// Verify CreateArtifact was called with application/zip MIME type
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
mimeType: expect.objectContaining({value: 'application/zip'})
})
)
})

it('should use the file basename as artifact name when skipArchive is true', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()

const singleFile = path.join(fixtures.uploadDirectory, 'file1.txt')

const createArtifactSpy = 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'
})
)

uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
onProgress?.({loadedBytes: chunk.length})
})
stream.on('end', () => {
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)

await uploadArtifact(
'original-name',
[singleFile],
fixtures.uploadDirectory,
{skipArchive: true}
)

// Verify CreateArtifact was called with the file basename, not the original name
expect(createArtifactSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'file1.txt'
})
)
})
})
})
Loading
Loading