| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/relay" |
| 13 | ) |
| 14 | |
| 15 | const ( |
| 16 | wikiOwnerSecret = "0000000000000000000000000000000000000000000000000000000000000001" |
| 17 | wikiMemberSecret = "2222222222222222222222222222222222222222222222222222222222222222" |
| 18 | wikiOtherSecret = "3333333333333333333333333333333333333333333333333333333333333333" |
| 19 | wikiThirdSecret = "4444444444444444444444444444444444444444444444444444444444444444" |
| 20 | ) |
| 21 | |
| 22 | func wikiKey(t *testing.T, secret string) string { |
| 23 | t.Helper() |
| 24 | pk, err := event.PublicKey(secret) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | return pk |
| 29 | } |
| 30 | |
| 31 | func wikiPublish(t *testing.T, tenant *Tenant, secret string, kind int, createdAt int64, content string, tags ...[]string) event.Event { |
| 32 | t.Helper() |
| 33 | e := event.Event{Kind: kind, CreatedAt: createdAt, Content: content, Tags: tags} |
| 34 | if err := event.Sign(&e, secret); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if _, err := tenant.Publish(context.Background(), e, relay.Session{PubKeys: []string{e.PubKey}, RelayURL: tenant.RelayURL()}); err != nil { |
| 38 | t.Fatalf("publish kind %d: %v" , kind, err) |
| 39 | } |
| 40 | return e |
| 41 | } |
| 42 | |
| 43 | func wikiArticle(t *testing.T, tenant *Tenant, secret, title string, createdAt int64, extra ...[]string) event.Event { |
| 44 | t.Helper() |
| 45 | tags := append([][]string{{"d" , strings.ToLower(strings.ReplaceAll(title, " " , "-" ))}, {"title" , title}, {"summary" , "Summary about " + title}}, extra...) |
| 46 | return wikiPublish(t, tenant, secret, kindWikiArticle, createdAt, "Content of " +title+" with a [[Wiki Link]]." , tags...) |
| 47 | } |
| 48 | |
| 49 | func wikiMember(t *testing.T, tenant *Tenant, pubkey string) { |
| 50 | t.Helper() |
| 51 | if _, err := tenant.community.Execute(context.Background(), tenant.Policy().Owner, "setmember" , []json.RawMessage{json.RawMessage(fmt.Sprintf("%q" , pubkey)), json.RawMessage(fmt.Sprintf(`{"name":"member-%s"}` , pubkey[:8]))}); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func wikiCall(t *testing.T, tenant *Tenant, actor, method string, params map[string]any) map[string]any { |
| 57 | t.Helper() |
| 58 | raw, err := json.Marshal(params) |
| 59 | if err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | value, err := tenant.Execute(context.Background(), actor, method, []json.RawMessage{raw}) |
| 63 | if err != nil { |
| 64 | t.Fatalf("%s %s: %v" , method, raw, err) |
| 65 | } |
| 66 | encoded, err := json.Marshal(value) |
| 67 | if err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | var result map[string]any |
| 71 | if err := json.Unmarshal(encoded, &result); err != nil { |
| 72 | t.Fatal(err) |
| 73 | } |
| 74 | return result |
| 75 | } |
| 76 | |
| 77 | func wikiItems(result map[string]any, key string) []map[string]any { |
| 78 | var items []map[string]any |
| 79 | for _, item := range result[key].([]any) { |
| 80 | items = append(items, item.(map[string]any)) |
| 81 | } |
| 82 | return items |
| 83 | } |
| 84 | |
| 85 | func TestWikiPreferenceOrder(t *testing.T) { |
| 86 | _, tenant := testTenant(t) |
| 87 | owner := tenant.Policy().Owner |
| 88 | member, other, third := wikiKey(t, wikiMemberSecret), wikiKey(t, wikiOtherSecret), wikiKey(t, wikiThirdSecret) |
| 89 | wikiMember(t, tenant, member) |
| 90 | wikiMember(t, tenant, third) |
| 91 | now := time.Now().Unix() |
| 92 | |
| 93 | ownerBitcoin := wikiArticle(t, tenant, wikiOwnerSecret, "Bitcoin" , now-300) |
| 94 | memberBitcoin := wikiArticle(t, tenant, wikiMemberSecret, "Bitcoin" , now-200) |
| 95 | otherBitcoin := wikiArticle(t, tenant, wikiOtherSecret, "Bitcoin" , now-100) |
| 96 | |
| 97 | memberLightning := wikiArticle(t, tenant, wikiMemberSecret, "Lightning" , now-300) |
| 98 | otherLightning := wikiArticle(t, tenant, wikiOtherSecret, "Lightning" , now-200) |
| 99 | wikiPublish(t, tenant, wikiOwnerSecret, kindReaction, now-150, "+" , []string{"e" , memberLightning.ID}) |
| 100 | wikiPublish(t, tenant, wikiThirdSecret, kindReaction, now-140, "" , []string{"a" , "30818:" + member + ":lightning" }) |
| 101 | wikiPublish(t, tenant, wikiThirdSecret, kindReaction, now-130, "-" , []string{"e" , memberLightning.ID}) |
| 102 | // Non-members do not count, and one member counts once per version. |
| 103 | wikiPublish(t, tenant, wikiOtherSecret, kindReaction, now-120, "+" , []string{"e" , otherLightning.ID}) |
| 104 | wikiPublish(t, tenant, wikiThirdSecret, kindReaction, now-110, "+" , []string{"e" , memberLightning.ID}) |
| 105 | |
| 106 | wikiArticle(t, tenant, wikiMemberSecret, "Nostr" , now-300) |
| 107 | otherNostr := wikiArticle(t, tenant, wikiOtherSecret, "Nostr" , now-200) |
| 108 | |
| 109 | page := wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "bitcoin" }) |
| 110 | version := page["version" ].(map[string]any) |
| 111 | if version["author" ] != owner || page["preferred_by" ] != "owner" || version["id" ] != ownerBitcoin.ID { |
| 112 | t.Fatalf("owner preference %v" , page) |
| 113 | } |
| 114 | if version["content" ] == "" || fmt.Sprint(version["links" ]) != "[wiki-link]" { |
| 115 | t.Fatalf("preferred version content %v" , version) |
| 116 | } |
| 117 | versions := wikiItems(page, "versions" ) |
| 118 | if len(versions) != 3 || versions[0]["id" ] != otherBitcoin.ID || versions[1]["id" ] != memberBitcoin.ID || versions[2]["id" ] != ownerBitcoin.ID { |
| 119 | t.Fatalf("versions newest first %v" , versions) |
| 120 | } |
| 121 | if _, ok := versions[0]["content" ]; ok { |
| 122 | t.Fatalf("version list carries content %v" , versions[0]) |
| 123 | } |
| 124 | page = wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "Bitcoin" , "author" : member}) |
| 125 | if page["version" ].(map[string]any)["id" ] != memberBitcoin.ID || page["preferred_by" ] != "author" { |
| 126 | t.Fatalf("author preference %v" , page) |
| 127 | } |
| 128 | page = wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "bitcoin" , "version" : otherBitcoin.ID}) |
| 129 | if page["version" ].(map[string]any)["id" ] != otherBitcoin.ID || page["preferred_by" ] != "version" { |
| 130 | t.Fatalf("version selection %v" , page) |
| 131 | } |
| 132 | if _, err := tenant.Execute(context.Background(), "" , "browsewikipage" , []json.RawMessage{json.RawMessage(`{"d":"bitcoin","version":"` + strings.Repeat("f" , 64) + `"}` )}); err == nil { |
| 133 | t.Fatal("unknown version selected" ) |
| 134 | } |
| 135 | |
| 136 | page = wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "lightning" }) |
| 137 | version = page["version" ].(map[string]any) |
| 138 | if version["id" ] != memberLightning.ID || page["preferred_by" ] != "reactions" || version["likes" ].(float64) != 2 { |
| 139 | t.Fatalf("reaction preference %v" , page) |
| 140 | } |
| 141 | for _, v := range wikiItems(page, "versions" ) { |
| 142 | if v["id" ] == otherLightning.ID && v["likes" ].(float64) != 0 { |
| 143 | t.Fatalf("non-member like counted %v" , v) |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | page = wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "nostr" }) |
| 148 | if page["version" ].(map[string]any)["id" ] != otherNostr.ID || page["preferred_by" ] != "newest" { |
| 149 | t.Fatalf("newest preference %v" , page) |
| 150 | } |
| 151 | |
| 152 | list := wikiCall(t, tenant, "" , "browsewiki" , nil) |
| 153 | items := wikiItems(list, "items" ) |
| 154 | if len(items) != 3 || items[0]["d" ] != "bitcoin" || items[1]["d" ] != "lightning" || items[2]["d" ] != "nostr" || list["next_cursor" ] != "" { |
| 155 | t.Fatalf("page list %v" , list) |
| 156 | } |
| 157 | if items[0]["versions" ].(float64) != 3 || items[0]["version" ].(map[string]any)["author" ] != owner || items[1]["version" ].(map[string]any)["id" ] != memberLightning.ID { |
| 158 | t.Fatalf("page list preference %v" , items) |
| 159 | } |
| 160 | list = wikiCall(t, tenant, "" , "browsewiki" , map[string]any{"author" : other}) |
| 161 | if wikiItems(list, "items" )[0]["version" ].(map[string]any)["author" ] != other { |
| 162 | t.Fatalf("author preference in list %v" , list) |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | func TestWikiSearchAndPagination(t *testing.T) { |
| 167 | _, tenant := testTenant(t) |
| 168 | now := time.Now().Unix() |
| 169 | names := []string{"Alpha" , "Beta" , "Delta" , "Epsilon" , "Gamma" } |
| 170 | for i, name := range names { |
| 171 | wikiArticle(t, tenant, wikiOwnerSecret, name, now-int64(i)) |
| 172 | } |
| 173 | var got []string |
| 174 | cursor := "" |
| 175 | for page := 0; page < 5; page++ { |
| 176 | result := wikiCall(t, tenant, "" , "browsewiki" , map[string]any{"limit" : 2, "cursor" : cursor}) |
| 177 | for _, item := range wikiItems(result, "items" ) { |
| 178 | got = append(got, item["d" ].(string)) |
| 179 | } |
| 180 | cursor = result["next_cursor" ].(string) |
| 181 | if cursor == "" { |
| 182 | break |
| 183 | } |
| 184 | } |
| 185 | if fmt.Sprint(got) != "[alpha beta delta epsilon gamma]" { |
| 186 | t.Fatalf("paginated pages = %v" , got) |
| 187 | } |
| 188 | result := wikiCall(t, tenant, "" , "browsewiki" , map[string]any{"q" : "about BETA" }) |
| 189 | if items := wikiItems(result, "items" ); len(items) != 1 || items[0]["d" ] != "beta" { |
| 190 | t.Fatalf("summary search %v" , result) |
| 191 | } |
| 192 | result = wikiCall(t, tenant, "" , "browsewiki" , map[string]any{"q" : "gamm" }) |
| 193 | if items := wikiItems(result, "items" ); len(items) != 1 || items[0]["title" ] != "Gamma" { |
| 194 | t.Fatalf("title search %v" , result) |
| 195 | } |
| 196 | if items := wikiItems(wikiCall(t, tenant, "" , "browsewiki" , map[string]any{"q" : "zeta" }), "items" ); len(items) != 0 { |
| 197 | t.Fatalf("unmatched search %v" , items) |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | func TestWikiMergeRequestStatusFollowsDestinationReactions(t *testing.T) { |
| 202 | _, tenant := testTenant(t) |
| 203 | ctx := context.Background() |
| 204 | owner := tenant.Policy().Owner |
| 205 | member := wikiKey(t, wikiMemberSecret) |
| 206 | wikiMember(t, tenant, member) |
| 207 | now := time.Now().Unix() |
| 208 | original := wikiArticle(t, tenant, wikiOwnerSecret, "Bitcoin" , now-500) |
| 209 | coordinate := "30818:" + owner + ":bitcoin" |
| 210 | fork := wikiArticle(t, tenant, wikiMemberSecret, "Bitcoin" , now-400, []string{"a" , coordinate, "" , "fork" }, []string{"e" , original.ID, "" , "fork" }) |
| 211 | merge := wikiPublish(t, tenant, wikiMemberSecret, kindWikiMerge, now-300, "Added the block size limit" , []string{"a" , coordinate}, []string{"e" , original.ID}, []string{"p" , owner}, []string{"e" , fork.ID, "" , "source" }) |
| 212 | |
| 213 | detail := wikiCall(t, tenant, "" , "browsewikimerge" , map[string]any{"id" : merge.ID}) |
| 214 | m := detail["merge" ].(map[string]any) |
| 215 | if m["status" ] != "open" || m["source" ] != fork.ID || m["base" ] != original.ID || m["destination" ] != owner || m["target_d" ] != "bitcoin" { |
| 216 | t.Fatalf("merge detail %v" , m) |
| 217 | } |
| 218 | if detail["proposed" ].(map[string]any)["id" ] != fork.ID || detail["target" ].(map[string]any)["id" ] != original.ID { |
| 219 | t.Fatalf("merge versions %v" , detail) |
| 220 | } |
| 221 | if proposed := detail["proposed" ].(map[string]any); proposed["fork" ].(map[string]any)["e" ] != original.ID || proposed["fork" ].(map[string]any)["a" ] != coordinate { |
| 222 | t.Fatalf("fork reference %v" , proposed) |
| 223 | } |
| 224 | page := wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "bitcoin" }) |
| 225 | if merges := wikiItems(page, "merges" ); len(merges) != 1 || merges[0]["id" ] != merge.ID || merges[0]["status" ] != "open" { |
| 226 | t.Fatalf("page merges %v" , page) |
| 227 | } |
| 228 | if items := wikiItems(wikiCall(t, tenant, "" , "browsewiki" , nil), "items" ); items[0]["open_merges" ].(float64) != 1 { |
| 229 | t.Fatalf("open merge count %v" , items) |
| 230 | } |
| 231 | |
| 232 | // Only the destination author's reaction answers the request. |
| 233 | wikiPublish(t, tenant, wikiMemberSecret, kindReaction, now-250, "+" , []string{"e" , merge.ID}, []string{"p" , owner}) |
| 234 | if answer, err := tenant.wikiMergeStatus(ctx, merge.ID, owner); err != nil || answer.Status != wikiMergeOpen { |
| 235 | t.Fatalf("foreign reaction %+v %v" , answer, err) |
| 236 | } |
| 237 | accept := wikiPublish(t, tenant, wikiOwnerSecret, kindReaction, now-200, "+" , []string{"e" , merge.ID}, []string{"p" , member}) |
| 238 | answer, err := tenant.wikiMergeStatus(ctx, merge.ID, owner) |
| 239 | if err != nil || answer.Status != wikiMergeAccepted || answer.AnsweredAt != now-200 || answer.Reaction != accept.ID { |
| 240 | t.Fatalf("accepted %+v %v" , answer, err) |
| 241 | } |
| 242 | reject := wikiPublish(t, tenant, wikiOwnerSecret, kindReaction, now-100, "-" , []string{"e" , merge.ID}) |
| 243 | answer, err = tenant.wikiMergeStatus(ctx, merge.ID, owner) |
| 244 | if err != nil || answer.Status != wikiMergeRejected || answer.Reaction != reject.ID { |
| 245 | t.Fatalf("rejected %+v %v" , answer, err) |
| 246 | } |
| 247 | // Other reaction content does not answer. |
| 248 | wikiPublish(t, tenant, wikiOwnerSecret, kindReaction, now-50, "🤔" , []string{"e" , merge.ID}) |
| 249 | if answer, _ := tenant.wikiMergeStatus(ctx, merge.ID, owner); answer.Status != wikiMergeRejected { |
| 250 | t.Fatalf("emoji reaction changed the answer %+v" , answer) |
| 251 | } |
| 252 | detail = wikiCall(t, tenant, "" , "browsewikimerge" , map[string]any{"event" : merge.ID}) |
| 253 | if detail["merge" ].(map[string]any)["status" ] != "rejected" { |
| 254 | t.Fatalf("merge detail status %v" , detail) |
| 255 | } |
| 256 | if items := wikiItems(wikiCall(t, tenant, "" , "browsewiki" , nil), "items" ); items[0]["open_merges" ].(float64) != 0 { |
| 257 | t.Fatalf("answered merge still open %v" , items) |
| 258 | } |
| 259 | if _, err := tenant.Execute(ctx, "" , "browsewikimerge" , []json.RawMessage{json.RawMessage(`{"id":"` + original.ID + `"}` )}); err == nil { |
| 260 | t.Fatal("article served as a merge request" ) |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | func TestWikiRedirects(t *testing.T) { |
| 265 | _, tenant := testTenant(t) |
| 266 | owner := tenant.Policy().Owner |
| 267 | now := time.Now().Unix() |
| 268 | wikiArticle(t, tenant, wikiOwnerSecret, "Bitcoin" , now-100) |
| 269 | redirect := wikiPublish(t, tenant, wikiMemberSecret, kindWikiRedirect, now-50, "" , []string{"d" , "btc" }, []string{"a" , "30818:" + owner + ":bitcoin" }) |
| 270 | page := wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "bitcoin" }) |
| 271 | if to := wikiItems(page, "redirects_to" ); len(to) != 1 || to[0]["d" ] != "btc" || to[0]["id" ] != redirect.ID || len(wikiItems(page, "redirects_from" )) != 0 { |
| 272 | t.Fatalf("redirects to %v" , page) |
| 273 | } |
| 274 | page = wikiCall(t, tenant, "" , "browsewikipage" , map[string]any{"d" : "BTC" }) |
| 275 | if from := wikiItems(page, "redirects_from" ); page["version" ] != nil || len(from) != 1 || from[0]["target_d" ] != "bitcoin" || from[0]["target_author" ] != owner { |
| 276 | t.Fatalf("redirects from %v" , page) |
| 277 | } |
| 278 | if _, err := tenant.Execute(context.Background(), "" , "browsewikipage" , []json.RawMessage{json.RawMessage(`{"d":"missing"}` )}); err == nil || !strings.Contains(err.Error(), "not found" ) { |
| 279 | t.Fatalf("missing page %v" , err) |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | func TestWikiMembersOnlyHidesPagesFromGuests(t *testing.T) { |
| 284 | _, tenant := testTenant(t) |
| 285 | ctx := context.Background() |
| 286 | member, other := wikiKey(t, wikiMemberSecret), wikiKey(t, wikiOtherSecret) |
| 287 | wikiMember(t, tenant, member) |
| 288 | wikiArticle(t, tenant, wikiOwnerSecret, "Bitcoin" , time.Now().Unix()-10) |
| 289 | if items := wikiItems(wikiCall(t, tenant, "" , "browsewiki" , nil), "items" ); len(items) != 1 { |
| 290 | t.Fatalf("open relay list %v" , items) |
| 291 | } |
| 292 | p := tenant.Policy() |
| 293 | p.Reads = "members" |
| 294 | if err := tenant.applyPolicy(ctx, p); err != nil { |
| 295 | t.Fatal(err) |
| 296 | } |
| 297 | for _, actor := range []string{"" , other} { |
| 298 | for _, method := range []string{"browsewiki" , "browsewikipage" , "browsewikimerge" } { |
| 299 | if _, err := tenant.Execute(ctx, actor, method, []json.RawMessage{json.RawMessage(`{"d":"bitcoin","id":"` + strings.Repeat("a" , 64) + `"}` )}); err == nil { |
| 300 | t.Fatalf("%s exposed to %q" , method, actor) |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | if items := wikiItems(wikiCall(t, tenant, member, "browsewiki" , nil), "items" ); len(items) != 1 { |
| 305 | t.Fatalf("member list %v" , items) |
| 306 | } |
| 307 | if page := wikiCall(t, tenant, tenant.Policy().Owner, "browsewikipage" , map[string]any{"d" : "bitcoin" }); page["version" ] == nil { |
| 308 | t.Fatalf("owner page %v" , page) |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | func TestWikiMergeRequestWakesDestinationDevices(t *testing.T) { |
| 313 | _, tenant := testTenant(t) |
| 314 | ctx := context.Background() |
| 315 | owner := tenant.Policy().Owner |
| 316 | member, other := wikiKey(t, wikiMemberSecret), wikiKey(t, wikiOtherSecret) |
| 317 | wikiMember(t, tenant, member) |
| 318 | now := time.Now().Unix() |
| 319 | for _, pubkey := range []string{owner, other} { |
| 320 | if _, err := tenant.store.DB().ExecContext(ctx, "INSERT INTO web_push(endpoint,pubkey,subscription,created_at) VALUES(?,?,?,?)" , "https://push.example/" +pubkey[:8], pubkey, "{}" , now); err != nil { |
| 321 | t.Fatal(err) |
| 322 | } |
| 323 | } |
| 324 | wikiArticle(t, tenant, wikiOwnerSecret, "Bitcoin Basics" , now-100) |
| 325 | // A request aimed at someone who is not a member, and one the owner |
| 326 | // sends to themselves, wake nobody. |
| 327 | wikiPublish(t, tenant, wikiMemberSecret, kindWikiMerge, now-90, "" , []string{"a" , "30818:" + other + ":bitcoin-basics" }, []string{"p" , other}, []string{"e" , strings.Repeat("a" , 64), "" , "source" }) |
| 328 | wikiPublish(t, tenant, wikiOwnerSecret, kindWikiMerge, now-80, "" , []string{"a" , "30818:" + owner + ":bitcoin-basics" }, []string{"p" , owner}, []string{"e" , strings.Repeat("b" , 64), "" , "source" }) |
| 329 | if got := countIntents(t, tenant, notificationPush); got != 0 { |
| 330 | t.Fatalf("unexpected notifications %d" , got) |
| 331 | } |
| 332 | merge := wikiPublish(t, tenant, wikiMemberSecret, kindWikiMerge, now-70, "Please merge" , []string{"a" , "30818:" + owner + ":bitcoin-basics" }, []string{"p" , owner}, []string{"e" , strings.Repeat("c" , 64), "" , "source" }) |
| 333 | var payload string |
| 334 | if err := tenant.store.DB().QueryRowContext(ctx, "SELECT payload FROM work_intents WHERE kind=? AND target=?" , notificationPush, owner).Scan(&payload); err != nil { |
| 335 | t.Fatalf("notification intent: %v" , err) |
| 336 | } |
| 337 | var notice pushPayload |
| 338 | if err := json.Unmarshal([]byte(payload), ¬ice); err != nil { |
| 339 | t.Fatal(err) |
| 340 | } |
| 341 | if notice.Kind != pushReplies || notice.Text != "Merge request for Bitcoin Basics" || notice.URL != "http://relay.test/wiki/bitcoin-basics?merge=" +merge.ID { |
| 342 | t.Fatalf("notice %+v" , notice) |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | func TestWikiMergeFromReadsEveryTagShape(t *testing.T) { |
| 347 | owner := strings.Repeat("a" , 64) |
| 348 | base, source := strings.Repeat("b" , 64), strings.Repeat("c" , 64) |
| 349 | coordinate := "30818:" + owner + ":bitcoin" |
| 350 | cases := []struct { |
| 351 | name string |
| 352 | tags [][]string |
| 353 | base, source string |
| 354 | }{ |
| 355 | {"source marker" , [][]string{{"a" , coordinate}, {"e" , base}, {"p" , owner}, {"e" , source, "" , "source" }}, base, source}, |
| 356 | {"source tag" , [][]string{{"a" , coordinate}, {"e" , base}, {"source" , source}}, base, source}, |
| 357 | {"single e" , [][]string{{"a" , coordinate}, {"e" , source}}, "" , source}, |
| 358 | {"two plain e" , [][]string{{"a" , coordinate}, {"e" , base}, {"e" , source}}, base, source}, |
| 359 | } |
| 360 | for _, tc := range cases { |
| 361 | m := wikiMergeFrom(event.Event{Kind: kindWikiMerge, Tags: tc.tags}) |
| 362 | if m.Base != tc.base || m.Source != tc.source || m.Destination != owner || m.TargetD != "bitcoin" || m.Status != wikiMergeOpen { |
| 363 | t.Errorf("%s: %+v" , tc.name, m) |
| 364 | } |
| 365 | } |
| 366 | if m := wikiMergeFrom(event.Event{Kind: kindWikiMerge, Tags: [][]string{{"a" , "30617:" + owner + ":repo" }}}); m.Destination != "" || m.TargetD != "" { |
| 367 | t.Errorf("foreign coordinate accepted %+v" , m) |
| 368 | } |
| 369 | } |