diff --git a/DevProxy.Tests/ProxyConfigurationCiDefaultsTests.cs b/DevProxy.Tests/ProxyConfigurationCiDefaultsTests.cs new file mode 100644 index 00000000..70baded2 --- /dev/null +++ b/DevProxy.Tests/ProxyConfigurationCiDefaultsTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using DevProxy.Commands; +using DevProxy.Proxy; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace DevProxy.Tests; + +public class ProxyConfigurationCiDefaultsTests +{ + private static ProxyConfiguration Create(bool isCi, Dictionary? settings = null) + { + var configurationRoot = new ConfigurationBuilder() + .AddInMemoryCollection(settings ?? []) + .Build(); + return new ProxyConfiguration(configurationRoot, isCi); + } + + [Fact] + public void NotCi_KeepsDefaults() + { + var config = Create(isCi: false); + + Assert.True(config.AsSystemProxy); + Assert.True(config.InstallCert); + Assert.False(config.AsSystemProxyDefaultedForCi); + Assert.False(config.InstallCertDefaultedForCi); + } + + [Fact] + public void Ci_NotSetInConfig_DisablesSystemProxyAndCertInstall() + { + var config = Create(isCi: true); + + Assert.False(config.AsSystemProxy); + Assert.False(config.InstallCert); + Assert.True(config.AsSystemProxyDefaultedForCi); + Assert.True(config.InstallCertDefaultedForCi); + } + + [Theory] + [InlineData("true", true)] + [InlineData("false", false)] + public void Ci_SetInConfig_KeepsConfiguredValues(string value, bool expected) + { + var config = Create(isCi: true, new() + { + ["asSystemProxy"] = value, + ["installCert"] = value, + }); + + Assert.Equal(expected, config.AsSystemProxy); + Assert.Equal(expected, config.InstallCert); + Assert.False(config.AsSystemProxyDefaultedForCi); + Assert.False(config.InstallCertDefaultedForCi); + } + + [Fact] + public void Ci_OnlyAsSystemProxyInConfig_DefaultsInstallCertOnly() + { + var config = Create(isCi: true, new() { ["asSystemProxy"] = "true" }); + + Assert.True(config.AsSystemProxy); + Assert.False(config.InstallCert); + Assert.False(config.AsSystemProxyDefaultedForCi); + Assert.True(config.InstallCertDefaultedForCi); + } + + [Fact] + public void ShouldLogCiDefaults_NotCi_ReturnsFalse() + { + Assert.False(DevProxyCommand.ShouldLogCiDefaults(Create(isCi: false), null, null)); + } + + [Fact] + public void ShouldLogCiDefaults_CiDefaultsApplied_ReturnsTrue() + { + Assert.True(DevProxyCommand.ShouldLogCiDefaults(Create(isCi: true), null, null)); + } + + [Fact] + public void ShouldLogCiDefaults_OneOverriddenOnCommandLine_ReturnsTrue() + { + Assert.True(DevProxyCommand.ShouldLogCiDefaults(Create(isCi: true), true, null)); + } + + [Fact] + public void ShouldLogCiDefaults_BothOverriddenOnCommandLine_ReturnsFalse() + { + Assert.False(DevProxyCommand.ShouldLogCiDefaults(Create(isCi: true), true, true)); + } +} diff --git a/DevProxy/Commands/DevProxyCommand.cs b/DevProxy/Commands/DevProxyCommand.cs index 6577c0bc..454517a6 100644 --- a/DevProxy/Commands/DevProxyCommand.cs +++ b/DevProxy/Commands/DevProxyCommand.cs @@ -490,8 +490,8 @@ private void ConfigureCommand(DevProxyConfigOptions configOptions) { try { - var asSystemProxy = input.GetValue(asSystemProxyOption) ?? true; - var installCert = input.GetValue(installCertOption) ?? true; + var asSystemProxy = input.GetValue(asSystemProxyOption) ?? _proxyConfiguration.AsSystemProxy; + var installCert = input.GetValue(installCertOption) ?? _proxyConfiguration.InstallCert; if (asSystemProxy && !installCert) { input.AddError($"Requires option '{AsSystemProxyOptionName}' to be 'false'"); @@ -746,6 +746,7 @@ private void ConfigureFromOptions(ParseResult parseResult) { _proxyConfiguration.InstallCert = installCert.Value; } + LogCiDefaults(asSystemProxy, installCert); var timeout = parseResult.GetValueOrDefault(TimeoutOptionName); if (timeout is not null) { @@ -771,6 +772,26 @@ private void ConfigureFromOptions(ParseResult parseResult) } } + private void LogCiDefaults(bool? asSystemProxyOption, bool? installCertOption) + { + if (_proxyConfiguration is not ProxyConfiguration proxyConfiguration || + !ShouldLogCiDefaults(proxyConfiguration, asSystemProxyOption, installCertOption)) + { + return; + } + + _logger.LogInformation( + "CI environment detected. Using asSystemProxy: {AsSystemProxy}, installCert: {InstallCert}. To change, use {AsSystemProxyOption} and {InstallCertOption} or set asSystemProxy and installCert in the config file.", + proxyConfiguration.AsSystemProxy.ToString().ToLowerInvariant(), + proxyConfiguration.InstallCert.ToString().ToLowerInvariant(), + AsSystemProxyOptionName, + InstallCertOptionName); + } + + internal static bool ShouldLogCiDefaults(ProxyConfiguration proxyConfiguration, bool? asSystemProxyOption, bool? installCertOption) => + (proxyConfiguration.AsSystemProxyDefaultedForCi && asSystemProxyOption is null) || + (proxyConfiguration.InstallCertDefaultedForCi && installCertOption is null); + private async Task CheckForNewVersionAsync() { var newReleaseInfo = await _updateNotification.CheckForNewVersionAsync(_proxyConfiguration.NewVersionNotification); diff --git a/DevProxy/Proxy/ProxyConfiguration.cs b/DevProxy/Proxy/ProxyConfiguration.cs index 27b65c73..2f53f310 100755 --- a/DevProxy/Proxy/ProxyConfiguration.cs +++ b/DevProxy/Proxy/ProxyConfiguration.cs @@ -53,10 +53,41 @@ public string ConfigFile public IEnumerable WatchProcessNames { get; set; } = []; public bool WatchProcessTree { get; set; } - public ProxyConfiguration(IConfigurationRoot configurationRoot) + // True when the value wasn't set in the config file and was turned off + // because Dev Proxy is running in CI. + internal bool AsSystemProxyDefaultedForCi { get; private set; } + internal bool InstallCertDefaultedForCi { get; private set; } + + internal static bool IsCi => Environment.GetEnvironmentVariable("CI") is not null; + + public ProxyConfiguration(IConfigurationRoot configurationRoot) : this(configurationRoot, IsCi) + { + } + + internal ProxyConfiguration(IConfigurationRoot configurationRoot, bool isCi) { _configurationRoot = configurationRoot; _configurationRoot.Bind(this); + if (isCi) + { + ApplyCiDefaults(_configurationRoot); + } + } + + // In CI, don't change machine-wide state (system proxy, trusted root + // certificates) unless the config file explicitly asks for it. + private void ApplyCiDefaults(IConfiguration configuration) + { + if (configuration[nameof(AsSystemProxy)] is null) + { + AsSystemProxy = false; + AsSystemProxyDefaultedForCi = true; + } + if (configuration[nameof(InstallCert)] is null) + { + InstallCert = false; + InstallCertDefaultedForCi = true; + } } internal static string GetConfigFilePath(IConfigurationRoot configurationRoot) diff --git a/skills/dev-proxy/references/ci-cd-integration.md b/skills/dev-proxy/references/ci-cd-integration.md index f3a98d22..b0c01159 100644 --- a/skills/dev-proxy/references/ci-cd-integration.md +++ b/skills/dev-proxy/references/ci-cd-integration.md @@ -4,6 +4,8 @@ Dev Proxy runs headlessly in CI/CD pipelines to automate API testing — catchin With Dev Proxy 3.3.1 or later, every management API request requires a bearer token. Set `CI=true` in the proxy process to suppress automatic token output, and retrieve credentials explicitly with `devproxy api token`. Keep shell tracing disabled around credentials. For multiple instances, add `--pid ` and use the actual API URL from `devproxy status`. +When `CI` is set, Dev Proxy doesn't register itself as the system proxy or install its root certificate, unless you set `asSystemProxy` or `installCert` in the config file or pass `--as-system-proxy` or `--install-cert`. Point your app at Dev Proxy with `HTTPS_PROXY`/`HTTP_PROXY` and trust the root certificate as shown below. + Use action versions that support the authenticated API. For integrations that have not yet added bearer authentication, use the CLI and authenticated HTTP examples below. ## GitHub Actions (Recommended) diff --git a/skills/dev-proxy/references/configuration.md b/skills/dev-proxy/references/configuration.md index 7cbd2222..75aabdb2 100644 --- a/skills/dev-proxy/references/configuration.md +++ b/skills/dev-proxy/references/configuration.md @@ -136,7 +136,8 @@ Plugin-specific override: | `watchPids` | — | `--watch-pids` | Only intercept from these PIDs | | `watchProcessNames` | — | `--watch-process-names` | Only intercept from these processes | | `watchProcessTree` | — | `--watch-process-tree` | Also intercept descendants of watched processes | -| `asSystemProxy` | `true` | `--as-system-proxy` | Register as system proxy | +| `asSystemProxy` | `true` (`false` when `CI` is set) | `--as-system-proxy` | Register as system proxy | +| `installCert` | `true` (`false` when `CI` is set) | `--install-cert` | Install and trust the root certificate | | — | off | `--detach` | Run in detached (background) mode | | — | `text` | `--output` | Output format: `text` or `json` |