clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / daemon / views_http_test.go
views_http_test.go 3.2K
1package daemon23import (4 "context"5 "net/http"6 "net/http/httptest"7 "strings"8 "testing"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/event"12 "github.com/FelineStateMachine/tinyrelay/internal/storage"13)1415func TestNamedViewsRespectTheirOwnAudience(t *testing.T) {16 app, tenant := testTenant(t)17 ctx := context.Background()18 p := tenant.Policy()19 p.Reads = "open"20 p.DirectoryPublic = false21 if err := tenant.applyPolicy(ctx, p); err != nil {22 t.Fatal(err)23 }24 for _, tc := range []struct {25 name string26 signed bool27 status int28 }{29 {"articles", false, http.StatusOK},30 {"profiles", false, http.StatusUnauthorized},31 {"profiles", true, http.StatusOK},32 {"missing", false, http.StatusNotFound},33 } {34 req := httptest.NewRequest(http.MethodGet, "http://relay.test/r/main/view/"+tc.name, nil)35 if tc.signed {36 signRequest(t, req, "")37 }38 res := httptest.NewRecorder()39 app.ServeHTTP(res, req)40 if res.Code != tc.status {41 t.Fatalf("%s signed=%v: status %d, %s", tc.name, tc.signed, res.Code, res.Body.String())42 }43 if res.Code == http.StatusOK {44 e, err := event.Parse(res.Body.Bytes())45 if err != nil || event.Tag(e, "d") != "bind.ws/view/"+tc.name {46 t.Fatalf("signed view response: %s, %v", res.Body.String(), err)47 }48 if res.Header().Get("Cache-Control") != "private, no-store" {49 t.Fatal("view response permits shared caching")50 }51 }52 }53 // A members-only projection must not become readable as a stored event.54 var private int55 err := tenant.store.DB().QueryRowContext(ctx, `SELECT count(*) FROM events WHERE kind=? AND d=?`, event.KIND_VIEW, "bind.ws/view/profiles").Scan(&private)56 if err != nil || private != 0 {57 t.Fatalf("private view persisted: %d, %v", private, err)58 }59}6061func TestSchedulerWakesForDurableWriteViewDeadline(t *testing.T) {62 _, tenant := testTenant(t)63 ctx := context.Background()64 var first int6465 waitFor := func(check func() bool) {66 t.Helper()67 deadline := time.NewTimer(8 * time.Second)68 defer deadline.Stop()69 ticker := time.NewTicker(20 * time.Millisecond)70 defer ticker.Stop()71 for !check() {72 select {73 case <-deadline.C:74 t.Fatal("view deadline was not serviced")75 case <-ticker.C:76 }77 }78 }79 waitFor(func() bool {80 return tenant.store.GetSetting(ctx, "records.view.articles.at", &first) == nil && first != 081 })82 e := event.Event{Kind: 30023, CreatedAt: time.Now().Unix(), Tags: [][]string{{"d", "scheduled"}, {"title", "Scheduled article"}}, Content: "body"}83 if err := event.Sign(&e, strings.Repeat("0", 63)+"1"); err != nil {84 t.Fatal(err)85 }86 if _, err := tenant.store.Save(ctx, e, storage.SaveOptions{Now: time.Now().Unix()}); err != nil {87 t.Fatal(err)88 }89 // Model a durable dirty marker recovered nine seconds into its debounce.90 if err := tenant.records.MarkView(ctx, "articles", time.Now().Unix()-9); err != nil {91 t.Fatal(err)92 }93 tenant.scheduleViews()94 waitFor(func() bool {95 result, err := tenant.store.Query(ctx, event.Filter{Kinds: []int{event.KIND_VIEW}, Tags: map[string][]string{"d": {"bind.ws/view/articles"}}}, storage.QueryOptions{Now: time.Now().Unix(), Access: storage.Access{All: true}})96 if err != nil || len(result.Events) != 1 {97 return false98 }99 return event.Tag(result.Events[0], "a") == "30023:"+e.PubKey+":scheduled"100 })101}