Files
TeslaSuite/Console/tests/TeslaConsole.DiffTests/PodRpcProtocolTests.cs
T
CydandClaude Opus 4.8 b9d8027cf6 Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64
Make the Console<->Launcher system source-built and modern now that the console
is under our control and the WinXP-era pods are gone.

Contract extraction (Contract/Tesla.Contract.csproj):
- One multi-targeted (net48;net8.0-windows) source project for the RPC contract,
  replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net
  replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib.

SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj):
- net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto,
  RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll.

Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned):
- Console<->Launcher RPC is now length-prefixed System.Text.Json frames
  (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by
  method name. Deleted the SerializationBinder / MethodInfoProxy machinery.
- Console-local BinaryFormatter (Site config, mission replays) intentionally
  retained: local net48 file I/O, not the network surface.

Runtime modernization:
- Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are
  64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat)
  with SYSLIB0041 suppressed and documented.

Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB
ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON
round-trip of every request/response shape); removed the now-obsolete
BinaryFormatter byte-identity guard.

Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a
NuGet restore collision between the two Launcher projects sharing one folder.

NOT runtime-verified against a live pod.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:15:17 -05:00

157 lines
5.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Tesla.Net;
using Xunit;
namespace TeslaConsole.DiffTests
{
/// <summary>
/// Exercises the framed JSON RPC protocol (PodRpc) that replaced the OFB +
/// BinaryFormatter wire. Both the Console client (PodManagerConnection) and the
/// Launcher Service share this exact code, so an in-process round-trip
/// (encode request → decode → encode response → decode) validates the wire
/// contract both ends depend on. The live console↔pod path still needs a pod;
/// this covers the serialization, which is the part that can silently break.
/// </summary>
public class PodRpcProtocolTests
{
private static readonly Guid Key = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94");
[Fact]
public void Request_RoundTrips_Method_And_Args()
{
var ms = new MemoryStream();
PodRpc.WriteRequest(ms, "KillApp", new object[] { Key, 4242 });
ms.Position = 0;
var req = PodRpc.ReadRequest(ms);
Assert.Equal("KillApp", req.Method);
Assert.Equal(2, req.Args.Count);
Assert.Equal(Key, req.Args[0].GetGuid());
Assert.Equal(4242, req.Args[1].GetInt32());
}
[Fact]
public void Request_With_No_Args_RoundTrips()
{
var ms = new MemoryStream();
PodRpc.WriteRequest(ms, "FullUpdate", null);
ms.Position = 0;
var req = PodRpc.ReadRequest(ms);
Assert.Equal("FullUpdate", req.Method);
Assert.Empty(req.Args);
}
[Fact]
public void Two_Frames_Read_Back_To_Back()
{
var ms = new MemoryStream();
PodRpc.WriteRequest(ms, "Ping", new object[] { new DateTime(2026, 6, 30, 12, 0, 0, DateTimeKind.Utc) });
PodRpc.WriteRequest(ms, "KillAllApps", null);
ms.Position = 0;
Assert.Equal("Ping", PodRpc.ReadRequest(ms).Method);
Assert.Equal("KillAllApps", PodRpc.ReadRequest(ms).Method);
}
[Fact]
public void Response_Error_RoundTrips()
{
var ms = new MemoryStream();
PodRpc.WriteResponse(ms, null, "boom on the pod");
ms.Position = 0;
var resp = PodRpc.ReadResponse(ms);
Assert.Equal("boom on the pod", resp.Error);
}
[Fact]
public void Response_Primitive_Results_RoundTrip()
{
Assert.Equal(99, RoundTripResult<int>(99));
Assert.Equal(Key, RoundTripResult<Guid>(Key));
Assert.Equal(0.5f, RoundTripResult<float>(0.5f));
Assert.Equal(new DateTime(2026, 6, 30), RoundTripResult<DateTime>(new DateTime(2026, 6, 30)));
}
[Fact]
public void Response_LaunchData_Array_RoundTrips_With_Fields()
{
var apps = new[]
{
new LaunchData
{
LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "Red Planet" },
WorkingDirectory = @"C:\Games\RedPlanet",
ExeFile = "RedPlanet.exe",
Arguments = "-res 1024 768",
AutoRestart = true,
},
};
var got = RoundTripResult<LaunchData[]>(apps);
Assert.Single(got);
Assert.Equal(Key, got[0].LaunchPair.LaunchKey);
Assert.Equal("Red Planet", got[0].LaunchPair.DisplayName);
Assert.Equal(@"C:\Games\RedPlanet", got[0].WorkingDirectory);
Assert.Equal("RedPlanet.exe", got[0].ExeFile);
Assert.Equal("-res 1024 768", got[0].Arguments);
Assert.True(got[0].AutoRestart);
}
[Fact]
public void Response_FullUpdateData_RoundTrips()
{
var full = new FullUpdateData
{
InstalledApps = new[]
{
new LaunchData { LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "RP" } },
},
LaunchedApps = new[] { new LaunchedAppData { LaunchKey = Key, ProcessId = 1234 } },
VolumeLevel = 0.75f,
};
var got = RoundTripResult<FullUpdateData>(full);
Assert.Equal(0.75f, got.VolumeLevel);
Assert.Equal("RP", got.InstalledApps[0].LaunchPair.DisplayName);
Assert.Equal(1234, got.LaunchedApps[0].ProcessId);
Assert.Equal(Key, got.LaunchedApps[0].LaunchKey);
}
[Fact]
public void Response_OutOfBandProgress_RoundTrips()
{
var p = new OutOfBandProgress { PercentComplete = 73, Status = "Extracting...", IsCompleted = false };
var got = RoundTripResult<OutOfBandProgress>(p);
Assert.Equal(73, got.PercentComplete);
Assert.Equal("Extracting...", got.Status);
Assert.False(got.IsCompleted);
}
[Fact]
public void Oversized_Frame_Length_Is_Rejected()
{
var ms = new MemoryStream();
// Craft a frame header claiming more than MaxFrameBytes.
ms.Write(BitConverter.GetBytes(PodRpc.MaxFrameBytes + 1), 0, 4);
ms.Position = 0;
Assert.Throws<IOException>(() => PodRpc.ReadFrame(ms));
}
// Serialize a result the way the Launcher does, read it the way the Console does.
private static T RoundTripResult<T>(object result)
{
var ms = new MemoryStream();
PodRpc.WriteResponse(ms, result, null);
ms.Position = 0;
var resp = PodRpc.ReadResponse(ms);
Assert.Null(resp.Error);
return resp.Result.Deserialize<T>(PodRpc.JsonOptions);
}
}
}