forage_test.go raw
1 package forage
2
3 import (
4 "testing"
5
6 "git.mleku.dev/mleku/dendrite/pkg/gap"
7 "git.mleku.dev/mleku/dendrite/pkg/ratio"
8 )
9
10 func TestAppetite(t *testing.T) {
11 gaps := []gap.Gap{
12 {Type: "missing_site", Tag: "hashtag", Severity: ratio.Half, Count: 10},
13 {Type: "orphan_event", Tag: "event", Severity: ratio.New(4, 5), Count: 20},
14 {Type: "low_occupancy", Tag: "occupancy", Severity: ratio.New(7, 10), Count: 50},
15 }
16
17 needs := Appetite(gaps)
18 if len(needs) != 3 {
19 t.Fatalf("expected 3 needs, got %d", len(needs))
20 }
21
22 // Highest priority first.
23 if needs[0].Priority.Less(needs[1].Priority) {
24 t.Error("needs should be sorted by priority descending")
25 }
26
27 // Check need types.
28 if needs[0].Type != NeedSpecification { // orphan_event → specification
29 t.Errorf("expected specification need for orphan, got %s", needs[0].Type)
30 }
31 }
32
33 func TestURLForNeed(t *testing.T) {
34 tests := []struct {
35 need Need
36 want string
37 }{
38 {
39 need: Need{Topic: "NIP-42"},
40 want: "https://raw.githubusercontent.com/nostr-protocol/nips/master/42.md",
41 },
42 {
43 need: Need{Topic: "encoding/json"},
44 want: "https://pkg.go.dev/encoding/json",
45 },
46 {
47 need: Need{Topic: "how to improve occupancy"},
48 want: "", // no known URL pattern
49 },
50 }
51
52 for _, tt := range tests {
53 got := URLForNeed(tt.need)
54 if got != tt.want {
55 t.Errorf("URLForNeed(%q) = %q, want %q", tt.need.Topic, got, tt.want)
56 }
57 }
58 }
59
60 func TestExtractDomain(t *testing.T) {
61 tests := []struct {
62 url string
63 want string
64 }{
65 {"https://go.dev/doc/effective_go", "go.dev"},
66 {"https://pkg.go.dev/encoding/json", "pkg.go.dev"},
67 {"https://github.com:443/nostr/nips", "github.com"},
68 {"http://localhost:8080/path", "localhost"},
69 }
70
71 for _, tt := range tests {
72 got := extractDomain(tt.url)
73 if got != tt.want {
74 t.Errorf("extractDomain(%q) = %q, want %q", tt.url, got, tt.want)
75 }
76 }
77 }
78
79 func TestDomainWhitelist(t *testing.T) {
80 f := NewForager()
81
82 if !f.DomainWhitelist["go.dev"] {
83 t.Error("go.dev should be whitelisted")
84 }
85 if f.DomainWhitelist["evil.com"] {
86 t.Error("evil.com should not be whitelisted")
87 }
88 }
89
90 func TestDigestMarkdown(t *testing.T) {
91 md := []byte(`# NIP-42: Authentication
92
93 Relays may require authentication.
94
95 ` + "```go" + `
96 func handleAuth(ev *Event) {
97 // verify signature
98 }
99 ` + "```" + `
100
101 Create a challenge and send it to the client.
102 `)
103
104 elems := digestMarkdown(md)
105 if len(elems) == 0 {
106 t.Fatal("expected elements from markdown")
107 }
108
109 types := make(map[string]int)
110 for _, e := range elems {
111 types[e.Type()]++
112 }
113
114 if types["heading"] == 0 {
115 t.Error("expected heading elements")
116 }
117 if types["code"] == 0 {
118 t.Error("expected code elements")
119 }
120 if types["word"] == 0 {
121 t.Error("expected word elements")
122 }
123 }
124
125 func TestDigestGoSource(t *testing.T) {
126 src := []byte(`package main
127
128 import "fmt"
129
130 type Relay struct {
131 Name string
132 }
133
134 func (r *Relay) Start() {
135 fmt.Println("starting")
136 }
137 `)
138
139 elems := digestGoSource(src)
140 types := make(map[string]int)
141 for _, e := range elems {
142 types[e.Type()]++
143 }
144
145 if types["package"] == 0 {
146 t.Error("expected package element")
147 }
148 if types["import"] == 0 {
149 t.Error("expected import element")
150 }
151 if types["type"] == 0 {
152 t.Error("expected type element")
153 }
154 if types["func"] == 0 {
155 t.Error("expected func element")
156 }
157 }
158
159 func TestDigestHTML(t *testing.T) {
160 html := []byte(`<html><body><h1>Title</h1><p>Some text here with <b>bold</b> words.</p></body></html>`)
161 elems := digestHTML(html)
162 if len(elems) == 0 {
163 t.Fatal("expected elements from HTML")
164 }
165 }
166
167 func TestStripHTMLTags(t *testing.T) {
168 html := `<p>Hello <b>world</b></p>`
169 got := stripHTMLTags(html)
170 if got != " Hello world " {
171 t.Errorf("stripHTMLTags = %q", got)
172 }
173 }
174
175 func TestBandwidthCap(t *testing.T) {
176 f := NewForager()
177 f.MaxBytesPerGen = 100
178 f.SetGeneration(0)
179
180 if f.BytesRemaining() != 100 {
181 t.Errorf("expected 100 bytes remaining, got %d", f.BytesRemaining())
182 }
183
184 // Simulate usage.
185 f.mu.Lock()
186 f.bytesUsed = 80
187 f.mu.Unlock()
188
189 if f.BytesRemaining() != 20 {
190 t.Errorf("expected 20 bytes remaining, got %d", f.BytesRemaining())
191 }
192 }
193
194 func TestFilterUsefulSources(t *testing.T) {
195 records := []SourceRecord{
196 {URL: "https://go.dev/a", BondRatio: 0.8},
197 {URL: "https://go.dev/b", BondRatio: 0.1},
198 {URL: "https://go.dev/c", BondRatio: 0.5},
199 }
200
201 useful := FilterUsefulSources(records, 0.3)
202 if len(useful) != 2 {
203 t.Errorf("expected 2 useful sources, got %d", len(useful))
204 }
205 }
206
207 func TestDigestRoutesCorrectly(t *testing.T) {
208 // Markdown by URL suffix.
209 md := &FetchResult{
210 URL: "https://example.com/doc.md",
211 Body: []byte("# Hello\n\nWorld"),
212 StatusCode: 200,
213 }
214 elems := Digest(md)
215 if len(elems) == 0 {
216 t.Error("expected elements from .md URL")
217 }
218
219 // Go source.
220 goSrc := &FetchResult{
221 URL: "https://example.com/main.go",
222 Body: []byte("package main\n\nfunc main() {}"),
223 StatusCode: 200,
224 }
225 elems = Digest(goSrc)
226 hasFunc := false
227 for _, e := range elems {
228 if e.Type() == "func" {
229 hasFunc = true
230 }
231 }
232 if !hasFunc {
233 t.Error("expected func element from .go source")
234 }
235
236 // Failed fetch should return nil.
237 failed := &FetchResult{
238 URL: "https://example.com/404",
239 StatusCode: 404,
240 }
241 if Digest(failed) != nil {
242 t.Error("expected nil from failed fetch")
243 }
244 }
245