clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / daemon / collaboration_acceptance_test.go
collaboration_acceptance_test.go 10.6K
1package daemon23import (4 "context"5 "encoding/json"6 "fmt"7 "net/url"8 "strings"9 "testing"1011 "github.com/FelineStateMachine/tinyrelay/internal/event"12 "github.com/FelineStateMachine/tinyrelay/internal/relay"13 "github.com/FelineStateMachine/tinyrelay/internal/storage"14)1516const collaborationCoordinateRepo = "collaboration"1718func collaborationFixtureRepo(t *testing.T, tenant *Tenant) (string, string) {19 t.Helper()20 ctx := context.Background()21 ownerSecret := strings.Repeat("0", 63) + "1"22 owner := tenant.Policy().Owner23 serviceURL, err := url.Parse(tenant.RelayURL())24 if err != nil {25 t.Fatal(err)26 }27 cloneURL := "http://" + serviceURL.Host + "/" + owner + "/" + collaborationCoordinateRepo + ".git"28 relayURL := "ws://" + serviceURL.Host29 announcement := event.Event{30 Kind: event.KIND_REPO,31 PubKey: owner,32 CreatedAt: 100,33 Tags: [][]string{{"d", collaborationCoordinateRepo}, {"clone", cloneURL}, {"relays", relayURL}},34 }35 if err := event.Sign(&announcement, ownerSecret); err != nil {36 t.Fatal(err)37 }38 if _, err := tenant.router.Publish(ctx, announcement, relay.Session{PubKeys: []string{owner}}); err != nil {39 t.Fatal(err)40 }41 return owner, "30617:" + owner + ":" + collaborationCoordinateRepo42}4344func signedCollaborationEvent(t *testing.T, secret string, kind int, created int64, tags [][]string, content string) event.Event {45 t.Helper()46 e := event.Event{Kind: kind, CreatedAt: created, Tags: tags, Content: content}47 if err := event.Sign(&e, secret); err != nil {48 t.Fatal(err)49 }50 return e51}5253func saveCollaborationEvents(t *testing.T, tenant *Tenant, rows ...event.Event) {54 t.Helper()55 for _, row := range rows {56 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {57 t.Fatal(err)58 }59 }60}6162func browseCollaboration(t *testing.T, tenant *Tenant, actor, method string, args map[string]any) map[string]any {63 t.Helper()64 params, err := json.Marshal(args)65 if err != nil {66 t.Fatal(err)67 }68 value, err := tenant.Execute(context.Background(), actor, method, []json.RawMessage{params})69 if err != nil {70 t.Fatal(err)71 }72 result, ok := value.(map[string]any)73 if !ok {74 t.Fatalf("%s result = %T, want object", method, value)75 }76 return result77}7879func browseCollaborationDetail(t *testing.T, tenant *Tenant, actor, method string, args map[string]any) collaborationDetail {80 t.Helper()81 params, err := json.Marshal(args)82 if err != nil {83 t.Fatal(err)84 }85 value, err := tenant.Execute(context.Background(), actor, method, []json.RawMessage{params})86 if err != nil {87 t.Fatal(err)88 }89 detail, ok := value.(collaborationDetail)90 if !ok {91 t.Fatalf("%s result = %T, want collaboration detail", method, value)92 }93 return detail94}9596func TestCollaborationListPaginationKeepsTimestampTies(t *testing.T) {97 _, tenant := testTenant(t)98 p := tenant.Policy()99 p.Features.Grasp = true100 p.Features.Grasp02 = true101 p.Features.Grasp05 = true102 p.GuestReplies = true103 if err := tenant.applyPolicy(context.Background(), p); err != nil {104 t.Fatal(err)105 }106 owner, coordinate := collaborationFixtureRepo(t, tenant)107 secret := strings.Repeat("1", 63) + "2"108 var ids []string109 for i := 0; i < 9; i++ {110 row := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, 200,111 [][]string{{"a", coordinate}, {"subject", fmt.Sprintf("Issue %02d", i)}}, "body")112 saveCollaborationEvents(t, tenant, row)113 ids = append(ids, row.ID)114 }115 seen := map[string]bool{}116 cursor := ""117 for page := 0; page < 20; page++ {118 result := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "cursor": cursor})119 items, ok := result["items"].([]collaborationItem)120 if !ok {121 t.Fatalf("items = %T", result["items"])122 }123 for _, item := range items {124 id := item.ID125 if seen[id] {126 t.Fatalf("duplicate item %s", id)127 }128 seen[id] = true129 }130 cursor, _ = result["next_cursor"].(string)131 if cursor == "" {132 break133 }134 }135 if len(seen) != len(ids) {136 t.Fatalf("pagination returned %d issues, want %d", len(seen), len(ids))137 }138}139140func TestCollaborationSearchAndStatusReachPastFirstPage(t *testing.T) {141 _, tenant := testTenant(t)142 p := tenant.Policy()143 p.Features.Grasp = true144 p.Features.Grasp02 = true145 p.Features.Grasp05 = true146 p.GuestReplies = true147 if err := tenant.applyPolicy(context.Background(), p); err != nil {148 t.Fatal(err)149 }150 owner, coordinate := collaborationFixtureRepo(t, tenant)151 secret := strings.Repeat("2", 63) + "3"152 for i := 0; i < 60; i++ {153 row := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, int64(300+i),154 [][]string{{"a", coordinate}, {"subject", fmt.Sprintf("Routine issue %02d", i)}}, "ordinary body")155 if i == 59 {156 row.Tags = append(row.Tags, []string{"t", "security"})157 row.Content = "Security regression"158 }159 saveCollaborationEvents(t, tenant, row)160 if i == 59 {161 status := signedCollaborationEvent(t, strings.Repeat("0", 63)+"1", 1632, 500,162 [][]string{{"e", row.ID}}, "closed")163 saveCollaborationEvents(t, tenant, status)164 }165 }166 result := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "q": "security", "label": "security"})167 items := result["items"].([]collaborationItem)168 if len(items) != 1 || items[0].Status != "closed" {169 t.Fatalf("search result = %#v", result)170 }171 filtered := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "state": "closed"})172 if len(filtered["items"].([]collaborationItem)) != 1 {173 t.Fatalf("status filter = %#v", filtered)174 }175}176177func TestCollaborationReplyPaginationUsesCursorAndFiltersMalformedEvents(t *testing.T) {178 _, tenant := testTenant(t)179 p := tenant.Policy()180 p.Features.Grasp = true181 p.Features.Grasp02 = true182 p.Features.Grasp05 = true183 p.GuestReplies = true184 if err := tenant.applyPolicy(context.Background(), p); err != nil {185 t.Fatal(err)186 }187 owner, coordinate := collaborationFixtureRepo(t, tenant)188 authorSecret := strings.Repeat("8", 63) + "9"189 author, err := event.PublicKey(authorSecret)190 if err != nil {191 t.Fatal(err)192 }193 issue := signedCollaborationEvent(t, authorSecret, event.KIND_GIT_ISSUE, 800,194 [][]string{{"a", coordinate}, {"subject", "Crowded thread"}}, "body")195 rows := []event.Event{issue}196 for i := 0; i < 205; i++ {197 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,198 [][]string{{"E", issue.ID}, {"K", "1621"}, {"P", author}}, fmt.Sprintf("reply %03d", i)))199 }200 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,201 [][]string{{"E", issue.ID}, {"K", "1618"}, {"P", author}}, "wrong root kind"))202 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,203 [][]string{{"E", issue.ID}, {"K", "1621"}, {"P", strings.Repeat("a", 64)}}, "wrong root author"))204 saveCollaborationEvents(t, tenant, rows...)205 seen := map[string]bool{}206 cursor := ""207 for page := 0; page < 20; page++ {208 result := browseCollaborationDetail(t, tenant, owner, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID, "limit": 37, "cursor": cursor})209 if len(result.Replies) > 37 {210 t.Fatalf("reply page has %d entries", len(result.Replies))211 }212 for _, reply := range result.Replies {213 if seen[reply.ID] {214 t.Fatalf("duplicate reply %s", reply.ID)215 }216 seen[reply.ID] = true217 }218 cursor = result.ReplyCursor219 if cursor == "" {220 break221 }222 }223 if len(seen) != 205 {224 t.Fatalf("reply pagination returned %d replies, want 205", len(seen))225 }226}227228func TestCollaborationDetailSeparatesRepliesAndAuthorizesStatus(t *testing.T) {229 _, tenant := testTenant(t)230 p := tenant.Policy()231 p.Features.Grasp = true232 p.Features.Grasp02 = true233 p.Features.Grasp05 = true234 p.GuestReplies = true235 if err := tenant.applyPolicy(context.Background(), p); err != nil {236 t.Fatal(err)237 }238 owner, coordinate := collaborationFixtureRepo(t, tenant)239 authorSecret := strings.Repeat("3", 63) + "4"240 author, err := event.PublicKey(authorSecret)241 if err != nil {242 t.Fatal(err)243 }244 issue := signedCollaborationEvent(t, authorSecret, event.KIND_GIT_ISSUE, 600,245 [][]string{{"a", coordinate}, {"subject", "Needs review"}, {"t", "bug"}}, "Please investigate")246 reply := signedCollaborationEvent(t, strings.Repeat("4", 63)+"5", 1111, 601,247 [][]string{{"e", issue.ID}, {"E", issue.ID}, {"K", "1621"}, {"P", author}}, "I can reproduce this")248 status := signedCollaborationEvent(t, authorSecret, 1631, 602, [][]string{{"e", issue.ID}}, "resolved")249 forged := signedCollaborationEvent(t, strings.Repeat("5", 63)+"6", 1632, 603, [][]string{{"e", issue.ID}}, "closed")250 saveCollaborationEvents(t, tenant, issue, reply, status, forged)251 for _, actor := range []string{owner, author} {252 result := browseCollaborationDetail(t, tenant, actor, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID})253 if !result.CanStatus {254 t.Fatalf("actor %s cannot status: %#v", actor, result)255 }256 if len(result.Replies) != 1 || len(result.Statuses) != 1 {257 t.Fatalf("authorized detail thread leaked or duplicated events: %#v", result)258 }259 }260 unauthorized := strings.Repeat("6", 63) + "7"261 unauthorizedKey, err := event.PublicKey(unauthorized)262 if err != nil {263 t.Fatal(err)264 }265 result := browseCollaborationDetail(t, tenant, unauthorizedKey, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID})266 if result.CanStatus {267 t.Fatalf("unauthorized actor can status: %#v", result)268 }269}270271func TestPrivateCollaborationRejectsAnonymousAndWrongRepository(t *testing.T) {272 ctx := context.Background()273 secret := strings.Repeat("7", 64)274 owner, err := event.PublicKey(secret)275 if err != nil {276 t.Fatal(err)277 }278 _, tenant, server := privatePeerTenant(t, "collaboration-private", owner)279 repo, _, _ := seedPrivateRepository(t, tenant, owner, secret, "private", server.URL)280 coordinate := "30617:" + owner + ":" + repo.Identifier281 issue := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, 700,282 [][]string{{"a", coordinate}, {"subject", "Private issue"}}, "secret")283 saveCollaborationEvents(t, tenant, issue)284 params := []json.RawMessage{json.RawMessage(fmt.Sprintf(`{"owner":%q,"repo":%q,"event":%q}`, owner, repo.Identifier, issue.ID))}285 if _, err := tenant.Execute(ctx, "", "browseissue", params); err == nil {286 t.Fatal("anonymous private collaboration detail was exposed")287 }288 _, _, _ = seedPrivateRepository(t, tenant, owner, secret, "other", server.URL)289 if _, err := tenant.Execute(ctx, owner, "browseissue", []json.RawMessage{json.RawMessage(fmt.Sprintf(`{"owner":%q,"repo":%q,"event":%q}`, owner, "other", issue.ID))}); err == nil {290 t.Fatal("collaboration root crossed repository boundary")291 }292}