package drand_test import ( "context" "encoding/hex" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" "g.activething.com/go/DateKeys/provider/drand" ) var sig1000 = hex.EncodeToString(testkit.Release(1000).Signature) func serve(t *testing.T, h http.HandlerFunc) string { t.Helper() s := httptest.NewServer(h) t.Cleanup(s.Close) return s.URL } func TestRaceWaitsForAValidSignature(t *testing.T) { p := profile.Quicknet() bad := serve(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, strings.Repeat("0", 96)) }) good := serve(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v2/chains/"+p.ChainHashHex()+"/rounds/1000" { t.Errorf("request not pinned to the chain: %s", r.URL.Path) } time.Sleep(25 * time.Millisecond) fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, sig1000) }) c := drand.NewWithHTTPClient(http.DefaultClient, bad, good) rel, err := c.Fetch(context.Background(), p, provider.Condition{Round: 1000}) if err != nil || hex.EncodeToString(rel.Signature) != sig1000 || rel.Round != 1000 { t.Fatalf("race failed: %+v %v", rel, err) } } func TestRejectMalformedRelayResponses(t *testing.T) { p := profile.Quicknet() for _, payload := range []string{ `{"round":999,"signature":"` + sig1000 + `"}`, `{"round":1000,"signature":"` + sig1000 + `","randomness":"fake"}`, `{"round":1000,"signature":"fake"}`, `{"round":1000,"signature":"` + sig1000 + `"} {}`, `{"round":1000,"signature":"` + hex.EncodeToString(testkit.Release(1001).Signature) + `"}`, strings.Repeat("x", 9000), ``, } { url := serve(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, payload) }) c := drand.NewWithHTTPClient(http.DefaultClient, url) _, err := c.Fetch(context.Background(), p, provider.Condition{Round: 1000}) if !errors.Is(err, datekeys.ErrReleaseUnavailable) { t.Errorf("accepted or misclassified %.40q: %v", payload, err) } } } func TestRandomnessMustMatchWhenPresent(t *testing.T) { p := profile.Quicknet() sum := "e1f1cb5a9ddd0e2ae4a4a8c5bf42e8e1e1ed8b5a9f1f7da1a3f1d2bb0f1b2c3d" url := serve(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"round":1000,"signature":"%s","randomness":"%s"}`, sig1000, sum) }) _, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(context.Background(), p, provider.Condition{Round: 1000}) if !errors.Is(err, datekeys.ErrReleaseInvalid) { t.Fatalf("wrong randomness accepted: %v", err) } } func TestUnavailabilityAndCancellation(t *testing.T) { p := profile.Quicknet() for _, status := range []int{404, 425, 503} { url := serve(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status) }) _, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(context.Background(), p, provider.Condition{Round: 1000}) if !errors.Is(err, datekeys.ErrReleaseUnavailable) { t.Fatalf("HTTP %d: %v", status, err) } } url := serve(t, func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Second) }) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := drand.NewWithHTTPClient(http.DefaultClient, url).Fetch(ctx, p, provider.Condition{Round: 1000}); !errors.Is(err, datekeys.ErrReleaseUnavailable) { t.Fatalf("ignored cancellation: %v", err) } } func TestRedirectsAreNotFollowed(t *testing.T) { p := profile.Quicknet() target := serve(t, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"round":1000,"signature":"%s"}`, sig1000) }) redirect := serve(t, func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target+r.URL.Path, http.StatusFound) }) if _, err := drand.New(redirect).Fetch(context.Background(), p, provider.Condition{Round: 1000}); err == nil { t.Fatal("followed a redirect") } } func TestInvalidRequests(t *testing.T) { c := drand.New("http://127.0.0.1:1") p := profile.Quicknet() if _, err := c.Fetch(context.Background(), p, provider.Condition{Round: 0}); !errors.Is(err, datekeys.ErrDateKeyInvalid) { t.Fatalf("round 0: %v", err) } other := profile.Quicknet() other.Provider = "other" if _, err := c.Fetch(context.Background(), other, provider.Condition{Round: 1}); !errors.Is(err, datekeys.ErrUnknownProfile) { t.Fatalf("non-drand profile: %v", err) } }