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