HTTPS to a relay board could not work. Nearly every board that offers it signs its own certificate — there is no authority anywhere that could have signed it — so the request failed verification before it left. A checkbox, per board, off by default. Not a blanket switch, because the other HTTPS case is real and opposite: a board reached from outside through a proxy with a genuine certificate, where verification is the only thing standing between an antenna switch and the internet. Same setting, two boards, different answers. Off by default is only safe if the failure explains itself, so a certificate error now names the box to tick. Go's own "x509: certificate signed by unknown authority" is accurate and tells an operator nothing about what to do next. Shown only once an https:// URL is actually in the board's configuration. A board on plain HTTP has no certificate to argue about, and an option that cannot matter yet is one more thing to wonder about. The flag joins the driver cache key: ticking it has to rebuild the driver, or the cached one would go on refusing the certificate with the verifying client it already holds. The boards that take a bare host — WebSwitch, KMTronic — keep verification. An https:// typed there is the proxy case by construction, since they default to plain HTTP on the LAN. Three tests against a real self-signed TLS server: accepted with the box, refused with a message naming it without the box, and one board's setting not leaking into another's.
173 lines
6.0 KiB
Go
173 lines
6.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, nil, false)
|
|
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, false)
|
|
|
|
_ = 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", ""}, false)
|
|
_ = 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, false)
|
|
_ = 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{"", ""}, false)
|
|
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, false)
|
|
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, false)
|
|
_ = 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)
|
|
}
|
|
}
|