package udp import ( "strings" "testing" ) // The escaping rule is the whole difference between a template that works on // the bench and one that fails on the first portable callsign. func TestRenderEscapesForURLsOnly(t *testing.T) { fields := map[string]string{ "call": "F4BPO/P", "name": "Jean Pierre", "band": "20m", "az": "137", } // UDP: the receiver wants the text as typed. if got := Render("{call}", fields, false); got != "F4BPO/P" { t.Errorf("UDP render = %q", got) } // URL: a slash in a callsign is a path separator, a space breaks the request. if got := Render("http://sw/a?c={call}&n={name}", fields, true); got != "http://sw/a?c=F4BPO%2FP&n=Jean+Pierre" { t.Errorf("URL render = %q", got) } } // A placeholder the trigger does not carry becomes empty, not the literal // "{freq}" — otherwise the braces go down the wire and a receiver files them. func TestRenderDropsUnknownFields(t *testing.T) { if got := Render("{az}{freq}", map[string]string{"az": "137"}, false); got != "137" { t.Errorf("got %q", got) } } // Text that merely looks like a placeholder must survive: an unclosed brace is // content, not a broken field. func TestRenderLeavesLiteralBraces(t *testing.T) { for _, c := range []struct{ in, want string }{ {"no fields here", "no fields here"}, {"{unclosed", "{unclosed"}, {"a } b", "a } b"}, } { if got := Render(c.in, map[string]string{}, false); got != c.want { t.Errorf("Render(%q) = %q, want %q", c.in, got, c.want) } } } // Field names are matched case- and space-insensitively: an operator typing // {Band} or { band } means the band. func TestRenderFieldNamesAreForgiving(t *testing.T) { f := map[string]string{"band": "20m"} for _, tmpl := range []string{"{band}", "{Band}", "{ BAND }"} { if got := Render(tmpl, f, false); got != "20m" { t.Errorf("Render(%q) = %q, want 20m", tmpl, got) } } } // Receivers that read a line at a time need a terminator, and a trailing space // in a text box is invisible — hence a choice rather than something to type. func TestLineEnding(t *testing.T) { for in, want := range map[string]string{ "": "", "lf": "\n", "LF": "\n", "crlf": "\r\n", " CRLF ": "\r\n", "nonsense": "", } { if got := lineEnding(in); got != want { t.Errorf("lineEnding(%q) = %q, want %q", in, got, want) } } } // These logs get sent to whoever is helping when something breaks. Storing a // switch password as typed was a deliberate choice for LAN gear; putting it in // a file that travels is a different one, and was never made. func TestRedactURLHidesThePassword(t *testing.T) { for in, want := range map[string]string{ "http://admin:secret@192.168.1.50/ant?b=20m": "http://admin:***@192.168.1.50/ant?b=20m", // No credentials, nothing to hide — and the URL must come back untouched, // because it is the thing being debugged. "http://192.168.1.50/ant?b=20m": "http://192.168.1.50/ant?b=20m", // A username with no password is not a secret. "http://admin@192.168.1.50/ant": "http://admin@192.168.1.50/ant", // Not a URL at all: leave it, the operator needs to see what they typed. "not a url {band}": "not a url {band}", } { if got := redactURL(in); got != want { t.Errorf("redactURL(%q) = %q, want %q", in, got, want) } } } // "sent 18 bytes" says nothing about whether the substitution came out right. // The payload is shown for custom rows and only those: an ADIF record or a // binary WSJT frame on every QSO would bury the log. func TestSentPreviewIsForCustomRowsOnly(t *testing.T) { custom := Config{ServiceType: ServiceCustom, Trigger: TriggerBandChange} got := sentPreview(custom, []byte("20\r\n")) if got != ` on band_change: "20\r\n"` { t.Errorf("custom preview = %q", got) } // A line ending is a choice the operator made — it has to be visible, and it // must not break the log line in two. if strings.ContainsAny(got, "\r\n") { t.Error("the preview put a real newline in the log line") } for _, s := range []ServiceType{ServiceDBUpdated, ServiceWSJTLog, ServiceN1MMRadio} { if p := sentPreview(Config{ServiceType: s}, []byte("VK9XX")); p != "" { t.Errorf("%s should not dump its payload, got %q", s, p) } } }