| 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 | } |