| 1 | package policy |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 9 | ) |
| 10 | |
| 11 | func TestPatchOmittedAndEmptySections(t *testing.T) { |
| 12 | cur := Defaults("owner" ) |
| 13 | cur.Tags = []string{"one" } |
| 14 | cur.Features.Files = true |
| 15 | got, err := Patch(cur, map[string]json.RawMessage{ |
| 16 | "tags" : json.RawMessage(`[]` ), |
| 17 | "features" : json.RawMessage(`{"search":"off"}` ), |
| 18 | }) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | if len(got.Tags) != 0 || got.Features.Search != "off" || !got.Features.Files { |
| 23 | t.Fatalf("patch did not preserve or clear fields: %#v" , got) |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | func TestJobsDefaultOnUntilSwitchedOff(t *testing.T) { |
| 28 | if !Defaults("owner" ).Features.Jobs { |
| 29 | t.Fatal("long tasks are off by default" ) |
| 30 | } |
| 31 | var stored Policy |
| 32 | if err := json.Unmarshal([]byte(`{"owner":"owner","writes":"open","reads":"open","features":{"search":"prose"}}` ), &stored); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | if !stored.Features.Jobs { |
| 36 | t.Fatal("a stored policy without the field lost long tasks" ) |
| 37 | } |
| 38 | patched, err := Patch(Defaults("owner" ), map[string]json.RawMessage{"features" : json.RawMessage(`{"jobs":false}` )}) |
| 39 | if err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | if patched.Features.Jobs || patched.Features.Search != "prose" { |
| 43 | t.Fatalf("patch did not switch long tasks off: %#v" , patched.Features) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func TestPrivateReadRequiresRecipient(t *testing.T) { |
| 48 | p := Defaults("owner" ) |
| 49 | p.Reads = "members" |
| 50 | e := event.Event{Kind: 1059, Tags: [][]string{{"p" , "recipient" }}} |
| 51 | if CanRead(p, e, Access{Member: true, PubKeys: []string{"other" }}) { |
| 52 | t.Fatal("member without recipient identity can read private event" ) |
| 53 | } |
| 54 | if !CanRead(p, e, Access{Member: true, PubKeys: []string{"recipient" }}) { |
| 55 | t.Fatal("recipient cannot read private event" ) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestPolicyJSONHasNoHostedEconomy(t *testing.T) { |
| 60 | b, err := json.Marshal(Defaults(strings.Repeat("a" , 64))) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | for _, forbidden := range []string{"lease" , "fuel" , "maxBlobMB" , "eventsPerMinute" , "reqsPerMinute" } { |
| 65 | if strings.Contains(string(b), forbidden) { |
| 66 | t.Fatalf("policy contains hosted field %q: %s" , forbidden, b) |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestGRASP08RequiresPrivateMembershipReads(t *testing.T) { |
| 72 | p := Defaults(strings.Repeat("a" , 64)) |
| 73 | p.Features.Grasp08 = true |
| 74 | p.Features.Grasp = true |
| 75 | if err := Validate(p); err == nil || !strings.Contains(err.Error(), "reads" ) { |
| 76 | t.Fatalf("GRASP-08 with open reads accepted: %v" , err) |
| 77 | } |
| 78 | p.Reads = "members" |
| 79 | if err := Validate(p); err != nil { |
| 80 | t.Fatalf("valid private GRASP-08 policy rejected: %v" , err) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | func TestGRASP08RequiresBaseGRASP(t *testing.T) { |
| 85 | p := Defaults(strings.Repeat("a" , 64)) |
| 86 | p.Features.Grasp08 = true |
| 87 | p.Reads = "members" |
| 88 | p.Features.Grasp = false |
| 89 | if err := Validate(p); err == nil || !strings.Contains(err.Error(), "subfeatures" ) { |
| 90 | t.Fatalf("GRASP-08 without base GRASP accepted: %v" , err) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestPrivateRepositoryRequiresGRASP08PrivateTenant(t *testing.T) { |
| 95 | e := event.Event{Kind: 30617, Tags: [][]string{{"private" , "true" }}} |
| 96 | if CanRead(Policy{Reads: "open" }, e, Access{}) { |
| 97 | t.Fatal("private repository was readable on an open tenant" ) |
| 98 | } |
| 99 | p := Policy{Reads: "members" } |
| 100 | if !CanRead(p, event.Event{Kind: 1}, Access{Member: true}) { |
| 101 | t.Fatal("ordinary event became unreadable on a member tenant" ) |
| 102 | } |
| 103 | p.Features.Grasp08 = true |
| 104 | if !CanRead(p, e, Access{Member: true}) { |
| 105 | t.Fatal("private repository was unreadable on a GRASP-08 member tenant" ) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestGRASPHistoryExtensionsRequireGRASP02(t *testing.T) { |
| 110 | cases := []struct { |
| 111 | name string |
| 112 | set func(*Features) |
| 113 | }{ |
| 114 | {name: "grasp03" , set: func(f *Features) { f.Grasp03 = true }}, |
| 115 | {name: "grasp05" , set: func(f *Features) { f.Grasp05 = true }}, |
| 116 | } |
| 117 | for _, tc := range cases { |
| 118 | t.Run(tc.name, func(t *testing.T) { |
| 119 | p := Defaults(strings.Repeat("a" , 64)) |
| 120 | p.Features.Grasp = true |
| 121 | tc.set(&p.Features) |
| 122 | if err := Validate(p); err == nil || !strings.Contains(err.Error(), "grasp02" ) { |
| 123 | t.Fatalf("accepted %s without GRASP-02: %v" , tc.name, err) |
| 124 | } |
| 125 | p.Features.Grasp02 = true |
| 126 | if err := Validate(p); err != nil { |
| 127 | t.Fatalf("rejected %s with GRASP-02: %v" , tc.name, err) |
| 128 | } |
| 129 | }) |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestValidatePrivatePeers(t *testing.T) { |
| 134 | p := Defaults(strings.Repeat("a" , 64)) |
| 135 | p.PrivatePeers = []string{"wss://peer.example/relay/" } |
| 136 | if err := Validate(p); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | p.PrivatePeers = []string{"https://peer.example/?token=bad" } |
| 140 | if err := Validate(p); err == nil { |
| 141 | t.Fatal("accepted private peer URL with query credentials" ) |
| 142 | } |
| 143 | } |