Skip to content

Commit cece509

Browse files
Make env/--env set environment variables for the Dev Proxy process (#1995)
* Make env/--env set environment variables for the Dev Proxy process. Closes #1953 Values from the config file's `env` object and `--env` are now set as process environment variables right after the config file is loaded, before plugins are created. `--env` values take precedence. They are still used to replace {name} placeholders in API spec files for the MinimalPermissions plugins. Adds `env` to rc.schema.json and rejects `--env` entries with an empty name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 846278cd-1ffb-4a4b-a0df-57d766209c4b * Fix environment override lifecycle and detached token suppression Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Keep management paths stable across environment overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Honor instance token suppression in status output Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Resolve startup paths and apply environment before HTTP requests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Warn when proxy environment changes require a process restart Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Refresh color policy after environment changes Recompute the shared NO_COLOR/TERM policy instead of retaining the first host's values. Cover config override additions, changes, removals, inherited defaults, and CLI precedence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 846278cd-1ffb-4a4b-a0df-57d766209c4b
1 parent 25fcff3 commit cece509

18 files changed

Lines changed: 905 additions & 49 deletions

‎DevProxy.Integration.Tests/ProcessSmokeTests.cs‎

Lines changed: 335 additions & 2 deletions
Large diffs are not rendered by default.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.State;
6+
using System.Text.Json;
7+
using Xunit;
8+
9+
namespace DevProxy.Tests;
10+
11+
public class DetachedTokenPolicyTests
12+
{
13+
[Theory]
14+
[InlineData(true)]
15+
[InlineData(false)]
16+
public void ShouldDisplayTokenFor_HonorsPersistedChildPolicy(bool suppressTokenOutput)
17+
{
18+
var state = new ProxyInstanceState { SuppressTokenOutput = suppressTokenOutput };
19+
var json = JsonSerializer.Serialize(state);
20+
var persisted = JsonSerializer.Deserialize<ProxyInstanceState>(json);
21+
22+
Assert.NotNull(persisted);
23+
Assert.Equal(suppressTokenOutput, persisted.SuppressTokenOutput);
24+
Assert.Equal(ApiSecurity.ShouldDisplayToken && !suppressTokenOutput,
25+
ApiSecurity.ShouldDisplayTokenFor(persisted));
26+
}
27+
28+
[Fact]
29+
public void ShouldDisplayTokenFor_LegacyState_PreservesParentPolicy()
30+
{
31+
var state = JsonSerializer.Deserialize<ProxyInstanceState>("{}");
32+
33+
Assert.NotNull(state);
34+
Assert.Equal(ApiSecurity.ShouldDisplayToken, ApiSecurity.ShouldDisplayTokenFor(state));
35+
}
36+
}

‎DevProxy.Tests/DevProxyCommandNoColorTests.cs‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,85 @@
33
// See the LICENSE file in the project root for more information.
44

55
using DevProxy.Commands;
6+
using DevProxy.Proxy;
7+
using Microsoft.Extensions.Configuration;
68

79
using Xunit;
810

911
namespace DevProxy.Tests;
1012

13+
[Collection("management-environment")]
1114
public class DevProxyCommandNoColorTests
1215
{
16+
[Theory]
17+
[InlineData("NO_COLOR", "1", "", false)]
18+
[InlineData("NO_COLOR", "1", "", true)]
19+
[InlineData("TERM", "dumb", "xterm-256color", false)]
20+
[InlineData("TERM", "dumb", "xterm-256color", true)]
21+
public void NoColor_AddedChangedAndRemovedEnvironmentOverride_UsesCurrentValues(
22+
string name, string disablingValue, string enablingValue, bool inheritedDisablesColor)
23+
{
24+
var configuration = new ConfigurationBuilder().Build();
25+
using var inherited = new ProcessEnvironment();
26+
inherited.Apply(configuration, new Dictionary<string, string>
27+
{
28+
["NO_COLOR"] = "",
29+
["TERM"] = "xterm-256color",
30+
});
31+
inherited.Apply(configuration, new Dictionary<string, string>
32+
{
33+
[name] = inheritedDisablesColor ? disablingValue : enablingValue,
34+
});
35+
Assert.Equal(inheritedDisablesColor, DevProxyCommand.NoColor);
36+
37+
foreach (var value in new[] { disablingValue, enablingValue, disablingValue, null })
38+
{
39+
var values = new Dictionary<string, string?>();
40+
if (value is not null)
41+
{
42+
values[$"env:{name}"] = value;
43+
}
44+
var config = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
45+
using var environment = new ProcessEnvironment();
46+
environment.Apply(config, null);
47+
Assert.Equal(value is null ? inheritedDisablesColor : value == disablingValue, DevProxyCommand.NoColor);
48+
}
49+
50+
Assert.Equal(inheritedDisablesColor, DevProxyCommand.NoColor);
51+
}
52+
53+
[Theory]
54+
[InlineData("NO_COLOR", "", "1", true)]
55+
[InlineData("TERM", "dumb", "xterm-256color", false)]
56+
public void NoColor_ConfigAndCommandLineOverrides_CommandLineWinsAcrossRuns(
57+
string name, string configValue, string commandLineValue, bool expected)
58+
{
59+
using var inherited = new ProcessEnvironment();
60+
inherited.Apply(new ConfigurationBuilder().Build(), new Dictionary<string, string>
61+
{
62+
["NO_COLOR"] = "",
63+
["TERM"] = "xterm-256color",
64+
});
65+
foreach (var value in new[] { configValue, commandLineValue })
66+
{
67+
var configuration = new ConfigurationBuilder()
68+
.AddInMemoryCollection(new Dictionary<string, string?> { [$"env:{name}"] = value })
69+
.Build();
70+
using var environment = new ProcessEnvironment();
71+
environment.Apply(configuration, new Dictionary<string, string> { [name] = commandLineValue });
72+
Assert.Equal(expected, DevProxyCommand.NoColor);
73+
}
74+
}
75+
76+
[Theory]
77+
[InlineData(null, null)]
78+
[InlineData("", "xterm-256color")]
79+
[InlineData("1", "dumb")]
80+
public void ShouldDisableColor_ExplicitOption_WinsOverEnvironmentValues(string? noColor, string? term)
81+
{
82+
Assert.True(DevProxyCommand.ShouldDisableColor(["--no-color"], noColor, term));
83+
}
84+
1385
[Theory]
1486
[InlineData(null, false)]
1587
[InlineData("", false)]

‎DevProxy.Tests/EnvOptionTests.cs‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Commands;
6+
using DevProxy.Proxy;
7+
using Microsoft.Extensions.Configuration;
8+
9+
using Xunit;
10+
11+
namespace DevProxy.Tests;
12+
13+
public class EnvOptionTests
14+
{
15+
[Fact]
16+
public void Env_ParsesCommandLineValues()
17+
{
18+
var options = new DevProxyConfigOptions();
19+
options.ParseOptions(["--env", "FOO=bar", "-e", "URL=https://x?a=b"]);
20+
21+
var env = options.Env;
22+
23+
Assert.NotNull(env);
24+
Assert.Equal("bar", env["FOO"]);
25+
Assert.Equal("https://x?a=b", env["URL"]);
26+
}
27+
28+
[Fact]
29+
public void Env_NotSpecified_ReturnsNull()
30+
{
31+
var options = new DevProxyConfigOptions();
32+
options.ParseOptions(["--port", "0"]);
33+
34+
Assert.Null(options.Env);
35+
}
36+
37+
[Fact]
38+
public void Apply_SetsConfigAndCommandLineValues_CommandLineWins()
39+
{
40+
var prefix = $"DEVPROXY_TEST_{Guid.NewGuid():N}_";
41+
var fromConfig = prefix + "CONFIG";
42+
var overridden = prefix + "OVERRIDDEN";
43+
var fromCli = prefix + "CLI";
44+
45+
var configuration = new ConfigurationBuilder()
46+
.AddInMemoryCollection(new Dictionary<string, string?>
47+
{
48+
[$"env:{fromConfig}"] = "config",
49+
[$"env:{overridden}"] = "config",
50+
})
51+
.Build();
52+
53+
try
54+
{
55+
using var environment = new ProcessEnvironment();
56+
environment.Apply(configuration, new Dictionary<string, string>
57+
{
58+
[overridden] = "cli",
59+
[fromCli] = "cli",
60+
});
61+
62+
Assert.Equal("config", Environment.GetEnvironmentVariable(fromConfig));
63+
Assert.Equal("cli", Environment.GetEnvironmentVariable(overridden));
64+
Assert.Equal("cli", Environment.GetEnvironmentVariable(fromCli));
65+
}
66+
finally
67+
{
68+
Environment.SetEnvironmentVariable(fromConfig, null);
69+
Environment.SetEnvironmentVariable(overridden, null);
70+
Environment.SetEnvironmentVariable(fromCli, null);
71+
}
72+
}
73+
74+
[Theory]
75+
[InlineData(null)]
76+
[InlineData("inherited")]
77+
public void Dispose_BeforeReload_RestoresRemovedOverridesAndPreservesCommandLineValues(string? inherited)
78+
{
79+
var prefix = $"DEVPROXY_TEST_{Guid.NewGuid():N}_";
80+
var removed = prefix + "REMOVED";
81+
var overridden = prefix + "OVERRIDDEN";
82+
var cli = new Dictionary<string, string> { [overridden] = "cli" };
83+
var configuration = new ConfigurationBuilder()
84+
.AddInMemoryCollection(new Dictionary<string, string?>
85+
{
86+
[$"env:{removed}"] = "config",
87+
[$"env:{overridden}"] = "config",
88+
})
89+
.Build();
90+
91+
Environment.SetEnvironmentVariable(removed, inherited);
92+
try
93+
{
94+
using (var environment = new ProcessEnvironment())
95+
{
96+
environment.Apply(configuration, cli);
97+
Assert.Equal("config", Environment.GetEnvironmentVariable(removed));
98+
Assert.Equal("cli", Environment.GetEnvironmentVariable(overridden));
99+
}
100+
101+
var reloadedConfiguration = new ConfigurationBuilder().Build();
102+
using (var environment = new ProcessEnvironment())
103+
{
104+
environment.Apply(reloadedConfiguration, cli);
105+
Assert.Equal(inherited, Environment.GetEnvironmentVariable(removed));
106+
Assert.Equal("cli", Environment.GetEnvironmentVariable(overridden));
107+
}
108+
109+
Assert.Equal(inherited, Environment.GetEnvironmentVariable(removed));
110+
Assert.Null(Environment.GetEnvironmentVariable(overridden));
111+
}
112+
finally
113+
{
114+
Environment.SetEnvironmentVariable(removed, null);
115+
Environment.SetEnvironmentVariable(overridden, null);
116+
}
117+
}
118+
119+
[Fact]
120+
public void Dispose_WhenApplyingOverridesFails_RestoresEarlierOverrides()
121+
{
122+
var name = $"DEVPROXY_TEST_{Guid.NewGuid():N}";
123+
var configuration = new ConfigurationBuilder().Build();
124+
125+
Assert.Throws<ArgumentException>(() =>
126+
{
127+
using var environment = new ProcessEnvironment();
128+
environment.Apply(configuration, new Dictionary<string, string>
129+
{
130+
[name] = "config",
131+
["invalid=name"] = "invalid",
132+
});
133+
});
134+
135+
Assert.Null(Environment.GetEnvironmentVariable(name));
136+
}
137+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Proxy;
6+
using Microsoft.Extensions.Configuration;
7+
using Xunit;
8+
9+
namespace DevProxy.Tests;
10+
11+
[Collection("management-environment")]
12+
public class HttpProxyEnvironmentTests
13+
{
14+
[Theory]
15+
[InlineData("HTTP_PROXY")]
16+
[InlineData("HTTPS_PROXY")]
17+
[InlineData("ALL_PROXY")]
18+
[InlineData("NO_PROXY")]
19+
[InlineData("http_proxy")]
20+
[InlineData("https_proxy")]
21+
[InlineData("all_proxy")]
22+
[InlineData("no_proxy")]
23+
public void GetChangedVariables_ChangedOrRemovedOverride_ReportsNameUntilInitialValueRestored(string name)
24+
{
25+
var configuration = new ConfigurationBuilder().Build();
26+
using var environment = new ProcessEnvironment();
27+
environment.Apply(configuration, new Dictionary<string, string> { [name] = "initial" });
28+
var monitor = new HttpProxyEnvironment();
29+
Assert.Empty(monitor.GetChangedVariables());
30+
31+
Environment.SetEnvironmentVariable(name, "changed");
32+
var expectedName = OperatingSystem.IsWindows() ? name.ToUpperInvariant() : name;
33+
Assert.Equal([expectedName], monitor.GetChangedVariables());
34+
Assert.Equal([expectedName], monitor.GetChangedVariables());
35+
36+
Environment.SetEnvironmentVariable(name, null);
37+
Assert.Equal([expectedName], monitor.GetChangedVariables());
38+
39+
Environment.SetEnvironmentVariable(name, "initial");
40+
Assert.Empty(monitor.GetChangedVariables());
41+
}
42+
43+
[Fact]
44+
public void GetChangedVariables_ConfigChangesShadowedByCommandLine_DoesNotReportChange()
45+
{
46+
var monitor = new HttpProxyEnvironment();
47+
var cli = new Dictionary<string, string> { ["HTTPS_PROXY"] = "cli" };
48+
foreach (var value in new[] { "config", "changed" })
49+
{
50+
var configuration = new ConfigurationBuilder()
51+
.AddInMemoryCollection(new Dictionary<string, string?> { ["env:HTTPS_PROXY"] = value })
52+
.Build();
53+
using var environment = new ProcessEnvironment();
54+
environment.Apply(configuration, cli);
55+
Assert.Empty(monitor.GetChangedVariables());
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)