package relaydev import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" ) // A relay board on the LAN signs its own certificate — there is no authority // anywhere that could have signed it. httptest.NewTLSServer presents exactly // that: a certificate from an unknown issuer, which is what the hardware does. func selfSignedRelay(t *testing.T) (*httptest.Server, func() []string) { t.Helper() var mu sync.Mutex var got []string srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() got = append(got, r.URL.Path) mu.Unlock() w.WriteHeader(http.StatusOK) })) t.Cleanup(srv.Close) return srv, func() []string { mu.Lock() defer mu.Unlock() return append([]string(nil), got...) } } // With the box ticked, the board answers. func TestHTTPSRelayWithASelfSignedCertificate(t *testing.T) { srv, seen := selfSignedRelay(t) d := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", srv.URL+"/off/{relay}", "", "", 2, nil, true) if err := d.Set(context.Background(), 1, true); err != nil { t.Fatalf("Set over HTTPS: %v", err) } if paths := seen(); len(paths) != 1 || paths[0] != "/on/1" { t.Errorf("the board was asked for %v, want /on/1", paths) } } // Without it, the request is refused — and the refusal has to name the box. // // Go's own message, "x509: certificate signed by unknown authority", is // accurate and tells an operator nothing about what to do next. This is the // difference between a dead end and an instruction, and it is the whole reason // the default can safely stay OFF. func TestARefusedCertificateNamesTheSetting(t *testing.T) { srv, seen := selfSignedRelay(t) d := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", srv.URL+"/off/{relay}", "", "", 2, nil, false) err := d.Set(context.Background(), 1, true) if err == nil { t.Fatal("an unverifiable certificate was accepted with the box unticked") } if !strings.Contains(err.Error(), "self-signed") { t.Errorf("the refusal reads %q — it does not say which setting to change", err) } if len(seen()) != 0 { t.Error("the request reached the board despite the certificate being refused") } } // The box belongs to ONE board. An operator with a self-signed switch on the // LAN and a second board reached through a proper HTTPS proxy must keep real // verification on the second — that link crosses the internet, and it commands // an antenna. func TestAcceptingOneBoardsCertificateDoesNotAffectAnother(t *testing.T) { srv, _ := selfSignedRelay(t) lan := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", "", "", "", 1, nil, true) if err := lan.Set(context.Background(), 1, true); err != nil { t.Fatalf("the LAN board: %v", err) } strict := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", "", "", "", 1, nil, false) if err := strict.Set(context.Background(), 1, true); err == nil { t.Error("the second board accepted the certificate too — the setting is not per board") } }