Skip to content

chore: rework RPC version to match new header spec #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 0 additions & 36 deletions Tests/Vpn.Proto/ApiVersionTest.cs

This file was deleted.

14 changes: 7 additions & 7 deletions Tests/Vpn.Proto/RpcHeaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ public class RpcHeaderTest
[Test(Description = "Parse and use some valid header strings")]
public void Valid()
{
var headerStr = "codervpn 2.1 manager";
var headerStr = "codervpn manager 1.3,2.1";
var header = RpcHeader.Parse(headerStr);
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Manager));
Assert.That(header.Version, Is.EqualTo(new ApiVersion(2, 1)));
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 1))));
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));

headerStr = "codervpn 1.0 tunnel";
headerStr = "codervpn tunnel 1.0";
header = RpcHeader.Parse(headerStr);
Assert.That(header.Role.ToString(), Is.EqualTo(RpcRole.Tunnel));
Assert.That(header.Version, Is.EqualTo(new ApiVersion(1, 0)));
Assert.That(header.VersionList, Is.EqualTo(new RpcVersionList(new RpcVersion(1, 0))));
Assert.That(header.ToString(), Is.EqualTo(headerStr + "\n"));
Assert.That(header.ToBytes().ToArray(), Is.EqualTo(Encoding.UTF8.GetBytes(headerStr + "\n")));
}
Expand All @@ -29,13 +29,13 @@ public void ParseInvalid()
{
var ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn"));
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0 manager cats"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn manager cats 1.0"));
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0"));
Assert.That(ex.Message, Does.Contain("Wrong number of parts"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("cats 1.0 manager"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("cats manager 1.0"));
Assert.That(ex.Message, Does.Contain("Invalid preamble"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn 1.0 cats"));
ex = Assert.Throws<ArgumentException>(() => RpcHeader.Parse("codervpn cats 1.0"));
Assert.That(ex.Message, Does.Contain("Unknown role 'cats'"));
}
}
101 changes: 101 additions & 0 deletions Tests/Vpn.Proto/RpcVersionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Coder.Desktop.Vpn.Proto;
using NUnit.Framework.Constraints;

namespace Coder.Desktop.Tests.Vpn.Proto;

[TestFixture]
public class RpcVersionTest
{
[Test(Description = "Parse a variety of version strings")]
public void Parse()
{
Assert.That(RpcVersion.Parse("2.1"), Is.EqualTo(new RpcVersion(2, 1)));
Assert.That(RpcVersion.Parse("1.0"), Is.EqualTo(new RpcVersion(1, 0)));

Assert.Throws<ArgumentException>(() => RpcVersion.Parse("cats"));
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("cats.dogs"));
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("1.dogs"));
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("1.0.1"));
Assert.Throws<ArgumentException>(() => RpcVersion.Parse("11"));
}

private void IsCompatibleWithBothWays(RpcVersion a, RpcVersion b, IResolveConstraint c)
{
Assert.That(a.IsCompatibleWith(b), c);
Assert.That(b.IsCompatibleWith(a), c);
}

[Test(Description = "Test that versions are compatible")]
public void IsCompatibleWith()
{
var twoOne = new RpcVersion(2, 1);
Assert.That(twoOne.IsCompatibleWith(twoOne), Is.EqualTo(twoOne));

// 2.1 && 2.2 => 2.1
IsCompatibleWithBothWays(twoOne, new RpcVersion(2, 2), Is.EqualTo(new RpcVersion(2, 1)));
// 2.1 && 2.0 => 2.0
IsCompatibleWithBothWays(twoOne, new RpcVersion(2, 0), Is.EqualTo(new RpcVersion(2, 0)));
// 2.1 && 3.1 => null
IsCompatibleWithBothWays(twoOne, new RpcVersion(3, 1), Is.Null);
// 2.1 && 1.1 => null
IsCompatibleWithBothWays(twoOne, new RpcVersion(1, 1), Is.Null);
}
}

[TestFixture]
public class RpcVersionListTest
{
[Test(Description = "Parse a variety of version list strings")]
public void Parse()
{
Assert.That(RpcVersionList.Parse("1.0"), Is.EqualTo(new RpcVersionList(new RpcVersion(1, 0))));
Assert.That(RpcVersionList.Parse("1.3,2.1"),
Is.EqualTo(new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 1))));

var ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("0.1"));
Assert.That(ex.InnerException, Is.Not.Null);
Assert.That(ex.InnerException.Message, Does.Contain("Invalid major version"));
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse(""));
Assert.That(ex.InnerException, Is.Not.Null);
Assert.That(ex.InnerException.Message, Does.Contain("Invalid version string"));
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("2.1,1.1"));
Assert.That(ex.InnerException, Is.Not.Null);
Assert.That(ex.InnerException.Message, Does.Contain("sorted"));
ex = Assert.Throws<ArgumentException>(() => RpcVersionList.Parse("1.1,1.2"));
Assert.That(ex.InnerException, Is.Not.Null);
Assert.That(ex.InnerException.Message, Does.Contain("Duplicate major version"));
}

[Test(Description = "Validate a variety of version lists to test every error")]
public void Validate()
{
Assert.DoesNotThrow(() =>
new RpcVersionList(new RpcVersion(1, 3), new RpcVersion(2, 4), new RpcVersion(3, 0)).Validate());

var ex = Assert.Throws<ArgumentException>(() => new RpcVersionList(new RpcVersion(0, 1)).Validate());
Assert.That(ex.Message, Does.Contain("Invalid major version"));
ex = Assert.Throws<ArgumentException>(() =>
new RpcVersionList(new RpcVersion(2, 1), new RpcVersion(1, 2)).Validate());
Assert.That(ex.Message, Does.Contain("sorted"));
ex = Assert.Throws<ArgumentException>(() =>
new RpcVersionList(new RpcVersion(1, 1), new RpcVersion(1, 2)).Validate());
Assert.That(ex.Message, Does.Contain("Duplicate major version"));
}

private void IsCompatibleWithBothWays(RpcVersionList a, RpcVersionList b, IResolveConstraint c)
{
Assert.That(a.IsCompatibleWith(b), c);
Assert.That(b.IsCompatibleWith(a), c);
}

[Test(Description = "Check a variety of lists against each other to determine compatible version")]
public void IsCompatibleWith()
{
var list1 = RpcVersionList.Parse("1.2,2.4,3.2");
Assert.That(list1.IsCompatibleWith(list1), Is.EqualTo(new RpcVersion(3, 2)));

IsCompatibleWithBothWays(list1, RpcVersionList.Parse("4.1,5.2"), Is.Null);
IsCompatibleWithBothWays(list1, RpcVersionList.Parse("1.2,2.3"), Is.EqualTo(new RpcVersion(2, 3)));
IsCompatibleWithBothWays(list1, RpcVersionList.Parse("2.3,3.3"), Is.EqualTo(new RpcVersion(3, 2)));
}
}
40 changes: 40 additions & 0 deletions Tests/Vpn/SpeakerTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Buffers;
using System.IO.Pipelines;
using System.Reflection;
using System.Text;
using System.Threading.Channels;
using Coder.Desktop.Vpn;
using Coder.Desktop.Vpn.Proto;
Expand Down Expand Up @@ -277,6 +278,45 @@ public async Task ReadLargeHeader()
Assert.That(gotEx.Message, Does.Contain("Header malformed or too large"));
}

[Test(Description = "Receive an invalid header")]
[Timeout(30_000)]
public async Task ReceiveInvalidHeader()
{
var cases = new Dictionary<string, (string, string?)>
{
{ "invalid\n", ("Failed to parse peer header", "Wrong number of parts in header string") },
{ "cats tunnel 1.0\n", ("Failed to parse peer header", "Invalid preamble in header string") },
{ "codervpn cats 1.0\n", ("Failed to parse peer header", "Unknown role 'cats'") },
{ "codervpn manager 1.0\n", ("Expected peer role 'tunnel' but got 'manager'", null) },
{
"codervpn tunnel 1000.1\n",
($"No RPC versions are compatible: local={RpcVersionList.Current}, remote=1000.1", null)
},
{ "codervpn tunnel 0.1\n", ("Failed to parse peer header", "Invalid version list '0.1'") },
{ "codervpn tunnel 1.0,1.2\n", ("Failed to parse peer header", "Invalid version list '1.0,1.2'") },
{ "codervpn tunnel 2.0,3.1,1.2\n", ("Failed to parse peer header", "Invalid version list '2.0,3.1,1.2'") },
};

foreach (var (header, (expectedOuter, expectedInner)) in cases)
{
var (stream1, stream2) = BidirectionalPipe.New();
await using var speaker1 = new Speaker<ManagerMessage, TunnelMessage>(stream1);

await stream2.WriteAsync(Encoding.UTF8.GetBytes(header));

var gotEx = Assert.CatchAsync(() => speaker1.StartAsync(), $"header: '{header}'");
Assert.That(gotEx.Message, Does.Contain(expectedOuter), $"header: '{header}'");
if (expectedInner is null)
{
Assert.That(gotEx.InnerException, Is.Null, $"header: '{header}'");
continue;
}

Assert.That(gotEx.InnerException, Is.Not.Null, $"header: '{header}'");
Assert.That(gotEx.InnerException!.Message, Does.Contain(expectedInner), $"header: '{header}'");
}
}

[Test(Description = "Encounter a write error during message send")]
[Timeout(30_000)]
public async Task SendMessageWriteError()
Expand Down
103 changes: 0 additions & 103 deletions Vpn.Proto/ApiVersion.cs

This file was deleted.

14 changes: 7 additions & 7 deletions Vpn.Proto/RpcHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ namespace Coder.Desktop.Vpn.Proto;
/// A header to write or read from a stream to identify the speaker's role and version.
/// </summary>
/// <param name="role">Role of the speaker</param>
/// <param name="version">Version of the speaker</param>
public class RpcHeader(RpcRole role, ApiVersion version)
/// <param name="versionList">Version of the speaker</param>
public class RpcHeader(RpcRole role, RpcVersionList versionList)
{
private const string Preamble = "codervpn";

public RpcRole Role { get; } = role;
public ApiVersion Version { get; } = version;
public RpcVersionList VersionList { get; } = versionList;

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

var version = ApiVersion.Parse(parts[1]);
var role = new RpcRole(parts[2]);
return new RpcHeader(role, version);
var role = new RpcRole(parts[1]);
var versionList = RpcVersionList.Parse(parts[2]);
return new RpcHeader(role, versionList);
}

/// <summary>
/// Construct a header string from the role and version with a trailing newline.
/// </summary>
public override string ToString()
{
return $"{Preamble} {Version} {Role}\n";
return $"{Preamble} {Role} {VersionList}\n";
}

public ReadOnlyMemory<byte> ToBytes()
Expand Down
Loading