Skip to content

Commit 559b111

Browse files
authored
chore: rework RPC version to match new header spec (#8)
1 parent 08487ac commit 559b111

File tree

8 files changed

+337
-157
lines changed

8 files changed

+337
-157
lines changed

Tests/Vpn.Proto/ApiVersionTest.cs

-36
This file was deleted.

Tests/Vpn.Proto/RpcHeaderTest.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ public class RpcHeaderTest
99
[Test(Description = "Parse and use some valid header strings")]
1010
public void Valid()
1111
{
12-
var headerStr = "codervpn 2.1 manager";
12+
var headerStr = "codervpn manager 1.3,2.1";
1313
var header = RpcHeader.Parse(headerStr);
1414
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Manager));
15-
Assert.That(header.Version, Is.EqualTo(new ApiVersion(2, 1)));
15+
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 1))));
1616
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
1717
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));
1818

19-
headerStr = "codervpn 1.0 tunnel";
19+
headerStr = "codervpn tunnel 1.0";
2020
header = RpcHeader.Parse(headerStr);
2121
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Tunnel));
22-
Assert.That(header.Version, Is.EqualTo(new ApiVersion(1, 0)));
22+
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 0))));
2323
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
2424
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));
2525
}
@@ -29,13 +29,13 @@ public void ParseInvalid()
2929
{
3030
var ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn"));
3131
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
32-
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0 manager cats"));
32+
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn manager cats 1.0"));
3333
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
3434
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0"));
3535
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
36-
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("cats 1.0 manager"));
36+
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("cats manager 1.0"));
3737
Assert.That(ex.Message, Does.Contain("Invalid preamble"));
38-
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0 cats"));
38+
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn cats 1.0"));
3939
Assert.That(ex.Message, Does.Contain("Unknown role 'cats'"));
4040
}
4141
}

Tests/Vpn.Proto/RpcVersionTest.cs

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Coder.Desktop.Vpn.Proto;
2+
using NUnit.Framework.Constraints;
3+
4+
namespace Coder.Desktop.Tests.Vpn.Proto;
5+
6+
[TestFixture]
7+
public class RpcVersionTest
8+
{
9+
[Test(Description = "Parse a variety of version strings")]
10+
public void Parse()
11+
{
12+
Assert.That(RpcVersion.Parse("2.1"), Is.EqualTo(new RpcVersion(2, 1)));
13+
Assert.That(RpcVersion.Parse("1.0"), Is.EqualTo(new RpcVersion(1, 0)));
14+
15+
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("cats"));
16+
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("cats.dogs"));
17+
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("1.dogs"));
18+
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("1.0.1"));
19+
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("11"));
20+
}
21+
22+
private void IsCompatibleWithBothWays(RpcVersion a, RpcVersion b, IResolveConstraint c)
23+
{
24+
Assert.That(a.IsCompatibleWith(b), c);
25+
Assert.That(b.IsCompatibleWith(a), c);
26+
}
27+
28+
[Test(Description = "Test that versions are compatible")]
29+
public void IsCompatibleWith()
30+
{
31+
var twoOne = new RpcVersion(2, 1);
32+
Assert.That(twoOne.IsCompatibleWith(twoOne), Is.EqualTo(twoOne));
33+
34+
// 2.1 && 2.2 => 2.1
35+
IsCompatibleWithBothWays(twoOne, new RpcVersion(2, 2), Is.EqualTo(new RpcVersion(2, 1)));
36+
// 2.1 && 2.0 => 2.0
37+
IsCompatibleWithBothWays(twoOne, new RpcVersion(2, 0), Is.EqualTo(new RpcVersion(2, 0)));
38+
// 2.1 && 3.1 => null
39+
IsCompatibleWithBothWays(twoOne, new RpcVersion(3, 1), Is.Null);
40+
// 2.1 && 1.1 => null
41+
IsCompatibleWithBothWays(twoOne, new RpcVersion(1, 1), Is.Null);
42+
}
43+
}
44+
45+
[TestFixture]
46+
public class RpcVersionListTest
47+
{
48+
[Test(Description = "Parse a variety of version list strings")]
49+
public void Parse()
50+
{
51+
Assert.That(RpcVersionList.Parse("1.0"), Is.EqualTo(new RpcVersionList(new RpcVersion(1, 0))));
52+
Assert.That(RpcVersionList.Parse("1.3,2.1"),
53+
Is.EqualTo(new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 1))));
54+
55+
var ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("0.1"));
56+
Assert.That(ex.InnerException, Is.Not.Null);
57+
Assert.That(ex.InnerException.Message, Does.Contain("Invalid major version"));
58+
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse(""));
59+
Assert.That(ex.InnerException, Is.Not.Null);
60+
Assert.That(ex.InnerException.Message, Does.Contain("Invalid version string"));
61+
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("2.1,1.1"));
62+
Assert.That(ex.InnerException, Is.Not.Null);
63+
Assert.That(ex.InnerException.Message, Does.Contain("sorted"));
64+
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("1.1,1.2"));
65+
Assert.That(ex.InnerException, Is.Not.Null);
66+
Assert.That(ex.InnerException.Message, Does.Contain("Duplicate major version"));
67+
}
68+
69+
[Test(Description = "Validate a variety of version lists to test every error")]
70+
public void Validate()
71+
{
72+
Assert.DoesNotThrow(() =>
73+
new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 4), new RpcVersion(3, 0)).Validate());
74+
75+
var ex = Assert.Throws<ArgumentException>(() => new RpcVersionList(new RpcVersion(0, 1)).Validate());
76+
Assert.That(ex.Message, Does.Contain("Invalid major version"));
77+
ex = Assert.Throws<ArgumentException>(() =>
78+
new RpcVersionList(new RpcVersion(2, 1), new RpcVersion(1, 2)).Validate());
79+
Assert.That(ex.Message, Does.Contain("sorted"));
80+
ex = Assert.Throws<ArgumentException>(() =>
81+
new RpcVersionList(new RpcVersion(1, 1), new RpcVersion(1, 2)).Validate());
82+
Assert.That(ex.Message, Does.Contain("Duplicate major version"));
83+
}
84+
85+
private void IsCompatibleWithBothWays(RpcVersionList a, RpcVersionList b, IResolveConstraint c)
86+
{
87+
Assert.That(a.IsCompatibleWith(b), c);
88+
Assert.That(b.IsCompatibleWith(a), c);
89+
}
90+
91+
[Test(Description = "Check a variety of lists against each other to determine compatible version")]
92+
public void IsCompatibleWith()
93+
{
94+
var list1 = RpcVersionList.Parse("1.2,2.4,3.2");
95+
Assert.That(list1.IsCompatibleWith(list1), Is.EqualTo(new RpcVersion(3, 2)));
96+
97+
IsCompatibleWithBothWays(list1, RpcVersionList.Parse("4.1,5.2"), Is.Null);
98+
IsCompatibleWithBothWays(list1, RpcVersionList.Parse("1.2,2.3"), Is.EqualTo(new RpcVersion(2, 3)));
99+
IsCompatibleWithBothWays(list1, RpcVersionList.Parse("2.3,3.3"), Is.EqualTo(new RpcVersion(3, 2)));
100+
}
101+
}

Tests/Vpn/SpeakerTest.cs

+40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers;
22
using System.IO.Pipelines;
33
using System.Reflection;
4+
using System.Text;
45
using System.Threading.Channels;
56
using Coder.Desktop.Vpn;
67
using Coder.Desktop.Vpn.Proto;
@@ -277,6 +278,45 @@ public async Task ReadLargeHeader()
277278
Assert.That(gotEx.Message, Does.Contain("Header malformed or too large"));
278279
}
279280

281+
[Test(Description = "Receive an invalid header")]
282+
[Timeout(30_000)]
283+
public async Task ReceiveInvalidHeader()
284+
{
285+
var cases = new Dictionary<string, (string, string?)>
286+
{
287+
{ "invalid\n", ("Failed to parse peer header", "Wrong number of parts in header string") },
288+
{ "cats tunnel 1.0\n", ("Failed to parse peer header", "Invalid preamble in header string") },
289+
{ "codervpn cats 1.0\n", ("Failed to parse peer header", "Unknown role 'cats'") },
290+
{ "codervpn manager 1.0\n", ("Expected peer role 'tunnel' but got 'manager'", null) },
291+
{
292+
"codervpn tunnel 1000.1\n",
293+
($"No RPC versions are compatible: local={RpcVersionList.Current}, remote=1000.1", null)
294+
},
295+
{ "codervpn tunnel 0.1\n", ("Failed to parse peer header", "Invalid version list '0.1'") },
296+
{ "codervpn tunnel 1.0,1.2\n", ("Failed to parse peer header", "Invalid version list '1.0,1.2'") },
297+
{ "codervpn tunnel 2.0,3.1,1.2\n", ("Failed to parse peer header", "Invalid version list '2.0,3.1,1.2'") },
298+
};
299+
300+
foreach (var (header, (expectedOuter, expectedInner)) in cases)
301+
{
302+
var (stream1, stream2) = BidirectionalPipe.New();
303+
await using var speaker1 = new Speaker<ManagerMessage, TunnelMessage>(stream1);
304+
305+
await stream2.WriteAsync(Encoding.UTF8.GetBytes(header));
306+
307+
var gotEx = Assert.CatchAsync(() => speaker1.StartAsync(), $"header: '{header}'");
308+
Assert.That(gotEx.Message, Does.Contain(expectedOuter), $"header: '{header}'");
309+
if (expectedInner is null)
310+
{
311+
Assert.That(gotEx.InnerException, Is.Null, $"header: '{header}'");
312+
continue;
313+
}
314+
315+
Assert.That(gotEx.InnerException, Is.Not.Null, $"header: '{header}'");
316+
Assert.That(gotEx.InnerException!.Message, Does.Contain(expectedInner), $"header: '{header}'");
317+
}
318+
}
319+
280320
[Test(Description = "Encounter a write error during message send")]
281321
[Timeout(30_000)]
282322
public async Task SendMessageWriteError()

Vpn.Proto/ApiVersion.cs

-103
This file was deleted.

Vpn.Proto/RpcHeader.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ namespace Coder.Desktop.Vpn.Proto;
66
/// A header to write or read from a stream to identify the speaker's role and version.
77
/// </summary>
88
/// <param name="role">Role of the speaker</param>
9-
/// <param name="version">Version of the speaker</param>
10-
public class RpcHeader(RpcRole role, ApiVersion version)
9+
/// <param name="versionList">Version of the speaker</param>
10+
public class RpcHeader(RpcRole role, RpcVersionList versionList)
1111
{
1212
private const string Preamble = "codervpn";
1313

1414
public RpcRole Role { get; } = role;
15-
public ApiVersion Version { get; } = version;
15+
public RpcVersionList VersionList { get; } = versionList;
1616

1717
/// <summary>
1818
/// Parse a header string into a <c>SpeakerHeader</c>.
@@ -26,17 +26,17 @@ public static RpcHeader Parse(string header)
2626
if (parts.Length != 3) throw new ArgumentException($"Wrong number of parts in header string '{header}'");
2727
if (parts[0] != Preamble) throw new ArgumentException($"Invalid preamble in header string '{header}'");
2828

29-
var version = ApiVersion.Parse(parts[1]);
30-
var role = new RpcRole(parts[2]);
31-
return new RpcHeader(role, version);
29+
var role = new RpcRole(parts[1]);
30+
var versionList = RpcVersionList.Parse(parts[2]);
31+
return new RpcHeader(role, versionList);
3232
}
3333

3434
/// <summary>
3535
/// Construct a header string from the role and version with a trailing newline.
3636
/// </summary>
3737
public override string ToString()
3838
{
39-
return $"{Preamble} {Version} {Role}\n";
39+
return $"{Preamble} {Role} {VersionList}\n";
4040
}
4141

4242
public ReadOnlyMemory<byte> ToBytes()

0 commit comments

Comments
 (0)