Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ coverage

# Transpiled JS
lib/

# Planning docs
docs/
236 changes: 230 additions & 6 deletions src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,14 @@ describe('run.ts', () => {
status: 200,
text: async () => 'v9.99.999'
} as Response
vi.stubGlobal('fetch', vi.fn().mockReturnValue(res))
vi.spyOn(globalThis, 'fetch').mockResolvedValue(res)
expect(await run.getLatestHelmVersion()).toBe('v9.99.999')
})

test('getLatestHelmVersion() - return the stable version of HELM when simulating a network error', async () => {
const errorMessage: string = 'Network Error'
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValueOnce(new Error(errorMessage))
vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(
new Error(errorMessage)
)
expect(await run.getLatestHelmVersion()).toBe(run.stableHelmVersion)
})
Expand Down Expand Up @@ -209,17 +208,24 @@ describe('run.ts', () => {
).toThrow("No helm version found in '.tool-versions'")
})

test('getVersionFromToolVersionsFile() - throw when the helm version is not semver-shaped', () => {
test('getVersionFromToolVersionsFile() - throw when the helm version is not valid', () => {
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue('helm latest')

expect(() =>
run.getVersionFromToolVersionsFile('.tool-versions')
).toThrow(
"The helm version 'latest' in '.tool-versions' is not a valid semantic version"
"The helm version 'latest' in '.tool-versions' is not valid. Provide a full version (e.g. '3.14.0') or a major.minor version (e.g. '3.14' or '3.14.x')"
)
})

test('getVersionFromToolVersionsFile() - accept a major.minor version', () => {
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14')

expect(run.getVersionFromToolVersionsFile('.tool-versions')).toBe('3.14')
})

test('isSemVerShaped() - accept semver-shaped versions with or without a v prefix', () => {
expect(run.isSemVerShaped('3.14.0')).toBe(true)
expect(run.isSemVerShaped('v3.14.0')).toBe(true)
Expand All @@ -232,6 +238,24 @@ describe('run.ts', () => {
expect(run.isSemVerShaped('abc')).toBe(false)
})

test('isMajorMinorShaped() - accept major.minor with or without a v prefix', () => {
expect(run.isMajorMinorShaped('3.14')).toBe(true)
expect(run.isMajorMinorShaped('v3.14')).toBe(true)
})

test('isMajorMinorShaped() - accept a wildcard patch (.x / .*)', () => {
expect(run.isMajorMinorShaped('3.14.x')).toBe(true)
expect(run.isMajorMinorShaped('v3.14.x')).toBe(true)
expect(run.isMajorMinorShaped('3.14.*')).toBe(true)
expect(run.isMajorMinorShaped('v3.14.*')).toBe(true)
})

test('isMajorMinorShaped() - reject full versions and other values', () => {
expect(run.isMajorMinorShaped('3.14.0')).toBe(false)
expect(run.isMajorMinorShaped('latest')).toBe(false)
expect(run.isMajorMinorShaped('3')).toBe(false)
})

// Stubs the download chain so run() resolves to a cached helm binary,
// letting these tests focus on version-vs-version-file resolution.
const stubDownloadChain = () => {
Expand Down Expand Up @@ -296,6 +320,206 @@ describe('run.ts', () => {
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.5.0')
})

// Spies on global fetch so HEAD probes report the given set of versions as
// existing (200) and everything else as missing (404). Using vi.spyOn (rather
// than vi.stubGlobal) lets restoreAllMocks() in afterEach reliably restore the
// real fetch, so the mock never leaks into later tests.
const stubPatchProbes = (existing: string[]) => {
const present = new Set(existing)
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const version =
String(input).match(/helm-(v\d+\.\d+\.\d+)-/)?.[1] ?? ''
const ok = present.has(version)
return {ok, status: ok ? 200 : 404} as Response
})
}

test('helmPatchExists() - return true when the artifact responds 200', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
status: 200
} as Response)

expect(await run.helmPatchExists(downloadBaseURL, 'v3.14.4')).toBe(true)
})

test('helmPatchExists() - return false when the artifact responds 404', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: false,
status: 404
} as Response)

expect(await run.helmPatchExists(downloadBaseURL, 'v3.14.99')).toBe(false)
})

test('helmPatchExists() - throw on a non-404 error status', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: false,
status: 429
} as Response)

await expect(
run.helmPatchExists(downloadBaseURL, 'v3.14.4')
).rejects.toThrow('Unexpected HTTP 429')
})

test('resolveLatestPatchVersion() - return the newest existing patch', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])

expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.4'
)
})

test('resolveLatestPatchVersion() - use the blob listing in a single request when supported', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const listing =
'<EnumerationResults>' +
'<Blob><Name>helm-v3.14.0-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.0-rc.1-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.4-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.4-linux-amd64.tar.gz.sha256</Name></Blob>' +
'</EnumerationResults>'
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
text: async () => listing
} as Response)

expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.4'
)
// A single listing request; no per-patch HEAD probing.
expect(fetchSpy).toHaveBeenCalledTimes(1)
})

test('resolveLatestPatchVersion() - fall back to probing when listing is unavailable', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const present = new Set(['v3.14.0', 'v3.14.1', 'v3.14.2'])
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = String(input)
if (url.includes('comp=list')) {
return {ok: false, status: 404} as Response
}
const version = url.match(/helm-(v\d+\.\d+\.\d+)-/)?.[1] ?? ''
const ok = present.has(version)
return {ok, status: ok ? 200 : 404} as Response
})

expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.2'
)
})

test('resolveLatestPatchViaListing() - return null when the response is not a listing', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
text: async () => '<html>directory index, not a blob listing</html>'
} as Response)

expect(
await run.resolveLatestPatchViaListing(downloadBaseURL, '3', '14')
).toBeNull()
})

test('resolveLatestPatchVersion() - tolerate a skipped patch number', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.3'])

expect(
await run.resolveLatestPatchVersion(downloadBaseURL, 'v3.14')
).toBe('v3.14.3')
})

test('resolveLatestPatchVersion() - accept a wildcard patch (.x / .*)', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.12.0', 'v3.12.1', 'v3.12.2', 'v3.12.3'])

expect(
await run.resolveLatestPatchVersion(downloadBaseURL, 'v3.12.x')
).toBe('v3.12.3')
expect(
await run.resolveLatestPatchVersion(downloadBaseURL, '3.12.*')
).toBe('v3.12.3')
})

test('resolveLatestPatchVersion() - throw when the minor has no releases', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes([])

await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '9.99')
).rejects.toThrow('No Helm releases found for 9.99')
})

test('resolveLatestPatchVersion() - propagate network errors', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockRejectedValue(
new Error('Network Error')
)

await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '3.14')
).rejects.toThrow('Network Error')
})

test('resolveLatestPatchVersion() - throw when the host reports every patch as existing', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({ok: true} as Response)

await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '3.14')
).rejects.toThrow('exceeded 100 probes')
})

test('run() - resolve the latest patch for a major.minor version input', async () => {
stubDownloadChain()
inputs('3.14', '')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])

await run.run()

expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.4')
})

test('run() - resolve the latest patch for a wildcard patch version input', async () => {
stubDownloadChain()
inputs('v3.12.x', '')
stubPatchProbes(['v3.12.0', 'v3.12.1', 'v3.12.2', 'v3.12.3'])

await run.run()

expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.12.3')
})

test('run() - resolve the latest patch for a major.minor version-file entry', async () => {
stubDownloadChain()
inputs('', '.tool-versions')
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])

await run.run()

expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.4')
})

test('walkSync() - return path to the all files matching fileToFind in dir', () => {
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder')
Expand Down
Loading
Loading