master
go 545 lines 19.4 KB
Raw
1 package cli
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "github.com/ipfs/kubo/test/cli/harness"
12 "github.com/stretchr/testify/require"
13 )
14
15 // pinInfo represents the JSON structure for pin ls output
16 type pinInfo struct {
17 Type string `json:"Type"`
18 Name string `json:"Name"`
19 }
20
21 // pinLsJSON represents the JSON output structure for pin ls command
22 type pinLsJSON struct {
23 Keys map[string]pinInfo `json:"Keys"`
24 }
25
26 // Helper function to initialize a test node with daemon
27 func setupTestNode(t *testing.T) *harness.Node {
28 t.Helper()
29 node := harness.NewT(t).NewNode().Init()
30 node.StartDaemon("--offline")
31 t.Cleanup(func() {
32 node.StopDaemon()
33 })
34 return node
35 }
36
37 // Helper function to assert pin name and CID are present in output
38 func assertPinOutput(t *testing.T, output, cid, pinName string) {
39 t.Helper()
40 require.Contains(t, output, pinName, "pin name '%s' not found in output: %s", pinName, output)
41 require.Contains(t, output, cid, "CID %s not found in output: %s", cid, output)
42 }
43
44 // Helper function to assert CID is present but name is not
45 func assertCIDOnly(t *testing.T, output, cid string) {
46 t.Helper()
47 require.Contains(t, output, cid, "CID %s not found in output: %s", cid, output)
48 }
49
50 // Helper function to assert neither CID nor name are present
51 func assertNotPresent(t *testing.T, output, cid, pinName string) {
52 t.Helper()
53 require.NotContains(t, output, cid, "CID %s should not be present in output: %s", cid, output)
54 require.NotContains(t, output, pinName, "pin name '%s' should not be present in output: %s", pinName, output)
55 }
56
57 // Test that pin ls returns names when querying specific CIDs with --names flag
58 func TestPinLsWithNamesForSpecificCIDs(t *testing.T) {
59 t.Parallel()
60
61 t.Run("pin ls with specific CID returns name", func(t *testing.T) {
62 t.Parallel()
63 node := setupTestNode(t)
64
65 // Add content without pinning
66 cidA := node.IPFSAddStr("content A", "--pin=false")
67 cidB := node.IPFSAddStr("content B", "--pin=false")
68 cidC := node.IPFSAddStr("content C", "--pin=false")
69
70 // Pin with names
71 node.IPFS("pin", "add", "--name=pin-a", cidA)
72 node.IPFS("pin", "add", "--name=pin-b", cidB)
73 node.IPFS("pin", "add", cidC) // No name
74
75 // Test: pin ls <cid> --names should return the name
76 res := node.IPFS("pin", "ls", cidA, "--names")
77 assertPinOutput(t, res.Stdout.String(), cidA, "pin-a")
78
79 res = node.IPFS("pin", "ls", cidB, "--names")
80 assertPinOutput(t, res.Stdout.String(), cidB, "pin-b")
81
82 // Test: pin without name should work
83 res = node.IPFS("pin", "ls", cidC, "--names")
84 output := res.Stdout.String()
85 assertCIDOnly(t, output, cidC)
86 require.Contains(t, output, "recursive", "pin type 'recursive' not found for CID %s in output: %s", cidC, output)
87
88 // Test: without --names flag, no names returned
89 res = node.IPFS("pin", "ls", cidA)
90 output = res.Stdout.String()
91 require.NotContains(t, output, "pin-a", "pin name 'pin-a' should not be present without --names flag, but found in: %s", output)
92 assertCIDOnly(t, output, cidA)
93 })
94
95 t.Run("pin ls with multiple CIDs returns names", func(t *testing.T) {
96 t.Parallel()
97 node := setupTestNode(t)
98
99 // Create test content
100 cidA := node.IPFSAddStr("multi A", "--pin=false")
101 cidB := node.IPFSAddStr("multi B", "--pin=false")
102
103 // Pin with names
104 node.IPFS("pin", "add", "--name=multi-pin-a", cidA)
105 node.IPFS("pin", "add", "--name=multi-pin-b", cidB)
106
107 // Test multiple CIDs at once
108 res := node.IPFS("pin", "ls", cidA, cidB, "--names")
109 output := res.Stdout.String()
110 assertPinOutput(t, output, cidA, "multi-pin-a")
111 assertPinOutput(t, output, cidB, "multi-pin-b")
112 })
113
114 t.Run("pin ls without CID lists all pins with names", func(t *testing.T) {
115 t.Parallel()
116 node := setupTestNode(t)
117
118 // Create and pin content with names
119 cidA := node.IPFSAddStr("list all A", "--pin=false")
120 cidB := node.IPFSAddStr("list all B", "--pin=false")
121 cidC := node.IPFSAddStr("list all C", "--pin=false")
122
123 node.IPFS("pin", "add", "--name=all-pin-a", cidA)
124 node.IPFS("pin", "add", "--name=all-pin-b", "--recursive=false", cidB)
125 node.IPFS("pin", "add", cidC) // No name
126
127 // Test: pin ls --names (without CID) should list all pins with their names
128 res := node.IPFS("pin", "ls", "--names")
129 output := res.Stdout.String()
130
131 // Should contain all pins with their names
132 assertPinOutput(t, output, cidA, "all-pin-a")
133 assertPinOutput(t, output, cidB, "all-pin-b")
134 assertCIDOnly(t, output, cidC)
135
136 // Pin C should appear but without a name (just type)
137 lines := strings.SplitSeq(output, "\n")
138 for line := range lines {
139 if strings.Contains(line, cidC) {
140 // Should have CID and type but no name
141 require.Contains(t, line, "recursive", "pin type 'recursive' not found for unnamed pin %s in line: %s", cidC, line)
142 require.NotContains(t, line, "all-pin", "pin name should not be present for unnamed pin %s, but found in line: %s", cidC, line)
143 }
144 }
145 })
146
147 t.Run("pin ls --type with --names", func(t *testing.T) {
148 t.Parallel()
149 node := setupTestNode(t)
150
151 // Create test content
152 cidDirect := node.IPFSAddStr("direct content", "--pin=false")
153 cidRecursive := node.IPFSAddStr("recursive content", "--pin=false")
154
155 // Create a DAG for indirect testing
156 childCid := node.IPFSAddStr("child for indirect", "--pin=false")
157 parentContent := fmt.Sprintf(`{"link": "/ipfs/%s"}`, childCid)
158 parentCid := node.PipeStrToIPFS(parentContent, "dag", "put", "--input-codec=json", "--store-codec=dag-cbor").Stdout.Trimmed()
159
160 // Pin with different types and names
161 node.IPFS("pin", "add", "--name=direct-pin", "--recursive=false", cidDirect)
162 node.IPFS("pin", "add", "--name=recursive-pin", cidRecursive)
163 node.IPFS("pin", "add", "--name=parent-pin", parentCid)
164
165 // Test: --type=direct --names
166 res := node.IPFS("pin", "ls", "--type=direct", "--names")
167 output := res.Stdout.String()
168 assertPinOutput(t, output, cidDirect, "direct-pin")
169 assertNotPresent(t, output, cidRecursive, "recursive-pin")
170
171 // Test: --type=recursive --names
172 res = node.IPFS("pin", "ls", "--type=recursive", "--names")
173 output = res.Stdout.String()
174 assertPinOutput(t, output, cidRecursive, "recursive-pin")
175 assertPinOutput(t, output, parentCid, "parent-pin")
176 assertNotPresent(t, output, cidDirect, "direct-pin")
177
178 // Test: --type=indirect with proper directory structure
179 // Create a directory with a file for indirect pin testing
180 dirPath := t.TempDir()
181 require.NoError(t, os.WriteFile(filepath.Join(dirPath, "file.txt"), []byte("test content"), 0644))
182
183 // Add directory recursively
184 dirAddRes := node.IPFS("add", "-r", "-q", dirPath)
185 dirCidStr := strings.TrimSpace(dirAddRes.Stdout.Lines()[len(dirAddRes.Stdout.Lines())-1])
186
187 // Add file separately without pinning to get its CID
188 fileAddRes := node.IPFS("add", "-q", "--pin=false", filepath.Join(dirPath, "file.txt"))
189 fileCidStr := strings.TrimSpace(fileAddRes.Stdout.String())
190
191 // Check if file shows as indirect
192 res = node.IPFS("pin", "ls", "--type=indirect", fileCidStr)
193 output = res.Stdout.String()
194 require.Contains(t, output, fileCidStr, "indirect pin CID %s not found in output: %s", fileCidStr, output)
195 require.Contains(t, output, "indirect through "+dirCidStr, "indirect relationship not found for CID %s through %s in output: %s", fileCidStr, dirCidStr, output)
196
197 // Test: --type=all --names
198 res = node.IPFS("pin", "ls", "--type=all", "--names")
199 output = res.Stdout.String()
200 assertPinOutput(t, output, cidDirect, "direct-pin")
201 assertPinOutput(t, output, cidRecursive, "recursive-pin")
202 assertPinOutput(t, output, parentCid, "parent-pin")
203 // Indirect pins are included in --type=all output
204 })
205
206 t.Run("pin ls JSON output with names", func(t *testing.T) {
207 t.Parallel()
208 node := setupTestNode(t)
209
210 // Add and pin content with name
211 cidA := node.IPFSAddStr("json content", "--pin=false")
212 node.IPFS("pin", "add", "--name=json-pin", cidA)
213
214 // Test JSON output with specific CID
215 res := node.IPFS("pin", "ls", cidA, "--names", "--enc=json")
216 var pinOutput pinLsJSON
217 err := json.Unmarshal([]byte(res.Stdout.String()), &pinOutput)
218 require.NoError(t, err, "failed to unmarshal JSON output: %s", res.Stdout.String())
219
220 pinData, ok := pinOutput.Keys[cidA]
221 require.True(t, ok, "CID %s should be in Keys map, got: %+v", cidA, pinOutput.Keys)
222 require.Equal(t, "recursive", pinData.Type, "expected pin type 'recursive', got '%s'", pinData.Type)
223 require.Equal(t, "json-pin", pinData.Name, "expected pin name 'json-pin', got '%s'", pinData.Name)
224
225 // Without names flag
226 res = node.IPFS("pin", "ls", cidA, "--enc=json")
227 err = json.Unmarshal([]byte(res.Stdout.String()), &pinOutput)
228 require.NoError(t, err, "failed to unmarshal JSON output: %s", res.Stdout.String())
229
230 pinData, ok = pinOutput.Keys[cidA]
231 require.True(t, ok, "CID %s should be in Keys map, got: %+v", cidA, pinOutput.Keys)
232 // Name should be empty without --names flag
233 require.Equal(t, "", pinData.Name, "pin name should be empty without --names flag, got '%s'", pinData.Name)
234
235 // Test JSON output without CID (list all)
236 res = node.IPFS("pin", "ls", "--names", "--enc=json")
237 var listOutput pinLsJSON
238 err = json.Unmarshal([]byte(res.Stdout.String()), &listOutput)
239 require.NoError(t, err, "failed to unmarshal JSON list output: %s", res.Stdout.String())
240 // Should have at least one pin (the one we just added)
241 require.NotEmpty(t, listOutput.Keys, "pin list should not be empty")
242 // Check that our pin is in the list
243 pinData, ok = listOutput.Keys[cidA]
244 require.True(t, ok, "our pin with CID %s should be in the list, got: %+v", cidA, listOutput.Keys)
245 require.Equal(t, "json-pin", pinData.Name, "expected pin name 'json-pin' in list, got '%s'", pinData.Name)
246 })
247
248 t.Run("direct and indirect pins with names", func(t *testing.T) {
249 t.Parallel()
250 node := setupTestNode(t)
251
252 // Create a small DAG: parent -> child
253 childCid := node.IPFSAddStr("child content", "--pin=false")
254
255 // Create parent that references child
256 parentContent := fmt.Sprintf(`{"link": "/ipfs/%s"}`, childCid)
257 parentCid := node.PipeStrToIPFS(parentContent, "dag", "put", "--input-codec=json", "--store-codec=dag-cbor").Stdout.Trimmed()
258
259 // Pin child directly with a name
260 node.IPFS("pin", "add", "--name=direct-child", "--recursive=false", childCid)
261
262 // Pin parent recursively with a name
263 node.IPFS("pin", "add", "--name=recursive-parent", parentCid)
264
265 // Check direct pin with specific CID
266 res := node.IPFS("pin", "ls", "--type=direct", childCid, "--names")
267 output := res.Stdout.String()
268 require.Contains(t, output, "direct-child", "pin name 'direct-child' not found in output: %s", output)
269 require.Contains(t, output, "direct", "pin type 'direct' not found in output: %s", output)
270
271 // Check recursive pin with specific CID
272 res = node.IPFS("pin", "ls", "--type=recursive", parentCid, "--names")
273 output = res.Stdout.String()
274 require.Contains(t, output, "recursive-parent", "pin name 'recursive-parent' not found in output: %s", output)
275 require.Contains(t, output, "recursive", "pin type 'recursive' not found in output: %s", output)
276
277 // Child is both directly pinned and indirectly pinned through parent
278 // Both relationships are valid and can be checked
279 })
280
281 t.Run("pin update preserves name", func(t *testing.T) {
282 t.Parallel()
283 node := setupTestNode(t)
284
285 // Create two pieces of content
286 cidOld := node.IPFSAddStr("old content", "--pin=false")
287 cidNew := node.IPFSAddStr("new content", "--pin=false")
288
289 // Pin with name
290 node.IPFS("pin", "add", "--name=my-pin", cidOld)
291
292 // Update pin
293 node.IPFS("pin", "update", cidOld, cidNew)
294
295 // Check that new pin has the same name
296 res := node.IPFS("pin", "ls", cidNew, "--names")
297 require.Contains(t, res.Stdout.String(), "my-pin", "pin name 'my-pin' not preserved after update, output: %s", res.Stdout.String())
298
299 // Old pin should not exist
300 res = node.RunIPFS("pin", "ls", cidOld)
301 require.Equal(t, 1, res.ExitCode(), "expected exit code 1 for unpinned CID, got %d", res.ExitCode())
302 require.Contains(t, res.Stderr.String(), "is not pinned", "expected 'is not pinned' error for old CID %s, got: %s", cidOld, res.Stderr.String())
303 })
304
305 t.Run("pin ls with invalid CID returns error", func(t *testing.T) {
306 t.Parallel()
307 node := harness.NewT(t).NewNode().Init()
308
309 res := node.RunIPFS("pin", "ls", "invalid-cid")
310 require.Equal(t, 1, res.ExitCode(), "expected exit code 1 for invalid CID, got %d", res.ExitCode())
311 require.Contains(t, res.Stderr.String(), "invalid", "expected 'invalid' in error message, got: %s", res.Stderr.String())
312 })
313
314 t.Run("pin ls with unpinned CID returns error", func(t *testing.T) {
315 t.Parallel()
316 node := setupTestNode(t)
317
318 // Add content without pinning
319 cid := node.IPFSAddStr("unpinned content", "--pin=false")
320
321 res := node.RunIPFS("pin", "ls", cid)
322 require.Equal(t, 1, res.ExitCode(), "expected exit code 1 for unpinned CID, got %d", res.ExitCode())
323 require.Contains(t, res.Stderr.String(), "is not pinned", "expected 'is not pinned' error for CID %s, got: %s", cid, res.Stderr.String())
324 })
325
326 t.Run("pin with special characters in name", func(t *testing.T) {
327 t.Parallel()
328 node := setupTestNode(t)
329
330 testCases := []struct {
331 name string
332 pinName string
333 }{
334 {"unicode", "test-📌-pin"},
335 {"spaces", "test pin name"},
336 {"special chars", "test!@#$%"},
337 {"path-like", "test/pin/name"},
338 {"dots", "test.pin.name"},
339 {"long name", strings.Repeat("a", 255)},
340 {"empty name", ""},
341 }
342
343 for _, tc := range testCases {
344 t.Run(tc.name, func(t *testing.T) {
345 cid := node.IPFSAddStr("content for "+tc.name, "--pin=false")
346 node.IPFS("pin", "add", "--name="+tc.pinName, cid)
347
348 res := node.IPFS("pin", "ls", cid, "--names")
349 if tc.pinName != "" {
350 require.Contains(t, res.Stdout.String(), tc.pinName,
351 "pin name '%s' not found in output for test case '%s'", tc.pinName, tc.name)
352 }
353 })
354 }
355 })
356
357 t.Run("concurrent pin operations with names", func(t *testing.T) {
358 t.Parallel()
359 node := setupTestNode(t)
360
361 // Create multiple goroutines adding pins with names
362 numPins := 10
363 done := make(chan struct{}, numPins)
364
365 for i := range numPins {
366 go func(idx int) {
367 defer func() { done <- struct{}{} }()
368
369 content := fmt.Sprintf("concurrent content %d", idx)
370 cid := node.IPFSAddStr(content, "--pin=false")
371 pinName := fmt.Sprintf("concurrent-pin-%d", idx)
372 node.IPFS("pin", "add", "--name="+pinName, cid)
373 }(i)
374 }
375
376 // Wait for all goroutines
377 for range numPins {
378 <-done
379 }
380
381 // Verify all pins have correct names
382 res := node.IPFS("pin", "ls", "--names")
383 output := res.Stdout.String()
384 for i := range numPins {
385 pinName := fmt.Sprintf("concurrent-pin-%d", i)
386 require.Contains(t, output, pinName,
387 "concurrent pin name '%s' not found in output", pinName)
388 }
389 })
390
391 t.Run("pin rm removes name association", func(t *testing.T) {
392 t.Parallel()
393 node := setupTestNode(t)
394
395 // Add and pin with name
396 cid := node.IPFSAddStr("content to remove", "--pin=false")
397 node.IPFS("pin", "add", "--name=to-be-removed", cid)
398
399 // Verify pin exists with name
400 res := node.IPFS("pin", "ls", cid, "--names")
401 require.Contains(t, res.Stdout.String(), "to-be-removed")
402
403 // Remove pin
404 node.IPFS("pin", "rm", cid)
405
406 // Verify pin and name are gone
407 res = node.RunIPFS("pin", "ls", cid)
408 require.Equal(t, 1, res.ExitCode())
409 require.Contains(t, res.Stderr.String(), "is not pinned")
410 })
411
412 t.Run("garbage collection preserves named pins", func(t *testing.T) {
413 t.Parallel()
414 node := setupTestNode(t)
415
416 // Add content with and without pin names
417 cidNamed := node.IPFSAddStr("named content", "--pin=false")
418 cidUnnamed := node.IPFSAddStr("unnamed content", "--pin=false")
419 cidUnpinned := node.IPFSAddStr("unpinned content", "--pin=false")
420
421 node.IPFS("pin", "add", "--name=important-data", cidNamed)
422 node.IPFS("pin", "add", cidUnnamed)
423
424 // Run garbage collection
425 node.IPFS("repo", "gc")
426
427 // Named and unnamed pins should still exist
428 res := node.IPFS("pin", "ls", cidNamed, "--names")
429 require.Contains(t, res.Stdout.String(), "important-data")
430
431 res = node.IPFS("pin", "ls", cidUnnamed)
432 require.Contains(t, res.Stdout.String(), cidUnnamed)
433
434 // Unpinned content should be gone (cat should fail)
435 res = node.RunIPFS("cat", cidUnpinned)
436 require.NotEqual(t, 0, res.ExitCode(), "unpinned content should be garbage collected")
437 })
438
439 t.Run("pin add with same name can be used for multiple pins", func(t *testing.T) {
440 t.Parallel()
441 node := setupTestNode(t)
442
443 // Add two different pieces of content
444 cid1 := node.IPFSAddStr("first content", "--pin=false")
445 cid2 := node.IPFSAddStr("second content", "--pin=false")
446
447 // Pin both with the same name - this is allowed
448 node.IPFS("pin", "add", "--name=shared-name", cid1)
449 node.IPFS("pin", "add", "--name=shared-name", cid2)
450
451 // List all pins with names
452 res := node.IPFS("pin", "ls", "--names")
453 output := res.Stdout.String()
454
455 // Both CIDs should be pinned
456 require.Contains(t, output, cid1)
457 require.Contains(t, output, cid2)
458
459 // Both pins can have the same name
460 lines := strings.Split(output, "\n")
461 foundCid1WithName := false
462 foundCid2WithName := false
463 for _, line := range lines {
464 if strings.Contains(line, cid1) && strings.Contains(line, "shared-name") {
465 foundCid1WithName = true
466 }
467 if strings.Contains(line, cid2) && strings.Contains(line, "shared-name") {
468 foundCid2WithName = true
469 }
470 }
471 require.True(t, foundCid1WithName, "first pin should have the name")
472 require.True(t, foundCid2WithName, "second pin should have the name")
473 })
474
475 t.Run("pin names persist across daemon restarts", func(t *testing.T) {
476 t.Parallel()
477 node := harness.NewT(t).NewNode().Init()
478 node.StartDaemon("--offline")
479
480 // Add content with pin name
481 cid := node.IPFSAddStr("persistent content")
482 node.IPFS("pin", "add", "--name=persistent-pin", cid)
483
484 // Restart daemon
485 node.StopDaemon()
486 node.StartDaemon("--offline")
487
488 // Check pin name persisted
489 res := node.IPFS("pin", "ls", cid, "--names")
490 require.Contains(t, res.Stdout.String(), "persistent-pin",
491 "pin name should persist across daemon restarts")
492
493 node.StopDaemon()
494 })
495 }
496
497 // TestPinLsEdgeCases tests edge cases for pin ls command
498 func TestPinLsEdgeCases(t *testing.T) {
499 t.Parallel()
500
501 t.Run("invalid pin type returns error", func(t *testing.T) {
502 t.Parallel()
503 node := setupTestNode(t)
504
505 // Try to list pins with invalid type
506 res := node.RunIPFS("pin", "ls", "--type=invalid")
507 require.NotEqual(t, 0, res.ExitCode())
508 require.Contains(t, res.Stderr.String(), "invalid type 'invalid'")
509 require.Contains(t, res.Stderr.String(), "must be one of {direct, indirect, recursive, all}")
510 })
511
512 t.Run("known but non-listable pin type returns error", func(t *testing.T) {
513 t.Parallel()
514 node := setupTestNode(t)
515
516 // "internal" is a valid pin.Mode in boxo but not a valid --type for pin ls.
517 // Before the fix, this caused a panic instead of returning an error.
518 res := node.RunIPFS("pin", "ls", "--type=internal")
519 require.NotEqual(t, 0, res.ExitCode())
520 require.Contains(t, res.Stderr.String(), "invalid type 'internal'")
521 })
522
523 t.Run("non-existent path returns proper error", func(t *testing.T) {
524 t.Parallel()
525 node := setupTestNode(t)
526
527 // Try to list a non-existent CID
528 fakeCID := "QmNonExistent123456789"
529 res := node.RunIPFS("pin", "ls", fakeCID)
530 require.NotEqual(t, 0, res.ExitCode())
531 })
532
533 t.Run("unpinned CID returns not pinned error", func(t *testing.T) {
534 t.Parallel()
535 node := setupTestNode(t)
536
537 // Add content but don't pin it explicitly (it's just in blockstore)
538 unpinnedCID := node.IPFSAddStr("unpinned content", "--pin=false")
539
540 // Try to list specific unpinned CID
541 res := node.RunIPFS("pin", "ls", unpinnedCID)
542 require.NotEqual(t, 0, res.ExitCode())
543 require.Contains(t, res.Stderr.String(), "is not pinned")
544 })
545 }