using System.Text.Json; using Fleet.Core; using MoShiro.Protocol; namespace MoShiro.Protocol.Tests; public sealed class CatalogTests { private static JsonElement Json(string text) => JsonDocument.Parse(text).RootElement.Clone(); [Fact] public void EveryKind_HasBoundedParams_ValidDefaults_AndUniqueNames() { Assert.Equal(Kinds.All.Count, Kinds.All.Select(k => k.Id).Distinct().Count()); foreach (KindSpec kind in Kinds.All) { ParamResolution defaults = ParamResolver.Resolve(kind.Params, null); Assert.True(defaults.IsValid, $"{kind.Id}: defaults must be valid"); Assert.Equal(kind.Params.Count, kind.Params.Select(p => p.Name).Distinct().Count()); Assert.True(kind.BudgetBitsPerSecond(defaults.Values!) >= 0); foreach (ParamSpec spec in kind.Params) { // 每个参数的默认值必须能通过自己的校验(上下限在构造时已写明) Assert.True(spec.TryNormalize(spec.DefaultValue, out _, out string error), $"{kind.Id}.{spec.Name}: {error}"); } foreach (StreamSpec stream in kind.Streams) { Assert.Equal(stream.Columns.Count, stream.Columns.Select(c => c.Name).Distinct().Count()); } } } [Fact] public void DefaultLineTemplates_StayWithinTheLineBudget() { long total = 0; foreach (KindSpec kind in new[] { Kinds.UdpEcho, Kinds.TcpEcho }) { total += kind.BudgetBitsPerSecond(ParamResolver.Resolve(kind.Params, null).Values!); } Assert.InRange(total, 1, ProtocolLimits.MaxLineBudgetBitsPerSecond); } [Fact] public void Resolve_FillsDefaults_AndReportsEveryProblem() { var input = new Dictionary { ["rate_hz"] = Json("500"), ["size"] = Json("\"big\""), ["bogus"] = Json("1"), }; ParamResolution result = ParamResolver.Resolve(Kinds.UdpEcho.Params, input); Assert.False(result.IsValid); Assert.Contains("between 1 and 200", result.Errors["rate_hz"]); Assert.Equal("must be an integer", result.Errors["size"]); Assert.Contains("not a parameter", result.Errors["bogus"]); Assert.False(result.Errors.ContainsKey("port")); } [Fact] public void Resolve_AcceptsBoundariesAndRejectsJustOutside() { Assert.True(ParamResolver.Resolve(Kinds.UdpEcho.Params, new Dictionary { ["rate_hz"] = Json("1") }).IsValid); Assert.True(ParamResolver.Resolve(Kinds.UdpEcho.Params, new Dictionary { ["rate_hz"] = Json("200") }).IsValid); Assert.False(ParamResolver.Resolve(Kinds.UdpEcho.Params, new Dictionary { ["rate_hz"] = Json("0") }).IsValid); Assert.False(ParamResolver.Resolve(Kinds.UdpEcho.Params, new Dictionary { ["rate_hz"] = Json("201") }).IsValid); Assert.False(ParamResolver.Resolve(Kinds.UdpEcho.Params, new Dictionary { ["rate_hz"] = Json("1.5") }).IsValid); Assert.False(ParamResolver.Resolve(Kinds.UdpTrain.Params, new Dictionary { ["direction"] = Json("\"sideways\"") }).IsValid); Assert.False(ParamResolver.Resolve(Kinds.TcpEcho.Params, new Dictionary { ["tls"] = Json("1") }).IsValid); } [Fact] public void Kinds_AreUnknownWhenNotDeclared() { Assert.Null(Kinds.Find("nope")); } [Theory] [InlineData("continuous", true)] [InlineData("once", true)] [InlineData("every:60", true)] [InlineData("every:59", false)] [InlineData("every:", false)] [InlineData("every:-5", false)] [InlineData("hourly", false)] public void Schedule_Validation(string schedule, bool valid) { Assert.Equal(valid, TaskSchedule.Validate(schedule).IsSuccess); } [Fact] public void TaskSet_RoundTripsThroughTheJsonContext() { var set = new TaskSet { Version = 7, Tasks = [new TaskSpec { Id = new TaskId("tsk_1"), Kind = "udp_echo", Params = new Dictionary { ["rate_hz"] = Json("10") } }], Peers = [new PeerSpec { ProbeId = new ProbeId("prb_1"), PublicKey = "AAAA" }], }; string text = JsonSerializer.Serialize(set, MoShiroJsonContext.Default.TaskSet); TaskSet back = JsonSerializer.Deserialize(text, MoShiroJsonContext.Default.TaskSet)!; Assert.Contains("\"id\":\"tsk_1\"", text); Assert.Equal(7, back.Version); Assert.Equal("tsk_1", back.Tasks[0].Id.Value); Assert.Equal(10, back.Tasks[0].Params["rate_hz"].GetInt64()); Assert.Equal("prb_1", back.Peers[0].ProbeId.Value); } [Fact] public void AgentEnrollMeta_ToleratesUnknownFields_AndKeepsDefaults() { AgentEnrollMeta meta = JsonSerializer.Deserialize("{\"kinds\":[\"udp_echo\"],\"future\":1}", MoShiroJsonContext.Default.AgentEnrollMeta)!; Assert.Equal(7443, meta.ProbeTcpPort); Assert.Equal(["udp_echo"], meta.Kinds); } [Fact] public void ErrorCodes_AreSnakeCase() { foreach (System.Reflection.FieldInfo field in typeof(MoShiroErrors).GetFields()) { string value = ((ErrorCode)field.GetValue(null)!).Value; Assert.Matches("^[a-z]+(_[a-z]+)*$", value); } } }