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