package relaydev import ( "context" "net/http" "net/http/httptest" "strings" "testing" ) // The status parsers are the risky part; pin them against the documented wire // formats using a stub HTTP server. func TestWebswitchStatus(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/relaystate/get2/") { t.Errorf("unexpected status path %q", r.URL.Path) } _, _ = w.Write([]byte("1,1\n2,0\n3,1\n4,0\n5,1\n")) })) defer srv.Close() d := NewWebswitch(strings.TrimPrefix(srv.URL, "http://")) st, err := d.Status(context.Background()) if err != nil { t.Fatal(err) } want := []bool{true, false, true, false, true} if len(st) != 5 { t.Fatalf("got %d relays, want 5", len(st)) } for i := range want { if st[i] != want[i] { t.Errorf("relay %d = %v, want %v", i+1, st[i], want[i]) } } } func TestWebswitchSetURL(t *testing.T) { var gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path })) defer srv.Close() d := NewWebswitch(strings.TrimPrefix(srv.URL, "http://")) if err := d.Set(context.Background(), 4, true); err != nil { t.Fatal(err) } if gotPath != "/relaycontrol/on/4" { t.Errorf("set path = %q, want /relaycontrol/on/4", gotPath) } _ = d.Set(context.Background(), 4, false) } func TestKMTronicStatus(t *testing.T) { // relay0 reserved (ignored); relay7 + relay8 ON. xmlBody := `` + `0000` + `00011` + `` srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/status.xml" { t.Errorf("unexpected path %q", r.URL.Path) } _, _ = w.Write([]byte(xmlBody)) })) defer srv.Close() d := NewKMTronic(strings.TrimPrefix(srv.URL, "http://"), "", "") st, err := d.Status(context.Background()) if err != nil { t.Fatal(err) } if len(st) != 8 { t.Fatalf("got %d relays, want 8", len(st)) } if st[6] != true || st[7] != true { t.Errorf("relay7/8 = %v/%v, want on/on", st[6], st[7]) } for i := 0; i < 6; i++ { if st[i] { t.Errorf("relay %d unexpectedly on", i+1) } } } func TestKMTronicSetURL(t *testing.T) { var gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path })) defer srv.Close() d := NewKMTronic(strings.TrimPrefix(srv.URL, "http://"), "", "") if err := d.Set(context.Background(), 8, true); err != nil { t.Fatal(err) } if gotPath != "/FF0801" { t.Errorf("set path = %q, want /FF0801", gotPath) } _ = d.Set(context.Background(), 1, false) // → /FF0100 } // Dingtian — the wire examples come straight from the IOT Relay Programming // Manual V1.9.8.1 §3.1/§3.2. The two traps pinned here: the URL relay index is // ZERO-based while the Device interface is 1-based, and a refusal (bad password // or session) answers HTTP 200 with a non-zero first field. func TestDingtianStatus(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/relay_cgi_load.cgi" { t.Errorf("unexpected status path %q", r.URL.Path) } // Manual's own example: ok, 4 relays, 1 on, 2 off, 3 on, 4 off. _, _ = w.Write([]byte("&0&4&1&0&1&0&")) })) defer srv.Close() d := NewDingtian(strings.TrimPrefix(srv.URL, "http://"), "", "", 2) st, err := d.Status(context.Background()) if err != nil { t.Fatal(err) } // The board said 4 relays even though 2 were configured — its count wins. want := []bool{true, false, true, false} if len(st) != len(want) { t.Fatalf("got %d relays, want %d", len(st), len(want)) } for i := range want { if st[i] != want[i] { t.Errorf("relay %d = %v, want %v", i+1, st[i], want[i]) } } if d.Count() != 4 { t.Errorf("Count() = %d, want 4 (taken from the board)", d.Count()) } } func TestDingtianSetUsesZeroBasedIndexAndSession(t *testing.T) { var gotQuery, gotCookie string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotQuery = r.URL.RawQuery gotCookie = r.Header.Get("Cookie") _, _ = w.Write([]byte("&0&0&2&1&0&")) })) defer srv.Close() d := NewDingtian(strings.TrimPrefix(srv.URL, "http://"), "12345678", "4660", 4) if err := d.Set(context.Background(), 3, true); err != nil { t.Fatal(err) } if !strings.Contains(gotQuery, "relay=2") { t.Errorf("relay 3 must go out as relay=2 (zero-based); query was %q", gotQuery) } if !strings.Contains(gotQuery, "on=1") || !strings.Contains(gotQuery, "type=0") || !strings.Contains(gotQuery, "pwd=4660") { t.Errorf("unexpected query %q", gotQuery) } if gotCookie != "session=12345678" { t.Errorf("session cookie = %q, want session=12345678", gotCookie) } } func TestDingtianSetRefusalIsAnError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { // Manual §3.4.4: a bad session still answers 200 OK. _, _ = w.Write([]byte("&302&/&")) })) defer srv.Close() d := NewDingtian(strings.TrimPrefix(srv.URL, "http://"), "", "", 2) if err := d.Set(context.Background(), 1, true); err == nil { t.Fatal("a refused command answered HTTP 200 and was reported as success") } }