Skip to content

Commit 8c1637b

Browse files
authored
Merge branch 'main' into dependabot/github_actions/github/codeql-action/analyze-4.38.0
2 parents aa3e214 + 6e1fbca commit 8c1637b

239 files changed

Lines changed: 18656 additions & 4232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/copilot-instructions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Dev Proxy is a cross-platform .NET API simulator and proxy for testing how appli
66

77
## Critical (Non-Obvious)
88

9-
- **Proxy engine:** Uses [svrooij/unobtanium-web-proxy](https://github.com/svrooij/unobtanium-web-proxy) (fork of Titanium.Web.Proxy)
9+
- **Proxy engine:** Built on [ASP.NET Core Kestrel](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel) (a custom forward-proxy + MITM engine; see `DevProxy.Proxy.Kestrel`)
1010
- **Config tokens:** Paths in config files can use `~appFolder` for resolution
1111
- **MCP Server:** Use [@devproxy/mcp](https://www.npmjs.com/package/@devproxy/mcp) to programmatically retrieve up-to-date docs and JSON schemas
1212

‎.github/policies/dev-proxy-branch-protection.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ configuration:
3838
requiredStatusChecks:
3939
- CodeQL
4040
# Require branches to be up to date before merging. boolean
41-
requiresStrictStatusChecks: true
41+
requiresStrictStatusChecks: false
4242
# Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush.
4343
restrictsPushes: false
4444
# Restrict who can dismiss pull request reviews. boolean
45-
restrictsReviewDismissals: false
45+
restrictsReviewDismissals: false

‎.github/workflows/create-release.yml‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ jobs:
3535
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
3636
with:
3737
dotnet-version: 10.0.x
38-
- name: Rename executable for beta
39-
if: contains(github.ref_name, '-beta')
38+
- name: Rename executable for prerelease
39+
if: contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha')
4040
run: |
4141
pushd
4242
cd ./DevProxy
4343
$content = Get-Content DevProxy.csproj
4444
$content -replace '<AssemblyName>devproxy</AssemblyName>', '<AssemblyName>devproxy-beta</AssemblyName>' | Set-Content DevProxy.csproj
4545
popd
46-
- name: Set newVersionNotification for beta
47-
if: contains(github.ref_name, '-beta')
46+
- name: Set newVersionNotification for prerelease
47+
if: contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha')
4848
run: |
4949
pushd
5050
cd ./DevProxy
@@ -138,21 +138,21 @@ jobs:
138138
- name: Add installer icon
139139
if: contains(matrix.architecture, 'win-')
140140
run: |
141-
if ('${{ github.ref_name }}'.Contains('beta')) {
141+
if ('${{ github.ref_name }}'.Contains('beta') -or '${{ github.ref_name }}'.Contains('alpha')) {
142142
cp ./media/icon-beta.ico ./${{ env.release }}
143143
} else {
144144
cp ./media/icon.ico ./${{ env.release }}
145145
}
146-
- name: Update version in beta installer
147-
if: contains(matrix.architecture, 'win-') && contains(github.ref_name, '-beta')
146+
- name: Update version in prerelease installer
147+
if: contains(matrix.architecture, 'win-') && (contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha'))
148148
run: |
149149
$content = Get-Content ./install-beta.iss
150150
$content -replace '#define MyAppVersion .*', "#define MyAppVersion `"$("${{ github.ref_name }}".Substring(1))`"" | Set-Content ./install-beta.iss
151151
- name: Set installer file name
152152
id: installer
153153
if: contains(matrix.architecture, 'win-')
154154
run: |
155-
if ('${{ github.ref_name }}'.Contains('beta')) {
155+
if ('${{ github.ref_name }}'.Contains('beta') -or '${{ github.ref_name }}'.Contains('alpha')) {
156156
Write-Output "filename=install-beta.iss" >> $env:GITHUB_OUTPUT
157157
} else {
158158
Write-Output "filename=install.iss" >> $env:GITHUB_OUTPUT
@@ -247,7 +247,7 @@ jobs:
247247
env:
248248
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
249249
DRAFT_RELEASE: "false"
250-
PRE_RELEASE: "false"
250+
PRE_RELEASE: "${{ contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}"
251251
CHANGELOG_FILE: "CHANGELOG.md"
252252
ALLOW_EMPTY_CHANGELOG: "true"
253253
with:
@@ -289,14 +289,15 @@ jobs:
289289
latest=false
290290
tags: |
291291
type=semver,pattern={{version}}
292-
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-beta') }}
292+
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-beta') && !contains(github.ref_name, '-alpha') }}
293293
type=raw,value=beta,enable=${{ contains(github.ref_name, '-beta') }}
294+
type=raw,value=alpha,enable=${{ contains(github.ref_name, '-alpha') }}
294295
labels: |
295296
org.opencontainers.image.description=${{ env.APP_DESCRIPTION }}
296297
annotations: |
297298
org.opencontainers.image.description=${{ env.APP_DESCRIPTION }}
298299
- name: Build and push Docker image
299-
if: "!contains(github.ref_name, '-beta')"
300+
if: ${{ !contains(github.ref_name, '-beta') && !contains(github.ref_name, '-alpha') }}
300301
id: push
301302
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
302303
with:
@@ -308,8 +309,8 @@ jobs:
308309
platforms: linux/amd64,linux/arm64
309310
build-args: |
310311
DEVPROXY_VERSION=${{ steps.meta.outputs.version }}
311-
- name: Build and push beta Docker image
312-
if: contains(github.ref_name, '-beta')
312+
- name: Build and push prerelease Docker image
313+
if: contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha')
313314
id: push_beta
314315
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
315316
with:

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ Typos are embarrassing! Most PRs that fix typos will be accepted immediately. To
2828

2929
## Our foundation
3030

31-
The Dev Proxy is built with .NET 9 and uses the [Unobtanium Web Proxy](https://github.com/svrooij/unobtanium-web-proxy).
31+
The Dev Proxy is built with .NET and uses [ASP.NET Core Kestrel](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel) for its forward-proxy engine.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<IsTestProject>true</IsTestProject>
9+
<!-- Underscored Method_Scenario_Expectation test names are idiomatic; inline literal
10+
arrays in assertions are fine in tests. -->
11+
<NoWarn>$(NoWarn);CA1707;CA1861</NoWarn>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
16+
<PackageReference Include="xunit" Version="2.9.2" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\DevProxy.Abstractions\DevProxy.Abstractions.csproj" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.Abstractions.Proxy.Http;
6+
using Xunit;
7+
8+
namespace DevProxy.Abstractions.Tests.Proxy.Http;
9+
10+
public class ForwardingInvariantsTests
11+
{
12+
[Theory]
13+
[InlineData("Connection")]
14+
[InlineData("proxy-connection")]
15+
[InlineData("KEEP-ALIVE")]
16+
[InlineData("Transfer-Encoding")]
17+
[InlineData("te")]
18+
[InlineData("Trailer")]
19+
[InlineData("Upgrade")]
20+
[InlineData("Proxy-Authenticate")]
21+
[InlineData("Proxy-Authorization")]
22+
public void HopByHopHeaders_ContainsExpected_CaseInsensitive(string name) =>
23+
Assert.Contains(name, (IReadOnlySet<string>)ForwardingInvariants.HopByHopHeaders);
24+
25+
[Theory]
26+
[InlineData("Content-Type")]
27+
[InlineData("Host")]
28+
[InlineData("Set-Cookie")]
29+
[InlineData("Authorization")]
30+
public void HopByHopHeaders_ExcludesEndToEndHeaders(string name) =>
31+
Assert.DoesNotContain(name, (IReadOnlySet<string>)ForwardingInvariants.HopByHopHeaders);
32+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.Abstractions.Proxy.Http;
6+
using Xunit;
7+
8+
namespace DevProxy.Abstractions.Tests.Proxy.Http;
9+
10+
public class HeaderCollectionTests
11+
{
12+
[Fact]
13+
public void Add_PreservesWireOrderAndDuplicates()
14+
{
15+
var headers = new HeaderCollection();
16+
headers.Add("Set-Cookie", "a=1");
17+
headers.Add("Set-Cookie", "b=2");
18+
19+
var all = headers.GetAll("set-cookie").ToList();
20+
21+
Assert.Equal(2, all.Count);
22+
Assert.Equal("a=1", all[0].Value);
23+
Assert.Equal("b=2", all[1].Value);
24+
}
25+
26+
[Fact]
27+
public void Contains_And_GetFirst_AreCaseInsensitive()
28+
{
29+
var headers = new HeaderCollection();
30+
headers.Add("Content-Type", "application/json");
31+
32+
Assert.True(headers.Contains("content-type"));
33+
Assert.Equal("application/json", headers.GetFirst("CONTENT-TYPE")!.Value);
34+
}
35+
36+
[Fact]
37+
public void GetFirst_ReturnsNull_WhenAbsent()
38+
{
39+
var headers = new HeaderCollection();
40+
Assert.Null(headers.GetFirst("X-Missing"));
41+
Assert.Empty(headers.GetAll("X-Missing"));
42+
}
43+
44+
[Fact]
45+
public void Replace_CollapsesDuplicatesToSingleValue()
46+
{
47+
var headers = new HeaderCollection();
48+
headers.Add("X-Test", "old1");
49+
headers.Add("X-Test", "old2");
50+
51+
headers.Replace("x-test", "new");
52+
53+
var all = headers.GetAll("X-Test").ToList();
54+
Assert.Single(all);
55+
Assert.Equal("new", all[0].Value);
56+
}
57+
58+
[Fact]
59+
public void Replace_AddsWhenAbsent()
60+
{
61+
var headers = new HeaderCollection();
62+
headers.Replace("X-New", "value");
63+
Assert.Equal("value", headers.GetFirst("X-New")!.Value);
64+
}
65+
66+
[Fact]
67+
public void Remove_RemovesAllOccurrences_AndReportsResult()
68+
{
69+
var headers = new HeaderCollection();
70+
headers.Add("X-Dup", "1");
71+
headers.Add("X-Dup", "2");
72+
73+
Assert.True(headers.Remove("x-dup"));
74+
Assert.Equal(0, headers.Count);
75+
Assert.False(headers.Remove("x-dup"));
76+
}
77+
78+
[Fact]
79+
public void AddRange_And_Linq_Work()
80+
{
81+
var headers = new HeaderCollection(new[]
82+
{
83+
new HttpHeader("A", "1"),
84+
new HttpHeader("B", "2"),
85+
});
86+
headers.AddRange(new[] { new HttpHeader("C", "3") });
87+
88+
Assert.Equal(3, headers.Count);
89+
Assert.Contains(headers, h => h.Name == "C");
90+
Assert.Equal("1", headers.First(h => h.Name == "A").Value);
91+
}
92+
93+
[Fact]
94+
public void SeedingConstructor_PreservesOrder()
95+
{
96+
var headers = new HeaderCollection(new[]
97+
{
98+
new HttpHeader("First", "1"),
99+
new HttpHeader("Second", "2"),
100+
});
101+
102+
var names = headers.Select(h => h.Name).ToList();
103+
Assert.Equal(new[] { "First", "Second" }, names);
104+
}
105+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.Abstractions.Proxy;
6+
using Xunit;
7+
8+
namespace DevProxy.Abstractions.Tests.Proxy;
9+
10+
public class RootTrustPolicyTests
11+
{
12+
// ── installCert gate ────────────────────────────────────────────────
13+
[Theory]
14+
[InlineData(true, false)] // mac
15+
[InlineData(false, true)] // windows
16+
[InlineData(false, false)] // linux
17+
public void Decide_InstallCertFalse_AlwaysSkips(bool isMac, bool isWindows)
18+
{
19+
var action = RootTrustPolicy.Decide(
20+
isMac, isWindows, installCert: false, noFirstRun: false, isFirstRun: true, firstRunAnswer: "y");
21+
22+
Assert.Equal(RootTrustAction.Skip, action);
23+
}
24+
25+
// ── Windows ─────────────────────────────────────────────────────────
26+
[Theory]
27+
[InlineData(true, false)] // first run, ignored on Windows
28+
[InlineData(false, true)] // noFirstRun, ignored on Windows
29+
public void Decide_Windows_InstallCert_AlwaysTrustsStore(bool isFirstRun, bool noFirstRun)
30+
{
31+
var action = RootTrustPolicy.Decide(
32+
isMac: false, isWindows: true, installCert: true, noFirstRun: noFirstRun, isFirstRun: isFirstRun, firstRunAnswer: null);
33+
34+
Assert.Equal(RootTrustAction.TrustWindowsStore, action);
35+
}
36+
37+
// ── macOS first-run flow ────────────────────────────────────────────
38+
[Fact]
39+
public void Decide_Mac_FirstRun_EmptyAnswer_Trusts()
40+
{
41+
var action = RootTrustPolicy.Decide(
42+
isMac: true, isWindows: false, installCert: true, noFirstRun: false, isFirstRun: true, firstRunAnswer: "");
43+
44+
Assert.Equal(RootTrustAction.TrustMacKeychain, action);
45+
}
46+
47+
[Theory]
48+
[InlineData("y")]
49+
[InlineData("Y")]
50+
[InlineData("yes")]
51+
[InlineData("anything")]
52+
public void Decide_Mac_FirstRun_NonNoAnswer_Trusts(string answer)
53+
{
54+
var action = RootTrustPolicy.Decide(
55+
isMac: true, isWindows: false, installCert: true, noFirstRun: false, isFirstRun: true, firstRunAnswer: answer);
56+
57+
Assert.Equal(RootTrustAction.TrustMacKeychain, action);
58+
}
59+
60+
[Theory]
61+
[InlineData("n")]
62+
[InlineData("N")]
63+
[InlineData(" n ")]
64+
public void Decide_Mac_FirstRun_AnswerNo_Skips(string answer)
65+
{
66+
var action = RootTrustPolicy.Decide(
67+
isMac: true, isWindows: false, installCert: true, noFirstRun: false, isFirstRun: true, firstRunAnswer: answer);
68+
69+
Assert.Equal(RootTrustAction.Skip, action);
70+
}
71+
72+
[Fact]
73+
public void Decide_Mac_NotFirstRun_Skips()
74+
{
75+
var action = RootTrustPolicy.Decide(
76+
isMac: true, isWindows: false, installCert: true, noFirstRun: false, isFirstRun: false, firstRunAnswer: "y");
77+
78+
Assert.Equal(RootTrustAction.Skip, action);
79+
}
80+
81+
[Fact]
82+
public void Decide_Mac_NoFirstRun_Skips()
83+
{
84+
var action = RootTrustPolicy.Decide(
85+
isMac: true, isWindows: false, installCert: true, noFirstRun: true, isFirstRun: true, firstRunAnswer: "y");
86+
87+
Assert.Equal(RootTrustAction.Skip, action);
88+
}
89+
90+
// ── Linux ───────────────────────────────────────────────────────────
91+
[Fact]
92+
public void Decide_Linux_InstallCert_IsManual()
93+
{
94+
var action = RootTrustPolicy.Decide(
95+
isMac: false, isWindows: false, installCert: true, noFirstRun: false, isFirstRun: true, firstRunAnswer: "y");
96+
97+
Assert.Equal(RootTrustAction.ManualLinux, action);
98+
}
99+
}

0 commit comments

Comments
 (0)