using System.Text;
using System.Text.Json;
using AVEVA.Historian.ReverseEngineering.Capture;
namespace AVEVA.Historian.Client.Tests;
///
/// Unit coverage for the CW-1 capture sanitizer and fixture writer — the reusable
/// "redact identity → emit committable fixture" core that all capture-tier work depends on.
///
public sealed class ProtocolCaptureSanitizerTests
{
private static byte[] Ascii(string s) => Encoding.ASCII.GetBytes(s);
private static byte[] Utf16(string s) => Encoding.Unicode.GetBytes(s);
[Fact]
public void Sanitize_RedactsAsciiOccurrence_PreservingLength()
{
byte[] buffer = [0x01, 0x02, .. Ascii("SECRETTAG"), 0x03];
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("tag", "SECRETTAG")]);
Assert.Equal(buffer.Length, result.Sanitized.Length);
Assert.Equal(0x01, result.Sanitized[0]);
Assert.Equal(0x03, result.Sanitized[^1]);
Assert.DoesNotContain(Ascii("SECRETTAG"), result.Sanitized); // value gone
Assert.Equal(1, result.Report[0].AsciiMatches);
Assert.Equal(0, result.Report[0].Utf16Matches);
}
[Fact]
public void Sanitize_RedactsUtf16Occurrence()
{
byte[] buffer = [0xAA, .. Utf16("HostName"), 0xBB];
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("host", "HostName")]);
Assert.Equal(0, result.Report[0].AsciiMatches);
Assert.Equal(1, result.Report[0].Utf16Matches);
Assert.Equal(0xAA, result.Sanitized[0]);
Assert.Equal(0xBB, result.Sanitized[^1]);
}
[Fact]
public void Sanitize_IsCaseInsensitiveForAsciiLetters()
{
byte[] buffer = Ascii("myserver01");
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("host", "MyServer01")]);
Assert.Equal(1, result.Report[0].AsciiMatches);
Assert.All(result.Sanitized, b => Assert.Equal(ProtocolCaptureSanitizer.FillByte, b));
}
[Fact]
public void Sanitize_RedactsMultipleOccurrences()
{
byte[] buffer = [.. Ascii("TagA"), 0x00, .. Ascii("TagA"), 0x00, .. Ascii("TagA")];
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("tag", "TagA")]);
Assert.Equal(3, result.Report[0].AsciiMatches);
Assert.Equal(3, result.TotalRedactions);
}
[Fact]
public void Sanitize_IgnoresShortSecrets_ToAvoidCollisionCorruption()
{
byte[] buffer = [0x41, 0x42, 0x43]; // "ABC"
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("x", "AB")]); // length 2 < MinSecretLength
Assert.Equal(buffer, result.Sanitized); // untouched
Assert.Equal(0, result.TotalRedactions);
}
[Fact]
public void Sanitize_LeavesUnrelatedBytesUntouched()
{
byte[] buffer = [.. Ascii("keepme"), .. Ascii("DROPME"), .. Ascii("keepme")];
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("s", "DROPME")]);
Assert.Equal(Ascii("keepme"), result.Sanitized[..6]);
Assert.Equal(Ascii("keepme"), result.Sanitized[^6..]);
}
[Fact]
public void AssertNoSecretsRemain_Passes_WhenRedacted()
{
byte[] buffer = Ascii("prefix-SECRET-suffix");
SanitizeResult result = ProtocolCaptureSanitizer.Sanitize(buffer, [new CaptureSecret("s", "SECRET")]);
ProtocolCaptureSanitizer.AssertNoSecretsRemain(result.Sanitized, [new CaptureSecret("s", "SECRET")]);
}
[Fact]
public void AssertNoSecretsRemain_Throws_WhenSecretSurvives()
{
byte[] buffer = Ascii("prefix-SECRET-suffix");
Assert.Throws(
() => ProtocolCaptureSanitizer.AssertNoSecretsRemain(buffer, [new CaptureSecret("s", "SECRET")]));
}
[Fact]
public void FixtureWriter_BuildJson_OmitsRawIdentity_AndRecordsScrubReport()
{
byte[] response = [0x4E, .. Utf16("CustomerTag.PV"), 0xFE, 0x00];
var capture = new ProtocolCapture("get-tag-info", Request: null, Response: response, Notes: "live 2020 server");
var secrets = new[] { new CaptureSecret("tag", "CustomerTag.PV") };
string json = ProtocolFixtureWriter.BuildFixtureJson(capture, secrets, "2026-06-19T00:00:00Z");
Assert.DoesNotContain("CustomerTag", json); // identity scrubbed from hex
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
Assert.Equal("get-tag-info", root.GetProperty("op").GetString());
Assert.Equal("2026-06-19T00:00:00Z", root.GetProperty("capturedUtc").GetString());
Assert.Equal(JsonValueKind.Null, root.GetProperty("request").ValueKind);
JsonElement resp = root.GetProperty("response");
Assert.Equal(response.Length, resp.GetProperty("length").GetInt32());
Assert.Equal(64, resp.GetProperty("sha256").GetString()!.Length);
Assert.Equal("tag", resp.GetProperty("redactions")[0].GetProperty("secret").GetString());
}
[Fact]
public void FixtureWriter_Write_CreatesOpSubdirectoryFile()
{
string root = Path.Combine(Path.GetTempPath(), "histsdk-fixture-test-" + Guid.NewGuid().ToString("N"));
try
{
var capture = new ProtocolCapture("get-tag-info", Request: null, Response: [0x01, 0x02, 0x03], Notes: null);
string path = ProtocolFixtureWriter.Write(root, "sample", capture, [], "2026-06-19T00:00:00Z");
Assert.True(File.Exists(path));
Assert.EndsWith(Path.Combine("get-tag-info", "sample.json"), path);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
}