1package tinyclient23import (4 "context"5 "encoding/json"6 "net/http"7 "net/http/httptest"8 "strings"9 "sync/atomic"10 "testing"11)1213type actorReadBackend struct{ actorFeedBackend }1415func (b *actorReadBackend) ListRooms(_ context.Context, actor, _ string, _ int) (RoomList, error) {16 return RoomList{Rooms: []RoomSummary{{ID: "public-article", Name: actor}}}, nil17}18func (b *actorReadBackend) ReadRoom(_ context.Context, actor, _, _ string, _ int) (RoomPage, error) {19 return RoomPage{Room: RoomSummary{ID: "public-article", Name: actor}}, nil20}21func (b *actorReadBackend) ReadThread(_ context.Context, actor, _, _, _ string, _ int) (RoomPage, error) {22 return RoomPage{Room: RoomSummary{ID: "public-article", Name: actor}}, nil23}24func (b *actorReadBackend) ReadChatActivity(_ context.Context, actor, _, _ string) (any, error) {25 return map[string]string{"id": "public-article", "actor": actor}, nil26}2728func TestRemoteReadsPreserveRequestedActor(t *testing.T) {29 owner, other := strings.Repeat("a", 64), strings.Repeat("b", 64)30 backend := &actorReadBackend{actorFeedBackend{fakeBackend{policy: DefaultPolicy(owner)}}}31 app, err := New(backend, Options{Actor: func(r *http.Request) (string, error) {32 if cookie, err := r.Cookie("tiny_session"); err == nil && cookie.Value == "valid-session" {33 return owner, nil34 }35 return "", nil36 }})37 if err != nil {38 t.Fatal(err)39 }40 var reads atomic.Int3241 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {42 reads.Add(1)43 app.BackendHandler().ServeHTTP(w, r)44 }))45 defer upstream.Close()46 handler, err := NewRemote(RemoteOptions{BackendURL: upstream.URL, PublicURL: backend.URL()})47 if err != nil {48 t.Fatal(err)49 }50 for _, tc := range []struct {51 name, cookie, actor string52 denied bool53 }{54 {name: "authenticated", cookie: "tiny_session=valid-session", actor: owner},55 {name: "anonymous read with session", cookie: "tiny_session=valid-session"},56 {name: "mismatched actor", cookie: "tiny_session=valid-session", actor: other, denied: true},57 {name: "anonymous"},58 {name: "forged actor without session", actor: owner, denied: true},59 {name: "forged actor with invalid session", cookie: "tiny_session=invalid", actor: owner, denied: true},60 } {61 t.Run(tc.name, func(t *testing.T) {62 b := &requestBackend{remote: handler.(*remoteClient), cookie: tc.cookie}63 if err := b.remote.read(context.Background(), b.cookie, nil, &b.snapshot); err != nil {64 t.Fatal(err)65 }66 for name, read := range map[string]func() (any, error){67 "query": func() (any, error) { return b.Query(context.Background(), "queryevents", nil, tc.actor) },68 "rooms": func() (any, error) { return (&remoteRooms{b}).ListRooms(context.Background(), tc.actor, "", 10) },69 "room": func() (any, error) {70 return (&remoteRooms{b}).ReadRoom(context.Background(), tc.actor, "build", "", 10)71 },72 "thread": func() (any, error) {73 return (&remoteRooms{b}).ReadThread(context.Background(), tc.actor, "build", "root", "", 10)74 },75 "activity": func() (any, error) { return b.ReadChatActivity(context.Background(), tc.actor, "build", "root") },76 } {77 t.Run(name, func(t *testing.T) {78 before := reads.Load()79 result, err := read()80 if tc.denied {81 if err == nil || reads.Load() != before {82 t.Fatalf("mismatched actor must fail before forwarding: err=%v upstream reads=%d", err, reads.Load()-before)83 }84 return85 }86 if err != nil {87 t.Fatal(err)88 }89 raw, err := json.Marshal(result)90 if err != nil {91 t.Fatal(err)92 }93 private := owner94 if name == "query" {95 private = "session-only-article"96 }97 if !strings.Contains(string(raw), "public-article") || strings.Contains(string(raw), private) != (tc.actor == owner) {98 t.Fatalf("read used wrong actor: %s", raw)99 }100 })101 }102 })103 }104}