It was built the other way round on a misreading of two screenshots: the pattern
held {value} and the per-relay boxes held numbers to drop into it. The ask was
simpler and better — {value} is the name typed in Relay labels, so a switch
addressed by antenna name is one pattern instead of eight URLs:
http://10.10.10.100/relay?on={value} relay 1 named Ant1 → ?on=Ant1
Renaming the antenna re-addresses it, and the name on the button and the name on
the wire cannot drift apart because they are the same string. It works in the
per-relay URLs and in the patterns alike, so the per-relay boxes go back to
holding URLs and nothing about them changes meaning any more.
The label is percent-encoded with %20 rather than "+" for a space: "+" is a
space only in a query string and a literal plus in a path, and this can land in
either half of a URL.
{value} on a relay with no label would send "?on=", an empty parameter that most
boards answer with a cheerful 200 and no movement. The driver refuses it and
names the label as what is missing; the editor warns while it is being typed,
beside the empty box rather than after an antenna fails to switch. The labels
also join the driver's cache key — they are part of the wire format now.
173 lines
5.9 KiB
Go
173 lines
5.9 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, nil)
|
|
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, nil)
|
|
|
|
_ = 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)
|
|
}
|
|
}
|
|
|
|
// {value} is the relay's LABEL: a switch addressed by antenna name rather than
|
|
// by channel number is one pattern instead of eight URLs.
|
|
func TestHTTPGenericValueIsTheRelayLabel(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(
|
|
[]string{srv.URL + "/relay?on={value}"}, // per-relay URL
|
|
nil,
|
|
"", srv.URL+"/relay?off={value}", // and the pattern, for the other direction
|
|
"", "", 3, []string{"Ant1", "Beam 20m", ""})
|
|
_ = d.Set(context.Background(), 1, true)
|
|
_ = d.Set(context.Background(), 2, false)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
// The space in "Beam 20m" must go out as %20 — a "+" would be a literal plus
|
|
// in a path, and this substitution can land in either half of a URL.
|
|
want := []string{"/relay?on=Ant1", "/relay?off=Beam%2020m"}
|
|
if strings.Join(got, " ") != strings.Join(want, " ") {
|
|
t.Errorf("requested %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// {relay-1} for a board whose channels are numbered from zero.
|
|
func TestHTTPGenericRelayOffset(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(nil, nil,
|
|
srv.URL+"/set0/{relay-1}/1", srv.URL+"/set0/{relay-1}/0", "", "", 4, nil)
|
|
_ = d.Set(context.Background(), 1, true)
|
|
_ = d.Set(context.Background(), 4, false)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
want := []string{"/set0/0/1", "/set0/3/0"}
|
|
if strings.Join(got, " ") != strings.Join(want, " ") {
|
|
t.Errorf("requested %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// A URL that uses {value} on an unlabelled relay would go out as "?on=" — an
|
|
// empty parameter, which most boards answer with a cheerful 200 and no
|
|
// movement. It must be refused, and the message must say the label is what is
|
|
// missing.
|
|
func TestHTTPGenericRefusesValueWithoutALabel(t *testing.T) {
|
|
d := NewHTTPGeneric(nil, nil, "http://x/relay?on={value}", "", "", "", 2, []string{"", ""})
|
|
err := d.Set(context.Background(), 1, true)
|
|
if err == nil || !strings.Contains(err.Error(), "label") {
|
|
t.Errorf("err = %v, want it to name the missing label", err)
|
|
}
|
|
}
|
|
|
|
// A URL typed without a scheme must still be sent — the named boards take a
|
|
// bare host and add http:// themselves, and this one has to behave the same.
|
|
// https:// is left exactly as typed.
|
|
func TestHTTPGenericSuppliesTheScheme(t *testing.T) {
|
|
for _, c := range []struct{ in, want string }{
|
|
{"192.168.1.9/Set0/1", "http://192.168.1.9/Set0/1"},
|
|
{"http://192.168.1.9/x", "http://192.168.1.9/x"},
|
|
{"https://relay.example.com/x", "https://relay.example.com/x"},
|
|
{"HTTPS://relay.example.com/x", "HTTPS://relay.example.com/x"},
|
|
} {
|
|
if got := withScheme(c.in); got != c.want {
|
|
t.Errorf("withScheme(%q) = %q, want %q", c.in, got, c.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, nil)
|
|
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, nil)
|
|
_ = 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)
|
|
}
|
|
}
|