diff --git a/action.yml b/action.yml index 170e4c9ea..ee1ee8650 100644 --- a/action.yml +++ b/action.yml @@ -37,6 +37,9 @@ inputs: http-proxy: description: Proxy to use for the AWS SDK agent required: false + no-proxy: + description: Hosts to skip for the proxy configuration + required: false mask-aws-account-id: description: Whether to mask the AWS account ID for these credentials as a secret value. By default the account ID will not be masked required: false diff --git a/package-lock.json b/package-lock.json index 05355308c..56accf016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@actions/core": "^1.11.1", "@aws-sdk/client-sts": "^3.883.0", "@smithy/node-http-handler": "^4.2.0", - "https-proxy-agent": "^7.0.6" + "proxy-agent": "^6.5.0" }, "devDependencies": { "@aws-sdk/credential-provider-env": "^3.883.0", @@ -2557,6 +2557,12 @@ "node": ">=18.0.0" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -2846,6 +2852,18 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-v8-to-istanbul": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz", @@ -2877,6 +2895,15 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bowser": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", @@ -4103,6 +4130,15 @@ "node": ">=8" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -4177,6 +4213,20 @@ "node": ">=6" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/del": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/del/-/del-8.0.0.tgz", @@ -4435,6 +4485,49 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4445,6 +4538,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -4661,6 +4763,20 @@ "xtend": "~4.0.1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", @@ -5282,6 +5398,19 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5339,6 +5468,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5943,6 +6081,15 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/nise": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", @@ -6083,6 +6230,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6261,6 +6440,40 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -6703,11 +6916,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index de64e9c93..aceb9a7f0 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@actions/core": "^1.11.1", "@aws-sdk/client-sts": "^3.883.0", "@smithy/node-http-handler": "^4.2.0", - "https-proxy-agent": "^7.0.6" + "proxy-agent": "^6.5.0" }, "keywords": [ "aws", diff --git a/src/CredentialsClient.ts b/src/CredentialsClient.ts index 989bd8687..1b79bb16d 100644 --- a/src/CredentialsClient.ts +++ b/src/CredentialsClient.ts @@ -2,14 +2,16 @@ import { info } from '@actions/core'; import { STSClient } from '@aws-sdk/client-sts'; import type { AwsCredentialIdentity } from '@aws-sdk/types'; import { NodeHttpHandler } from '@smithy/node-http-handler'; -import { HttpsProxyAgent } from 'https-proxy-agent'; +import { ProxyAgent } from 'proxy-agent'; import { errorMessage, getCallerIdentity } from './helpers'; +import { ProxyResolver } from './ProxyResolver'; const USER_AGENT = 'configure-aws-credentials-for-github-actions'; export interface CredentialsClientProps { region?: string; proxyServer?: string; + noProxy?: string; } export class CredentialsClient { @@ -21,10 +23,15 @@ export class CredentialsClient { this.region = props.region; if (props.proxyServer) { info('Configuring proxy handler for STS client'); - const handler = new HttpsProxyAgent(props.proxyServer); + const getProxyForUrl = new ProxyResolver({ + httpProxy: props.proxyServer, + httpsProxy: props.proxyServer, + noProxy: props.noProxy, + }).getProxyForUrl; + const handler = new ProxyAgent({ getProxyForUrl }); this.requestHandler = new NodeHttpHandler({ - httpAgent: handler, httpsAgent: handler, + httpAgent: handler, }); } } diff --git a/src/ProxyResolver.ts b/src/ProxyResolver.ts new file mode 100644 index 000000000..d2bc052a2 --- /dev/null +++ b/src/ProxyResolver.ts @@ -0,0 +1,70 @@ +// Based on https://github.com/Rob--W/proxy-from-env/tree/caf8c32301afdac8b5feaf346028bd8240690144 +// See https://github.com/Rob--W/proxy-from-env/blob/caf8c32301afdac8b5feaf346028bd8240690144/LICENSE +import type * as http from 'node:http'; + +const DEFAULT_PORTS: Record = { + http: 80, + https: 443, +}; +export interface ProxyOptions { + readonly noProxy?: string; + readonly httpsProxy?: string; + readonly httpProxy?: string; +} + +export class ProxyResolver { + options: ProxyOptions; + constructor(options: ProxyOptions) { + this.options = options; + } + + getProxyForUrl(url: string, _req: http.ClientRequest): string { + return this.getProxyForUrlOptions(url, this.options); + } + + private getProxyForUrlOptions(url: string | URL, options?: ProxyOptions): string { + let parsedUrl: URL; + try { + parsedUrl = typeof url === 'string' ? new URL(url) : url; + } catch (_) { + return ''; // Don't proxy invalid URLs. + } + const proto = parsedUrl.protocol.split(':', 1)[0]; + if (!proto) return ''; // Don't proxy URLs without a protocol. + const hostname = parsedUrl.host; + const port = parseInt(parsedUrl.port || '') || DEFAULT_PORTS[proto] || 0; + + if (options?.noProxy && !this.shouldProxy(hostname, port, options.noProxy)) return ''; + if (proto === 'http' && options?.httpProxy) return options.httpProxy; + if (proto === 'https' && options?.httpsProxy) return options.httpsProxy; + return ''; // No proxy configured for this protocol or unknown protocol + } + + private shouldProxy(hostname: string, port: number, noProxy: string): boolean { + if (!noProxy) return true; + if (noProxy === '*') return false; // Never proxy if wildcard is set. + + return noProxy.split(/[,\s]/).every((proxy) => { + if (!proxy) return true; // Skip zero-length hosts. + + const parsedProxy = proxy.match(/^(.+):(\d+)$/); + const parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy; + const parsedProxyPort = parsedProxy?.[2] ? parseInt(parsedProxy[2]) : 0; + + if (parsedProxyPort && parsedProxyPort !== port) return true; // Skip if ports don't match. + + if (parsedProxyHostname && !/^[.*]/.test(parsedProxyHostname)) { + // No wildcards, so stop proxying if there is an exact match. + return hostname !== parsedProxyHostname; + } + + let cleanProxyHostname = parsedProxyHostname; + if (parsedProxyHostname && parsedProxyHostname.charAt(0) === '*') { + // Remove leading wildcard. + cleanProxyHostname = parsedProxyHostname.slice(1); + } + // Stop proxying if the hostname ends with the no_proxy host. + return !cleanProxyHostname || !hostname.endsWith(cleanProxyHostname); + }); + } +} diff --git a/src/helpers.ts b/src/helpers.ts index f5edf60e3..2b19ba14f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -28,6 +28,7 @@ export function translateEnvVariables() { 'RETRY_MAX_ATTEMPTS', 'SPECIAL_CHARACTERS_WORKAROUND', 'USE_EXISTING_CREDENTIALS', + 'NO_PROXY', ]; // Treat HTTPS_PROXY as HTTP_PROXY. Precedence is HTTPS_PROXY > HTTP_PROXY if (process.env.HTTPS_PROXY) process.env.HTTP_PROXY = process.env.HTTPS_PROXY; diff --git a/src/index.ts b/src/index.ts index 0cb6c915d..a9f15c45f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,6 +56,7 @@ export async function run() { .split(',') .map((s) => s.trim()); const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false }); + const noProxy = core.getInput('no-proxy', { required: false }); if (forceSkipOidc && roleToAssume && !AccessKeyId && !webIdentityTokenFile) { throw new Error( @@ -109,7 +110,7 @@ export async function run() { exportRegion(region, outputEnvCredentials); // Instantiate credentials client - const credentialsClient = new CredentialsClient({ region, proxyServer }); + const credentialsClient = new CredentialsClient({ region, proxyServer, noProxy }); let sourceAccountId: string; let webIdentityToken: string; diff --git a/test/ProxyResolver.test.ts b/test/ProxyResolver.test.ts new file mode 100644 index 000000000..ae9a6c711 --- /dev/null +++ b/test/ProxyResolver.test.ts @@ -0,0 +1,100 @@ +import type * as http from 'node:http'; +import { describe, expect, test } from 'vitest'; +import { type ProxyOptions, ProxyResolver } from '../src/ProxyResolver'; + +describe('ProxyResolver', () => { + const mockReq = {} as http.ClientRequest; + + test('returns http proxy for http URLs', () => { + const options: ProxyOptions = { httpProxy: 'http://proxy:8080' }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe('http://proxy:8080'); + }); + + test('returns https proxy for https URLs', () => { + const options: ProxyOptions = { httpsProxy: 'https://proxy:8080' }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('https://example.com', mockReq)).toBe('https://proxy:8080'); + }); + + test('returns empty string when no proxy configured', () => { + const resolver = new ProxyResolver({}); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + }); + + test('respects noProxy setting', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: 'example.com', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://other.com', mockReq)).toBe('http://proxy:8080'); + }); + + test('handles invalid URLs', () => { + const resolver = new ProxyResolver({ httpProxy: 'http://proxy:8080' }); + + expect(resolver.getProxyForUrl('invalid-url', mockReq)).toBe(''); + }); + + test('handles wildcard noProxy', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: '*', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + }); + + test('handles comma-separated noProxy list', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: 'example.com,test.com', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://test.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://other.com', mockReq)).toBe('http://proxy:8080'); + }); + + test('handles port-specific noProxy', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: 'example.com:80', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://example.com:8080', mockReq)).toBe('http://proxy:8080'); + }); + + test('handles wildcard domain noProxy', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: '*.example.com', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://sub.example.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe('http://proxy:8080'); + expect(resolver.getProxyForUrl('http://other.com', mockReq)).toBe('http://proxy:8080'); + }); + + test('handles empty noProxy entries', () => { + const options: ProxyOptions = { + httpProxy: 'http://proxy:8080', + noProxy: 'example.com, ,test.com', + }; + const resolver = new ProxyResolver(options); + + expect(resolver.getProxyForUrl('http://example.com', mockReq)).toBe(''); + expect(resolver.getProxyForUrl('http://test.com', mockReq)).toBe(''); + }); +}); diff --git a/test/helpers.test.ts b/test/helpers.test.ts index 6aa475f6e..7e96f0452 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -61,4 +61,39 @@ describe('Configure AWS Credentials helpers', {}, () => { expect(core.setSecret).toHaveBeenCalledTimes(3); expect(core.exportVariable).toHaveBeenCalledTimes(0); }); + + it('verifies credentials without special characters', {}, () => { + expect(helpers.verifyKeys({ AccessKeyId: 'AKIATEST', SecretAccessKey: 'secretkey' })).toBe(true); + expect(helpers.verifyKeys({ AccessKeyId: 'AKIA!@#$', SecretAccessKey: 'secret' })).toBe(false); + expect(helpers.verifyKeys(undefined)).toBe(false); + }); + + it('translates environment variables', {}, () => { + process.env.AWS_REGION = 'us-east-1'; + process.env.HTTPS_PROXY = 'https://proxy:8080'; + helpers.translateEnvVariables(); + expect(process.env['INPUT_AWS-REGION']).toBe('us-east-1'); + expect(process.env.HTTP_PROXY).toBe('https://proxy:8080'); + }); + + it('handles getBooleanInput correctly', {}, () => { + vi.spyOn(core, 'getInput').mockReturnValue('true'); + expect(helpers.getBooleanInput('test')).toBe(true); + + vi.spyOn(core, 'getInput').mockReturnValue('false'); + expect(helpers.getBooleanInput('test')).toBe(false); + + vi.spyOn(core, 'getInput').mockReturnValue(''); + expect(helpers.getBooleanInput('test', { default: true })).toBe(true); + + vi.spyOn(core, 'getInput').mockReturnValue('invalid'); + expect(() => helpers.getBooleanInput('test')).toThrow(); + }); + + it('clears session token when not provided', {}, () => { + vi.spyOn(core, 'exportVariable').mockImplementation(() => {}); + process.env.AWS_SESSION_TOKEN = 'old-token'; + helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test' }, false, true); + expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', ''); + }); }); diff --git a/test/index.test.ts b/test/index.test.ts index 7ded8cd29..557ed006c 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -341,10 +341,12 @@ describe('Configure AWS Credentials', {}, () => { }); it('skips OIDC when force-skip-oidc is true with IAM credentials', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_ASSUMEROLE_INPUTS, - 'force-skip-oidc': 'true' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_ASSUMEROLE_INPUTS, + 'force-skip-oidc': 'true', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); @@ -353,17 +355,19 @@ describe('Configure AWS Credentials', {}, () => { .mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' }) .mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' }); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.getIDToken).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled(); }); it('skips OIDC when force-skip-oidc is true with web identity token file', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.WEBIDENTITY_TOKEN_FILE_INPUTS, - 'force-skip-oidc': 'true' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.WEBIDENTITY_TOKEN_FILE_INPUTS, + 'force-skip-oidc': 'true', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); @@ -372,7 +376,7 @@ describe('Configure AWS Credentials', {}, () => { vol.reset(); fs.mkdirSync('/home/github', { recursive: true }); fs.writeFileSync('/home/github/file.txt', 'test-token'); - + await run(); expect(core.getIDToken).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Assuming role with web identity token file'); @@ -380,34 +384,38 @@ describe('Configure AWS Credentials', {}, () => { }); it('fails when force-skip-oidc is true but no alternative credentials provided', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', - 'aws-region': 'fake-region-1', - 'force-skip-oidc': 'true' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + 'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE', + 'aws-region': 'fake-region-1', + 'force-skip-oidc': 'true', + }), + ); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.setFailed).toHaveBeenCalledWith( - "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set" + "If 'force-skip-oidc' is true and 'role-to-assume' is set, 'aws-access-key-id' or 'web-identity-token-file' must be set", ); }); it('allows force-skip-oidc without role-to-assume', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'force-skip-oidc': 'true' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'force-skip-oidc': 'true', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); - + mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.getIDToken).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Proceeding with IAM user credentials'); @@ -415,15 +423,17 @@ describe('Configure AWS Credentials', {}, () => { }); it('uses OIDC when force-skip-oidc is false (default behavior)', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.GH_OIDC_INPUTS, - 'force-skip-oidc': 'false' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.GH_OIDC_INPUTS, + 'force-skip-oidc': 'false', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.getIDToken).toHaveBeenCalledWith(''); expect(core.info).toHaveBeenCalledWith('Assuming role with OIDC'); @@ -436,7 +446,7 @@ describe('Configure AWS Credentials', {}, () => { mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.getIDToken).toHaveBeenCalledWith(''); expect(core.info).toHaveBeenCalledWith('Assuming role with OIDC'); @@ -444,12 +454,14 @@ describe('Configure AWS Credentials', {}, () => { }); it('works with role chaining when force-skip-oidc is true', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.EXISTING_ROLE_INPUTS, - 'force-skip-oidc': 'true', - 'aws-access-key-id': 'MYAWSACCESSKEYID', - 'aws-secret-access-key': 'MYAWSSECRETACCESSKEY' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.EXISTING_ROLE_INPUTS, + 'force-skip-oidc': 'true', + 'aws-access-key-id': 'MYAWSACCESSKEYID', + 'aws-secret-access-key': 'MYAWSSECRETACCESSKEY', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); @@ -458,164 +470,182 @@ describe('Configure AWS Credentials', {}, () => { .mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' }) .mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' }); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - + await run(); expect(core.getIDToken).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled(); }); }); - describe('Account ID Validation', {}, () => { + describe('Account ID Validation', {}, () => { beforeEach(() => { vi.clearAllMocks(); mockedSTSClient.reset(); }); - + it('succeeds when account ID matches allowed list', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '111111111111' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '111111111111', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Proceeding with IAM user credentials'); }); it('succeeds with multiple allowed account IDs when account matches', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '999999999999,111111111111,222222222222' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '999999999999,111111111111,222222222222', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).not.toHaveBeenCalled(); }); it('fails when account ID does not match allowed list', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '999999999999' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '999999999999', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).toHaveBeenCalledWith( - 'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999' + 'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999', ); }); it('fails when account ID does not match any in multiple allowed accounts', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '999999999999,888888888888' - })); - - mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '999999999999,888888888888', + }), + ); + + mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', - }); - + }); + await run(); expect(core.setFailed).toHaveBeenCalledWith( - 'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999, 888888888888' + 'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999, 888888888888', ); }); it('works with assume role when account ID matches', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_ASSUMEROLE_INPUTS, - 'allowed-account-ids': '111111111111' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_ASSUMEROLE_INPUTS, + 'allowed-account-ids': '111111111111', + }), + ); mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials') .mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' }) .mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' }); - + await run(); expect(core.setFailed).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Authenticated as assumedRoleId AROAFAKEASSUMEDROLEID'); }); it('works with OIDC when account ID matches', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.GH_OIDC_INPUTS, - 'allowed-account-ids': '111111111111' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.GH_OIDC_INPUTS, + 'allowed-account-ids': '111111111111', + }), + ); vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken'); mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token'; - - await run(); - expect(core.setFailed).not.toHaveBeenCalled(); + + await run(); + expect(core.setFailed).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Authenticated as assumedRoleId AROAFAKEASSUMEDROLEID'); }); it('handles GetCallerIdentity API failure gracefully', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '111111111111' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '111111111111', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).rejects(new Error('API Error')); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).toHaveBeenCalledWith('Could not validate account ID of credentials: API Error'); }); it('ignores validation when allowed-account-ids is empty', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': '' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': '', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).not.toHaveBeenCalled(); expect(core.info).toHaveBeenCalledWith('Proceeding with IAM user credentials'); }); it('handles whitespace in allowed-account-ids input', async () => { - vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput({ - ...mocks.IAM_USER_INPUTS, - 'allowed-account-ids': ' 111111111111 , 222222222222 ' - })); + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.IAM_USER_INPUTS, + 'allowed-account-ids': ' 111111111111 , 222222222222 ', + }), + ); mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY }); // biome-ignore lint/suspicious/noExplicitAny: any required to mock private method vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({ accessKeyId: 'MYAWSACCESSKEYID', }); - + await run(); expect(core.setFailed).not.toHaveBeenCalled(); }); - }); - + }); + describe('HTTP Proxy Configuration', {}, () => { beforeEach(() => { vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.GH_OIDC_INPUTS)); @@ -630,12 +660,12 @@ describe('Configure AWS Credentials', {}, () => { vi.spyOn(core, 'getInput').mockImplementation( mocks.getInput({ ...mocks.GH_OIDC_INPUTS, - 'http-proxy': 'http://proxy.example.com:8080' - }) + 'http-proxy': 'http://proxy.example.com:8080', + }), ); - + await run(); - + expect(infoSpy).toHaveBeenCalledWith('Configuring proxy handler for STS client'); expect(core.setFailed).not.toHaveBeenCalled(); }); @@ -643,9 +673,9 @@ describe('Configure AWS Credentials', {}, () => { it('configures proxy from HTTP_PROXY environment variable', async () => { const infoSpy = vi.spyOn(core, 'info'); process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; - + await run(); - + expect(infoSpy).toHaveBeenCalledWith('Configuring proxy handler for STS client'); expect(core.setFailed).not.toHaveBeenCalled(); }); @@ -653,9 +683,9 @@ describe('Configure AWS Credentials', {}, () => { it('configures proxy from HTTPS_PROXY environment variable', async () => { const infoSpy = vi.spyOn(core, 'info'); process.env.HTTPS_PROXY = 'https://proxy.example.com:8080'; - + await run(); - + expect(infoSpy).toHaveBeenCalledWith('Configuring proxy handler for STS client'); expect(core.setFailed).not.toHaveBeenCalled(); }); @@ -666,30 +696,50 @@ describe('Configure AWS Credentials', {}, () => { vi.spyOn(core, 'getInput').mockImplementation( mocks.getInput({ ...mocks.GH_OIDC_INPUTS, - 'http-proxy': 'http://input-proxy.example.com:8080' - }) + 'http-proxy': 'http://input-proxy.example.com:8080', + }), ); - + await run(); - + expect(infoSpy).toHaveBeenCalledWith('Configuring proxy handler for STS client'); expect(core.setFailed).not.toHaveBeenCalled(); }); it('properly configures proxy agent in STS client', async () => { const infoSpy = vi.spyOn(core, 'info'); - + vi.spyOn(core, 'getInput').mockImplementation( mocks.getInput({ ...mocks.GH_OIDC_INPUTS, - 'http-proxy': 'http://proxy.example.com:8080' - }) + 'http-proxy': 'http://proxy.example.com:8080', + }), ); - + await run(); - + expect(infoSpy).toHaveBeenCalledWith('Configuring proxy handler for STS client'); expect(core.setFailed).not.toHaveBeenCalled(); }); + + it('configures no-proxy setting', async () => { + vi.spyOn(core, 'getInput').mockImplementation( + mocks.getInput({ + ...mocks.GH_OIDC_INPUTS, + 'http-proxy': 'http://proxy.example.com:8080', + 'no-proxy': 'localhost,127.0.0.1', + }), + ); + + await run(); + + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it('works without proxy configuration', async () => { + await run(); + + expect(core.setFailed).not.toHaveBeenCalled(); + }); }); });