master
go 969 lines 34.3 KB
Raw
1 // Tests for `ipfs name` CLI commands.
2 // - TestName: tests name publish, resolve, and inspect
3 // - TestNameGetPut: tests name get and put for raw IPNS record handling
4
5 package cli
6
7 import (
8 "bytes"
9 "encoding/json"
10 "fmt"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "github.com/ipfs/boxo/ipns"
17 "github.com/ipfs/kubo/core/commands/name"
18 "github.com/ipfs/kubo/test/cli/harness"
19 "github.com/stretchr/testify/require"
20 )
21
22 func TestName(t *testing.T) {
23 const (
24 fixturePath = "fixtures/TestName.car"
25 fixtureCid = "bafybeidg3uxibfrt7uqh7zd5yaodetik7wjwi4u7rwv2ndbgj6ec7lsv2a"
26 dagCid = "bafyreidgts62p4rtg3rtmptmbv2dt46zjzin275fr763oku3wfod3quzay"
27 )
28
29 makeDaemon := func(t *testing.T, initArgs []string) *harness.Node {
30 node := harness.NewT(t).NewNode().Init(append([]string{"--profile=test"}, initArgs...)...)
31 r, err := os.Open(fixturePath)
32 require.Nil(t, err)
33 defer r.Close()
34 err = node.IPFSDagImport(r, fixtureCid)
35 require.NoError(t, err)
36 return node
37 }
38
39 testPublishingWithSelf := func(keyType string) {
40 t.Run("Publishing with self (keyType = "+keyType+")", func(t *testing.T) {
41 t.Parallel()
42
43 args := []string{}
44 if keyType != "default" {
45 args = append(args, "-a="+keyType)
46 }
47
48 node := makeDaemon(t, args)
49 name := ipns.NameFromPeer(node.PeerID())
50
51 t.Run("Publishing a CID", func(t *testing.T) {
52 publishPath := "/ipfs/" + fixtureCid
53
54 res := node.IPFS("name", "publish", "--allow-offline", publishPath)
55 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
56
57 res = node.IPFS("name", "resolve", "/ipns/"+name.String())
58 require.Equal(t, publishPath+"\n", res.Stdout.String())
59 })
60
61 t.Run("Publishing a CID with -Q option", func(t *testing.T) {
62 publishPath := "/ipfs/" + fixtureCid
63
64 res := node.IPFS("name", "publish", "--allow-offline", "-Q", publishPath)
65 require.Equal(t, name.String()+"\n", res.Stdout.String())
66
67 res = node.IPFS("name", "resolve", "/ipns/"+name.String())
68 require.Equal(t, publishPath+"\n", res.Stdout.String())
69 })
70
71 t.Run("Publishing a CID+subpath", func(t *testing.T) {
72 publishPath := "/ipfs/" + fixtureCid + "/hello"
73
74 res := node.IPFS("name", "publish", "--allow-offline", publishPath)
75 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
76
77 res = node.IPFS("name", "resolve", "/ipns/"+name.String())
78 require.Equal(t, publishPath+"\n", res.Stdout.String())
79 })
80
81 t.Run("Publishing nothing fails", func(t *testing.T) {
82 res := node.RunIPFS("name", "publish")
83 require.Error(t, res.Err)
84 require.Equal(t, 1, res.ExitCode())
85 require.Contains(t, res.Stderr.String(), `argument "ipfs-path" is required`)
86 })
87
88 t.Run("Publishing with IPLD works", func(t *testing.T) {
89 publishPath := "/ipld/" + dagCid + "/thing"
90 res := node.IPFS("name", "publish", "--allow-offline", publishPath)
91 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
92
93 res = node.IPFS("name", "resolve", "/ipns/"+name.String())
94 require.Equal(t, publishPath+"\n", res.Stdout.String())
95 })
96
97 publishPath := "/ipfs/" + fixtureCid
98 res := node.IPFS("name", "publish", "--allow-offline", publishPath)
99 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
100
101 t.Run("Resolving self offline succeeds (daemon off)", func(t *testing.T) {
102 res = node.IPFS("name", "resolve", "--offline", "/ipns/"+name.String())
103 require.Equal(t, publishPath+"\n", res.Stdout.String())
104
105 // Test without cache.
106 res = node.IPFS("name", "resolve", "--offline", "-n", "/ipns/"+name.String())
107 require.Equal(t, publishPath+"\n", res.Stdout.String())
108 })
109
110 node.StartDaemon()
111 defer node.StopDaemon()
112
113 t.Run("Resolving self offline succeeds (daemon on)", func(t *testing.T) {
114 res = node.IPFS("name", "resolve", "--offline", "/ipns/"+name.String())
115 require.Equal(t, publishPath+"\n", res.Stdout.String())
116
117 // Test without cache.
118 res = node.IPFS("name", "resolve", "--offline", "-n", "/ipns/"+name.String())
119 require.Equal(t, publishPath+"\n", res.Stdout.String())
120 })
121 })
122 }
123
124 testPublishingWithSelf("default")
125 testPublishingWithSelf("rsa")
126 testPublishingWithSelf("ed25519")
127
128 testPublishWithKey := func(name string, keyArgs ...string) {
129 t.Run(name, func(t *testing.T) {
130 t.Parallel()
131 node := makeDaemon(t, nil)
132
133 keyGenArgs := []string{"key", "gen"}
134 keyGenArgs = append(keyGenArgs, keyArgs...)
135 keyGenArgs = append(keyGenArgs, "key")
136
137 res := node.IPFS(keyGenArgs...)
138 key := strings.TrimSpace(res.Stdout.String())
139
140 publishPath := "/ipfs/" + fixtureCid
141 name, err := ipns.NameFromString(key)
142 require.NoError(t, err)
143
144 res = node.IPFS("name", "publish", "--allow-offline", "--key="+key, publishPath)
145 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
146 })
147 }
148
149 testPublishWithKey("Publishing with RSA (with b58mh) Key", "--ipns-base=b58mh", "--type=rsa", "--size=2048")
150 testPublishWithKey("Publishing with ED25519 (with b58mh) Key", "--ipns-base=b58mh", "--type=ed25519")
151 testPublishWithKey("Publishing with ED25519 (with base36) Key", "--ipns-base=base36", "--type=ed25519")
152
153 t.Run("Fails to publish in offline mode", func(t *testing.T) {
154 t.Parallel()
155 node := makeDaemon(t, nil).StartDaemon("--offline")
156 defer node.StopDaemon()
157 res := node.RunIPFS("name", "publish", "/ipfs/"+fixtureCid)
158 require.Error(t, res.Err)
159 require.Equal(t, 1, res.ExitCode())
160 require.Contains(t, res.Stderr.String(), "can't publish while offline: pass `--allow-offline` to override or `--allow-delegated` if Ipns.DelegatedPublishers are set up")
161 })
162
163 t.Run("Publish V2-only record", func(t *testing.T) {
164 t.Parallel()
165
166 node := makeDaemon(t, nil).StartDaemon()
167 defer node.StopDaemon()
168 ipnsName := ipns.NameFromPeer(node.PeerID()).String()
169 ipnsPath := ipns.NamespacePrefix + ipnsName
170 publishPath := "/ipfs/" + fixtureCid
171
172 res := node.IPFS("name", "publish", "--ttl=30m", "--v1compat=false", publishPath)
173 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", ipnsName, publishPath), res.Stdout.String())
174
175 res = node.IPFS("name", "resolve", ipnsPath)
176 require.Equal(t, publishPath+"\n", res.Stdout.String())
177
178 res = node.IPFS("routing", "get", ipnsPath)
179 record := res.Stdout.Bytes()
180
181 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect")
182 out := res.Stdout.String()
183 require.Contains(t, out, "This record was not verified.")
184 require.Contains(t, out, publishPath)
185 require.Contains(t, out, "30m")
186
187 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+ipnsPath)
188 out = res.Stdout.String()
189 require.Contains(t, out, "Valid: true")
190 require.Contains(t, out, "Signature Type: V2")
191 require.Contains(t, out, fmt.Sprintf("Protobuf Size: %d", len(record)))
192 })
193
194 t.Run("Publish with TTL and inspect record", func(t *testing.T) {
195 t.Parallel()
196
197 node := makeDaemon(t, nil).StartDaemon()
198 t.Cleanup(func() { node.StopDaemon() })
199 ipnsPath := ipns.NamespacePrefix + ipns.NameFromPeer(node.PeerID()).String()
200 publishPath := "/ipfs/" + fixtureCid
201
202 _ = node.IPFS("name", "publish", "--ttl=30m", publishPath)
203 res := node.IPFS("routing", "get", ipnsPath)
204 record := res.Stdout.Bytes()
205
206 t.Run("Inspect record shows correct TTL and that it is not validated", func(t *testing.T) {
207 t.Parallel()
208 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect")
209 out := res.Stdout.String()
210 require.Contains(t, out, "This record was not verified.")
211 require.Contains(t, out, publishPath)
212 require.Contains(t, out, "30m")
213 require.Contains(t, out, "Signature Type: V1+V2")
214 require.Contains(t, out, fmt.Sprintf("Protobuf Size: %d", len(record)))
215 })
216
217 t.Run("Inspect record shows valid with correct name", func(t *testing.T) {
218 t.Parallel()
219 res := node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--enc=json", "--verify="+ipnsPath)
220 val := name.IpnsInspectResult{}
221 err := json.Unmarshal(res.Stdout.Bytes(), &val)
222 require.NoError(t, err)
223 require.True(t, val.Validation.Valid)
224 })
225
226 t.Run("Inspect record shows invalid with wrong name", func(t *testing.T) {
227 t.Parallel()
228 res := node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--enc=json", "--verify=12D3KooWRirYjmmQATx2kgHBfky6DADsLP7ex1t7BRxJ6nqLs9WH")
229 val := name.IpnsInspectResult{}
230 err := json.Unmarshal(res.Stdout.Bytes(), &val)
231 require.NoError(t, err)
232 require.False(t, val.Validation.Valid)
233 })
234 })
235
236 t.Run("Inspect with verification using wrong RSA key errors", func(t *testing.T) {
237 t.Parallel()
238 node := makeDaemon(t, nil).StartDaemon()
239 defer node.StopDaemon()
240
241 // Prepare RSA Key 1
242 res := node.IPFS("key", "gen", "--type=rsa", "--size=4096", "key1")
243 key1 := strings.TrimSpace(res.Stdout.String())
244 name1, err := ipns.NameFromString(key1)
245 require.NoError(t, err)
246
247 // Prepare RSA Key 2
248 res = node.IPFS("key", "gen", "--type=rsa", "--size=4096", "key2")
249 key2 := strings.TrimSpace(res.Stdout.String())
250 name2, err := ipns.NameFromString(key2)
251 require.NoError(t, err)
252
253 // Publish using Key 1
254 publishPath := "/ipfs/" + fixtureCid
255 res = node.IPFS("name", "publish", "--allow-offline", "--key="+key1, publishPath)
256 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name1.String(), publishPath), res.Stdout.String())
257
258 // Get IPNS Record
259 res = node.IPFS("routing", "get", ipns.NamespacePrefix+name1.String())
260 record := res.Stdout.Bytes()
261
262 // Validate with correct key succeeds
263 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+name1.String(), "--enc=json")
264 val := name.IpnsInspectResult{}
265 err = json.Unmarshal(res.Stdout.Bytes(), &val)
266 require.NoError(t, err)
267 require.True(t, val.Validation.Valid)
268
269 // Validate with wrong key fails
270 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+name2.String(), "--enc=json")
271 val = name.IpnsInspectResult{}
272 err = json.Unmarshal(res.Stdout.Bytes(), &val)
273 require.NoError(t, err)
274 require.False(t, val.Validation.Valid)
275 })
276
277 t.Run("Publishing with custom sequence number", func(t *testing.T) {
278 t.Parallel()
279
280 node := makeDaemon(t, nil)
281 publishPath := "/ipfs/" + fixtureCid
282 name := ipns.NameFromPeer(node.PeerID())
283
284 t.Run("Publish with sequence=0 is not allowed", func(t *testing.T) {
285 // Sequence=0 is never valid, even on a fresh node
286 res := node.RunIPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=0", publishPath)
287 require.NotEqual(t, 0, res.ExitCode(), "Expected publish with sequence=0 to fail")
288 require.Contains(t, res.Stderr.String(), "sequence number must be greater than the current record sequence")
289 })
290
291 t.Run("Publish with sequence=1 on fresh node", func(t *testing.T) {
292 // Sequence=1 is the minimum valid sequence number for first publish
293 res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=1", publishPath)
294 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
295 })
296
297 t.Run("Publish with sequence=42", func(t *testing.T) {
298 res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=42", publishPath)
299 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
300 })
301
302 t.Run("Publish with large sequence number", func(t *testing.T) {
303 res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=18446744073709551615", publishPath) // Max uint64
304 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
305 })
306 })
307
308 t.Run("Sequence number monotonic check", func(t *testing.T) {
309 t.Parallel()
310
311 node := makeDaemon(t, nil).StartDaemon()
312 defer node.StopDaemon()
313 publishPath1 := "/ipfs/" + fixtureCid
314 publishPath2 := "/ipfs/" + dagCid // Different content
315 name := ipns.NameFromPeer(node.PeerID())
316
317 // First, publish with a high sequence number (1000)
318 res := node.IPFS("name", "publish", "--ttl=0", "--sequence=1000", publishPath1)
319 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath1), res.Stdout.String())
320
321 // Verify the record was published successfully
322 res = node.IPFS("name", "resolve", name.String())
323 require.Contains(t, res.Stdout.String(), publishPath1)
324
325 // Now try to publish different content with a LOWER sequence number (500)
326 // This should fail due to monotonic sequence check
327 res = node.RunIPFS("name", "publish", "--ttl=0", "--sequence=500", publishPath2)
328 require.NotEqual(t, 0, res.ExitCode(), "Expected publish with lower sequence to fail")
329 require.Contains(t, res.Stderr.String(), "sequence number", "Expected error about sequence number")
330
331 // Verify the original content is still published (not overwritten)
332 res = node.IPFS("name", "resolve", name.String())
333 require.Contains(t, res.Stdout.String(), publishPath1, "Original content should still be published")
334 require.NotContains(t, res.Stdout.String(), publishPath2, "New content should not have been published")
335
336 // Publishing with a HIGHER sequence number should succeed
337 res = node.IPFS("name", "publish", "--ttl=0", "--sequence=2000", publishPath2)
338 require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath2), res.Stdout.String())
339
340 // Verify the new content is now published
341 res = node.IPFS("name", "resolve", name.String())
342 require.Contains(t, res.Stdout.String(), publishPath2, "New content should now be published")
343 })
344 }
345
346 func TestNameGetPut(t *testing.T) {
347 t.Parallel()
348
349 const (
350 fixturePath = "fixtures/TestName.car"
351 fixtureCid = "bafybeidg3uxibfrt7uqh7zd5yaodetik7wjwi4u7rwv2ndbgj6ec7lsv2a"
352 )
353
354 makeDaemon := func(t *testing.T, daemonArgs ...string) *harness.Node {
355 node := harness.NewT(t).NewNode().Init("--profile=test")
356 r, err := os.Open(fixturePath)
357 require.NoError(t, err)
358 defer r.Close()
359 err = node.IPFSDagImport(r, fixtureCid)
360 require.NoError(t, err)
361 return node.StartDaemon(daemonArgs...)
362 }
363
364 // makeKey creates a unique IPNS key for a test and returns the IPNS name
365 makeKey := func(t *testing.T, node *harness.Node, keyName string) ipns.Name {
366 res := node.IPFS("key", "gen", "--type=ed25519", keyName)
367 keyID := strings.TrimSpace(res.Stdout.String())
368 name, err := ipns.NameFromString(keyID)
369 require.NoError(t, err)
370 return name
371 }
372
373 // makeExternalRecord creates an IPNS record on an ephemeral node that is
374 // shut down before returning. This ensures the test node has no local
375 // knowledge of the record, properly testing put/get functionality.
376 // We use short --lifetime so if IPNS records from tests get published on
377 // the public DHT, they won't waste storage for long.
378 makeExternalRecord := func(t *testing.T, h *harness.Harness, publishPath string, publishArgs ...string) (ipns.Name, []byte) {
379 node := h.NewNode().Init("--profile=test")
380
381 r, err := os.Open(fixturePath)
382 require.NoError(t, err)
383 defer r.Close()
384 err = node.IPFSDagImport(r, fixtureCid)
385 require.NoError(t, err)
386
387 node.StartDaemon()
388
389 res := node.IPFS("key", "gen", "--type=ed25519", "ephemeral-key")
390 keyID := strings.TrimSpace(res.Stdout.String())
391 ipnsName, err := ipns.NameFromString(keyID)
392 require.NoError(t, err)
393
394 args := []string{"name", "publish", "--key=ephemeral-key", "--lifetime=5m"}
395 args = append(args, publishArgs...)
396 args = append(args, publishPath)
397 node.IPFS(args...)
398
399 res = node.IPFS("name", "get", ipnsName.String())
400 record := res.Stdout.Bytes()
401 require.NotEmpty(t, record)
402
403 node.StopDaemon()
404
405 return ipnsName, record
406 }
407
408 t.Run("name get retrieves IPNS record", func(t *testing.T) {
409 t.Parallel()
410 node := makeDaemon(t)
411 defer node.StopDaemon()
412
413 publishPath := "/ipfs/" + fixtureCid
414 ipnsName := makeKey(t, node, "testkey")
415
416 // publish a record first
417 node.IPFS("name", "publish", "--key=testkey", "--lifetime=5m", publishPath)
418
419 // retrieve the record using name get
420 res := node.IPFS("name", "get", ipnsName.String())
421 record := res.Stdout.Bytes()
422 require.NotEmpty(t, record, "expected non-empty IPNS record")
423
424 // verify the record is valid by inspecting it
425 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+ipnsName.String())
426 require.Contains(t, res.Stdout.String(), "Valid: true")
427 require.Contains(t, res.Stdout.String(), publishPath)
428 })
429
430 t.Run("name get accepts /ipns/ prefix", func(t *testing.T) {
431 t.Parallel()
432 node := makeDaemon(t)
433 defer node.StopDaemon()
434
435 publishPath := "/ipfs/" + fixtureCid
436 ipnsName := makeKey(t, node, "testkey")
437
438 node.IPFS("name", "publish", "--key=testkey", "--lifetime=5m", publishPath)
439
440 // retrieve with /ipns/ prefix
441 res := node.IPFS("name", "get", "/ipns/"+ipnsName.String())
442 record := res.Stdout.Bytes()
443 require.NotEmpty(t, record)
444
445 // verify the record
446 res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+ipnsName.String())
447 require.Contains(t, res.Stdout.String(), "Valid: true")
448 })
449
450 t.Run("name get fails for non-existent name", func(t *testing.T) {
451 t.Parallel()
452 node := makeDaemon(t)
453 defer node.StopDaemon()
454
455 // try to get a record for a random peer ID that doesn't exist
456 res := node.RunIPFS("name", "get", "12D3KooWRirYjmmQATx2kgHBfky6DADsLP7ex1t7BRxJ6nqLs9WH")
457 require.Error(t, res.Err)
458 require.NotEqual(t, 0, res.ExitCode())
459 })
460
461 t.Run("name get fails for invalid name format", func(t *testing.T) {
462 t.Parallel()
463 node := makeDaemon(t)
464 defer node.StopDaemon()
465
466 res := node.RunIPFS("name", "get", "not-a-valid-ipns-name")
467 require.Error(t, res.Err)
468 require.NotEqual(t, 0, res.ExitCode())
469 })
470
471 t.Run("name put accepts /ipns/ prefix", func(t *testing.T) {
472 t.Parallel()
473 node := makeDaemon(t)
474 defer node.StopDaemon()
475
476 publishPath := "/ipfs/" + fixtureCid
477 ipnsName := makeKey(t, node, "testkey")
478
479 node.IPFS("name", "publish", "--key=testkey", "--lifetime=5m", publishPath)
480
481 res := node.IPFS("name", "get", ipnsName.String())
482 record := res.Stdout.Bytes()
483
484 // put with /ipns/ prefix
485 res = node.PipeToIPFS(bytes.NewReader(record), "name", "put", "--force", "/ipns/"+ipnsName.String())
486 require.NoError(t, res.Err)
487 })
488
489 t.Run("name put fails for invalid name format", func(t *testing.T) {
490 t.Parallel()
491 node := makeDaemon(t)
492 defer node.StopDaemon()
493
494 // create a dummy file
495 recordFile := filepath.Join(node.Dir, "dummy.bin")
496 err := os.WriteFile(recordFile, []byte("dummy"), 0644)
497 require.NoError(t, err)
498
499 res := node.RunIPFS("name", "put", "not-a-valid-ipns-name", recordFile)
500 require.Error(t, res.Err)
501 require.Contains(t, res.Stderr.String(), "invalid IPNS name")
502 })
503
504 t.Run("name put rejects oversized record", func(t *testing.T) {
505 t.Parallel()
506 node := makeDaemon(t)
507 defer node.StopDaemon()
508
509 ipnsName := makeKey(t, node, "testkey")
510
511 // create a file larger than 10 KiB
512 oversizedRecord := make([]byte, 11*1024)
513 recordFile := filepath.Join(node.Dir, "oversized.bin")
514 err := os.WriteFile(recordFile, oversizedRecord, 0644)
515 require.NoError(t, err)
516
517 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
518 require.Error(t, res.Err)
519 require.Contains(t, res.Stderr.String(), "exceeds maximum size")
520 })
521
522 t.Run("name put --force skips size check", func(t *testing.T) {
523 t.Parallel()
524 node := makeDaemon(t)
525 defer node.StopDaemon()
526
527 ipnsName := makeKey(t, node, "testkey")
528
529 // create a file larger than 10 KiB
530 oversizedRecord := make([]byte, 11*1024)
531 recordFile := filepath.Join(node.Dir, "oversized.bin")
532 err := os.WriteFile(recordFile, oversizedRecord, 0644)
533 require.NoError(t, err)
534
535 // with --force, size check is skipped (but routing will likely reject it)
536 res := node.RunIPFS("name", "put", "--force", ipnsName.String(), recordFile)
537 // the command itself should not fail on size, but routing may reject
538 // we just verify it doesn't fail with "exceeds maximum size"
539 if res.Err != nil {
540 require.NotContains(t, res.Stderr.String(), "exceeds maximum size")
541 }
542 })
543
544 t.Run("name put stores IPNS record", func(t *testing.T) {
545 t.Parallel()
546 h := harness.NewT(t)
547 publishPath := "/ipfs/" + fixtureCid
548
549 // create a record on an ephemeral node (shut down before test node starts)
550 ipnsName, record := makeExternalRecord(t, h, publishPath)
551
552 // start test node (has no local knowledge of the record)
553 node := makeDaemon(t)
554 defer node.StopDaemon()
555
556 // put the record (should succeed since no existing record)
557 recordFile := filepath.Join(node.Dir, "record.bin")
558 err := os.WriteFile(recordFile, record, 0644)
559 require.NoError(t, err)
560
561 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
562 require.NoError(t, res.Err)
563
564 // verify the record was stored by getting it back
565 res = node.IPFS("name", "get", ipnsName.String())
566 retrievedRecord := res.Stdout.Bytes()
567 require.Equal(t, record, retrievedRecord, "stored record should match original")
568 })
569
570 t.Run("name put with --force overwrites existing record", func(t *testing.T) {
571 t.Parallel()
572 h := harness.NewT(t)
573 publishPath := "/ipfs/" + fixtureCid
574
575 // create a record on an ephemeral node
576 ipnsName, record := makeExternalRecord(t, h, publishPath)
577
578 // start test node
579 node := makeDaemon(t)
580 defer node.StopDaemon()
581
582 // first put the record normally
583 recordFile := filepath.Join(node.Dir, "record.bin")
584 err := os.WriteFile(recordFile, record, 0644)
585 require.NoError(t, err)
586
587 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
588 require.NoError(t, res.Err)
589
590 // put the same record again (identical record republishing is allowed)
591 res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
592 require.NoError(t, res.Err)
593
594 // put the record with --force (should succeed)
595 res = node.RunIPFS("name", "put", "--force", ipnsName.String(), recordFile)
596 require.NoError(t, res.Err)
597 })
598
599 t.Run("name put validates signature against name", func(t *testing.T) {
600 t.Parallel()
601 h := harness.NewT(t)
602 publishPath := "/ipfs/" + fixtureCid
603
604 // create a record on an ephemeral node
605 _, record := makeExternalRecord(t, h, publishPath)
606
607 // start test node
608 node := makeDaemon(t)
609 defer node.StopDaemon()
610
611 // write the record to a file
612 recordFile := filepath.Join(node.Dir, "record.bin")
613 err := os.WriteFile(recordFile, record, 0644)
614 require.NoError(t, err)
615
616 // try to put with a wrong name (should fail validation)
617 wrongName := "12D3KooWRirYjmmQATx2kgHBfky6DADsLP7ex1t7BRxJ6nqLs9WH"
618 res := node.RunIPFS("name", "put", wrongName, recordFile)
619 require.Error(t, res.Err)
620 require.Contains(t, res.Stderr.String(), "record validation failed")
621 })
622
623 t.Run("name put with --force skips command validation", func(t *testing.T) {
624 t.Parallel()
625 h := harness.NewT(t)
626 publishPath := "/ipfs/" + fixtureCid
627
628 // create a record on an ephemeral node
629 ipnsName, record := makeExternalRecord(t, h, publishPath)
630
631 // start test node
632 node := makeDaemon(t)
633 defer node.StopDaemon()
634
635 // with --force the command skips its own validation (signature, sequence check)
636 // and passes the record directly to the routing layer
637 res := node.PipeToIPFS(bytes.NewReader(record), "name", "put", "--force", ipnsName.String())
638 require.NoError(t, res.Err)
639 })
640
641 t.Run("name put rejects empty record", func(t *testing.T) {
642 t.Parallel()
643 node := makeDaemon(t)
644 defer node.StopDaemon()
645
646 ipnsName := makeKey(t, node, "testkey")
647
648 // create an empty file
649 recordFile := filepath.Join(node.Dir, "empty.bin")
650 err := os.WriteFile(recordFile, []byte{}, 0644)
651 require.NoError(t, err)
652
653 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
654 require.Error(t, res.Err)
655 require.Contains(t, res.Stderr.String(), "record is empty")
656 })
657
658 t.Run("name put rejects invalid record", func(t *testing.T) {
659 t.Parallel()
660 node := makeDaemon(t)
661 defer node.StopDaemon()
662
663 ipnsName := makeKey(t, node, "testkey")
664
665 // create a file with garbage data
666 recordFile := filepath.Join(node.Dir, "garbage.bin")
667 err := os.WriteFile(recordFile, []byte("not a valid ipns record"), 0644)
668 require.NoError(t, err)
669
670 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
671 require.Error(t, res.Err)
672 require.Contains(t, res.Stderr.String(), "invalid IPNS record")
673 })
674
675 t.Run("name put accepts stdin", func(t *testing.T) {
676 t.Parallel()
677 h := harness.NewT(t)
678 publishPath := "/ipfs/" + fixtureCid
679
680 // create a record on an ephemeral node
681 ipnsName, record := makeExternalRecord(t, h, publishPath)
682
683 // start test node (has no local knowledge of the record)
684 node := makeDaemon(t)
685 defer node.StopDaemon()
686
687 // put via stdin (no --force needed since no existing record)
688 res := node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
689 require.NoError(t, res.Err)
690 })
691
692 t.Run("name put fails when offline without --allow-offline", func(t *testing.T) {
693 t.Parallel()
694 h := harness.NewT(t)
695 publishPath := "/ipfs/" + fixtureCid
696
697 // create a record on an ephemeral node
698 ipnsName, record := makeExternalRecord(t, h, publishPath)
699
700 // write the record to a file
701 recordFile := filepath.Join(h.Dir, "record.bin")
702 err := os.WriteFile(recordFile, record, 0644)
703 require.NoError(t, err)
704
705 // start test node in offline mode
706 node := h.NewNode().Init("--profile=test")
707 node.StartDaemon("--offline")
708 defer node.StopDaemon()
709
710 // try to put without --allow-offline (should fail)
711 res := node.RunIPFS("name", "put", ipnsName.String(), recordFile)
712 require.Error(t, res.Err)
713 // error can come from our command or from the routing layer
714 stderr := res.Stderr.String()
715 require.True(t, strings.Contains(stderr, "offline") || strings.Contains(stderr, "online mode"),
716 "expected offline-related error, got: %s", stderr)
717 })
718
719 t.Run("name put succeeds with --allow-offline", func(t *testing.T) {
720 t.Parallel()
721 h := harness.NewT(t)
722 publishPath := "/ipfs/" + fixtureCid
723
724 // create a record on an ephemeral node
725 ipnsName, record := makeExternalRecord(t, h, publishPath)
726
727 // write the record to a file
728 recordFile := filepath.Join(h.Dir, "record.bin")
729 err := os.WriteFile(recordFile, record, 0644)
730 require.NoError(t, err)
731
732 // start test node in offline mode
733 node := h.NewNode().Init("--profile=test")
734 node.StartDaemon("--offline")
735 defer node.StopDaemon()
736
737 // put with --allow-offline (should succeed, no --force needed since no existing record)
738 res := node.RunIPFS("name", "put", "--allow-offline", ipnsName.String(), recordFile)
739 require.NoError(t, res.Err)
740 })
741
742 t.Run("name get/put round trip preserves record bytes", func(t *testing.T) {
743 t.Parallel()
744 h := harness.NewT(t)
745 publishPath := "/ipfs/" + fixtureCid
746
747 // create a record on an ephemeral node
748 ipnsName, originalRecord := makeExternalRecord(t, h, publishPath)
749
750 // start test node (has no local knowledge of the record)
751 node := makeDaemon(t)
752 defer node.StopDaemon()
753
754 // put the record
755 res := node.PipeToIPFS(bytes.NewReader(originalRecord), "name", "put", ipnsName.String())
756 require.NoError(t, res.Err)
757
758 // get the record back
759 res = node.IPFS("name", "get", ipnsName.String())
760 retrievedRecord := res.Stdout.Bytes()
761
762 // the records should be byte-for-byte identical
763 require.Equal(t, originalRecord, retrievedRecord, "record bytes should be preserved after get/put round trip")
764 })
765
766 t.Run("name put --force allows storing lower sequence record", func(t *testing.T) {
767 t.Parallel()
768 h := harness.NewT(t)
769 publishPath := "/ipfs/" + fixtureCid
770
771 // create an ephemeral node to generate two records with different sequences
772 ephNode := h.NewNode().Init("--profile=test")
773
774 r, err := os.Open(fixturePath)
775 require.NoError(t, err)
776 err = ephNode.IPFSDagImport(r, fixtureCid)
777 r.Close()
778 require.NoError(t, err)
779
780 ephNode.StartDaemon()
781
782 res := ephNode.IPFS("key", "gen", "--type=ed25519", "ephemeral-key")
783 keyID := strings.TrimSpace(res.Stdout.String())
784 ipnsName, err := ipns.NameFromString(keyID)
785 require.NoError(t, err)
786
787 // publish record with sequence 100
788 ephNode.IPFS("name", "publish", "--key=ephemeral-key", "--lifetime=5m", "--sequence=100", publishPath)
789 res = ephNode.IPFS("name", "get", ipnsName.String())
790 record100 := res.Stdout.Bytes()
791
792 // publish record with sequence 200
793 ephNode.IPFS("name", "publish", "--key=ephemeral-key", "--lifetime=5m", "--sequence=200", publishPath)
794 res = ephNode.IPFS("name", "get", ipnsName.String())
795 record200 := res.Stdout.Bytes()
796
797 ephNode.StopDaemon()
798
799 // start test node (has no local knowledge of the records)
800 node := makeDaemon(t)
801 defer node.StopDaemon()
802
803 // helper to get sequence from record
804 getSequence := func(record []byte) uint64 {
805 res := node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--enc=json")
806 var result name.IpnsInspectResult
807 err := json.Unmarshal(res.Stdout.Bytes(), &result)
808 require.NoError(t, err)
809 require.NotNil(t, result.Entry.Sequence)
810 return *result.Entry.Sequence
811 }
812
813 // verify we have the right records
814 require.Equal(t, uint64(100), getSequence(record100))
815 require.Equal(t, uint64(200), getSequence(record200))
816
817 // put record with sequence 200 first
818 res = node.PipeToIPFS(bytes.NewReader(record200), "name", "put", ipnsName.String())
819 require.NoError(t, res.Err)
820
821 // verify current record has sequence 200
822 res = node.IPFS("name", "get", ipnsName.String())
823 require.Equal(t, uint64(200), getSequence(res.Stdout.Bytes()))
824
825 // now put the lower sequence record (100) with --force
826 // this should succeed (--force bypasses our sequence check)
827 res = node.PipeToIPFS(bytes.NewReader(record100), "name", "put", "--force", ipnsName.String())
828 require.NoError(t, res.Err, "putting lower sequence record with --force should succeed")
829
830 // note: when we get the record, IPNS resolution returns the "best" record
831 // (highest sequence), so we'll get the sequence 200 record back
832 // this is expected IPNS behavior - the put succeeded, but get returns the best record
833 res = node.IPFS("name", "get", ipnsName.String())
834 retrievedSeq := getSequence(res.Stdout.Bytes())
835 require.Equal(t, uint64(200), retrievedSeq, "IPNS get returns the best (highest sequence) record")
836 })
837
838 t.Run("name put sequence conflict detection", func(t *testing.T) {
839 t.Parallel()
840 h := harness.NewT(t)
841 publishPath := "/ipfs/" + fixtureCid
842
843 // create an ephemeral node to generate two records with different sequences
844 ephNode := h.NewNode().Init("--profile=test")
845
846 r, err := os.Open(fixturePath)
847 require.NoError(t, err)
848 err = ephNode.IPFSDagImport(r, fixtureCid)
849 r.Close()
850 require.NoError(t, err)
851
852 ephNode.StartDaemon()
853
854 res := ephNode.IPFS("key", "gen", "--type=ed25519", "ephemeral-key")
855 keyID := strings.TrimSpace(res.Stdout.String())
856 ipnsName, err := ipns.NameFromString(keyID)
857 require.NoError(t, err)
858
859 // publish record with sequence 100
860 ephNode.IPFS("name", "publish", "--key=ephemeral-key", "--lifetime=5m", "--sequence=100", publishPath)
861 res = ephNode.IPFS("name", "get", ipnsName.String())
862 record100 := res.Stdout.Bytes()
863
864 // publish record with sequence 200
865 ephNode.IPFS("name", "publish", "--key=ephemeral-key", "--lifetime=5m", "--sequence=200", publishPath)
866 res = ephNode.IPFS("name", "get", ipnsName.String())
867 record200 := res.Stdout.Bytes()
868
869 ephNode.StopDaemon()
870
871 // start test node (has no local knowledge of the records)
872 node := makeDaemon(t)
873 defer node.StopDaemon()
874
875 // put record with sequence 200 first
876 res = node.PipeToIPFS(bytes.NewReader(record200), "name", "put", ipnsName.String())
877 require.NoError(t, res.Err)
878
879 // try to put record with sequence 100 (lower than current 200)
880 recordFile := filepath.Join(node.Dir, "record100.bin")
881 err = os.WriteFile(recordFile, record100, 0644)
882 require.NoError(t, err)
883
884 res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
885 require.Error(t, res.Err)
886 require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 200 >= new record sequence 100")
887 })
888
889 t.Run("name put allows identical record republishing", func(t *testing.T) {
890 t.Parallel()
891 h := harness.NewT(t)
892 publishPath := "/ipfs/" + fixtureCid
893
894 ipnsName, record := makeExternalRecord(t, h, publishPath, "--sequence=100")
895
896 node := makeDaemon(t)
897 defer node.StopDaemon()
898
899 // put the record
900 res := node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
901 require.NoError(t, res.Err)
902
903 // put the exact same record again (same bytes, same sequence)
904 // this should succeed: republishing an identical record is a valid use case
905 res = node.PipeToIPFS(bytes.NewReader(record), "name", "put", ipnsName.String())
906 require.NoError(t, res.Err)
907 require.Contains(t, res.Stdout.String(), ipnsName.String())
908 })
909
910 t.Run("name put rejects different record with same sequence", func(t *testing.T) {
911 t.Parallel()
912 h := harness.NewT(t)
913
914 // create two different records signed by the same key with the same
915 // sequence number by using two ephemeral nodes that share a key
916 ephNode1 := h.NewNode().Init("--profile=test")
917 r, err := os.Open(fixturePath)
918 require.NoError(t, err)
919 err = ephNode1.IPFSDagImport(r, fixtureCid)
920 r.Close()
921 require.NoError(t, err)
922 ephNode1.StartDaemon()
923
924 res := ephNode1.IPFS("key", "gen", "--type=ed25519", "shared-key")
925 keyID := strings.TrimSpace(res.Stdout.String())
926 ipnsName, err := ipns.NameFromString(keyID)
927 require.NoError(t, err)
928
929 // publish record A (sequence=100, value=fixtureCid)
930 ephNode1.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/"+fixtureCid)
931 res = ephNode1.IPFS("name", "get", ipnsName.String())
932 recordA := res.Stdout.Bytes()
933
934 // export key and import into second ephemeral node
935 keyFile := filepath.Join(ephNode1.Dir, "shared-key.key")
936 ephNode1.IPFS("key", "export", "--output="+keyFile, "shared-key")
937 ephNode1.StopDaemon()
938
939 ephNode2 := h.NewNode().Init("--profile=test")
940 ephNode2.StartDaemon()
941 ephNode2.IPFS("key", "import", "shared-key", keyFile)
942
943 // publish record B (sequence=100, different value)
944 ephNode2.IPFS("name", "publish", "--key=shared-key", "--lifetime=5m", "--sequence=100", "/ipfs/bafkqaaa")
945 res = ephNode2.IPFS("name", "get", ipnsName.String())
946 recordB := res.Stdout.Bytes()
947 ephNode2.StopDaemon()
948
949 // verify records have same sequence but different bytes
950 require.NotEqual(t, recordA, recordB, "records should have different bytes")
951
952 // start test node and try the put scenario
953 node := makeDaemon(t)
954 defer node.StopDaemon()
955
956 // put record A
957 res = node.PipeToIPFS(bytes.NewReader(recordA), "name", "put", ipnsName.String())
958 require.NoError(t, res.Err)
959
960 // try to put record B (different bytes, same sequence=100)
961 recordFile := filepath.Join(node.Dir, "recordB.bin")
962 err = os.WriteFile(recordFile, recordB, 0644)
963 require.NoError(t, err)
964
965 res = node.RunIPFS("name", "put", ipnsName.String(), recordFile)
966 require.Error(t, res.Err)
967 require.Contains(t, res.Stderr.String(), "existing IPNS record has sequence 100 >= new record sequence 100")
968 })
969 }