Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcb0e62b16 | |||
| 3e2837ddce | |||
| 3048a9d72c | |||
| 91f9153ca8 | |||
| eef3e92175 | |||
| ed87cc6ce3 | |||
| af45ad8eaa | |||
| 367a6c2423 | |||
| 8f2bd5d713 | |||
| 7654d97eb6 | |||
| 0b2505c754 | |||
| f8a69bc473 | |||
| 03eca1b0c7 | |||
| edee7cde32 | |||
| 6295f5d25b | |||
| 339dd63bec | |||
| d27bf857e6 | |||
| ec5c955c0a | |||
| 302a5b31d8 | |||
| ab2b23c50d | |||
| 70a01b86d3 | |||
| ff80a82f7c | |||
| 0fc0befe24 | |||
| 7d95d2cec9 | |||
| c42d30607b | |||
| ac58d176ba | |||
| 518ef1b79e | |||
| d496b07cc0 | |||
| 7a2eceac36 |
+33
-1
@@ -1,5 +1,6 @@
|
||||
import {retry} from '../src/internal/requestUtils'
|
||||
import {retry, retryTypedResponse} from '../src/internal/requestUtils'
|
||||
import {HttpClientError} from '@actions/http-client'
|
||||
import * as requestUtils from '../src/internal/requestUtils'
|
||||
|
||||
interface ITestResponse {
|
||||
statusCode: number
|
||||
@@ -145,3 +146,34 @@ test('retry converts errors to response object', async () => {
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('retryTypedResponse gives an error with error message', async () => {
|
||||
const httpClientError = new HttpClientError(
|
||||
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes',
|
||||
400
|
||||
)
|
||||
jest.spyOn(requestUtils, 'retry').mockReturnValue(
|
||||
new Promise(resolve => {
|
||||
resolve(httpClientError)
|
||||
})
|
||||
)
|
||||
try {
|
||||
await retryTypedResponse<string>(
|
||||
'reserveCache',
|
||||
async () =>
|
||||
new Promise(resolve => {
|
||||
resolve({
|
||||
statusCode: 400,
|
||||
result: '',
|
||||
headers: {},
|
||||
error: httpClientError
|
||||
})
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
expect(error).toHaveProperty(
|
||||
'message',
|
||||
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
+85
-12
@@ -8,8 +8,9 @@ import * as tar from '../src/internal/tar'
|
||||
import {ITypedResponse} from '@actions/http-client/interfaces'
|
||||
import {
|
||||
ReserveCacheResponse,
|
||||
ITypedResponseWithErrorMessage
|
||||
ITypedResponseWithError
|
||||
} from '../src/internal/contracts'
|
||||
import {HttpClientError} from '@actions/http-client'
|
||||
|
||||
jest.mock('../src/internal/cacheHttpClient')
|
||||
jest.mock('../src/internal/cacheUtils')
|
||||
@@ -21,16 +22,13 @@ beforeAll(() => {
|
||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||
|
||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||
return actualUtils.getCacheFileName(cm)
|
||||
})
|
||||
|
||||
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
|
||||
return filePaths.map(x => path.resolve(x))
|
||||
})
|
||||
|
||||
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
||||
return Promise.resolve('/foo/bar')
|
||||
})
|
||||
@@ -60,10 +58,91 @@ test('save with large cache outputs should fail', async () => {
|
||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||
.mockReturnValueOnce(Promise.resolve(compression))
|
||||
|
||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
||||
'Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
||||
)
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledWith(
|
||||
archiveFolder,
|
||||
cachePaths,
|
||||
compression
|
||||
)
|
||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('save with large cache outputs should fail in GHES with error message', async () => {
|
||||
const filePath = 'node_modules'
|
||||
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||
const cachePaths = [path.resolve(filePath)]
|
||||
|
||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||
|
||||
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||
jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValueOnce(cacheSize)
|
||||
const compression = CompressionMethod.Gzip
|
||||
const getCompressionMock = jest
|
||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||
.mockReturnValueOnce(Promise.resolve(compression))
|
||||
|
||||
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
|
||||
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, 'reserveCache')
|
||||
.mockImplementation(async () => {
|
||||
const response: ITypedResponse<ReserveCacheResponse> = {
|
||||
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
||||
statusCode: 400,
|
||||
result: null,
|
||||
headers: {},
|
||||
error: new HttpClientError(
|
||||
'The cache filesize must be between 0 and 1073741824 bytes',
|
||||
400
|
||||
)
|
||||
}
|
||||
return response
|
||||
})
|
||||
|
||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
||||
'The cache filesize must be between 0 and 1073741824 bytes'
|
||||
)
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledWith(
|
||||
archiveFolder,
|
||||
cachePaths,
|
||||
compression
|
||||
)
|
||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('save with large cache outputs should fail in GHES without error message', async () => {
|
||||
const filePath = 'node_modules'
|
||||
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||
const cachePaths = [path.resolve(filePath)]
|
||||
|
||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||
|
||||
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||
jest
|
||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||
.mockReturnValueOnce(cacheSize)
|
||||
const compression = CompressionMethod.Gzip
|
||||
const getCompressionMock = jest
|
||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||
.mockReturnValueOnce(Promise.resolve(compression))
|
||||
|
||||
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
|
||||
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, 'reserveCache')
|
||||
.mockImplementation(async () => {
|
||||
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
||||
statusCode: 400,
|
||||
result: null,
|
||||
headers: {}
|
||||
@@ -72,7 +151,7 @@ test('save with large cache outputs should fail', async () => {
|
||||
})
|
||||
|
||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
||||
'Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
||||
'Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
|
||||
)
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
@@ -156,17 +235,14 @@ test('save with server error should fail', async () => {
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
||||
compressionMethod: compression
|
||||
})
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledWith(
|
||||
archiveFolder,
|
||||
cachePaths,
|
||||
compression
|
||||
)
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||
@@ -202,17 +278,14 @@ test('save with valid inputs uploads a cache', async () => {
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
||||
compressionMethod: compression
|
||||
})
|
||||
|
||||
const archiveFolder = '/foo/bar'
|
||||
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledWith(
|
||||
archiveFolder,
|
||||
cachePaths,
|
||||
compression
|
||||
)
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
+610
-2
@@ -1,8 +1,616 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/cache",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/glob": "^0.1.0",
|
||||
"@actions/http-client": "^1.0.9",
|
||||
"@actions/io": "^1.0.1",
|
||||
"@azure/ms-rest-js": "^2.6.0",
|
||||
"@azure/storage-blob": "^12.8.0",
|
||||
"semver": "^6.1.0",
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||
},
|
||||
"node_modules/@actions/exec": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz",
|
||||
"integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==",
|
||||
"dependencies": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/glob": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.0.tgz",
|
||||
"integrity": "sha512-lx8SzyQ2FE9+UUvjqY1f28QbTJv+w8qP7kHHbfQRhphrlcx0Mdmm1tZdGJzfxv1jxREa/sLW4Oy8CbGQKCJySA==",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.9.tgz",
|
||||
"integrity": "sha512-0O4SsJ7q+MK0ycvXPl2e6bMXV7dxAXOGjrXS1eTF9s2S401Tp6c/P3c3Joz04QefC1J6Gt942Wl2jbm3f4mLcg==",
|
||||
"dependencies": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client/node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/io": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
|
||||
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
|
||||
},
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
|
||||
"integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/abort-controller/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/core-asynciterator-polyfill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz",
|
||||
"integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg=="
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz",
|
||||
"integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/core-http": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.3.tgz",
|
||||
"integrity": "sha512-xr8AeszxP418rI//W38NfJDDr0kbVAPZkURZnZ+Fle+lLWeURjDE5zNIuocA1wUPoKSP8iXc0ApW6nPtbLGswA==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.6",
|
||||
"process": "^0.11.10",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"tslib": "^2.2.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^8.3.0",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/tough-cookie": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.3.tgz",
|
||||
"integrity": "sha512-UMdlR9NsqDCLTba3EUbRjfMF4gDmWvld196JmUjbz9WWhJ2XT00OR5MXeWiR+vmGT+ETiO4hHFCi2/eGO5YVtg==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/core-paging": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.2.1.tgz",
|
||||
"integrity": "sha512-UtH5iMlYsvg+nQYIl4UHlvvSrsBjOlRF4fs0j7mxd3rWdAStrKYrh2durOpHs5C9yZbVhsVDaisoyaf/lL1EVA==",
|
||||
"dependencies": {
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-paging/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.13",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
|
||||
"integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.1",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-tracing/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz",
|
||||
"integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@azure/ms-rest-js": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz",
|
||||
"integrity": "sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==",
|
||||
"dependencies": {
|
||||
"@azure/core-auth": "^1.1.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"form-data": "^2.5.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"tough-cookie": "^3.0.1",
|
||||
"tslib": "^1.10.0",
|
||||
"tunnel": "0.0.6",
|
||||
"uuid": "^8.3.2",
|
||||
"xml2js": "^0.4.19"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/ms-rest-js/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob": {
|
||||
"version": "12.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.8.0.tgz",
|
||||
"integrity": "sha512-c8+Wz19xauW0bGkTCoqZH4dYfbtBniPiGiRQOn1ca6G5jsjr4azwaTk9gwjVY8r3vY2Taf95eivLzipfIfiS4A==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-http": "^2.0.0",
|
||||
"@azure/core-lro": "^2.2.0",
|
||||
"@azure/core-paging": "^1.1.1",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"events": "^3.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob/node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz",
|
||||
"integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz",
|
||||
"integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng=="
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
|
||||
"integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch/node_modules/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz",
|
||||
"integrity": "sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tunnel": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
|
||||
"integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.9.tgz",
|
||||
"integrity": "sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-regex": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
|
||||
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
|
||||
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.34",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
|
||||
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
|
||||
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
|
||||
"dependencies": {
|
||||
"ip-regex": "^2.1.0",
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
|
||||
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.6",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"preview": true,
|
||||
"description": "Actions cache lib",
|
||||
"keywords": [
|
||||
|
||||
Vendored
+4
-5
@@ -174,6 +174,7 @@ export async function saveCache(
|
||||
const fileSizeLimit = 10 * 1024 * 1024 * 1024 // 10GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||
core.debug(`File Size: ${archiveFileSize}`)
|
||||
|
||||
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
|
||||
if (archiveFileSize > fileSizeLimit && !utils.isGhes()) {
|
||||
throw new Error(
|
||||
@@ -183,21 +184,20 @@ export async function saveCache(
|
||||
)
|
||||
}
|
||||
|
||||
const cacheSize = archiveFileSize
|
||||
core.debug('Reserving Cache')
|
||||
const reserveCacheResponse = await cacheHttpClient.reserveCache(
|
||||
key,
|
||||
paths,
|
||||
{
|
||||
compressionMethod,
|
||||
cacheSize
|
||||
cacheSize: archiveFileSize
|
||||
}
|
||||
)
|
||||
|
||||
if (reserveCacheResponse?.result?.cacheId) {
|
||||
cacheId = reserveCacheResponse?.result?.cacheId
|
||||
} else if (reserveCacheResponse?.statusCode === 400) {
|
||||
throw new ReserveCacheError(
|
||||
throw new Error(
|
||||
reserveCacheResponse?.error?.message ??
|
||||
`Cache size of ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
@@ -205,11 +205,10 @@ export async function saveCache(
|
||||
)
|
||||
} else {
|
||||
throw new ReserveCacheError(
|
||||
`Unable to reserve cache with key ${key}, another job may be creating this cache.`
|
||||
`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${reserveCacheResponse?.error?.message}`
|
||||
)
|
||||
}
|
||||
|
||||
core.debug(`Cache ID: ${cacheId}`)
|
||||
core.debug(`Saving Cache (ID: ${cacheId})`)
|
||||
await cacheHttpClient.saveCache(cacheId, archivePath, options)
|
||||
} finally {
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import {
|
||||
CommitCacheRequest,
|
||||
ReserveCacheRequest,
|
||||
ReserveCacheResponse,
|
||||
ITypedResponseWithErrorMessage
|
||||
ITypedResponseWithError
|
||||
} from './contracts'
|
||||
import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils'
|
||||
import {
|
||||
@@ -144,7 +144,7 @@ export async function reserveCache(
|
||||
key: string,
|
||||
paths: string[],
|
||||
options?: InternalCacheOptions
|
||||
): Promise<ITypedResponseWithErrorMessage<ReserveCacheResponse>> {
|
||||
): Promise<ITypedResponseWithError<ReserveCacheResponse>> {
|
||||
const httpClient = createHttpClient()
|
||||
const version = getCacheVersion(paths, options?.compressionMethod)
|
||||
|
||||
|
||||
+3
-3
@@ -126,7 +126,7 @@ export function assertDefined<T>(name: string, value?: T): T {
|
||||
|
||||
export function isGhes(): boolean {
|
||||
const ghUrl = new URL(
|
||||
process.env["GITHUB_SERVER_URL"] || "https://github.com"
|
||||
);
|
||||
return ghUrl.hostname.toUpperCase() !== "GITHUB.COM";
|
||||
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import {CompressionMethod} from './constants'
|
||||
import {ITypedResponse} from '@actions/http-client/interfaces'
|
||||
import {HttpClientError} from '@actions/http-client'
|
||||
|
||||
export interface ITypedResponseWithErrorMessage<T> extends ITypedResponse<T> {
|
||||
export interface ITypedResponseWithError<T> extends ITypedResponse<T> {
|
||||
error?: HttpClientError
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
|
||||
import {HttpCodes, HttpClientError} from '@actions/http-client'
|
||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
||||
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
|
||||
import {ITypedResponseWithErrorMessage} from './contracts'
|
||||
import {ITypedResponseWithError} from './contracts'
|
||||
|
||||
export function isSuccessStatusCode(statusCode?: number): boolean {
|
||||
if (!statusCode) {
|
||||
@@ -92,14 +92,14 @@ export async function retry<T>(
|
||||
|
||||
export async function retryTypedResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<ITypedResponseWithErrorMessage<T>>,
|
||||
method: () => Promise<ITypedResponseWithError<T>>,
|
||||
maxAttempts = DefaultRetryAttempts,
|
||||
delay = DefaultRetryDelay
|
||||
): Promise<ITypedResponseWithErrorMessage<T>> {
|
||||
): Promise<ITypedResponseWithError<T>> {
|
||||
return await retry(
|
||||
name,
|
||||
method,
|
||||
(response: ITypedResponseWithErrorMessage<T>) => response.statusCode,
|
||||
(response: ITypedResponseWithError<T>) => response.statusCode,
|
||||
maxAttempts,
|
||||
delay,
|
||||
// If the error object contains the statusCode property, extract it and return
|
||||
@@ -110,7 +110,7 @@ export async function retryTypedResponse<T>(
|
||||
statusCode: error.statusCode,
|
||||
result: null,
|
||||
headers: {},
|
||||
error: error
|
||||
error
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# @actions/core Releases
|
||||
|
||||
### 1.7.0
|
||||
- [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014)
|
||||
|
||||
### 1.6.0
|
||||
- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
|
||||
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import path from 'path'
|
||||
import {markdownSummary, SUMMARY_ENV_VAR} from '../src/markdown-summary'
|
||||
|
||||
const testFilePath = path.join(__dirname, 'test', 'test-summary.md')
|
||||
|
||||
async function assertSummary(expected: string): Promise<void> {
|
||||
const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'})
|
||||
expect(file).toEqual(expected)
|
||||
}
|
||||
|
||||
const fixtures = {
|
||||
text: 'hello world 🌎',
|
||||
code: `func fork() {
|
||||
for {
|
||||
go fork()
|
||||
}
|
||||
}`,
|
||||
list: ['foo', 'bar', 'baz', '💣'],
|
||||
table: [
|
||||
[
|
||||
{
|
||||
data: 'foo',
|
||||
header: true
|
||||
},
|
||||
{
|
||||
data: 'bar',
|
||||
header: true
|
||||
},
|
||||
{
|
||||
data: 'baz',
|
||||
header: true
|
||||
},
|
||||
{
|
||||
data: 'tall',
|
||||
rowspan: '3'
|
||||
}
|
||||
],
|
||||
['one', 'two', 'three'],
|
||||
[
|
||||
{
|
||||
data: 'wide',
|
||||
colspan: '3'
|
||||
}
|
||||
]
|
||||
],
|
||||
details: {
|
||||
label: 'open me',
|
||||
content: '🎉 surprise'
|
||||
},
|
||||
img: {
|
||||
src: 'https://github.com/actions.png',
|
||||
alt: 'actions logo',
|
||||
options: {
|
||||
width: '32',
|
||||
height: '32'
|
||||
}
|
||||
},
|
||||
quote: {
|
||||
text: 'Where the world builds software',
|
||||
cite: 'https://github.com/about'
|
||||
},
|
||||
link: {
|
||||
text: 'GitHub',
|
||||
href: 'https://github.com/'
|
||||
}
|
||||
}
|
||||
|
||||
describe('@actions/core/src/markdown-summary', () => {
|
||||
beforeEach(async () => {
|
||||
process.env[SUMMARY_ENV_VAR] = testFilePath
|
||||
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||
markdownSummary.emptyBuffer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.promises.unlink(testFilePath)
|
||||
})
|
||||
|
||||
it('throws if summary env var is undefined', async () => {
|
||||
process.env[SUMMARY_ENV_VAR] = undefined
|
||||
const write = markdownSummary.addRaw(fixtures.text).write()
|
||||
|
||||
await expect(write).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('throws if summary file does not exist', async () => {
|
||||
await fs.promises.unlink(testFilePath)
|
||||
const write = markdownSummary.addRaw(fixtures.text).write()
|
||||
|
||||
await expect(write).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('appends text to summary file', async () => {
|
||||
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||
await markdownSummary.addRaw(fixtures.text).write()
|
||||
await assertSummary(`# ${fixtures.text}`)
|
||||
})
|
||||
|
||||
it('overwrites text to summary file', async () => {
|
||||
await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'})
|
||||
await markdownSummary.addRaw(fixtures.text).write({overwrite: true})
|
||||
await assertSummary(fixtures.text)
|
||||
})
|
||||
|
||||
it('appends text with EOL to summary file', async () => {
|
||||
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||
await markdownSummary.addRaw(fixtures.text, true).write()
|
||||
await assertSummary(`# ${fixtures.text}${os.EOL}`)
|
||||
})
|
||||
|
||||
it('chains appends text to summary file', async () => {
|
||||
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||
await markdownSummary
|
||||
.addRaw(fixtures.text)
|
||||
.addRaw(fixtures.text)
|
||||
.addRaw(fixtures.text)
|
||||
.write()
|
||||
await assertSummary([fixtures.text, fixtures.text, fixtures.text].join(''))
|
||||
})
|
||||
|
||||
it('empties buffer after write', async () => {
|
||||
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||
await markdownSummary.addRaw(fixtures.text).write()
|
||||
await assertSummary(fixtures.text)
|
||||
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns summary buffer as string', () => {
|
||||
markdownSummary.addRaw(fixtures.text)
|
||||
expect(markdownSummary.stringify()).toEqual(fixtures.text)
|
||||
})
|
||||
|
||||
it('return correct values for isEmptyBuffer', () => {
|
||||
markdownSummary.addRaw(fixtures.text)
|
||||
expect(markdownSummary.isEmptyBuffer()).toBe(false)
|
||||
|
||||
markdownSummary.emptyBuffer()
|
||||
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||
})
|
||||
|
||||
it('clears a buffer and summary file', async () => {
|
||||
await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'})
|
||||
await markdownSummary.clear()
|
||||
await assertSummary('')
|
||||
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||
})
|
||||
|
||||
it('adds EOL', async () => {
|
||||
await markdownSummary
|
||||
.addRaw(fixtures.text)
|
||||
.addEOL()
|
||||
.write()
|
||||
await assertSummary(fixtures.text + os.EOL)
|
||||
})
|
||||
|
||||
it('adds a code block without language', async () => {
|
||||
await markdownSummary.addCodeBlock(fixtures.code).write()
|
||||
const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a code block with a language', async () => {
|
||||
await markdownSummary.addCodeBlock(fixtures.code, 'go').write()
|
||||
const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds an unordered list', async () => {
|
||||
await markdownSummary.addList(fixtures.list).write()
|
||||
const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds an ordered list', async () => {
|
||||
await markdownSummary.addList(fixtures.list, true).write()
|
||||
const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a table', async () => {
|
||||
await markdownSummary.addTable(fixtures.table).write()
|
||||
const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a details element', async () => {
|
||||
await markdownSummary
|
||||
.addDetails(fixtures.details.label, fixtures.details.content)
|
||||
.write()
|
||||
const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds an image with alt text', async () => {
|
||||
await markdownSummary.addImage(fixtures.img.src, fixtures.img.alt).write()
|
||||
const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds an image with custom dimensions', async () => {
|
||||
await markdownSummary
|
||||
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||
.write()
|
||||
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds an image with custom dimensions', async () => {
|
||||
await markdownSummary
|
||||
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||
.write()
|
||||
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds headings h1...h6', async () => {
|
||||
for (const i of [1, 2, 3, 4, 5, 6]) {
|
||||
markdownSummary.addHeading('heading', i)
|
||||
}
|
||||
await markdownSummary.write()
|
||||
const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds h1 if heading level not specified', async () => {
|
||||
await markdownSummary.addHeading('heading').write()
|
||||
const expected = `<h1>heading</h1>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('uses h1 if heading level is garbage or out of range', async () => {
|
||||
await markdownSummary
|
||||
.addHeading('heading', 'foobar')
|
||||
.addHeading('heading', 1337)
|
||||
.addHeading('heading', -1)
|
||||
.addHeading('heading', Infinity)
|
||||
.write()
|
||||
const expected = `<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a separator', async () => {
|
||||
await markdownSummary.addSeparator().write()
|
||||
const expected = `<hr>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a break', async () => {
|
||||
await markdownSummary.addBreak().write()
|
||||
const expected = `<br>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a quote', async () => {
|
||||
await markdownSummary.addQuote(fixtures.quote.text).write()
|
||||
const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a quote with citation', async () => {
|
||||
await markdownSummary
|
||||
.addQuote(fixtures.quote.text, fixtures.quote.cite)
|
||||
.write()
|
||||
const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
|
||||
it('adds a link with href', async () => {
|
||||
await markdownSummary
|
||||
.addLink(fixtures.link.text, fixtures.link.href)
|
||||
.write()
|
||||
const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}`
|
||||
await assertSummary(expected)
|
||||
})
|
||||
})
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
|
||||
@@ -359,3 +359,8 @@ export function getState(name: string): string {
|
||||
export async function getIDToken(aud?: string): Promise<string> {
|
||||
return await OidcClient.getIDToken(aud)
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown summary exports
|
||||
*/
|
||||
export {markdownSummary} from './markdown-summary'
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
import {EOL} from 'os'
|
||||
import {constants, promises} from 'fs'
|
||||
const {access, appendFile, writeFile} = promises
|
||||
|
||||
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
|
||||
export const SUMMARY_DOCS_URL =
|
||||
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary'
|
||||
|
||||
export type SummaryTableRow = (SummaryTableCell | string)[]
|
||||
|
||||
export interface SummaryTableCell {
|
||||
/**
|
||||
* Cell content
|
||||
*/
|
||||
data: string
|
||||
/**
|
||||
* Render cell as header
|
||||
* (optional) default: false
|
||||
*/
|
||||
header?: boolean
|
||||
/**
|
||||
* Number of columns the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
colspan?: string
|
||||
/**
|
||||
* Number of rows the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
rowspan?: string
|
||||
}
|
||||
|
||||
export interface SummaryImageOptions {
|
||||
/**
|
||||
* The width of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
width?: string
|
||||
/**
|
||||
* The height of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
height?: string
|
||||
}
|
||||
|
||||
export interface SummaryWriteOptions {
|
||||
/**
|
||||
* Replace all existing content in summary file with buffer contents
|
||||
* (optional) default: false
|
||||
*/
|
||||
overwrite?: boolean
|
||||
}
|
||||
|
||||
class MarkdownSummary {
|
||||
private _buffer: string
|
||||
private _filePath?: string
|
||||
|
||||
constructor() {
|
||||
this._buffer = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||
* Also checks r/w permissions.
|
||||
*
|
||||
* @returns step summary file path
|
||||
*/
|
||||
private async filePath(): Promise<string> {
|
||||
if (this._filePath) {
|
||||
return this._filePath
|
||||
}
|
||||
|
||||
const pathFromEnv = process.env[SUMMARY_ENV_VAR]
|
||||
if (!pathFromEnv) {
|
||||
throw new Error(
|
||||
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports markdown summaries.`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await access(pathFromEnv, constants.R_OK | constants.W_OK)
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`
|
||||
)
|
||||
}
|
||||
|
||||
this._filePath = pathFromEnv
|
||||
return this._filePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps content in an HTML tag, adding any HTML attributes
|
||||
*
|
||||
* @param {string} tag HTML tag to wrap
|
||||
* @param {string | null} content content within the tag
|
||||
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||
*
|
||||
* @returns {string} content wrapped in HTML element
|
||||
*/
|
||||
private wrap(
|
||||
tag: string,
|
||||
content: string | null,
|
||||
attrs: {[attribute: string]: string} = {}
|
||||
): string {
|
||||
const htmlAttrs = Object.entries(attrs)
|
||||
.map(([key, value]) => ` ${key}="${value}"`)
|
||||
.join('')
|
||||
|
||||
if (!content) {
|
||||
return `<${tag}${htmlAttrs}>`
|
||||
}
|
||||
|
||||
return `<${tag}${htmlAttrs}>${content}</${tag}>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||
*
|
||||
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||
*
|
||||
* @returns {Promise<MarkdownSummary>} markdown summary instance
|
||||
*/
|
||||
async write(options?: SummaryWriteOptions): Promise<MarkdownSummary> {
|
||||
const overwrite = !!options?.overwrite
|
||||
const filePath = await this.filePath()
|
||||
const writeFunc = overwrite ? writeFile : appendFile
|
||||
await writeFunc(filePath, this._buffer, {encoding: 'utf8'})
|
||||
return this.emptyBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the summary buffer and wipes the summary file
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
async clear(): Promise<MarkdownSummary> {
|
||||
return this.emptyBuffer().write({overwrite: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current summary buffer as a string
|
||||
*
|
||||
* @returns {string} string of summary buffer
|
||||
*/
|
||||
stringify(): string {
|
||||
return this._buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* If the summary buffer is empty
|
||||
*
|
||||
* @returns {boolen} true if the buffer is empty
|
||||
*/
|
||||
isEmptyBuffer(): boolean {
|
||||
return this._buffer.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the summary buffer without writing to summary file
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
emptyBuffer(): MarkdownSummary {
|
||||
this._buffer = ''
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds raw text to the summary buffer
|
||||
*
|
||||
* @param {string} text content to add
|
||||
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addRaw(text: string, addEOL = false): MarkdownSummary {
|
||||
this._buffer += text
|
||||
return addEOL ? this.addEOL() : this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the operating system-specific end-of-line marker to the buffer
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addEOL(): MarkdownSummary {
|
||||
return this.addRaw(EOL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML codeblock to the summary buffer
|
||||
*
|
||||
* @param {string} code content to render within fenced code block
|
||||
* @param {string} lang (optional) language to syntax highlight code
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addCodeBlock(code: string, lang?: string): MarkdownSummary {
|
||||
const attrs = {
|
||||
...(lang && {lang})
|
||||
}
|
||||
const element = this.wrap('pre', this.wrap('code', code), attrs)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML list to the summary buffer
|
||||
*
|
||||
* @param {string[]} items list of items to render
|
||||
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addList(items: string[], ordered = false): MarkdownSummary {
|
||||
const tag = ordered ? 'ol' : 'ul'
|
||||
const listItems = items.map(item => this.wrap('li', item)).join('')
|
||||
const element = this.wrap(tag, listItems)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML table to the summary buffer
|
||||
*
|
||||
* @param {SummaryTableCell[]} rows table rows
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addTable(rows: SummaryTableRow[]): MarkdownSummary {
|
||||
const tableBody = rows
|
||||
.map(row => {
|
||||
const cells = row
|
||||
.map(cell => {
|
||||
if (typeof cell === 'string') {
|
||||
return this.wrap('td', cell)
|
||||
}
|
||||
|
||||
const {header, data, colspan, rowspan} = cell
|
||||
const tag = header ? 'th' : 'td'
|
||||
const attrs = {
|
||||
...(colspan && {colspan}),
|
||||
...(rowspan && {rowspan})
|
||||
}
|
||||
|
||||
return this.wrap(tag, data, attrs)
|
||||
})
|
||||
.join('')
|
||||
|
||||
return this.wrap('tr', cells)
|
||||
})
|
||||
.join('')
|
||||
|
||||
const element = this.wrap('table', tableBody)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a collapsable HTML details element to the summary buffer
|
||||
*
|
||||
* @param {string} label text for the closed state
|
||||
* @param {string} content collapsable content
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addDetails(label: string, content: string): MarkdownSummary {
|
||||
const element = this.wrap('details', this.wrap('summary', label) + content)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML image tag to the summary buffer
|
||||
*
|
||||
* @param {string} src path to the image you to embed
|
||||
* @param {string} alt text description of the image
|
||||
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addImage(
|
||||
src: string,
|
||||
alt: string,
|
||||
options?: SummaryImageOptions
|
||||
): MarkdownSummary {
|
||||
const {width, height} = options || {}
|
||||
const attrs = {
|
||||
...(width && {width}),
|
||||
...(height && {height})
|
||||
}
|
||||
|
||||
const element = this.wrap('img', null, {src, alt, ...attrs})
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML section heading element
|
||||
*
|
||||
* @param {string} text heading text
|
||||
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addHeading(text: string, level?: number | string): MarkdownSummary {
|
||||
const tag = `h${level}`
|
||||
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
|
||||
? tag
|
||||
: 'h1'
|
||||
const element = this.wrap(allowedTag, text)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addSeparator(): MarkdownSummary {
|
||||
const element = this.wrap('hr', null)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML line break (<br>) to the summary buffer
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addBreak(): MarkdownSummary {
|
||||
const element = this.wrap('br', null)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML blockquote to the summary buffer
|
||||
*
|
||||
* @param {string} text quote text
|
||||
* @param {string} cite (optional) citation url
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addQuote(text: string, cite?: string): MarkdownSummary {
|
||||
const attrs = {
|
||||
...(cite && {cite})
|
||||
}
|
||||
const element = this.wrap('blockquote', text, attrs)
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an HTML anchor tag to the summary buffer
|
||||
*
|
||||
* @param {string} text link text/content
|
||||
* @param {string} href hyperlink
|
||||
*
|
||||
* @returns {MarkdownSummary} markdown summary instance
|
||||
*/
|
||||
addLink(text: string, href: string): MarkdownSummary {
|
||||
const element = this.wrap('a', text, {href})
|
||||
return this.addRaw(element).addEOL()
|
||||
}
|
||||
}
|
||||
|
||||
// singleton export
|
||||
export const markdownSummary = new MarkdownSummary()
|
||||
@@ -1,5 +1,8 @@
|
||||
# @actions/glob Releases
|
||||
|
||||
### 0.3.0
|
||||
- Added a `verbose` option to HashFiles [#1052](https://github.com/actions/toolkit/pull/1052/files)
|
||||
|
||||
### 0.2.1
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1023](https://github.com/actions/toolkit/pull/1023)
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/glob",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"preview": true,
|
||||
"description": "Actions glob lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -26,12 +26,13 @@ export async function create(
|
||||
*/
|
||||
export async function hashFiles(
|
||||
patterns: string,
|
||||
options?: HashFileOptions
|
||||
options?: HashFileOptions,
|
||||
verbose: Boolean = false
|
||||
): Promise<string> {
|
||||
let followSymbolicLinks = true
|
||||
if (options && typeof options.followSymbolicLinks === 'boolean') {
|
||||
followSymbolicLinks = options.followSymbolicLinks
|
||||
}
|
||||
const globber = await create(patterns, {followSymbolicLinks})
|
||||
return _hashFiles(globber)
|
||||
return _hashFiles(globber, verbose)
|
||||
}
|
||||
|
||||
@@ -6,19 +6,23 @@ import * as util from 'util'
|
||||
import * as path from 'path'
|
||||
import {Globber} from './glob'
|
||||
|
||||
export async function hashFiles(globber: Globber): Promise<string> {
|
||||
export async function hashFiles(
|
||||
globber: Globber,
|
||||
verbose: Boolean = false
|
||||
): Promise<string> {
|
||||
const writeDelegate = verbose ? core.info : core.debug
|
||||
let hasMatch = false
|
||||
const githubWorkspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd()
|
||||
const result = crypto.createHash('sha256')
|
||||
let count = 0
|
||||
for await (const file of globber.globGenerator()) {
|
||||
core.debug(file)
|
||||
writeDelegate(file)
|
||||
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
|
||||
core.debug(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
|
||||
writeDelegate(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
|
||||
continue
|
||||
}
|
||||
if (fs.statSync(file).isDirectory()) {
|
||||
core.debug(`Skip directory '${file}'.`)
|
||||
writeDelegate(`Skip directory '${file}'.`)
|
||||
continue
|
||||
}
|
||||
const hash = crypto.createHash('sha256')
|
||||
@@ -33,10 +37,10 @@ export async function hashFiles(globber: Globber): Promise<string> {
|
||||
result.end()
|
||||
|
||||
if (hasMatch) {
|
||||
core.debug(`Found ${count} files to hash.`)
|
||||
writeDelegate(`Found ${count} files to hash.`)
|
||||
return result.digest('hex')
|
||||
} else {
|
||||
core.debug(`No matches found for glob`)
|
||||
writeDelegate(`No matches found for glob`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
_out
|
||||
node_modules
|
||||
.DS_Store
|
||||
testoutput.txt
|
||||
npm-debug.log
|
||||
@@ -0,0 +1,21 @@
|
||||
Actions Http Client for Node.js
|
||||
|
||||
Copyright (c) GitHub, Inc.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="actions.png">
|
||||
</p>
|
||||
|
||||
# Actions Http-Client
|
||||
|
||||
[](https://github.com/actions/http-client/actions)
|
||||
|
||||
A lightweight HTTP client optimized for use with actions, TypeScript with generics and async await.
|
||||
|
||||
## Features
|
||||
|
||||
- HTTP client with TypeScript generics and async/await/Promises
|
||||
- Typings included so no need to acquire separately (great for intellisense and no versioning drift)
|
||||
- [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner
|
||||
- Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+.
|
||||
- Basic, Bearer and PAT Support out of the box. Extensible handlers for others.
|
||||
- Redirects supported
|
||||
|
||||
Features and releases [here](./RELEASES.md)
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @actions/http-client --save
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
See the [HTTP](./__tests__) tests for detailed examples.
|
||||
|
||||
## Errors
|
||||
|
||||
### HTTP
|
||||
|
||||
The HTTP client does not throw unless truly exceptional.
|
||||
|
||||
* A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body.
|
||||
* Redirects (3xx) will be followed by default.
|
||||
|
||||
See [HTTP tests](./__tests__) for detailed examples.
|
||||
|
||||
## Debugging
|
||||
|
||||
To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible:
|
||||
|
||||
```
|
||||
export NODE_DEBUG=http
|
||||
```
|
||||
|
||||
## Node support
|
||||
|
||||
The http-client is built using the latest LTS version of Node 12. It may work on previous node LTS versions but it's tested and officially supported on Node12+.
|
||||
|
||||
## Support and Versioning
|
||||
|
||||
We follow semver and will hold compatibility between major versions and increment the minor version with new features and capabilities (while holding compat).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome PRs. Please create an issue and if applicable, a design before proceeding with code.
|
||||
|
||||
once:
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
To build:
|
||||
|
||||
```bash
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
```bash
|
||||
$ npm test
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
## Releases
|
||||
|
||||
## 1.0.11
|
||||
|
||||
Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42)
|
||||
|
||||
## 1.0.9
|
||||
Throw HttpClientError instead of a generic Error from the \<verb>Json() helper methods when the server responds with a non-successful status code.
|
||||
|
||||
## 1.0.8
|
||||
Fixed security issue where a redirect (e.g. 302) to another domain would pass headers. The fix was to strip the authorization header if the hostname was different. More [details in PR #27](https://github.com/actions/http-client/pull/27)
|
||||
|
||||
## 1.0.7
|
||||
Update NPM dependencies and add 429 to the list of HttpCodes
|
||||
|
||||
## 1.0.6
|
||||
Automatically sends Content-Type and Accept application/json headers for \<verb>Json() helper methods if not set in the client or parameters.
|
||||
|
||||
## 1.0.5
|
||||
Adds \<verb>Json() helper methods for json over http scenarios.
|
||||
|
||||
## 1.0.4
|
||||
Started to add \<verb>Json() helper methods. Do not use this release for that. Use >= 1.0.5 since there was an issue with types.
|
||||
|
||||
## 1.0.1 to 1.0.3
|
||||
Adds proxy support.
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as httpm from '../_out'
|
||||
import * as am from '../_out/auth'
|
||||
|
||||
describe('auth', () => {
|
||||
beforeEach(() => {})
|
||||
|
||||
afterEach(() => {})
|
||||
|
||||
it('does basic http get request with basic auth', async () => {
|
||||
let bh: am.BasicCredentialHandler = new am.BasicCredentialHandler(
|
||||
'johndoe',
|
||||
'password'
|
||||
)
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [bh])
|
||||
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
let auth: string = obj.headers.Authorization
|
||||
let creds: string = Buffer.from(
|
||||
auth.substring('Basic '.length),
|
||||
'base64'
|
||||
).toString()
|
||||
expect(creds).toBe('johndoe:password')
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
})
|
||||
|
||||
it('does basic http get request with pat token auth', async () => {
|
||||
let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
|
||||
let ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler(
|
||||
token
|
||||
)
|
||||
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
|
||||
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
let auth: string = obj.headers.Authorization
|
||||
let creds: string = Buffer.from(
|
||||
auth.substring('Basic '.length),
|
||||
'base64'
|
||||
).toString()
|
||||
expect(creds).toBe('PAT:' + token)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
})
|
||||
|
||||
it('does basic http get request with pat token auth', async () => {
|
||||
let token: string = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs'
|
||||
let ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token)
|
||||
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ph])
|
||||
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
let auth: string = obj.headers.Authorization
|
||||
expect(auth).toBe('Bearer ' + token)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,375 @@
|
||||
import * as httpm from '../_out'
|
||||
import * as ifm from '../_out/interfaces'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
let sampleFilePath: string = path.join(__dirname, 'testoutput.txt')
|
||||
|
||||
interface HttpBinData {
|
||||
url: string
|
||||
data: any
|
||||
json: any
|
||||
headers: any
|
||||
args?: any
|
||||
}
|
||||
|
||||
describe('basics', () => {
|
||||
let _http: httpm.HttpClient
|
||||
|
||||
beforeEach(() => {
|
||||
_http = new httpm.HttpClient('http-client-tests')
|
||||
})
|
||||
|
||||
afterEach(() => {})
|
||||
|
||||
it('constructs', () => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests')
|
||||
expect(http).toBeDefined()
|
||||
})
|
||||
|
||||
// responses from httpbin return something like:
|
||||
// {
|
||||
// "args": {},
|
||||
// "headers": {
|
||||
// "Connection": "close",
|
||||
// "Host": "httpbin.org",
|
||||
// "User-Agent": "typed-test-client-tests"
|
||||
// },
|
||||
// "origin": "173.95.152.44",
|
||||
// "url": "https://httpbin.org/get"
|
||||
// }
|
||||
|
||||
it('does basic http get request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
expect(obj.headers['User-Agent']).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http get request with no user agent', async done => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient()
|
||||
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
expect(obj.headers['User-Agent']).toBeFalsy()
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic https get request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http get request with default headers', async done => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
let res: httpm.HttpClientResponse = await http.get('http://httpbin.org/get')
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.headers.Accept).toBe('application/json')
|
||||
expect(obj.headers['Content-Type']).toBe('application/json')
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http get request with merged headers', async done => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [], {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
let res: httpm.HttpClientResponse = await http.get(
|
||||
'http://httpbin.org/get',
|
||||
{
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.headers.Accept).toBe('application/json')
|
||||
expect(obj.headers['Content-Type']).toBe(
|
||||
'application/x-www-form-urlencoded'
|
||||
)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('pipes a get request', () => {
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
let file: NodeJS.WritableStream = fs.createWriteStream(sampleFilePath)
|
||||
;(await _http.get('https://httpbin.org/get')).message
|
||||
.pipe(file)
|
||||
.on('close', () => {
|
||||
let body: string = fs.readFileSync(sampleFilePath).toString()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does basic get request with redirects', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://httpbin.org/get')
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic get request with redirects (303)', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://httpbin.org/get') +
|
||||
'&status_code=303'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('returns 404 for not found get request on redirect', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://httpbin.org/status/404') +
|
||||
'&status_code=303'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(404)
|
||||
let body: string = await res.readBody()
|
||||
done()
|
||||
})
|
||||
|
||||
it('does not follow redirects if disabled', async done => {
|
||||
let http: httpm.HttpClient = new httpm.HttpClient(
|
||||
'typed-test-client-tests',
|
||||
null,
|
||||
{allowRedirects: false}
|
||||
)
|
||||
let res: httpm.HttpClientResponse = await http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://httpbin.org/get')
|
||||
)
|
||||
expect(res.message.statusCode).toBe(302)
|
||||
let body: string = await res.readBody()
|
||||
done()
|
||||
})
|
||||
|
||||
it('does not pass auth with diff hostname redirects', async done => {
|
||||
let headers = {
|
||||
accept: 'application/json',
|
||||
authorization: 'shhh'
|
||||
}
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://www.httpbin.org/get'),
|
||||
headers
|
||||
)
|
||||
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
// httpbin "fixes" the casing
|
||||
expect(obj.headers['Accept']).toBe('application/json')
|
||||
expect(obj.headers['Authorization']).toBeUndefined()
|
||||
expect(obj.headers['authorization']).toBeUndefined()
|
||||
expect(obj.url).toBe('https://www.httpbin.org/get')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('does not pass Auth with diff hostname redirects', async done => {
|
||||
let headers = {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'shhh'
|
||||
}
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'https://httpbin.org/redirect-to?url=' +
|
||||
encodeURIComponent('https://www.httpbin.org/get'),
|
||||
headers
|
||||
)
|
||||
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
// httpbin "fixes" the casing
|
||||
expect(obj.headers['Accept']).toBe('application/json')
|
||||
expect(obj.headers['Authorization']).toBeUndefined()
|
||||
expect(obj.headers['authorization']).toBeUndefined()
|
||||
expect(obj.url).toBe('https://www.httpbin.org/get')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic head request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.head(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http delete request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.del(
|
||||
'http://httpbin.org/delete'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http post request', async done => {
|
||||
let b: string = 'Hello World!'
|
||||
let res: httpm.HttpClientResponse = await _http.post(
|
||||
'http://httpbin.org/post',
|
||||
b
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.data).toBe(b)
|
||||
expect(obj.url).toBe('http://httpbin.org/post')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http patch request', async done => {
|
||||
let b: string = 'Hello World!'
|
||||
let res: httpm.HttpClientResponse = await _http.patch(
|
||||
'http://httpbin.org/patch',
|
||||
b
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.data).toBe(b)
|
||||
expect(obj.url).toBe('http://httpbin.org/patch')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http options request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.options(
|
||||
'http://httpbin.org'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
done()
|
||||
})
|
||||
|
||||
it('returns 404 for not found get request', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'http://httpbin.org/status/404'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(404)
|
||||
let body: string = await res.readBody()
|
||||
done()
|
||||
})
|
||||
|
||||
it('gets a json object', async () => {
|
||||
let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
|
||||
HttpBinData
|
||||
>('https://httpbin.org/get')
|
||||
expect(jsonObj.statusCode).toBe(200)
|
||||
expect(jsonObj.result).toBeDefined()
|
||||
expect(jsonObj.result.url).toBe('https://httpbin.org/get')
|
||||
expect(jsonObj.result.headers['Accept']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('getting a non existent json object returns null', async () => {
|
||||
let jsonObj: ifm.ITypedResponse<HttpBinData> = await _http.getJson<
|
||||
HttpBinData
|
||||
>('https://httpbin.org/status/404')
|
||||
expect(jsonObj.statusCode).toBe(404)
|
||||
expect(jsonObj.result).toBeNull()
|
||||
})
|
||||
|
||||
it('posts a json object', async () => {
|
||||
let res: any = {name: 'foo'}
|
||||
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.postJson<
|
||||
HttpBinData
|
||||
>('https://httpbin.org/post', res)
|
||||
expect(restRes.statusCode).toBe(200)
|
||||
expect(restRes.result).toBeDefined()
|
||||
expect(restRes.result.url).toBe('https://httpbin.org/post')
|
||||
expect(restRes.result.json.name).toBe('foo')
|
||||
expect(restRes.result.headers['Accept']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.result.headers['Content-Type']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('puts a json object', async () => {
|
||||
let res: any = {name: 'foo'}
|
||||
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.putJson<
|
||||
HttpBinData
|
||||
>('https://httpbin.org/put', res)
|
||||
expect(restRes.statusCode).toBe(200)
|
||||
expect(restRes.result).toBeDefined()
|
||||
expect(restRes.result.url).toBe('https://httpbin.org/put')
|
||||
expect(restRes.result.json.name).toBe('foo')
|
||||
|
||||
expect(restRes.result.headers['Accept']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.result.headers['Content-Type']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('patch a json object', async () => {
|
||||
let res: any = {name: 'foo'}
|
||||
let restRes: ifm.ITypedResponse<HttpBinData> = await _http.patchJson<
|
||||
HttpBinData
|
||||
>('https://httpbin.org/patch', res)
|
||||
expect(restRes.statusCode).toBe(200)
|
||||
expect(restRes.result).toBeDefined()
|
||||
expect(restRes.result.url).toBe('https://httpbin.org/patch')
|
||||
expect(restRes.result.json.name).toBe('foo')
|
||||
expect(restRes.result.headers['Accept']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.result.headers['Content-Type']).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
expect(restRes.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as httpm from '../_out'
|
||||
import * as ifm from '../_out/interfaces'
|
||||
|
||||
describe('headers', () => {
|
||||
let _http: httpm.HttpClient
|
||||
|
||||
beforeEach(() => {
|
||||
_http = new httpm.HttpClient('http-client-tests')
|
||||
})
|
||||
|
||||
it('preserves existing headers on getJson', async () => {
|
||||
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
|
||||
let jsonObj: ifm.ITypedResponse<any> = await _http.getJson<any>(
|
||||
'https://httpbin.org/get',
|
||||
additionalHeaders
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('foo')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
|
||||
let httpWithHeaders = new httpm.HttpClient()
|
||||
httpWithHeaders.requestOptions = {
|
||||
headers: {
|
||||
[httpm.Headers.Accept]: 'baz'
|
||||
}
|
||||
}
|
||||
jsonObj = await httpWithHeaders.getJson<any>('https://httpbin.org/get')
|
||||
expect(jsonObj.result.headers['Accept']).toBe('baz')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves existing headers on postJson', async () => {
|
||||
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
|
||||
let jsonObj: ifm.ITypedResponse<any> = await _http.postJson<any>(
|
||||
'https://httpbin.org/post',
|
||||
{},
|
||||
additionalHeaders
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('foo')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
|
||||
let httpWithHeaders = new httpm.HttpClient()
|
||||
httpWithHeaders.requestOptions = {
|
||||
headers: {
|
||||
[httpm.Headers.Accept]: 'baz'
|
||||
}
|
||||
}
|
||||
jsonObj = await httpWithHeaders.postJson<any>(
|
||||
'https://httpbin.org/post',
|
||||
{}
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('baz')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves existing headers on putJson', async () => {
|
||||
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
|
||||
let jsonObj: ifm.ITypedResponse<any> = await _http.putJson<any>(
|
||||
'https://httpbin.org/put',
|
||||
{},
|
||||
additionalHeaders
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('foo')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
|
||||
let httpWithHeaders = new httpm.HttpClient()
|
||||
httpWithHeaders.requestOptions = {
|
||||
headers: {
|
||||
[httpm.Headers.Accept]: 'baz'
|
||||
}
|
||||
}
|
||||
jsonObj = await httpWithHeaders.putJson<any>('https://httpbin.org/put', {})
|
||||
expect(jsonObj.result.headers['Accept']).toBe('baz')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves existing headers on patchJson', async () => {
|
||||
let additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
|
||||
let jsonObj: ifm.ITypedResponse<any> = await _http.patchJson<any>(
|
||||
'https://httpbin.org/patch',
|
||||
{},
|
||||
additionalHeaders
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('foo')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
|
||||
let httpWithHeaders = new httpm.HttpClient()
|
||||
httpWithHeaders.requestOptions = {
|
||||
headers: {
|
||||
[httpm.Headers.Accept]: 'baz'
|
||||
}
|
||||
}
|
||||
jsonObj = await httpWithHeaders.patchJson<any>(
|
||||
'https://httpbin.org/patch',
|
||||
{}
|
||||
)
|
||||
expect(jsonObj.result.headers['Accept']).toBe('baz')
|
||||
expect(jsonObj.headers[httpm.Headers.ContentType]).toBe(
|
||||
httpm.MediaTypes.ApplicationJson
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as httpm from '../_out'
|
||||
|
||||
describe('basics', () => {
|
||||
let _http: httpm.HttpClient
|
||||
|
||||
beforeEach(() => {
|
||||
_http = new httpm.HttpClient('http-client-tests', [], {keepAlive: true})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
_http.dispose()
|
||||
})
|
||||
|
||||
it('does basic http get request with keepAlive true', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.get(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic head request with keepAlive true', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.head(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http delete request with keepAlive true', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.del(
|
||||
'http://httpbin.org/delete'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http post request with keepAlive true', async done => {
|
||||
let b: string = 'Hello World!'
|
||||
let res: httpm.HttpClientResponse = await _http.post(
|
||||
'http://httpbin.org/post',
|
||||
b
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.data).toBe(b)
|
||||
expect(obj.url).toBe('http://httpbin.org/post')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http patch request with keepAlive true', async done => {
|
||||
let b: string = 'Hello World!'
|
||||
let res: httpm.HttpClientResponse = await _http.patch(
|
||||
'http://httpbin.org/patch',
|
||||
b
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.data).toBe(b)
|
||||
expect(obj.url).toBe('http://httpbin.org/patch')
|
||||
done()
|
||||
})
|
||||
|
||||
it('does basic http options request with keepAlive true', async done => {
|
||||
let res: httpm.HttpClientResponse = await _http.options(
|
||||
'http://httpbin.org'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,228 @@
|
||||
import * as http from 'http'
|
||||
import * as httpm from '../_out'
|
||||
import * as pm from '../_out/proxy'
|
||||
import * as proxy from 'proxy'
|
||||
import * as tunnelm from 'tunnel'
|
||||
|
||||
let _proxyConnects: string[]
|
||||
let _proxyServer: http.Server
|
||||
let _proxyUrl = 'http://127.0.0.1:8080'
|
||||
|
||||
describe('proxy', () => {
|
||||
beforeAll(async () => {
|
||||
// Start proxy server
|
||||
_proxyServer = proxy()
|
||||
await new Promise(resolve => {
|
||||
const port = Number(_proxyUrl.split(':')[2])
|
||||
_proxyServer.listen(port, () => resolve())
|
||||
})
|
||||
_proxyServer.on('connect', req => {
|
||||
_proxyConnects.push(req.url)
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
_proxyConnects = []
|
||||
_clearVars()
|
||||
})
|
||||
|
||||
afterEach(() => {})
|
||||
|
||||
afterAll(async () => {
|
||||
_clearVars()
|
||||
|
||||
// Stop proxy server
|
||||
await new Promise(resolve => {
|
||||
_proxyServer.once('close', () => resolve())
|
||||
_proxyServer.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('getProxyUrl does not return proxyUrl if variables not set', () => {
|
||||
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
|
||||
expect(proxyUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => {
|
||||
process.env['https_proxy'] = 'https://myproxysvr'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
|
||||
expect(proxyUrl).toBeDefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => {
|
||||
process.env['http_proxy'] = 'https://myproxysvr'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
|
||||
expect(proxyUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => {
|
||||
process.env['http_proxy'] = 'http://myproxysvr'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
|
||||
expect(proxyUrl).toBeDefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => {
|
||||
process.env['https_proxy'] = 'https://myproxysvr'
|
||||
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('https://myserver'))
|
||||
expect(proxyUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => {
|
||||
process.env['https_proxy'] = 'https://myproxysvr'
|
||||
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('https://github.com'))
|
||||
expect(proxyUrl).toBeDefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => {
|
||||
process.env['http_proxy'] = 'http://myproxysvr'
|
||||
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('http://myserver'))
|
||||
expect(proxyUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => {
|
||||
process.env['http_proxy'] = 'http://myproxysvr'
|
||||
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
|
||||
let proxyUrl = pm.getProxyUrl(new URL('http://github.com'))
|
||||
expect(proxyUrl).toBeDefined()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host as no_proxy list', () => {
|
||||
process.env['no_proxy'] = 'myserver'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host in no_proxy list', () => {
|
||||
process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host in no_proxy list with spaces', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host in no_proxy list with port', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver:8080'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host with port in no_proxy list without port', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver:8080'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host in no_proxy list with default https port', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver'
|
||||
let bypass = pm.checkBypass(new URL('https://myserver'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns true if host in no_proxy list with default http port', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver'
|
||||
let bypass = pm.checkBypass(new URL('http://myserver'))
|
||||
expect(bypass).toBeTruthy()
|
||||
})
|
||||
|
||||
it('checkBypass returns false if host not in no_proxy list', () => {
|
||||
process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080'
|
||||
let bypass = pm.checkBypass(new URL('https://github.com'))
|
||||
expect(bypass).toBeFalsy()
|
||||
})
|
||||
|
||||
it('checkBypass returns false if empty no_proxy', () => {
|
||||
process.env['no_proxy'] = ''
|
||||
let bypass = pm.checkBypass(new URL('https://github.com'))
|
||||
expect(bypass).toBeFalsy()
|
||||
})
|
||||
|
||||
it('HttpClient does basic http get request through proxy', async () => {
|
||||
process.env['http_proxy'] = _proxyUrl
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let res: httpm.HttpClientResponse = await httpClient.get(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
expect(_proxyConnects).toEqual(['httpbin.org:80'])
|
||||
})
|
||||
|
||||
it('HttoClient does basic http get request when bypass proxy', async () => {
|
||||
process.env['http_proxy'] = _proxyUrl
|
||||
process.env['no_proxy'] = 'httpbin.org'
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let res: httpm.HttpClientResponse = await httpClient.get(
|
||||
'http://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('http://httpbin.org/get')
|
||||
expect(_proxyConnects).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('HttpClient does basic https get request through proxy', async () => {
|
||||
process.env['https_proxy'] = _proxyUrl
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let res: httpm.HttpClientResponse = await httpClient.get(
|
||||
'https://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
expect(_proxyConnects).toEqual(['httpbin.org:443'])
|
||||
})
|
||||
|
||||
it('HttpClient does basic https get request when bypass proxy', async () => {
|
||||
process.env['https_proxy'] = _proxyUrl
|
||||
process.env['no_proxy'] = 'httpbin.org'
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let res: httpm.HttpClientResponse = await httpClient.get(
|
||||
'https://httpbin.org/get'
|
||||
)
|
||||
expect(res.message.statusCode).toBe(200)
|
||||
let body: string = await res.readBody()
|
||||
let obj: any = JSON.parse(body)
|
||||
expect(obj.url).toBe('https://httpbin.org/get')
|
||||
expect(_proxyConnects).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('proxyAuth not set in tunnel agent when authentication is not provided', async () => {
|
||||
process.env['https_proxy'] = 'http://127.0.0.1:8080'
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
|
||||
console.log(agent)
|
||||
expect(agent.proxyOptions.host).toBe('127.0.0.1')
|
||||
expect(agent.proxyOptions.port).toBe('8080')
|
||||
expect(agent.proxyOptions.proxyAuth).toBe(undefined)
|
||||
})
|
||||
|
||||
it('proxyAuth is set in tunnel agent when authentication is provided', async () => {
|
||||
process.env['https_proxy'] = 'http://user:password@127.0.0.1:8080'
|
||||
const httpClient = new httpm.HttpClient()
|
||||
let agent: tunnelm.TunnelingAgent = httpClient.getAgent('https://some-url')
|
||||
console.log(agent)
|
||||
expect(agent.proxyOptions.host).toBe('127.0.0.1')
|
||||
expect(agent.proxyOptions.port).toBe('8080')
|
||||
expect(agent.proxyOptions.proxyAuth).toBe('user:password')
|
||||
})
|
||||
})
|
||||
|
||||
function _clearVars() {
|
||||
delete process.env.http_proxy
|
||||
delete process.env.HTTP_PROXY
|
||||
delete process.env.https_proxy
|
||||
delete process.env.HTTPS_PROXY
|
||||
delete process.env.no_proxy
|
||||
delete process.env.NO_PROXY
|
||||
}
|
||||
Generated
+10494
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@actions/http-client",
|
||||
"version": "1.0.11",
|
||||
"description": "Actions Http Client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "rm -Rf ./_out && tsc && cp package*.json ./_out && cp *.md ./_out && cp LICENSE ./_out && cp actions.png ./_out",
|
||||
"test": "jest",
|
||||
"format": "prettier --write *.ts && prettier --write **/*.ts",
|
||||
"format-check": "prettier --check *.ts && prettier --check **/*.ts",
|
||||
"audit-check": "npm audit --audit-level=moderate"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/http-client.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Actions",
|
||||
"Http"
|
||||
],
|
||||
"author": "GitHub, Inc.",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/http-client/issues"
|
||||
},
|
||||
"homepage": "https://github.com/actions/http-client#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^12.12.31",
|
||||
"jest": "^25.1.0",
|
||||
"prettier": "^2.0.4",
|
||||
"proxy": "^1.0.1",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import ifm = require('./interfaces')
|
||||
|
||||
export class BasicCredentialHandler implements ifm.IRequestHandler {
|
||||
username: string
|
||||
password: string
|
||||
|
||||
constructor(username: string, password: string) {
|
||||
this.username = username
|
||||
this.password = password
|
||||
}
|
||||
|
||||
prepareRequest(options: any): void {
|
||||
options.headers['Authorization'] =
|
||||
'Basic ' +
|
||||
Buffer.from(this.username + ':' + this.password).toString('base64')
|
||||
}
|
||||
|
||||
// This handler cannot handle 401
|
||||
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
handleAuthentication(
|
||||
httpClient: ifm.IHttpClient,
|
||||
requestInfo: ifm.IRequestInfo,
|
||||
objs
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class BearerCredentialHandler implements ifm.IRequestHandler {
|
||||
token: string
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token
|
||||
}
|
||||
|
||||
// currently implements pre-authorization
|
||||
// TODO: support preAuth = false where it hooks on 401
|
||||
prepareRequest(options: any): void {
|
||||
options.headers['Authorization'] = 'Bearer ' + this.token
|
||||
}
|
||||
|
||||
// This handler cannot handle 401
|
||||
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
handleAuthentication(
|
||||
httpClient: ifm.IHttpClient,
|
||||
requestInfo: ifm.IRequestInfo,
|
||||
objs
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class PersonalAccessTokenCredentialHandler
|
||||
implements ifm.IRequestHandler {
|
||||
token: string
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token
|
||||
}
|
||||
|
||||
// currently implements pre-authorization
|
||||
// TODO: support preAuth = false where it hooks on 401
|
||||
prepareRequest(options: any): void {
|
||||
options.headers['Authorization'] =
|
||||
'Basic ' + Buffer.from('PAT:' + this.token).toString('base64')
|
||||
}
|
||||
|
||||
// This handler cannot handle 401
|
||||
canHandleAuthentication(response: ifm.IHttpClientResponse): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
handleAuthentication(
|
||||
httpClient: ifm.IHttpClient,
|
||||
requestInfo: ifm.IRequestInfo,
|
||||
objs
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
import http = require('http')
|
||||
import https = require('https')
|
||||
import ifm = require('./interfaces')
|
||||
import pm = require('./proxy')
|
||||
|
||||
let tunnel: any
|
||||
|
||||
export enum HttpCodes {
|
||||
OK = 200,
|
||||
MultipleChoices = 300,
|
||||
MovedPermanently = 301,
|
||||
ResourceMoved = 302,
|
||||
SeeOther = 303,
|
||||
NotModified = 304,
|
||||
UseProxy = 305,
|
||||
SwitchProxy = 306,
|
||||
TemporaryRedirect = 307,
|
||||
PermanentRedirect = 308,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
PaymentRequired = 402,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
MethodNotAllowed = 405,
|
||||
NotAcceptable = 406,
|
||||
ProxyAuthenticationRequired = 407,
|
||||
RequestTimeout = 408,
|
||||
Conflict = 409,
|
||||
Gone = 410,
|
||||
TooManyRequests = 429,
|
||||
InternalServerError = 500,
|
||||
NotImplemented = 501,
|
||||
BadGateway = 502,
|
||||
ServiceUnavailable = 503,
|
||||
GatewayTimeout = 504
|
||||
}
|
||||
|
||||
export enum Headers {
|
||||
Accept = 'accept',
|
||||
ContentType = 'content-type'
|
||||
}
|
||||
|
||||
export enum MediaTypes {
|
||||
ApplicationJson = 'application/json'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proxy URL, depending upon the supplied url and proxy environment variables.
|
||||
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
|
||||
*/
|
||||
export function getProxyUrl(serverUrl: string): string {
|
||||
let proxyUrl = pm.getProxyUrl(new URL(serverUrl))
|
||||
return proxyUrl ? proxyUrl.href : ''
|
||||
}
|
||||
|
||||
const HttpRedirectCodes: number[] = [
|
||||
HttpCodes.MovedPermanently,
|
||||
HttpCodes.ResourceMoved,
|
||||
HttpCodes.SeeOther,
|
||||
HttpCodes.TemporaryRedirect,
|
||||
HttpCodes.PermanentRedirect
|
||||
]
|
||||
const HttpResponseRetryCodes: number[] = [
|
||||
HttpCodes.BadGateway,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.GatewayTimeout
|
||||
]
|
||||
const RetryableHttpVerbs: string[] = ['OPTIONS', 'GET', 'DELETE', 'HEAD']
|
||||
const ExponentialBackoffCeiling = 10
|
||||
const ExponentialBackoffTimeSlice = 5
|
||||
|
||||
export class HttpClientError extends Error {
|
||||
constructor(message: string, statusCode: number) {
|
||||
super(message)
|
||||
this.name = 'HttpClientError'
|
||||
this.statusCode = statusCode
|
||||
Object.setPrototypeOf(this, HttpClientError.prototype)
|
||||
}
|
||||
|
||||
public statusCode: number
|
||||
public result?: any
|
||||
}
|
||||
|
||||
export class HttpClientResponse implements ifm.IHttpClientResponse {
|
||||
constructor(message: http.IncomingMessage) {
|
||||
this.message = message
|
||||
}
|
||||
|
||||
public message: http.IncomingMessage
|
||||
readBody(): Promise<string> {
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
let output = Buffer.alloc(0)
|
||||
|
||||
this.message.on('data', (chunk: Buffer) => {
|
||||
output = Buffer.concat([output, chunk])
|
||||
})
|
||||
|
||||
this.message.on('end', () => {
|
||||
resolve(output.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function isHttps(requestUrl: string) {
|
||||
let parsedUrl: URL = new URL(requestUrl)
|
||||
return parsedUrl.protocol === 'https:'
|
||||
}
|
||||
|
||||
export class HttpClient {
|
||||
userAgent: string | undefined
|
||||
handlers: ifm.IRequestHandler[]
|
||||
requestOptions: ifm.IRequestOptions
|
||||
|
||||
private _ignoreSslError: boolean = false
|
||||
private _socketTimeout: number
|
||||
private _allowRedirects: boolean = true
|
||||
private _allowRedirectDowngrade: boolean = false
|
||||
private _maxRedirects: number = 50
|
||||
private _allowRetries: boolean = false
|
||||
private _maxRetries: number = 1
|
||||
private _agent
|
||||
private _proxyAgent
|
||||
private _keepAlive: boolean = false
|
||||
private _disposed: boolean = false
|
||||
|
||||
constructor(
|
||||
userAgent?: string,
|
||||
handlers?: ifm.IRequestHandler[],
|
||||
requestOptions?: ifm.IRequestOptions
|
||||
) {
|
||||
this.userAgent = userAgent
|
||||
this.handlers = handlers || []
|
||||
this.requestOptions = requestOptions
|
||||
if (requestOptions) {
|
||||
if (requestOptions.ignoreSslError != null) {
|
||||
this._ignoreSslError = requestOptions.ignoreSslError
|
||||
}
|
||||
|
||||
this._socketTimeout = requestOptions.socketTimeout
|
||||
|
||||
if (requestOptions.allowRedirects != null) {
|
||||
this._allowRedirects = requestOptions.allowRedirects
|
||||
}
|
||||
|
||||
if (requestOptions.allowRedirectDowngrade != null) {
|
||||
this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade
|
||||
}
|
||||
|
||||
if (requestOptions.maxRedirects != null) {
|
||||
this._maxRedirects = Math.max(requestOptions.maxRedirects, 0)
|
||||
}
|
||||
|
||||
if (requestOptions.keepAlive != null) {
|
||||
this._keepAlive = requestOptions.keepAlive
|
||||
}
|
||||
|
||||
if (requestOptions.allowRetries != null) {
|
||||
this._allowRetries = requestOptions.allowRetries
|
||||
}
|
||||
|
||||
if (requestOptions.maxRetries != null) {
|
||||
this._maxRetries = requestOptions.maxRetries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public options(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('OPTIONS', requestUrl, null, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public get(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('GET', requestUrl, null, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public del(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('DELETE', requestUrl, null, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public post(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('POST', requestUrl, data, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public patch(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('PATCH', requestUrl, data, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public put(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('PUT', requestUrl, data, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public head(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request('HEAD', requestUrl, null, additionalHeaders || {})
|
||||
}
|
||||
|
||||
public sendStream(
|
||||
verb: string,
|
||||
requestUrl: string,
|
||||
stream: NodeJS.ReadableStream,
|
||||
additionalHeaders?: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return this.request(verb, requestUrl, stream, additionalHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a typed object from an endpoint
|
||||
* Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise
|
||||
*/
|
||||
public async getJson<T>(
|
||||
requestUrl: string,
|
||||
additionalHeaders: ifm.IHeaders = {}
|
||||
): Promise<ifm.ITypedResponse<T>> {
|
||||
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.Accept,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
let res: ifm.IHttpClientResponse = await this.get(
|
||||
requestUrl,
|
||||
additionalHeaders
|
||||
)
|
||||
return this._processResponse<T>(res, this.requestOptions)
|
||||
}
|
||||
|
||||
public async postJson<T>(
|
||||
requestUrl: string,
|
||||
obj: any,
|
||||
additionalHeaders: ifm.IHeaders = {}
|
||||
): Promise<ifm.ITypedResponse<T>> {
|
||||
let data: string = JSON.stringify(obj, null, 2)
|
||||
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.Accept,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.ContentType,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
let res: ifm.IHttpClientResponse = await this.post(
|
||||
requestUrl,
|
||||
data,
|
||||
additionalHeaders
|
||||
)
|
||||
return this._processResponse<T>(res, this.requestOptions)
|
||||
}
|
||||
|
||||
public async putJson<T>(
|
||||
requestUrl: string,
|
||||
obj: any,
|
||||
additionalHeaders: ifm.IHeaders = {}
|
||||
): Promise<ifm.ITypedResponse<T>> {
|
||||
let data: string = JSON.stringify(obj, null, 2)
|
||||
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.Accept,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.ContentType,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
let res: ifm.IHttpClientResponse = await this.put(
|
||||
requestUrl,
|
||||
data,
|
||||
additionalHeaders
|
||||
)
|
||||
return this._processResponse<T>(res, this.requestOptions)
|
||||
}
|
||||
|
||||
public async patchJson<T>(
|
||||
requestUrl: string,
|
||||
obj: any,
|
||||
additionalHeaders: ifm.IHeaders = {}
|
||||
): Promise<ifm.ITypedResponse<T>> {
|
||||
let data: string = JSON.stringify(obj, null, 2)
|
||||
additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.Accept,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(
|
||||
additionalHeaders,
|
||||
Headers.ContentType,
|
||||
MediaTypes.ApplicationJson
|
||||
)
|
||||
let res: ifm.IHttpClientResponse = await this.patch(
|
||||
requestUrl,
|
||||
data,
|
||||
additionalHeaders
|
||||
)
|
||||
return this._processResponse<T>(res, this.requestOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a raw http request.
|
||||
* All other methods such as get, post, patch, and request ultimately call this.
|
||||
* Prefer get, del, post and patch
|
||||
*/
|
||||
public async request(
|
||||
verb: string,
|
||||
requestUrl: string,
|
||||
data: string | NodeJS.ReadableStream,
|
||||
headers: ifm.IHeaders
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
if (this._disposed) {
|
||||
throw new Error('Client has already been disposed.')
|
||||
}
|
||||
|
||||
let parsedUrl = new URL(requestUrl)
|
||||
let info: ifm.IRequestInfo = this._prepareRequest(verb, parsedUrl, headers)
|
||||
|
||||
// Only perform retries on reads since writes may not be idempotent.
|
||||
let maxTries: number =
|
||||
this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
|
||||
? this._maxRetries + 1
|
||||
: 1
|
||||
let numTries: number = 0
|
||||
|
||||
let response: HttpClientResponse
|
||||
while (numTries < maxTries) {
|
||||
response = await this.requestRaw(info, data)
|
||||
|
||||
// Check if it's an authentication challenge
|
||||
if (
|
||||
response &&
|
||||
response.message &&
|
||||
response.message.statusCode === HttpCodes.Unauthorized
|
||||
) {
|
||||
let authenticationHandler: ifm.IRequestHandler
|
||||
|
||||
for (let i = 0; i < this.handlers.length; i++) {
|
||||
if (this.handlers[i].canHandleAuthentication(response)) {
|
||||
authenticationHandler = this.handlers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (authenticationHandler) {
|
||||
return authenticationHandler.handleAuthentication(this, info, data)
|
||||
} else {
|
||||
// We have received an unauthorized response but have no handlers to handle it.
|
||||
// Let the response return to the caller.
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
let redirectsRemaining: number = this._maxRedirects
|
||||
while (
|
||||
HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
|
||||
this._allowRedirects &&
|
||||
redirectsRemaining > 0
|
||||
) {
|
||||
const redirectUrl: string | null = response.message.headers['location']
|
||||
if (!redirectUrl) {
|
||||
// if there's no location to redirect to, we won't
|
||||
break
|
||||
}
|
||||
let parsedRedirectUrl = new URL(redirectUrl)
|
||||
if (
|
||||
parsedUrl.protocol == 'https:' &&
|
||||
parsedUrl.protocol != parsedRedirectUrl.protocol &&
|
||||
!this._allowRedirectDowngrade
|
||||
) {
|
||||
throw new Error(
|
||||
'Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'
|
||||
)
|
||||
}
|
||||
|
||||
// we need to finish reading the response before reassigning response
|
||||
// which will leak the open socket.
|
||||
await response.readBody()
|
||||
|
||||
// strip authorization header if redirected to a different hostname
|
||||
if (parsedRedirectUrl.hostname !== parsedUrl.hostname) {
|
||||
for (let header in headers) {
|
||||
// header names are case insensitive
|
||||
if (header.toLowerCase() === 'authorization') {
|
||||
delete headers[header]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// let's make the request with the new redirectUrl
|
||||
info = this._prepareRequest(verb, parsedRedirectUrl, headers)
|
||||
response = await this.requestRaw(info, data)
|
||||
redirectsRemaining--
|
||||
}
|
||||
|
||||
if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) {
|
||||
// If not a retry code, return immediately instead of retrying
|
||||
return response
|
||||
}
|
||||
|
||||
numTries += 1
|
||||
|
||||
if (numTries < maxTries) {
|
||||
await response.readBody()
|
||||
await this._performExponentialBackoff(numTries)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be called if keepAlive is set to true in request options.
|
||||
*/
|
||||
public dispose() {
|
||||
if (this._agent) {
|
||||
this._agent.destroy()
|
||||
}
|
||||
|
||||
this._disposed = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw request.
|
||||
* @param info
|
||||
* @param data
|
||||
*/
|
||||
public requestRaw(
|
||||
info: ifm.IRequestInfo,
|
||||
data: string | NodeJS.ReadableStream
|
||||
): Promise<ifm.IHttpClientResponse> {
|
||||
return new Promise<ifm.IHttpClientResponse>((resolve, reject) => {
|
||||
let callbackForResult = function (
|
||||
err: any,
|
||||
res: ifm.IHttpClientResponse
|
||||
) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
resolve(res)
|
||||
}
|
||||
|
||||
this.requestRawWithCallback(info, data, callbackForResult)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw request with callback.
|
||||
* @param info
|
||||
* @param data
|
||||
* @param onResult
|
||||
*/
|
||||
public requestRawWithCallback(
|
||||
info: ifm.IRequestInfo,
|
||||
data: string | NodeJS.ReadableStream,
|
||||
onResult: (err: any, res: ifm.IHttpClientResponse) => void
|
||||
): void {
|
||||
let socket
|
||||
|
||||
if (typeof data === 'string') {
|
||||
info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8')
|
||||
}
|
||||
|
||||
let callbackCalled: boolean = false
|
||||
let handleResult = (err: any, res: HttpClientResponse) => {
|
||||
if (!callbackCalled) {
|
||||
callbackCalled = true
|
||||
onResult(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
let req: http.ClientRequest = info.httpModule.request(
|
||||
info.options,
|
||||
(msg: http.IncomingMessage) => {
|
||||
let res: HttpClientResponse = new HttpClientResponse(msg)
|
||||
handleResult(null, res)
|
||||
}
|
||||
)
|
||||
|
||||
req.on('socket', sock => {
|
||||
socket = sock
|
||||
})
|
||||
|
||||
// If we ever get disconnected, we want the socket to timeout eventually
|
||||
req.setTimeout(this._socketTimeout || 3 * 60000, () => {
|
||||
if (socket) {
|
||||
socket.end()
|
||||
}
|
||||
handleResult(new Error('Request timeout: ' + info.options.path), null)
|
||||
})
|
||||
|
||||
req.on('error', function (err) {
|
||||
// err has statusCode property
|
||||
// res should have headers
|
||||
handleResult(err, null)
|
||||
})
|
||||
|
||||
if (data && typeof data === 'string') {
|
||||
req.write(data, 'utf8')
|
||||
}
|
||||
|
||||
if (data && typeof data !== 'string') {
|
||||
data.on('close', function () {
|
||||
req.end()
|
||||
})
|
||||
|
||||
data.pipe(req)
|
||||
} else {
|
||||
req.end()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an http agent. This function is useful when you need an http agent that handles
|
||||
* routing through a proxy server - depending upon the url and proxy environment variables.
|
||||
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
|
||||
*/
|
||||
public getAgent(serverUrl: string): http.Agent {
|
||||
let parsedUrl = new URL(serverUrl)
|
||||
return this._getAgent(parsedUrl)
|
||||
}
|
||||
|
||||
private _prepareRequest(
|
||||
method: string,
|
||||
requestUrl: URL,
|
||||
headers: ifm.IHeaders
|
||||
): ifm.IRequestInfo {
|
||||
const info: ifm.IRequestInfo = <ifm.IRequestInfo>{}
|
||||
|
||||
info.parsedUrl = requestUrl
|
||||
const usingSsl: boolean = info.parsedUrl.protocol === 'https:'
|
||||
info.httpModule = usingSsl ? https : http
|
||||
const defaultPort: number = usingSsl ? 443 : 80
|
||||
|
||||
info.options = <http.RequestOptions>{}
|
||||
info.options.host = info.parsedUrl.hostname
|
||||
info.options.port = info.parsedUrl.port
|
||||
? parseInt(info.parsedUrl.port)
|
||||
: defaultPort
|
||||
info.options.path =
|
||||
(info.parsedUrl.pathname || '') + (info.parsedUrl.search || '')
|
||||
info.options.method = method
|
||||
info.options.headers = this._mergeHeaders(headers)
|
||||
if (this.userAgent != null) {
|
||||
info.options.headers['user-agent'] = this.userAgent
|
||||
}
|
||||
|
||||
info.options.agent = this._getAgent(info.parsedUrl)
|
||||
|
||||
// gives handlers an opportunity to participate
|
||||
if (this.handlers) {
|
||||
this.handlers.forEach(handler => {
|
||||
handler.prepareRequest(info.options)
|
||||
})
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
private _mergeHeaders(headers: ifm.IHeaders): ifm.IHeaders {
|
||||
const lowercaseKeys = obj =>
|
||||
Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
|
||||
|
||||
if (this.requestOptions && this.requestOptions.headers) {
|
||||
return Object.assign(
|
||||
{},
|
||||
lowercaseKeys(this.requestOptions.headers),
|
||||
lowercaseKeys(headers)
|
||||
)
|
||||
}
|
||||
|
||||
return lowercaseKeys(headers || {})
|
||||
}
|
||||
|
||||
private _getExistingOrDefaultHeader(
|
||||
additionalHeaders: ifm.IHeaders,
|
||||
header: string,
|
||||
_default: string
|
||||
) {
|
||||
const lowercaseKeys = obj =>
|
||||
Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {})
|
||||
|
||||
let clientHeader: string
|
||||
if (this.requestOptions && this.requestOptions.headers) {
|
||||
clientHeader = lowercaseKeys(this.requestOptions.headers)[header]
|
||||
}
|
||||
return additionalHeaders[header] || clientHeader || _default
|
||||
}
|
||||
|
||||
private _getAgent(parsedUrl: URL): http.Agent {
|
||||
let agent
|
||||
let proxyUrl: URL = pm.getProxyUrl(parsedUrl)
|
||||
let useProxy = proxyUrl && proxyUrl.hostname
|
||||
|
||||
if (this._keepAlive && useProxy) {
|
||||
agent = this._proxyAgent
|
||||
}
|
||||
|
||||
if (this._keepAlive && !useProxy) {
|
||||
agent = this._agent
|
||||
}
|
||||
|
||||
// if agent is already assigned use that agent.
|
||||
if (!!agent) {
|
||||
return agent
|
||||
}
|
||||
|
||||
const usingSsl = parsedUrl.protocol === 'https:'
|
||||
let maxSockets = 100
|
||||
if (!!this.requestOptions) {
|
||||
maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets
|
||||
}
|
||||
|
||||
if (useProxy) {
|
||||
// If using proxy, need tunnel
|
||||
if (!tunnel) {
|
||||
tunnel = require('tunnel')
|
||||
}
|
||||
|
||||
const agentOptions = {
|
||||
maxSockets: maxSockets,
|
||||
keepAlive: this._keepAlive,
|
||||
proxy: {
|
||||
...((proxyUrl.username || proxyUrl.password) && {
|
||||
proxyAuth: `${proxyUrl.username}:${proxyUrl.password}`
|
||||
}),
|
||||
host: proxyUrl.hostname,
|
||||
port: proxyUrl.port
|
||||
}
|
||||
}
|
||||
|
||||
let tunnelAgent: Function
|
||||
const overHttps = proxyUrl.protocol === 'https:'
|
||||
if (usingSsl) {
|
||||
tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp
|
||||
} else {
|
||||
tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp
|
||||
}
|
||||
|
||||
agent = tunnelAgent(agentOptions)
|
||||
this._proxyAgent = agent
|
||||
}
|
||||
|
||||
// if reusing agent across request and tunneling agent isn't assigned create a new agent
|
||||
if (this._keepAlive && !agent) {
|
||||
const options = {keepAlive: this._keepAlive, maxSockets: maxSockets}
|
||||
agent = usingSsl ? new https.Agent(options) : new http.Agent(options)
|
||||
this._agent = agent
|
||||
}
|
||||
|
||||
// if not using private agent and tunnel agent isn't setup then use global agent
|
||||
if (!agent) {
|
||||
agent = usingSsl ? https.globalAgent : http.globalAgent
|
||||
}
|
||||
|
||||
if (usingSsl && this._ignoreSslError) {
|
||||
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
|
||||
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
|
||||
// we have to cast it to any and change it directly
|
||||
agent.options = Object.assign(agent.options || {}, {
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
private _performExponentialBackoff(retryNumber: number): Promise<void> {
|
||||
retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber)
|
||||
const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber)
|
||||
return new Promise(resolve => setTimeout(() => resolve(), ms))
|
||||
}
|
||||
|
||||
private static dateTimeDeserializer(key: any, value: any): any {
|
||||
if (typeof value === 'string') {
|
||||
let a = new Date(value)
|
||||
if (!isNaN(a.valueOf())) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private async _processResponse<T>(
|
||||
res: ifm.IHttpClientResponse,
|
||||
options: ifm.IRequestOptions
|
||||
): Promise<ifm.ITypedResponse<T>> {
|
||||
return new Promise<ifm.ITypedResponse<T>>(async (resolve, reject) => {
|
||||
const statusCode: number = res.message.statusCode
|
||||
|
||||
const response: ifm.ITypedResponse<T> = {
|
||||
statusCode: statusCode,
|
||||
result: null,
|
||||
headers: {}
|
||||
}
|
||||
|
||||
// not found leads to null obj returned
|
||||
if (statusCode == HttpCodes.NotFound) {
|
||||
resolve(response)
|
||||
}
|
||||
|
||||
let obj: any
|
||||
let contents: string
|
||||
|
||||
// get the result from the body
|
||||
try {
|
||||
contents = await res.readBody()
|
||||
if (contents && contents.length > 0) {
|
||||
if (options && options.deserializeDates) {
|
||||
obj = JSON.parse(contents, HttpClient.dateTimeDeserializer)
|
||||
} else {
|
||||
obj = JSON.parse(contents)
|
||||
}
|
||||
|
||||
response.result = obj
|
||||
}
|
||||
|
||||
response.headers = res.message.headers
|
||||
} catch (err) {
|
||||
// Invalid resource (contents not json); leaving result obj null
|
||||
}
|
||||
|
||||
// note that 3xx redirects are handled by the http layer.
|
||||
if (statusCode > 299) {
|
||||
let msg: string
|
||||
|
||||
// if exception/error in body, attempt to get better error
|
||||
if (obj && obj.message) {
|
||||
msg = obj.message
|
||||
} else if (contents && contents.length > 0) {
|
||||
// it may be the case that the exception is in the body message as string
|
||||
msg = contents
|
||||
} else {
|
||||
msg = 'Failed request: (' + statusCode + ')'
|
||||
}
|
||||
|
||||
let err = new HttpClientError(msg, statusCode)
|
||||
err.result = response.result
|
||||
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import http = require('http')
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface IHttpClient {
|
||||
options(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
get(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
del(
|
||||
requestUrl: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
post(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
patch(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
put(
|
||||
requestUrl: string,
|
||||
data: string,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
sendStream(
|
||||
verb: string,
|
||||
requestUrl: string,
|
||||
stream: NodeJS.ReadableStream,
|
||||
additionalHeaders?: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
request(
|
||||
verb: string,
|
||||
requestUrl: string,
|
||||
data: string | NodeJS.ReadableStream,
|
||||
headers: IHeaders
|
||||
): Promise<IHttpClientResponse>
|
||||
requestRaw(
|
||||
info: IRequestInfo,
|
||||
data: string | NodeJS.ReadableStream
|
||||
): Promise<IHttpClientResponse>
|
||||
requestRawWithCallback(
|
||||
info: IRequestInfo,
|
||||
data: string | NodeJS.ReadableStream,
|
||||
onResult: (err: any, res: IHttpClientResponse) => void
|
||||
): void
|
||||
}
|
||||
|
||||
export interface IRequestHandler {
|
||||
prepareRequest(options: http.RequestOptions): void
|
||||
canHandleAuthentication(response: IHttpClientResponse): boolean
|
||||
handleAuthentication(
|
||||
httpClient: IHttpClient,
|
||||
requestInfo: IRequestInfo,
|
||||
objs
|
||||
): Promise<IHttpClientResponse>
|
||||
}
|
||||
|
||||
export interface IHttpClientResponse {
|
||||
message: http.IncomingMessage
|
||||
readBody(): Promise<string>
|
||||
}
|
||||
|
||||
export interface IRequestInfo {
|
||||
options: http.RequestOptions
|
||||
parsedUrl: URL
|
||||
httpModule: any
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
headers?: IHeaders
|
||||
socketTimeout?: number
|
||||
ignoreSslError?: boolean
|
||||
allowRedirects?: boolean
|
||||
allowRedirectDowngrade?: boolean
|
||||
maxRedirects?: number
|
||||
maxSockets?: number
|
||||
keepAlive?: boolean
|
||||
deserializeDates?: boolean
|
||||
// Allows retries only on Read operations (since writes may not be idempotent)
|
||||
allowRetries?: boolean
|
||||
maxRetries?: number
|
||||
}
|
||||
|
||||
export interface ITypedResponse<T> {
|
||||
statusCode: number
|
||||
result: T | null
|
||||
headers: Object
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export function getProxyUrl(reqUrl: URL): URL | undefined {
|
||||
let usingSsl = reqUrl.protocol === 'https:'
|
||||
|
||||
let proxyUrl: URL
|
||||
if (checkBypass(reqUrl)) {
|
||||
return proxyUrl
|
||||
}
|
||||
|
||||
let proxyVar: string
|
||||
if (usingSsl) {
|
||||
proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY']
|
||||
} else {
|
||||
proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY']
|
||||
}
|
||||
|
||||
if (proxyVar) {
|
||||
proxyUrl = new URL(proxyVar)
|
||||
}
|
||||
|
||||
return proxyUrl
|
||||
}
|
||||
|
||||
export function checkBypass(reqUrl: URL): boolean {
|
||||
if (!reqUrl.hostname) {
|
||||
return false
|
||||
}
|
||||
|
||||
let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''
|
||||
if (!noProxy) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Determine the request port
|
||||
let reqPort: number
|
||||
if (reqUrl.port) {
|
||||
reqPort = Number(reqUrl.port)
|
||||
} else if (reqUrl.protocol === 'http:') {
|
||||
reqPort = 80
|
||||
} else if (reqUrl.protocol === 'https:') {
|
||||
reqPort = 443
|
||||
}
|
||||
|
||||
// Format the request hostname and hostname with port
|
||||
let upperReqHosts = [reqUrl.hostname.toUpperCase()]
|
||||
if (typeof reqPort === 'number') {
|
||||
upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`)
|
||||
}
|
||||
|
||||
// Compare request host against noproxy
|
||||
for (let upperNoProxyItem of noProxy
|
||||
.split(',')
|
||||
.map(x => x.trim().toUpperCase())
|
||||
.filter(x => x)) {
|
||||
if (upperReqHosts.some(x => x === upperNoProxyItem)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user