dev vouch dev on at. thats about it
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add tests to check

Luna 2eb4a093 eb41f985

+490 -50
+397
cli/check_test.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "testing" 6 + ) 7 + 8 + // Fake identity directory: handle <-> DID mappings 9 + type fakeIdentity struct { 10 + handleToDID map[string]string 11 + didToHandle map[string]string 12 + } 13 + 14 + func newFakeIdentity(mappings map[string]string) *fakeIdentity { 15 + fi := &fakeIdentity{ 16 + handleToDID: make(map[string]string), 17 + didToHandle: make(map[string]string), 18 + } 19 + for handle, did := range mappings { 20 + fi.handleToDID[handle] = did 21 + fi.didToHandle[did] = handle 22 + } 23 + return fi 24 + } 25 + 26 + func (fi *fakeIdentity) resolveHandle(handle string) (string, error) { 27 + did, ok := fi.handleToDID[handle] 28 + if !ok { 29 + return "", fmt.Errorf("slingshot returned 404") 30 + } 31 + return did, nil 32 + } 33 + 34 + func (fi *fakeIdentity) resolveDidToHandle(did string) (string, error) { 35 + handle, ok := fi.didToHandle[did] 36 + if !ok { 37 + return "", fmt.Errorf("slingshot returned 404") 38 + } 39 + return handle, nil 40 + } 41 + 42 + // fakeGraph represents a vouch graph: voucherDID -> set of subjectDIDs they vouch for. 43 + // The microcosm API returns the reverse: given a target, who vouches for them. 44 + type fakeGraph struct { 45 + // vouches[subject] = set of DIDs that vouch for subject 46 + vouchers map[string][]string 47 + } 48 + 49 + func newFakeGraph() *fakeGraph { 50 + return &fakeGraph{vouchers: make(map[string][]string)} 51 + } 52 + 53 + // addVouch records that voucher vouches for subject. 54 + func (fg *fakeGraph) addVouch(voucher, subject string) { 55 + fg.vouchers[subject] = append(fg.vouchers[subject], voucher) 56 + } 57 + 58 + // fetchVouchers mimics the microcosm API: returns DIDs that vouch for targetDID. 59 + func (fg *fakeGraph) fetchVouchers(targetDID string) ([]string, error) { 60 + return fg.vouchers[targetDID], nil 61 + } 62 + 63 + func makeDeps(identity *fakeIdentity, graph *fakeGraph, myDID string, myVouches []string) checkDeps { 64 + return checkDeps{ 65 + myDID: myDID, 66 + resolveHandle: identity.resolveHandle, 67 + resolveDidToHandle: identity.resolveDidToHandle, 68 + fetchVouchers: graph.fetchVouchers, 69 + listMyVouches: func() ([]string, error) { 70 + return myVouches, nil 71 + }, 72 + } 73 + } 74 + 75 + func TestCheck_DirectVouch(t *testing.T) { 76 + identity := newFakeIdentity(map[string]string{ 77 + "alice.bsky.social": "did:plc:alice", 78 + "bob.bsky.social": "did:plc:bob", 79 + }) 80 + 81 + graph := newFakeGraph() 82 + // Graph doesn't matter for direct vouch — we check myVouches first 83 + 84 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 85 + 86 + result, err := checkWithDeps("bob.bsky.social", deps) 87 + if err != nil { 88 + t.Fatal(err) 89 + } 90 + 91 + if result.targetDID != "did:plc:bob" { 92 + t.Fatalf("expected targetDID did:plc:bob, got %s", result.targetDID) 93 + } 94 + if result.paths != nil { 95 + t.Fatalf("expected nil paths for direct vouch, got %v", result.paths) 96 + } 97 + } 98 + 99 + func TestCheck_NoRoutes(t *testing.T) { 100 + identity := newFakeIdentity(map[string]string{ 101 + "alice.bsky.social": "did:plc:alice", 102 + "charlie.bsky.social": "did:plc:charlie", 103 + }) 104 + 105 + graph := newFakeGraph() 106 + // No one vouches for charlie 107 + 108 + deps := makeDeps(identity, graph, "did:plc:alice", []string{}) 109 + 110 + result, err := checkWithDeps("charlie.bsky.social", deps) 111 + if err != nil { 112 + t.Fatal(err) 113 + } 114 + 115 + if len(result.paths) != 0 { 116 + t.Fatalf("expected no paths, got %v", result.paths) 117 + } 118 + } 119 + 120 + func TestCheck_TwoHopPath(t *testing.T) { 121 + // alice -> bob -> charlie 122 + // alice vouches for bob, bob vouches for charlie 123 + identity := newFakeIdentity(map[string]string{ 124 + "alice.bsky.social": "did:plc:alice", 125 + "bob.bsky.social": "did:plc:bob", 126 + "charlie.bsky.social": "did:plc:charlie", 127 + }) 128 + 129 + graph := newFakeGraph() 130 + graph.addVouch("did:plc:bob", "did:plc:charlie") // bob vouches for charlie 131 + 132 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 133 + 134 + result, err := checkWithDeps("charlie.bsky.social", deps) 135 + if err != nil { 136 + t.Fatal(err) 137 + } 138 + 139 + if len(result.paths) != 1 { 140 + t.Fatalf("expected 1 path, got %d", len(result.paths)) 141 + } 142 + 143 + path := result.paths[0] 144 + expected := []string{"did:plc:alice", "did:plc:bob", "did:plc:charlie"} 145 + if len(path) != len(expected) { 146 + t.Fatalf("expected path len %d, got %d", len(expected), len(path)) 147 + } 148 + for i := range expected { 149 + if path[i] != expected[i] { 150 + t.Fatalf("path[%d]: expected %s, got %s", i, expected[i], path[i]) 151 + } 152 + } 153 + 154 + // Check handle resolution 155 + if result.handleMap["did:plc:bob"] != "bob.bsky.social" { 156 + t.Fatalf("expected bob handle, got %s", result.handleMap["did:plc:bob"]) 157 + } 158 + } 159 + 160 + func TestCheck_ThreeHopPath(t *testing.T) { 161 + // alice -> bob -> carol -> dave 162 + identity := newFakeIdentity(map[string]string{ 163 + "alice.bsky.social": "did:plc:alice", 164 + "bob.bsky.social": "did:plc:bob", 165 + "carol.bsky.social": "did:plc:carol", 166 + "dave.bsky.social": "did:plc:dave", 167 + }) 168 + 169 + graph := newFakeGraph() 170 + graph.addVouch("did:plc:carol", "did:plc:dave") // carol vouches for dave 171 + graph.addVouch("did:plc:bob", "did:plc:carol") // bob vouches for carol 172 + 173 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 174 + 175 + result, err := checkWithDeps("dave.bsky.social", deps) 176 + if err != nil { 177 + t.Fatal(err) 178 + } 179 + 180 + if len(result.paths) != 1 { 181 + t.Fatalf("expected 1 path, got %d", len(result.paths)) 182 + } 183 + 184 + path := result.paths[0] 185 + expected := []string{"did:plc:alice", "did:plc:bob", "did:plc:carol", "did:plc:dave"} 186 + if len(path) != len(expected) { 187 + t.Fatalf("expected path len %d, got %d", len(expected), len(path)) 188 + } 189 + for i := range expected { 190 + if path[i] != expected[i] { 191 + t.Fatalf("path[%d]: expected %s, got %s", i, expected[i], path[i]) 192 + } 193 + } 194 + } 195 + 196 + func TestCheck_MultipleRoutes(t *testing.T) { 197 + // alice vouches for bob AND carol 198 + // both bob and carol vouch for dave 199 + // expected: two 2-hop paths 200 + identity := newFakeIdentity(map[string]string{ 201 + "alice.bsky.social": "did:plc:alice", 202 + "bob.bsky.social": "did:plc:bob", 203 + "carol.bsky.social": "did:plc:carol", 204 + "dave.bsky.social": "did:plc:dave", 205 + }) 206 + 207 + graph := newFakeGraph() 208 + graph.addVouch("did:plc:bob", "did:plc:dave") // bob vouches for dave 209 + graph.addVouch("did:plc:carol", "did:plc:dave") // carol vouches for dave 210 + 211 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob", "did:plc:carol"}) 212 + 213 + result, err := checkWithDeps("dave.bsky.social", deps) 214 + if err != nil { 215 + t.Fatal(err) 216 + } 217 + 218 + if len(result.paths) != 2 { 219 + t.Fatalf("expected 2 paths, got %d: %v", len(result.paths), result.paths) 220 + } 221 + 222 + // Both paths should be 3 elements (2-hop) 223 + for i, path := range result.paths { 224 + if len(path) != 3 { 225 + t.Fatalf("path %d: expected len 3, got %d", i, len(path)) 226 + } 227 + if path[0] != "did:plc:alice" || path[2] != "did:plc:dave" { 228 + t.Fatalf("path %d: unexpected endpoints: %v", i, path) 229 + } 230 + } 231 + } 232 + 233 + func TestCheck_HandleResolutionFallback(t *testing.T) { 234 + // When slingshot can't resolve a DID to a handle, fall back to showing the DID 235 + identity := newFakeIdentity(map[string]string{ 236 + "alice.bsky.social": "did:plc:alice", 237 + "charlie.bsky.social": "did:plc:charlie", 238 + // bob is NOT in identity — simulates slingshot failure 239 + }) 240 + 241 + graph := newFakeGraph() 242 + graph.addVouch("did:plc:bob", "did:plc:charlie") 243 + 244 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 245 + 246 + result, err := checkWithDeps("charlie.bsky.social", deps) 247 + if err != nil { 248 + t.Fatal(err) 249 + } 250 + 251 + if len(result.paths) != 1 { 252 + t.Fatalf("expected 1 path, got %d", len(result.paths)) 253 + } 254 + 255 + // bob's DID should fall back to raw DID since slingshot can't resolve it 256 + if result.handleMap["did:plc:bob"] != "did:plc:bob" { 257 + t.Fatalf("expected DID fallback for bob, got %s", result.handleMap["did:plc:bob"]) 258 + } 259 + } 260 + 261 + func TestCheck_UnresolvableHandle(t *testing.T) { 262 + identity := newFakeIdentity(map[string]string{}) 263 + 264 + graph := newFakeGraph() 265 + deps := makeDeps(identity, graph, "did:plc:alice", nil) 266 + 267 + _, err := checkWithDeps("nobody.bsky.social", deps) 268 + if err == nil { 269 + t.Fatal("expected error for unresolvable handle") 270 + } 271 + } 272 + 273 + func TestCheck_FourHopNotFound(t *testing.T) { 274 + // alice -> bob -> carol -> dave -> eve 275 + // This is 4 hops — beyond the 3-hop limit, so no path should be found 276 + identity := newFakeIdentity(map[string]string{ 277 + "alice.bsky.social": "did:plc:alice", 278 + "bob.bsky.social": "did:plc:bob", 279 + "carol.bsky.social": "did:plc:carol", 280 + "dave.bsky.social": "did:plc:dave", 281 + "eve.bsky.social": "did:plc:eve", 282 + }) 283 + 284 + graph := newFakeGraph() 285 + graph.addVouch("did:plc:dave", "did:plc:eve") // dave vouches for eve 286 + graph.addVouch("did:plc:carol", "did:plc:dave") // carol vouches for dave 287 + graph.addVouch("did:plc:bob", "did:plc:carol") // bob vouches for carol 288 + 289 + // alice only vouches for bob, so the chain is 4 hops: alice->bob->carol->dave->eve 290 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 291 + 292 + result, err := checkWithDeps("eve.bsky.social", deps) 293 + if err != nil { 294 + t.Fatal(err) 295 + } 296 + 297 + if len(result.paths) != 0 { 298 + t.Fatalf("expected no paths for 4-hop chain, got %d: %v", len(result.paths), result.paths) 299 + } 300 + } 301 + 302 + func TestCheck_CyclicVouches(t *testing.T) { 303 + // alice -> bob -> carol -> alice (cycle) 304 + // alice checks carol: should find alice -> bob -> carol (2-hop) 305 + // The cycle back to alice should not cause infinite loops or duplicate paths 306 + identity := newFakeIdentity(map[string]string{ 307 + "alice.bsky.social": "did:plc:alice", 308 + "bob.bsky.social": "did:plc:bob", 309 + "carol.bsky.social": "did:plc:carol", 310 + }) 311 + 312 + graph := newFakeGraph() 313 + graph.addVouch("did:plc:bob", "did:plc:carol") // bob vouches for carol 314 + graph.addVouch("did:plc:carol", "did:plc:alice") // carol vouches for alice 315 + graph.addVouch("did:plc:alice", "did:plc:bob") // alice vouches for bob 316 + 317 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 318 + 319 + result, err := checkWithDeps("carol.bsky.social", deps) 320 + if err != nil { 321 + t.Fatal(err) 322 + } 323 + 324 + if len(result.paths) != 1 { 325 + t.Fatalf("expected 1 path, got %d: %v", len(result.paths), result.paths) 326 + } 327 + 328 + expected := []string{"did:plc:alice", "did:plc:bob", "did:plc:carol"} 329 + path := result.paths[0] 330 + if len(path) != len(expected) { 331 + t.Fatalf("expected path len %d, got %d", len(expected), len(path)) 332 + } 333 + for i := range expected { 334 + if path[i] != expected[i] { 335 + t.Fatalf("path[%d]: expected %s, got %s", i, expected[i], path[i]) 336 + } 337 + } 338 + } 339 + 340 + func TestCheck_MutualVouches(t *testing.T) { 341 + // alice and bob vouch for each other, bob and carol vouch for each other 342 + // alice checks carol: should find alice -> bob -> carol (2-hop) 343 + // and NOT produce spurious paths from the mutual edges 344 + identity := newFakeIdentity(map[string]string{ 345 + "alice.bsky.social": "did:plc:alice", 346 + "bob.bsky.social": "did:plc:bob", 347 + "carol.bsky.social": "did:plc:carol", 348 + }) 349 + 350 + graph := newFakeGraph() 351 + graph.addVouch("did:plc:alice", "did:plc:bob") // alice vouches for bob 352 + graph.addVouch("did:plc:bob", "did:plc:alice") // bob vouches for alice 353 + graph.addVouch("did:plc:bob", "did:plc:carol") // bob vouches for carol 354 + graph.addVouch("did:plc:carol", "did:plc:bob") // carol vouches for bob 355 + 356 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:bob"}) 357 + 358 + result, err := checkWithDeps("carol.bsky.social", deps) 359 + if err != nil { 360 + t.Fatal(err) 361 + } 362 + 363 + if len(result.paths) != 1 { 364 + t.Fatalf("expected 1 path, got %d: %v", len(result.paths), result.paths) 365 + } 366 + 367 + expected := []string{"did:plc:alice", "did:plc:bob", "did:plc:carol"} 368 + path := result.paths[0] 369 + if len(path) != len(expected) { 370 + t.Fatalf("expected path len %d, got %d", len(expected), len(path)) 371 + } 372 + for i := range expected { 373 + if path[i] != expected[i] { 374 + t.Fatalf("path[%d]: expected %s, got %s", i, expected[i], path[i]) 375 + } 376 + } 377 + } 378 + 379 + func TestCheck_SelfVouch(t *testing.T) { 380 + // Check what happens when checking yourself (you vouch for yourself) 381 + identity := newFakeIdentity(map[string]string{ 382 + "alice.bsky.social": "did:plc:alice", 383 + }) 384 + 385 + graph := newFakeGraph() 386 + deps := makeDeps(identity, graph, "did:plc:alice", []string{"did:plc:alice"}) 387 + 388 + result, err := checkWithDeps("alice.bsky.social", deps) 389 + if err != nil { 390 + t.Fatal(err) 391 + } 392 + 393 + // Should be detected as direct vouch 394 + if result.paths != nil { 395 + t.Fatalf("expected nil paths (direct vouch), got %v", result.paths) 396 + } 397 + }
+93 -50
cli/main.go
··· 230 230 return nil 231 231 } 232 232 233 + // checkDeps holds injectable dependencies for the check logic. 234 + type checkDeps struct { 235 + myDID string 236 + resolveHandle func(handle string) (string, error) 237 + resolveDidToHandle func(did string) (string, error) 238 + fetchVouchers func(targetDID string) ([]string, error) 239 + listMyVouches func() ([]string, error) 240 + } 241 + 242 + // checkResult holds the output of a check operation. 243 + type checkResult struct { 244 + targetDID string 245 + paths [][]string // each path is a list of DIDs 246 + handleMap map[string]string // DID -> handle 247 + } 248 + 233 249 func check(ctx context.Context, handle string) error { 234 250 session, err := resumeSession(ctx) 235 251 if err != nil { ··· 239 255 client := session.APIClient() 240 256 myDID := session.Data.AccountDID.String() 241 257 242 - // Resolve target handle to DID 243 - targetDID, err := slingshotResolveHandle(handle) 258 + deps := checkDeps{ 259 + myDID: myDID, 260 + resolveHandle: slingshotResolveHandle, 261 + resolveDidToHandle: slingshotResolveDidToHandle, 262 + fetchVouchers: fetchVouchersFromMicrocosm, 263 + listMyVouches: func() ([]string, error) { 264 + return listVouchSubjects(ctx, client, myDID) 265 + }, 266 + } 267 + 268 + result, err := checkWithDeps(handle, deps) 244 269 if err != nil { 245 - return fmt.Errorf("resolving handle %q: %w", handle, err) 270 + return err 246 271 } 247 272 248 - fmt.Printf("Checking vouch paths to %s (%s)...\n", handle, targetDID) 273 + fmt.Printf("Checking vouch paths to %s (%s)...\n", handle, result.targetDID) 249 274 250 - // Fetch my vouches (people I vouch for) 251 - myVouches, err := listVouchSubjects(ctx, client, myDID) 275 + if result.paths == nil { 276 + fmt.Printf("\nyou -> %s\n", handle) 277 + return nil 278 + } 279 + 280 + if len(result.paths) == 0 { 281 + fmt.Println("no vouch routes found") 282 + return nil 283 + } 284 + 285 + fmt.Printf("\nFound %d vouch route(s):\n", len(result.paths)) 286 + for _, path := range result.paths { 287 + parts := make([]string, len(path)) 288 + for i, did := range path { 289 + parts[i] = result.handleMap[did] 290 + } 291 + fmt.Println(strings.Join(parts, " -> ")) 292 + } 293 + 294 + return nil 295 + } 296 + 297 + // checkWithDeps contains the core check logic with injected dependencies. 298 + // Returns a checkResult where paths == nil means direct vouch found, 299 + // paths == empty means no routes, otherwise contains discovered paths. 300 + func checkWithDeps(handle string, deps checkDeps) (*checkResult, error) { 301 + targetDID, err := deps.resolveHandle(handle) 252 302 if err != nil { 253 - return fmt.Errorf("fetching your vouches: %w", err) 303 + return nil, fmt.Errorf("resolving handle %q: %w", handle, err) 304 + } 305 + 306 + myVouches, err := deps.listMyVouches() 307 + if err != nil { 308 + return nil, fmt.Errorf("fetching your vouches: %w", err) 254 309 } 255 310 256 311 // Direct vouch check (depth 1) 257 312 for _, did := range myVouches { 258 313 if did == targetDID { 259 - fmt.Printf("\nyou -> %s\n", handle) 260 - return nil 314 + return &checkResult{targetDID: targetDID, paths: nil}, nil 261 315 } 262 316 } 263 317 ··· 266 320 reverseGraph := make(map[string]map[string]bool) 267 321 268 322 // Level 1: who vouches for target 269 - level1, err := fetchVouchersFromMicrocosm(targetDID) 323 + level1, err := deps.fetchVouchers(targetDID) 270 324 if err != nil { 271 - return fmt.Errorf("querying microcosm: %w", err) 325 + return nil, fmt.Errorf("querying microcosm: %w", err) 272 326 } 273 327 reverseGraph[targetDID] = toSet(level1) 274 328 275 329 // Level 2: who vouches for each level-1 voucher 276 330 level2DIDs := []string{} 277 331 for _, did := range level1 { 278 - vouchers, err := fetchVouchersFromMicrocosm(did) 332 + vouchers, err := deps.fetchVouchers(did) 279 333 if err != nil { 280 - return fmt.Errorf("querying microcosm: %w", err) 334 + return nil, fmt.Errorf("querying microcosm: %w", err) 281 335 } 282 336 reverseGraph[did] = toSet(vouchers) 283 337 level2DIDs = append(level2DIDs, vouchers...) ··· 288 342 if _, exists := reverseGraph[did]; exists { 289 343 continue // already fetched 290 344 } 291 - vouchers, err := fetchVouchersFromMicrocosm(did) 345 + vouchers, err := deps.fetchVouchers(did) 292 346 if err != nil { 293 - return fmt.Errorf("querying microcosm: %w", err) 347 + return nil, fmt.Errorf("querying microcosm: %w", err) 294 348 } 295 349 reverseGraph[did] = toSet(vouchers) 296 350 } 297 351 298 352 // Find all paths: me -> (someone I vouch for) -> ... -> target 299 - // A path me -> A -> B -> target means: 300 - // I vouch for A, A vouches for B, B vouches for target 301 - // In reverseGraph terms: B is in reverseGraph[target], A is in reverseGraph[B] 302 353 myVouchSet := toSet(myVouches) 303 354 var paths [][]string 304 355 305 356 // Depth 2: me -> X -> target (X vouches for target, I vouch for X) 306 357 for voucher := range reverseGraph[targetDID] { 307 358 if myVouchSet[voucher] { 308 - paths = append(paths, []string{myDID, voucher, targetDID}) 359 + paths = append(paths, []string{deps.myDID, voucher, targetDID}) 309 360 } 310 361 } 311 362 ··· 313 364 for yDID := range reverseGraph[targetDID] { 314 365 for xDID := range reverseGraph[yDID] { 315 366 if myVouchSet[xDID] { 316 - paths = append(paths, []string{myDID, xDID, yDID, targetDID}) 367 + paths = append(paths, []string{deps.myDID, xDID, yDID, targetDID}) 317 368 } 318 369 } 319 370 } 320 371 321 - if len(paths) == 0 { 322 - fmt.Println("no vouch routes found") 323 - return nil 324 - } 325 - 326 372 // Resolve all unique DIDs to handles for display 327 - uniqueDIDs := make(map[string]bool) 328 - for _, path := range paths { 329 - for _, did := range path { 330 - uniqueDIDs[did] = true 331 - } 332 - } 333 - 334 373 handleMap := make(map[string]string) 335 - handleMap[targetDID] = handle // we already know this one 336 - for did := range uniqueDIDs { 337 - if _, exists := handleMap[did]; exists { 338 - continue 339 - } 340 - resolved, err := slingshotResolveDidToHandle(did) 341 - if err != nil { 342 - handleMap[did] = did // fallback to DID 343 - } else { 344 - handleMap[did] = resolved 374 + if len(paths) > 0 { 375 + uniqueDIDs := make(map[string]bool) 376 + for _, path := range paths { 377 + for _, did := range path { 378 + uniqueDIDs[did] = true 379 + } 345 380 } 346 - } 347 381 348 - fmt.Printf("\nFound %d vouch route(s):\n", len(paths)) 349 - for _, path := range paths { 350 - parts := make([]string, len(path)) 351 - for i, did := range path { 352 - parts[i] = handleMap[did] 382 + handleMap[targetDID] = handle // we already know this one 383 + for did := range uniqueDIDs { 384 + if _, exists := handleMap[did]; exists { 385 + continue 386 + } 387 + resolved, err := deps.resolveDidToHandle(did) 388 + if err != nil { 389 + handleMap[did] = did // fallback to DID 390 + } else { 391 + handleMap[did] = resolved 392 + } 353 393 } 354 - fmt.Println(strings.Join(parts, " -> ")) 355 394 } 356 395 357 - return nil 396 + return &checkResult{ 397 + targetDID: targetDID, 398 + paths: paths, 399 + handleMap: handleMap, 400 + }, nil 358 401 } 359 402 360 403 // listVouchSubjects returns the DIDs that the given repo has vouched for.