tinyrelayfile
tinyrelay/internal/daemon/custom_views_access_test.go/

internal/daemon/custom_views_access_test.go raw
1package daemon
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/FelineStateMachine/tinyrelay/internal/event"
15 "github.com/FelineStateMachine/tinyrelay/internal/relay"
16 "github.com/FelineStateMachine/tinyrelay/internal/views"
17)
18
19func TestCustomViewArtifactSourceVisibilityCoversMediaAndEventReads(t *testing.T) {
20 tenant, server, _ := viewTenant(t, []int{1}, nil)
21 server.answer(http.StatusOK, svgArtifact(0))
22 first := signedEvent(t, testOwnerSecret, 1, time.Now().Unix(), nil, "```mermaid\ngraph TD; a-->b;\n```")
23 if err := publishAs(t, tenant, first); err != nil {
24 t.Fatal(err)
25 }
26 runPendingView(t, tenant, "diagrams", first.ID)
27 second := signedEvent(t, testOwnerSecret, 1, time.Now().Unix()+1, nil, first.Content)
28 if err := publishAs(t, tenant, second); err != nil {
29 t.Fatal(err)
30 }
31 runPendingView(t, tenant, "diagrams", second.ID)
32 ctx := context.Background()
33 if _, err := tenant.store.DB().ExecContext(ctx, `INSERT INTO hidden_events(id,reason) VALUES(?,'test')`, first.ID); err != nil {
34 t.Fatal(err)
35 }
36 _, image, ok := strings.Cut(views.Figure("diagrams", "mermaid", "graph TD; a-->b;"), `<img src="`)
37 if !ok {
38 t.Fatal("custom view has no image")
39 }
40 path, _, _ := strings.Cut(image, `"`)
41 if response := getArtifact(t, tenant, path, ""); response.Code != http.StatusOK {
42 t.Fatalf("visible source should serve shared media: %d", response.Code)
43 }
44 hash := views.Hash("mermaid", "graph TD; a-->b;")
45 rows, err := tenant.Query(ctx, []event.Filter{{Kinds: []int{event.KIND_VIEW}, Tags: map[string][]string{"d": {"bind.ws/view/diagrams/" + hash}}}}, relay.Session{})
46 if err != nil {
47 t.Fatal(err)
48 }
49 if len(rows) != 0 {
50 t.Fatalf("artifact event leaked a hidden source: %d", len(rows))
51 }
52}
53
54func TestCustomViewArtifactSourceLifecycleBlocksMediaAndEventReads(t *testing.T) {
55 tests := []struct {
56 name string
57 mutate func(context.Context, *Tenant, event.Event) error
58 }{
59 {name: "hidden", mutate: func(ctx context.Context, tenant *Tenant, source event.Event) error {
60 _, err := tenant.store.DB().ExecContext(ctx, `INSERT INTO hidden_events(id,reason) VALUES(?,'test')`, source.ID)
61 return err
62 }},
63 {name: "deleted", mutate: func(ctx context.Context, tenant *Tenant, source event.Event) error {
64 _, err := tenant.store.DB().ExecContext(ctx, `DELETE FROM events WHERE id=?`, source.ID)
65 return err
66 }},
67 {name: "expired", mutate: func(ctx context.Context, tenant *Tenant, source event.Event) error {
68 _, err := tenant.store.DB().ExecContext(ctx, `UPDATE events SET expires=? WHERE id=?`, time.Now().Unix()-1, source.ID)
69 return err
70 }},
71 }
72 for _, tc := range tests {
73 t.Run(tc.name, func(t *testing.T) {
74 tenant, server, _ := viewTenant(t, []int{1}, nil)
75 server.answer(http.StatusOK, svgArtifact(0))
76 source := signedEvent(t, testOwnerSecret, 1, time.Now().Unix(), nil, "```mermaid\nA\n```")
77 if err := publishAs(t, tenant, source); err != nil {
78 t.Fatal(err)
79 }
80 runPendingView(t, tenant, "diagrams", source.ID)
81 hash := views.Hash("mermaid", "A")
82 path := "/views/diagrams/" + hash + ".svg"
83 if response := getArtifact(t, tenant, path, ""); response.Code != http.StatusOK {
84 t.Fatalf("baseline artifact: %d", response.Code)
85 }
86 if err := tc.mutate(context.Background(), tenant, source); err != nil {
87 t.Fatal(err)
88 }
89 if response := getArtifact(t, tenant, path, ""); response.Code != http.StatusNotFound {
90 t.Fatalf("inaccessible source served media: %d", response.Code)
91 }
92 rows, err := tenant.Query(context.Background(), []event.Filter{{Kinds: []int{event.KIND_VIEW}, Tags: map[string][]string{"d": {"bind.ws/view/diagrams/" + hash}}}}, relay.Session{})
93 if err != nil {
94 t.Fatal(err)
95 }
96 if len(rows) != 0 {
97 t.Fatalf("inaccessible source leaked artifact event: %d", len(rows))
98 }
99 })
100 }
101}
102
103func TestCustomViewDoesNotTransformExpiredQueuedSource(t *testing.T) {
104 tenant, server, _ := viewTenant(t, []int{1}, nil)
105 server.answer(http.StatusOK, svgArtifact(0))
106 now := time.Now().Unix()
107 source := signedEvent(t, testOwnerSecret, 1, now, [][]string{{"expiration", fmt.Sprint(now + 3600)}}, "```mermaid\nA\n```")
108 if err := publishAs(t, tenant, source); err != nil {
109 t.Fatal(err)
110 }
111 queued := pendingViewIntents(t, tenant, "diagrams", source.ID)
112 if len(queued) != 1 {
113 t.Fatalf("queued intents = %d", len(queued))
114 }
115 if _, err := tenant.store.DB().ExecContext(context.Background(), `UPDATE events SET expires=? WHERE id=?`, now-1, source.ID); err != nil {
116 t.Fatal(err)
117 }
118 runViewIntent(t, tenant, queued[0])
119 if server.count() != 0 {
120 t.Fatalf("expired source was sent to transform: %d requests", server.count())
121 }
122}
123
124func TestCustomViewArtifactCoordinateRequiresCurrentMatchingSource(t *testing.T) {
125 tenant, server, _ := viewTenant(t, []int{event.KIND_REPO_STATE}, nil)
126 ctx := context.Background()
127 p := tenant.Policy()
128 p.Features.Grasp = true
129 if err := tenant.applyPolicy(ctx, p); err != nil {
130 t.Fatal(err)
131 }
132 announcement := signedEvent(t, testOwnerSecret, event.KIND_REPO, time.Now().Unix(), [][]string{{"d", "docs"}, {"clone", "http://127.0.0.1:8080/npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d/docs.git"}, {"relays", "ws://127.0.0.1:8080"}}, "")
133 if err := publishAs(t, tenant, announcement); err != nil {
134 t.Fatal(err)
135 }
136 source := filepath.Join(t.TempDir(), "source.git")
137 gitTest(t, "init", "--bare", source)
138 work := filepath.Join(t.TempDir(), "work")
139 gitTest(t, "clone", source, work)
140 gitTest(t, "-C", work, "config", "user.email", "test@example.com")
141 gitTest(t, "-C", work, "config", "user.name", "Test")
142 if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Docs\n\n```mermaid\ngraph LR; a-->b;\n```\n"), 0o600); err != nil {
143 t.Fatal(err)
144 }
145 gitTest(t, "-C", work, "add", "README.md")
146 gitTest(t, "-C", work, "commit", "-m", "readme")
147 gitTest(t, "-C", work, "branch", "-M", "main")
148 sha := strings.TrimSpace(gitOutput(t, "-C", work, "rev-parse", "HEAD"))
149 state := signedEvent(t, testOwnerSecret, event.KIND_REPO_STATE, time.Now().Unix(), [][]string{{"d", "docs"}, {"HEAD", "ref: refs/heads/main"}, {"refs/heads/main", sha}}, "")
150 if err := publishAs(t, tenant, state); err != nil {
151 t.Fatal(err)
152 }
153 relayServer := httptest.NewServer(tenant)
154 defer relayServer.Close()
155 gitTest(t, "-C", work, "push", relayServer.URL+"/npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d/docs.git", "refs/heads/main:refs/heads/main")
156 queued := pendingViewIntents(t, tenant, "diagrams", state.ID)
157 if len(queued) != 1 {
158 t.Fatalf("state intents = %d", len(queued))
159 }
160 server.answer(http.StatusOK, svgArtifact(0))
161 runViewIntent(t, tenant, queued[0])
162 hash := views.Hash("mermaid", "graph LR; a-->b;")
163 path := "/views/diagrams/" + hash + ".svg"
164 if blocks, blockErr := tenant.customViews.sourceBlocks(ctx, state); blockErr != nil {
165 t.Fatalf("initial source blocks: %v", blockErr)
166 } else if len(blocks) != 1 {
167 t.Fatalf("initial source blocks = %+v", blocks)
168 }
169 if response := getArtifact(t, tenant, path, ""); response.Code != http.StatusOK {
170 t.Fatalf("artifact through current state: %d", response.Code)
171 }
172 rows, err := tenant.Query(ctx, []event.Filter{{Kinds: []int{event.KIND_VIEW}, Tags: map[string][]string{"d": {"bind.ws/view/diagrams/" + hash}}}}, relay.Session{})
173 if err != nil {
174 t.Fatal(err)
175 }
176 if len(rows) != 1 {
177 t.Fatalf("artifact query through current state = %d, want 1", len(rows))
178 }
179 if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Docs\n\nNo diagrams here.\n"), 0o600); err != nil {
180 t.Fatal(err)
181 }
182 gitTest(t, "-C", work, "add", "README.md")
183 gitTest(t, "-C", work, "commit", "-m", "remove diagram")
184 newSHA := strings.TrimSpace(gitOutput(t, "-C", work, "rev-parse", "HEAD"))
185 replacement := signedEvent(t, testOwnerSecret, event.KIND_REPO_STATE, time.Now().Unix()+1, [][]string{{"d", "docs"}, {"HEAD", "ref: refs/heads/main"}, {"refs/heads/main", newSHA}}, "")
186 if err := publishAs(t, tenant, replacement); err != nil {
187 t.Fatal(err)
188 }
189 gitTest(t, "-C", work, "push", relayServer.URL+"/npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d/docs.git", "refs/heads/main:refs/heads/main")
190 if _, ok := storedArtifact(t, tenant, "diagrams", hash); !ok {
191 t.Fatal("replacement removed the artifact before the access check")
192 }
193 if response := getArtifact(t, tenant, path, ""); response.Code != http.StatusNotFound {
194 t.Fatalf("stale coordinate served media: %d", response.Code)
195 }
196 rows, err = tenant.Query(ctx, []event.Filter{{Kinds: []int{event.KIND_VIEW}, Tags: map[string][]string{"d": {"bind.ws/view/diagrams/" + hash}}}}, relay.Session{})
197 if err != nil {
198 t.Fatal(err)
199 }
200 if len(rows) != 0 {
201 t.Fatalf("stale coordinate leaked artifact event: %d", len(rows))
202 }
203}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs