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 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, tenant40}4142func 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 comes62 // 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.Code73 }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}105106func 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}146147func 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}