| 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 testTenant(t *testing.T) (*App, *Tenant) { |
| 16 | t.Helper() |
| 17 | ctx := context.Background() |
| 18 | a, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main" }) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | t.Cleanup(func() { |
| 23 | if err := a.Close(ctx); err != nil { |
| 24 | t.Error(err) |
| 25 | } |
| 26 | }) |
| 27 | pk, err := event.PublicKey(strings.Repeat("0" , 63) + "1" ) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | meta, err := a.Create(ctx, CreateOptions{Name: "main" , Owner: pk, Template: "default" }) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | tenant, err := a.tenant(ctx, meta, "http://relay.test" ) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | return a, tenant |
| 40 | } |
| 41 | |
| 42 | func TestSignedSessionSurvivesNavigationAndRevokes(t *testing.T) { |
| 43 | a, tenant := testTenant(t) |
| 44 | req := httptest.NewRequest("POST" , "http://relay.test/session" , strings.NewReader("" )) |
| 45 | signRequest(t, req, "" ) |
| 46 | res := httptest.NewRecorder() |
| 47 | a.ServeHTTP(res, req) |
| 48 | if res.Code != 200 { |
| 49 | t.Fatalf("login %d: %s" , res.Code, res.Body.String()) |
| 50 | } |
| 51 | cookies := res.Result().Cookies() |
| 52 | if len(cookies) != 1 || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode { |
| 53 | t.Fatalf("session cookie %+v" , cookies) |
| 54 | } |
| 55 | page := httptest.NewRequest("GET" , "http://relay.test/manage" , nil) |
| 56 | page.AddCookie(cookies[0]) |
| 57 | actor, err := tenant.resolveUIActor(page) |
| 58 | if err != nil || actor != tenant.Policy().Owner { |
| 59 | t.Fatalf("session actor %s %v" , actor, err) |
| 60 | } |
| 61 | // Reads through the query bridge use the session when the request comes |
| 62 | // from this origin; foreign origins and publishing still need a signature. |
| 63 | query := func(path, origin string) int { |
| 64 | req := httptest.NewRequest("POST" , "http://relay.test" +path, strings.NewReader(`[{"kinds":[10318],"limit":1}]` )) |
| 65 | req.Header.Set("Content-Type" , "application/json" ) |
| 66 | if origin != "" { |
| 67 | req.Header.Set("Origin" , origin) |
| 68 | } |
| 69 | req.AddCookie(cookies[0]) |
| 70 | res := httptest.NewRecorder() |
| 71 | a.ServeHTTP(res, req) |
| 72 | return res.Code |
| 73 | } |
| 74 | if code := query("/query" , "http://relay.test" ); code != 200 { |
| 75 | t.Fatalf("session query %d" , code) |
| 76 | } |
| 77 | if code := query("/query" , "https://foreign.test" ); code != 401 { |
| 78 | t.Fatalf("foreign origin query %d" , code) |
| 79 | } |
| 80 | if code := query("/query" , "" ); code != 401 { |
| 81 | t.Fatalf("query without origin %d" , code) |
| 82 | } |
| 83 | if code := query("/events" , "http://relay.test" ); code != 401 { |
| 84 | t.Fatalf("cookie publish %d" , code) |
| 85 | } |
| 86 | logout := httptest.NewRequest("POST" , "http://relay.test/session/logout" , nil) |
| 87 | logout.AddCookie(cookies[0]) |
| 88 | logout.Header.Set("Origin" , "https://foreign.test" ) |
| 89 | denied := httptest.NewRecorder() |
| 90 | a.ServeHTTP(denied, logout) |
| 91 | if denied.Code != 403 { |
| 92 | t.Fatal("foreign logout accepted" ) |
| 93 | } |
| 94 | logout.Header.Set("Origin" , "http://relay.test" ) |
| 95 | ok := httptest.NewRecorder() |
| 96 | a.ServeHTTP(ok, logout) |
| 97 | if ok.Code != 200 { |
| 98 | t.Fatalf("logout %d" , ok.Code) |
| 99 | } |
| 100 | actor, err = tenant.resolveUIActor(page) |
| 101 | if err != nil || actor != "" { |
| 102 | t.Fatalf("revoked session %s %v" , actor, err) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestChatPreferencesArePrivateAndDurablePerAccount(t *testing.T) { |
| 107 | a, tenant := testTenant(t) |
| 108 | login := httptest.NewRequest(http.MethodPost, "http://relay.test/session" , strings.NewReader("" )) |
| 109 | signRequest(t, login, "" ) |
| 110 | loginResult := httptest.NewRecorder() |
| 111 | a.ServeHTTP(loginResult, login) |
| 112 | if loginResult.Code != http.StatusOK { |
| 113 | t.Fatalf("login: %d %s" , loginResult.Code, loginResult.Body.String()) |
| 114 | } |
| 115 | cookie := loginResult.Result().Cookies()[0] |
| 116 | read := httptest.NewRequest(http.MethodGet, "http://relay.test/chat/preferences" , nil) |
| 117 | read.AddCookie(cookie) |
| 118 | result := httptest.NewRecorder() |
| 119 | a.ServeHTTP(result, read) |
| 120 | if result.Code != http.StatusOK || result.Body.String() != `{"share_presence":false}` { |
| 121 | t.Fatalf("default preference: %d %s" , result.Code, result.Body.String()) |
| 122 | } |
| 123 | update := httptest.NewRequest(http.MethodPost, "http://relay.test/chat/preferences" , strings.NewReader(`{"share_presence":true}` )) |
| 124 | signRequest(t, update, `{"share_presence":true}` ) |
| 125 | updated := httptest.NewRecorder() |
| 126 | a.ServeHTTP(updated, update) |
| 127 | if updated.Code != http.StatusOK || updated.Body.String() != `{"share_presence":true}` { |
| 128 | t.Fatalf("updated preference: %d %s" , updated.Code, updated.Body.String()) |
| 129 | } |
| 130 | if !tenant.chatSharePresence(context.Background(), tenant.Policy().Owner) { |
| 131 | t.Fatal("preference was not persisted" ) |
| 132 | } |
| 133 | reloaded := httptest.NewRequest(http.MethodGet, "http://relay.test/chat/preferences" , nil) |
| 134 | reloaded.AddCookie(cookie) |
| 135 | reloadedResult := httptest.NewRecorder() |
| 136 | a.ServeHTTP(reloadedResult, reloaded) |
| 137 | if reloadedResult.Code != http.StatusOK || reloadedResult.Body.String() != `{"share_presence":true}` { |
| 138 | t.Fatalf("reloaded preference: %d %s" , reloadedResult.Code, reloadedResult.Body.String()) |
| 139 | } |
| 140 | guest := httptest.NewRecorder() |
| 141 | a.ServeHTTP(guest, httptest.NewRequest(http.MethodGet, "http://relay.test/chat/preferences" , nil)) |
| 142 | if guest.Code != http.StatusUnauthorized { |
| 143 | t.Fatalf("guest preference: %d" , guest.Code) |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | func TestTenantMaintenanceUsesPersistedRetention(t *testing.T) { |
| 148 | _, tenant := testTenant(t) |
| 149 | ctx := context.Background() |
| 150 | now := time.Now().Unix() |
| 151 | e := event.Event{Kind: 1, CreatedAt: now - 3*86400, Tags: [][]string{}, Content: "aged note" } |
| 152 | if err := event.Sign(&e, strings.Repeat("0" , 63)+"2" ); err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | if _, err := tenant.store.Save(ctx, e, storage.SaveOptions{Now: now}); err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | if _, err := tenant.store.DB().ExecContext(ctx, "INSERT OR REPLACE INTO community_retention(kind,days) VALUES(1,1)" ); err != nil { |
| 159 | t.Fatal(err) |
| 160 | } |
| 161 | if err := tenant.sweep(ctx, now); err != nil { |
| 162 | t.Fatal(err) |
| 163 | } |
| 164 | rows, err := tenant.store.Query(ctx, event.Filter{IDs: []string{e.ID}}, storage.QueryOptions{Now: now}) |
| 165 | if err != nil || len(rows.Events) != 0 { |
| 166 | t.Fatalf("retention did not remove old event: %+v %v" , rows, err) |
| 167 | } |
| 168 | } |