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