Files
OpsLog/internal/relaydev/httpgen_test.go
T
rouggy 26b8dade20 feat(relays): generic HTTP relay board, and point band switching at it
An operator was about to build band→relay mapping into the custom URL rows.
It already exists, and better: relay auto-control has a per-relay "band" rule
driving webswitch, KMTronic, Dingtian, Denkovi and USB boards. It holds state
so a relay is only commanded when its wanted position changed, reads the
boards live before the first apply so it does not re-command one already in
place, and carries per-relay labels. A URL fired on a band change has none of
that, and would have been a second definition of "which antenna on which band"
— the drift that cost us a week on counties.

So the gap was only the hardware: a hand-made switch is none of the five named
types. It is now the sixth, "HTTP relay": an ON URL and an OFF URL, either as
one pattern with {relay} substituted, or one pair per relay. The per-relay
form is the reason it exists — these boxes often have URLs with nothing in
common between channels (…/FF0101 and …/FF0201), which no pattern can express.

Status is remembered rather than read: most have no endpoint worth trusting.
The cost is stated in the code and the panel — after a restart every relay is
re-commanded once, which is harmless on a board with no memory and far better
than assuming an antenna is already selected.

And the Connections panel now says so, exactly where the wrong choice is made:
picking a band change with a URL transport shows a note pointing at Station
Control. The URL transport stays — a lookup pushed to a webhook or a QSO to a
dashboard is an event, not a state, and Station Control has no place for it.
2026-08-15 02:23:03 +02:00

93 lines
3.0 KiB
Go

package relaydev
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// The pattern form: one URL pair for the whole board, {relay} substituted.
func TestHTTPGenericPattern(t *testing.T) {
var mu sync.Mutex
var got []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
got = append(got, r.URL.String())
mu.Unlock()
}))
defer srv.Close()
d := NewHTTPGeneric(nil, nil,
srv.URL+"/relay?n={relay}&state=on",
srv.URL+"/relay?n={relay}&state=off", "", "", 4)
if err := d.Set(context.Background(), 2, true); err != nil {
t.Fatalf("Set on: %v", err)
}
if err := d.Set(context.Background(), 3, false); err != nil {
t.Fatalf("Set off: %v", err)
}
mu.Lock()
defer mu.Unlock()
want := []string{"/relay?n=2&state=on", "/relay?n=3&state=off"}
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Errorf("requested %v, want %v", got, want)
}
}
// The per-relay form, which is the reason this driver exists: a hand-made
// switch often has URLs with nothing in common between channels, and no
// pattern can express that.
func TestHTTPGenericPerRelayURLsWinOverThePattern(t *testing.T) {
var mu sync.Mutex
var got []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
got = append(got, r.URL.Path)
mu.Unlock()
}))
defer srv.Close()
d := NewHTTPGeneric(
[]string{srv.URL + "/FF0101", "", srv.URL + "/weird/on"},
[]string{srv.URL + "/FF0100", "", ""},
srv.URL+"/pattern/on/{relay}", srv.URL+"/pattern/off/{relay}", "", "", 3)
_ = d.Set(context.Background(), 1, true) // its own URL
_ = d.Set(context.Background(), 2, true) // empty → falls back to the pattern
_ = d.Set(context.Background(), 3, false) // no OFF of its own → pattern
mu.Lock()
defer mu.Unlock()
want := []string{"/FF0101", "/pattern/on/2", "/pattern/off/3"}
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Errorf("requested %v, want %v", got, want)
}
}
// A switch with the ON URLs filled and OFF left empty latches. The error has to
// name the direction, or the operator cannot tell which half is missing.
func TestHTTPGenericNamesTheMissingDirection(t *testing.T) {
d := NewHTTPGeneric([]string{"http://x/on"}, nil, "", "", "", "", 1)
err := d.Set(context.Background(), 1, false)
if err == nil || !strings.Contains(err.Error(), "OFF") {
t.Errorf("err = %v, want it to name the OFF direction", err)
}
}
// Status reports what was commanded: these boxes have nothing to read.
func TestHTTPGenericRemembersWhatItCommanded(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
defer srv.Close()
d := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", srv.URL+"/off/{relay}", "", "", 3)
_ = d.Set(context.Background(), 2, true)
st, err := d.Status(context.Background())
if err != nil {
t.Fatalf("Status: %v", err)
}
if len(st) != 3 || st[0] || !st[1] || st[2] {
t.Errorf("state = %v, want only relay 2 on", st)
}
}