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)
}
}
}