| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 10 | gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit" |
| 11 | ) |
| 12 | |
| 13 | func TestGitLiveEnabledRequiresGRASP02(t *testing.T) { |
| 14 | _, tenant := testTenant(t) |
| 15 | p := tenant.Policy() |
| 16 | p.Features.Grasp = true |
| 17 | p.Features.Grasp02 = false |
| 18 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | if tenant.gitLiveEnabled() { |
| 22 | t.Fatal("live synchronization enabled without GRASP-02" ) |
| 23 | } |
| 24 | p.Features.Grasp02 = true |
| 25 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | if !tenant.gitLiveEnabled() { |
| 29 | t.Fatal("live synchronization disabled with GRASP-02" ) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestGitLivePrivateTargetRequiresConfiguredPeer(t *testing.T) { |
| 34 | _, tenant := testTenant(t) |
| 35 | repo := gitrelay.Repository{Private: true} |
| 36 | if tenant.gitLiveAuthorized(repo, "wss://peer.example" ) { |
| 37 | t.Fatal("unconfigured private target was authorized" ) |
| 38 | } |
| 39 | p := tenant.Policy() |
| 40 | p.Features.Grasp, p.Features.Grasp02, p.Features.Grasp08 = true, true, true |
| 41 | p.Reads = "members" |
| 42 | p.PrivatePeers = []string{"wss://peer.example" } |
| 43 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | if !tenant.gitLiveAuthorized(repo, "wss://peer.example" ) { |
| 47 | t.Fatal("configured private target was rejected" ) |
| 48 | } |
| 49 | if tenant.gitLiveAuthorized(repo, "wss://other.example" ) { |
| 50 | t.Fatal("different private target was authorized" ) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestGitLiveFiltersIncludeConversationRootsAndReplies(t *testing.T) { |
| 55 | repo := gitrelay.Repository{Owner: strings.Repeat("a" , 64), Identifier: "repo" } |
| 56 | root := event.Event{Kind: event.KIND_GIT_ISSUE, ID: strings.Repeat("b" , 64)} |
| 57 | filters := gitLiveFiltersForRoots(repo, []event.Event{root}) |
| 58 | if len(filters) != 5 { |
| 59 | t.Fatalf("got %d filters, want metadata, conversation and two reply filters" , len(filters)) |
| 60 | } |
| 61 | for _, key := range []string{"e" , "E" } { |
| 62 | found := false |
| 63 | for _, filter := range filters { |
| 64 | if len(filter.Tags[key]) == 1 && filter.Tags[key][0] == root.ID { |
| 65 | found = true |
| 66 | } |
| 67 | } |
| 68 | if !found { |
| 69 | t.Fatalf("missing reply filter for %s" , key) |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestGitLiveCohortRespectsSocketCapAndRotates(t *testing.T) { |
| 75 | keys := make([]string, maxGitLivePeers+7) |
| 76 | for i := range keys { |
| 77 | keys[i] = strings.Repeat("a" , i+1) |
| 78 | } |
| 79 | first := gitLiveCohort(keys, 0) |
| 80 | second := gitLiveCohort(keys, maxGitLivePeers) |
| 81 | if len(first) != maxGitLivePeers || len(second) != maxGitLivePeers { |
| 82 | t.Fatalf("cohort sizes = %d, %d; want %d" , len(first), len(second), maxGitLivePeers) |
| 83 | } |
| 84 | _, firstHas := first[keys[maxGitLivePeers-1]] |
| 85 | _, secondHas := second[keys[maxGitLivePeers-1]] |
| 86 | if firstHas == secondHas { |
| 87 | t.Fatal("live cohort did not rotate" ) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | func TestRotateGitAuthorsUsesDurableOffset(t *testing.T) { |
| 92 | authors := []string{"a" , "b" , "c" } |
| 93 | got := rotateGitAuthors(authors, 1) |
| 94 | if strings.Join(got, "," ) != "b,c,a" { |
| 95 | t.Fatalf("rotated authors = %v" , got) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func TestStopGitLiveWorkersCancelsAndWaits(t *testing.T) { |
| 100 | done := make(chan struct{}) |
| 101 | canceled := make(chan struct{}) |
| 102 | workers := map[string]gitLiveWorker{"peer" : { |
| 103 | cancel: func() { close(canceled); close(done) }, |
| 104 | done: done, |
| 105 | }} |
| 106 | finished := make(chan struct{}) |
| 107 | go func() { |
| 108 | stopGitLiveWorkers(workers) |
| 109 | close(finished) |
| 110 | }() |
| 111 | select { |
| 112 | case <-canceled: |
| 113 | case <-time.After(time.Second): |
| 114 | t.Fatal("worker was not canceled" ) |
| 115 | } |
| 116 | select { |
| 117 | case <-finished: |
| 118 | case <-time.After(time.Second): |
| 119 | t.Fatal("stop returned before worker completion" ) |
| 120 | } |
| 121 | if len(workers) != 0 { |
| 122 | t.Fatal("stopped workers remain in the registry" ) |
| 123 | } |
| 124 | } |