buildDeviceDriver had no case for "httpgen", so the generic board fell through
to the WebSwitch driver: it polled an address it had never been given, reported
itself offline, greyed out every relay button, and sent none of the configured
URLs. Nothing in the interface said so — the board was configured, saved and
listed, and simply did nothing. deviceKey did not cover the URLs either, so once
that is fixed, correcting a typo in one would still have handed back the cached
driver holding the old address until a restart.
Two shapes of home-made switch could not be described at all:
- a bit-mask board whose four URLs differ by one character
(/Set0/1, /Set0/2, /Set0/4, /Set0/8) — {value} in the pattern now takes the
number from the per-relay box, keeping the address in one place;
- a board numbering its channels from zero — {relay-1}, since giving up the
pattern for eight hand-typed URLs was the only alternative.
The pattern decides what the per-relay boxes hold, and the grid says which as
soon as {value} is typed: guessing per box ("does this look like a URL?") would
change meaning on a typo, which is not a thing to do to something wired to an
antenna. A URL typed without a scheme gets http:// like the named boards get
from relayBase; https:// is passed through untouched.
Host and the connection test are gone for this type. It has no address of its
own — its relays may each live on a different box — and no status to read, so a
test could only ever answer "OK, 4 relays". Save was greyed out without a host,
which made a complete configuration of four full URLs impossible to store.
Cluster filters no longer persist across launches. A band lock set weeks earlier
is invisible to whoever set it: the counter reads 76 spots live, the grid is
empty, and the search goes to the cluster instead. Nobody loses work by
re-ticking a chip. Grouping and the panel state still persist — they change how
spots look, never whether they appear.
174 lines
5.9 KiB
Go
174 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)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// The {value} form: one address, and the per-relay boxes hold the number that
|
|
// goes into it. A bit-mask board (qro.cz) is the case — /Set0/1, /Set0/2,
|
|
// /Set0/4, /Set0/8 — where four full URLs differ by one character.
|
|
func TestHTTPGenericValueSubstitution(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{"1", "2", "4", "8"},
|
|
[]string{"0", "0", "0", "0"},
|
|
srv.URL+"/Set0/{value}", srv.URL+"/Set0/{value}", "", "", 4)
|
|
_ = d.Set(context.Background(), 3, true)
|
|
_ = d.Set(context.Background(), 1, false)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
want := []string{"/Set0/4", "/Set0/0"}
|
|
if strings.Join(got, " ") != strings.Join(want, " ") {
|
|
t.Errorf("requested %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// {value} and {relay-1} together: the other API of the same board, whose
|
|
// channels are numbered from zero. Without the offset the pattern has to be
|
|
// abandoned for four hand-typed URLs.
|
|
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(
|
|
[]string{"1", "1", "1", "1"},
|
|
[]string{"0", "0", "0", "0"},
|
|
srv.URL+"/set0/{relay-1}/{value}", srv.URL+"/set0/{relay-1}/{value}", "", "", 4)
|
|
_ = 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)
|
|
}
|
|
}
|
|
|
|
// With {value} in the pattern the per-relay boxes hold values, so an empty one
|
|
// must be reported as a missing VALUE. Telling the operator to enter a URL in a
|
|
// box that wants "4" sends them to rewrite a configuration that was nearly right.
|
|
func TestHTTPGenericNamesAMissingValue(t *testing.T) {
|
|
d := NewHTTPGeneric(nil, nil, "http://x/Set0/{value}", "http://x/Set0/{value}", "", "", 2)
|
|
err := d.Set(context.Background(), 1, true)
|
|
if err == nil || !strings.Contains(err.Error(), "value") {
|
|
t.Errorf("err = %v, want it to name the missing value", 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)
|
|
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)
|
|
}
|
|
}
|