| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 13 | ) |
| 14 | |
| 15 | func TestNamedViewsRespectTheirOwnAudience(t *testing.T) { |
| 16 | app, tenant := testTenant(t) |
| 17 | ctx := context.Background() |
| 18 | p := tenant.Policy() |
| 19 | p.Reads = "open" |
| 20 | p.DirectoryPublic = false |
| 21 | if err := tenant.applyPolicy(ctx, p); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | for _, tc := range []struct { |
| 25 | name string |
| 26 | signed bool |
| 27 | status int |
| 28 | }{ |
| 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 int |
| 55 | 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 | } |
| 60 | |
| 61 | func TestSchedulerWakesForDurableWriteViewDeadline(t *testing.T) { |
| 62 | _, tenant := testTenant(t) |
| 63 | ctx := context.Background() |
| 64 | var first int64 |
| 65 | 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 != 0 |
| 81 | }) |
| 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 false |
| 98 | } |
| 99 | return event.Tag(result.Events[0], "a" ) == "30023:" +e.PubKey+":scheduled" |
| 100 | }) |
| 101 | } |