@cryptotaxi247 / kubo / commits / 579175f81

feat: add basic CLI tests using Go Test

This is intended as a replacement for sharness. These are vanilla Go tests which can be run in your IDE for quick iteration on end-to-end CLI tests. This also removes IPTB by duplicating its functionality in the test harness. This isn't a big deal...IPTB's complexity is mostly around the fact that its state needs to be saved to disk in between `iptb` command invocations, and that it uses Go plugins to inject functionality, neither of which are relevant here. If we merge this, we'll have to live with bifurcated tests for a while until they are all migrated. I'd recommend we self-enforce a rule that, if we need to touch a sharness test, we migrate it and one more test over to Go tests first. Then eventually we will have migrated everything.

Gus Eggert committed Dec 12, 2022 at 09:17 UTC 579175f81d400000881af5701c06351373df3fb8
22 files changed +1657 -609
.golangci.yml
+5
@@ -1,3 +1,8 @@
1 linters:
2 enable:
3 - stylecheck
4 +
5 +linters-settings:
6 + stylecheck:
7 + dot-import-whitelist:
8 + - github.com/ipfs/kubo/test/cli/testutils
coverage/Rules.mk
+1 -1
@@ -2,7 +2,7 @@ include mk/header.mk
2
3 GOCC ?= go
4
5 -$(d)/coverage_deps: $$(DEPS_GO)
5 +$(d)/coverage_deps: $$(DEPS_GO) cmd/ipfs/ipfs
6 rm -rf $(@D)/unitcover && mkdir $(@D)/unitcover
7 rm -rf $(@D)/sharnesscover && mkdir $(@D)/sharnesscover
8
go.mod
+1 -1
@@ -111,6 +111,7 @@ require (
111 go.uber.org/fx v1.18.2
112 go.uber.org/zap v1.24.0
113 golang.org/x/crypto v0.3.0
114 + golang.org/x/mod v0.7.0
115 golang.org/x/sync v0.1.0
116 golang.org/x/sys v0.3.0
117 )
@@ -233,7 +234,6 @@ require (
234 go.uber.org/multierr v1.8.0 // indirect
235 go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
236 golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
236 - golang.org/x/mod v0.7.0 // indirect
237 golang.org/x/net v0.3.0 // indirect
238 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
239 golang.org/x/term v0.3.0 // indirect
test/cli/basic_commands_test.go new
+238
@@ -0,0 +1,238 @@
1 +package cli
2 +
3 +import (
4 + "fmt"
5 + "regexp"
6 + "strings"
7 + "testing"
8 +
9 + "github.com/blang/semver/v4"
10 + "github.com/ipfs/kubo/test/cli/harness"
11 + . "github.com/ipfs/kubo/test/cli/testutils"
12 + "github.com/stretchr/testify/assert"
13 + gomod "golang.org/x/mod/module"
14 +)
15 +
16 +var versionRegexp = regexp.MustCompile(`^ipfs version (.+)$`)
17 +
18 +func parseVersionOutput(s string) semver.Version {
19 + versString := versionRegexp.FindStringSubmatch(s)[1]
20 + v, err := semver.Parse(versString)
21 + if err != nil {
22 + panic(err)
23 + }
24 + return v
25 +}
26 +
27 +func TestCurDirIsWritable(t *testing.T) {
28 + t.Parallel()
29 + h := harness.NewT(t)
30 + h.WriteFile("test.txt", "It works!")
31 +}
32 +
33 +func TestIPFSVersionCommandMatchesFlag(t *testing.T) {
34 + t.Parallel()
35 + node := harness.NewT(t).NewNode()
36 + commandVersionStr := node.IPFS("version").Stdout.String()
37 + commandVersionStr = strings.TrimSpace(commandVersionStr)
38 + commandVersion := parseVersionOutput(commandVersionStr)
39 +
40 + flagVersionStr := node.IPFS("--version").Stdout.String()
41 + flagVersionStr = strings.TrimSpace(flagVersionStr)
42 + flagVersion := parseVersionOutput(flagVersionStr)
43 +
44 + assert.Equal(t, commandVersion, flagVersion)
45 +}
46 +
47 +func TestIPFSVersionAll(t *testing.T) {
48 + t.Parallel()
49 + node := harness.NewT(t).NewNode()
50 + res := node.IPFS("version", "--all").Stdout.String()
51 + res = strings.TrimSpace(res)
52 + assert.Contains(t, res, "Kubo version")
53 + assert.Contains(t, res, "Repo version")
54 + assert.Contains(t, res, "System version")
55 + assert.Contains(t, res, "Golang version")
56 +}
57 +
58 +func TestIPFSVersionDeps(t *testing.T) {
59 + t.Parallel()
60 + node := harness.NewT(t).NewNode()
61 + res := node.IPFS("version", "deps").Stdout.String()
62 + res = strings.TrimSpace(res)
63 + lines := SplitLines(res)
64 +
65 + assert.Equal(t, "github.com/ipfs/kubo@(devel)", lines[0])
66 +
67 + for _, depLine := range lines[1:] {
68 + split := strings.Split(depLine, " => ")
69 + for _, moduleVersion := range split {
70 + splitModVers := strings.Split(moduleVersion, "@")
71 + modPath := splitModVers[0]
72 + modVers := splitModVers[1]
73 + assert.NoError(t, gomod.Check(modPath, modVers), "path: %s, version: %s", modPath, modVers)
74 + }
75 + }
76 +}
77 +
78 +func TestIPFSCommands(t *testing.T) {
79 + t.Parallel()
80 + node := harness.NewT(t).NewNode()
81 + cmds := node.IPFSCommands()
82 + assert.Contains(t, cmds, "ipfs add")
83 + assert.Contains(t, cmds, "ipfs daemon")
84 + assert.Contains(t, cmds, "ipfs update")
85 +}
86 +
87 +func TestAllSubcommandsAcceptHelp(t *testing.T) {
88 + t.Parallel()
89 + node := harness.NewT(t).NewNode()
90 + for _, cmd := range node.IPFSCommands() {
91 + t.Run(fmt.Sprintf("command %q accepts help", cmd), func(t *testing.T) {
92 + t.Parallel()
93 + splitCmd := strings.Split(cmd, " ")[1:]
94 + node.IPFS(StrCat("help", splitCmd)...)
95 + node.IPFS(StrCat(splitCmd, "--help")...)
96 + })
97 + }
98 +}
99 +
100 +func TestAllRootCommandsAreMentionedInHelpText(t *testing.T) {
101 + t.Parallel()
102 + node := harness.NewT(t).NewNode()
103 + cmds := node.IPFSCommands()
104 + var rootCmds []string
105 + for _, cmd := range cmds {
106 + splitCmd := strings.Split(cmd, " ")
107 + if len(splitCmd) == 2 {
108 + rootCmds = append(rootCmds, splitCmd[1])
109 + }
110 + }
111 +
112 + // a few base commands are not expected to be in the help message
113 + // but we default to requiring them to be in the help message, so that we
114 + // have to make an conscious decision to exclude them
115 + notInHelp := map[string]bool{
116 + "object": true,
117 + "shutdown": true,
118 + "tar": true,
119 + "urlstore": true,
120 + "dns": true,
121 + }
122 +
123 + helpMsg := strings.TrimSpace(node.IPFS("--help").Stdout.String())
124 + for _, rootCmd := range rootCmds {
125 + if _, ok := notInHelp[rootCmd]; ok {
126 + continue
127 + }
128 + assert.Contains(t, helpMsg, fmt.Sprintf(" %s", rootCmd))
129 + }
130 +}
131 +
132 +func TestCommandDocsWidth(t *testing.T) {
133 + t.Parallel()
134 + node := harness.NewT(t).NewNode()
135 +
136 + // require new commands to explicitly opt in to longer lines
137 + allowList := map[string]bool{
138 + "ipfs add": true,
139 + "ipfs block put": true,
140 + "ipfs daemon": true,
141 + "ipfs config profile": true,
142 + "ipfs pin remote service": true,
143 + "ipfs name pubsub": true,
144 + "ipfs object patch": true,
145 + "ipfs swarm connect": true,
146 + "ipfs p2p forward": true,
147 + "ipfs p2p close": true,
148 + "ipfs swarm disconnect": true,
149 + "ipfs swarm addrs listen": true,
150 + "ipfs dag resolve": true,
151 + "ipfs dag get": true,
152 + "ipfs object stat": true,
153 + "ipfs pin remote add": true,
154 + "ipfs config show": true,
155 + "ipfs config edit": true,
156 + "ipfs pin remote rm": true,
157 + "ipfs pin remote ls": true,
158 + "ipfs pin verify": true,
159 + "ipfs dht get": true,
160 + "ipfs pin remote service add": true,
161 + "ipfs file ls": true,
162 + "ipfs pin update": true,
163 + "ipfs pin rm": true,
164 + "ipfs p2p": true,
165 + "ipfs resolve": true,
166 + "ipfs dag stat": true,
167 + "ipfs name publish": true,
168 + "ipfs object diff": true,
169 + "ipfs object patch add-link": true,
170 + "ipfs name": true,
171 + "ipfs object patch append-data": true,
172 + "ipfs object patch set-data": true,
173 + "ipfs dht put": true,
174 + "ipfs diag profile": true,
175 + "ipfs diag cmds": true,
176 + "ipfs swarm addrs local": true,
177 + "ipfs files ls": true,
178 + "ipfs stats bw": true,
179 + "ipfs urlstore add": true,
180 + "ipfs swarm peers": true,
181 + "ipfs pubsub sub": true,
182 + "ipfs repo fsck": true,
183 + "ipfs files write": true,
184 + "ipfs swarm limit": true,
185 + "ipfs commands completion fish": true,
186 + "ipfs key export": true,
187 + "ipfs routing get": true,
188 + "ipfs refs": true,
189 + "ipfs refs local": true,
190 + "ipfs cid base32": true,
191 + "ipfs pubsub pub": true,
192 + "ipfs repo ls": true,
193 + "ipfs routing put": true,
194 + "ipfs key import": true,
195 + "ipfs swarm peering add": true,
196 + "ipfs swarm peering rm": true,
197 + "ipfs swarm peering ls": true,
198 + "ipfs update": true,
199 + "ipfs swarm stats": true,
200 + }
201 + for _, cmd := range node.IPFSCommands() {
202 + if _, ok := allowList[cmd]; ok {
203 + continue
204 + }
205 + t.Run(fmt.Sprintf("command %q conforms to docs width limit", cmd), func(t *testing.T) {
206 + splitCmd := strings.Split(cmd, " ")
207 + resStr := node.IPFS(StrCat(splitCmd[1:], "--help")...)
208 + res := strings.TrimSpace(resStr.Stdout.String())
209 + for _, line := range SplitLines(res) {
210 + assert.LessOrEqualf(t, len(line), 80, "expected width %d < 80 for %q", len(line), cmd)
211 + }
212 +
213 + })
214 + }
215 +}
216 +
217 +func TestAllCommandsFailWhenPassedBadFlag(t *testing.T) {
218 + t.Parallel()
219 + node := harness.NewT(t).NewNode()
220 +
221 + for _, cmd := range node.IPFSCommands() {
222 + t.Run(fmt.Sprintf("command %q fails when passed a bad flag", cmd), func(t *testing.T) {
223 + splitCmd := strings.Split(cmd, " ")
224 + res := node.RunIPFS(StrCat(splitCmd, "--badflag")...)
225 + assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
226 + })
227 + }
228 +
229 +}
230 +
231 +func TestCommandsFlags(t *testing.T) {
232 + t.Parallel()
233 + node := harness.NewT(t).NewNode()
234 + resStr := node.IPFS("commands", "--flags").Stdout.String()
235 + assert.Contains(t, resStr, "ipfs pin add --recursive / ipfs pin add -r")
236 + assert.Contains(t, resStr, "ipfs id --format / ipfs id -f")
237 + assert.Contains(t, resStr, "ipfs repo gc --quiet / ipfs repo gc -q")
238 +}
test/cli/completion_test.go new
+31
@@ -0,0 +1,31 @@
1 +package cli
2 +
3 +import (
4 + "fmt"
5 + "testing"
6 +
7 + "github.com/ipfs/kubo/test/cli/harness"
8 + . "github.com/ipfs/kubo/test/cli/testutils"
9 + "github.com/stretchr/testify/assert"
10 +)
11 +
12 +func TestBashCompletion(t *testing.T) {
13 + t.Parallel()
14 + h := harness.NewT(t)
15 + node := h.NewNode()
16 +
17 + res := node.IPFS("commands", "completion", "bash")
18 +
19 + length := len(res.Stdout.String())
20 + if length < 100 {
21 + t.Fatalf("expected a long Bash completion file, but got one of length %d", length)
22 + }
23 +
24 + t.Run("completion file can be loaded in bash", func(t *testing.T) {
25 + RequiresLinux(t)
26 +
27 + completionFile := h.WriteToTemp(res.Stdout.String())
28 + res = h.Sh(fmt.Sprintf("source %s && type -t _ipfs", completionFile))
29 + assert.NoError(t, res.Err)
30 + })
31 +}
test/cli/delegated_routing_http_test.go new
+121
@@ -0,0 +1,121 @@
1 +package cli
2 +
3 +import (
4 + "net/http"
5 + "net/http/httptest"
6 + "testing"
7 +
8 + "github.com/ipfs/kubo/config"
9 + "github.com/ipfs/kubo/test/cli/harness"
10 + . "github.com/ipfs/kubo/test/cli/testutils"
11 + "github.com/stretchr/testify/assert"
12 +)
13 +
14 +func TestHTTPDelegatedRouting(t *testing.T) {
15 + t.Parallel()
16 + node := harness.NewT(t).NewNode().Init().StartDaemon()
17 +
18 + fakeServer := func(resp string) *httptest.Server {
19 + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 + _, err := w.Write([]byte(resp))
21 + if err != nil {
22 + panic(err)
23 + }
24 + }))
25 + }
26 +
27 + findProvsCID := "baeabep4vu3ceru7nerjjbk37sxb7wmftteve4hcosmyolsbsiubw2vr6pqzj6mw7kv6tbn6nqkkldnklbjgm5tzbi4hkpkled4xlcr7xz4bq"
28 + prov := "12D3KooWARYacCc6eoCqvsS9RW9MA2vo51CV75deoiqssx3YgyYJ"
29 +
30 + t.Run("default routing config has no routers defined", func(t *testing.T) {
31 + assert.Nil(t, node.ReadConfig().Routing.Routers)
32 + })
33 +
34 + t.Run("no routers means findprovs returns no results", func(t *testing.T) {
35 + res := node.IPFS("routing", "findprovs", findProvsCID).Stdout.String()
36 + assert.Empty(t, res)
37 + })
38 +
39 + t.Run("no routers means findprovs returns no results", func(t *testing.T) {
40 + res := node.IPFS("routing", "findprovs", findProvsCID).Stdout.String()
41 + assert.Empty(t, res)
42 + })
43 +
44 + node.StopDaemon()
45 +
46 + t.Run("missing method params make the daemon fail", func(t *testing.T) {
47 + node.UpdateConfig(func(cfg *config.Config) {
48 + cfg.Routing.Type = config.NewOptionalString("custom")
49 + cfg.Routing.Methods = config.Methods{
50 + "find-peers": {RouterName: "TestDelegatedRouter"},
51 + "find-providers": {RouterName: "TestDelegatedRouter"},
52 + "get-ipns": {RouterName: "TestDelegatedRouter"},
53 + "provide": {RouterName: "TestDelegatedRouter"},
54 + }
55 + })
56 + res := node.RunIPFS("daemon")
57 + assert.Equal(t, 1, res.ExitErr.ProcessState.ExitCode())
58 + assert.Contains(
59 + t,
60 + res.Stderr.String(),
61 + `method name "put-ipns" is missing from Routing.Methods config param`,
62 + )
63 + })
64 +
65 + t.Run("having wrong methods makes daemon fail", func(t *testing.T) {
66 + node.UpdateConfig(func(cfg *config.Config) {
67 + cfg.Routing.Type = config.NewOptionalString("custom")
68 + cfg.Routing.Methods = config.Methods{
69 + "find-peers": {RouterName: "TestDelegatedRouter"},
70 + "find-providers": {RouterName: "TestDelegatedRouter"},
71 + "get-ipns": {RouterName: "TestDelegatedRouter"},
72 + "provide": {RouterName: "TestDelegatedRouter"},
73 + "put-ipns": {RouterName: "TestDelegatedRouter"},
74 + "NOT_SUPPORTED": {RouterName: "TestDelegatedRouter"},
75 + }
76 + })
77 + res := node.RunIPFS("daemon")
78 + assert.Equal(t, 1, res.ExitErr.ProcessState.ExitCode())
79 + assert.Contains(
80 + t,
81 + res.Stderr.String(),
82 + `method name "NOT_SUPPORTED" is not a supported method on Routing.Methods config param`,
83 + )
84 + })
85 +
86 + t.Run("adding HTTP delegated routing endpoint to Routing.Routers config works", func(t *testing.T) {
87 + server := fakeServer(ToJSONStr(JSONObj{
88 + "Providers": []JSONObj{{
89 + "Protocol": "transport-bitswap",
90 + "Schema": "bitswap",
91 + "ID": prov,
92 + "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"},
93 + }},
94 + }))
95 + t.Cleanup(server.Close)
96 +
97 + node.IPFS("config", "Routing.Type", "--json", `"custom"`)
98 + node.IPFS("config", "Routing.Routers.TestDelegatedRouter", "--json", ToJSONStr(JSONObj{
99 + "Type": "http",
100 + "Parameters": JSONObj{
101 + "Endpoint": server.URL,
102 + },
103 + }))
104 + node.IPFS("config", "Routing.Methods", "--json", ToJSONStr(JSONObj{
105 + "find-peers": JSONObj{"RouterName": "TestDelegatedRouter"},
106 + "find-providers": JSONObj{"RouterName": "TestDelegatedRouter"},
107 + "get-ipns": JSONObj{"RouterName": "TestDelegatedRouter"},
108 + "provide": JSONObj{"RouterName": "TestDelegatedRouter"},
109 + "put-ipns": JSONObj{"RouterName": "TestDelegatedRouter"},
110 + }))
111 +
112 + res := node.IPFS("config", "Routing.Routers.TestDelegatedRouter.Parameters.Endpoint")
113 + assert.Equal(t, res.Stdout.Trimmed(), server.URL)
114 +
115 + node.StartDaemon()
116 +
117 + res = node.IPFS("routing", "findprovs", findProvsCID)
118 + assert.Equal(t, prov, res.Stdout.Trimmed())
119 + })
120 +
121 +}
test/cli/harness/buffer.go new
+45
@@ -0,0 +1,45 @@
1 +package harness
2 +
3 +import (
4 + "strings"
5 + "sync"
6 +
7 + "github.com/ipfs/kubo/test/cli/testutils"
8 +)
9 +
10 +// Buffer is a thread-safe byte buffer.
11 +type Buffer struct {
12 + b strings.Builder
13 + m sync.Mutex
14 +}
15 +
16 +func (b *Buffer) Write(p []byte) (n int, err error) {
17 + b.m.Lock()
18 + defer b.m.Unlock()
19 + return b.b.Write(p)
20 +}
21 +
22 +func (b *Buffer) String() string {
23 + b.m.Lock()
24 + defer b.m.Unlock()
25 + return b.b.String()
26 +}
27 +
28 +// Trimmed returns the bytes as a string, with leading and trailing whitespace removed.
29 +func (b *Buffer) Trimmed() string {
30 + b.m.Lock()
31 + defer b.m.Unlock()
32 + return strings.TrimSpace(b.b.String())
33 +}
34 +
35 +func (b *Buffer) Bytes() []byte {
36 + b.m.Lock()
37 + defer b.m.Unlock()
38 + return []byte(b.b.String())
39 +}
40 +
41 +func (b *Buffer) Lines() []string {
42 + b.m.Lock()
43 + defer b.m.Unlock()
44 + return testutils.SplitLines(b.b.String())
45 +}
test/cli/harness/harness.go new
+187
@@ -0,0 +1,187 @@
1 +package harness
2 +
3 +import (
4 + "errors"
5 + "fmt"
6 + "os"
7 + "path/filepath"
8 + "strings"
9 + "testing"
10 + "time"
11 +
12 + logging "github.com/ipfs/go-log/v2"
13 + . "github.com/ipfs/kubo/test/cli/testutils"
14 +)
15 +
16 +// Harness tracks state for a test, such as temp dirs and IFPS nodes, and cleans them up after the test.
17 +type Harness struct {
18 + Dir string
19 + IPFSBin string
20 + Runner *Runner
21 + NodesRoot string
22 + Nodes Nodes
23 +}
24 +
25 +// TODO: use zaptest.NewLogger(t) instead
26 +func EnableDebugLogging() {
27 + err := logging.SetLogLevel("testharness", "DEBUG")
28 + if err != nil {
29 + panic(err)
30 + }
31 +}
32 +
33 +// NewT constructs a harness that cleans up after the given test is done.
34 +func NewT(t *testing.T, options ...func(h *Harness)) *Harness {
35 + h := New(options...)
36 + t.Cleanup(h.Cleanup)
37 + return h
38 +}
39 +
40 +func New(options ...func(h *Harness)) *Harness {
41 + h := &Harness{Runner: &Runner{Env: osEnviron()}}
42 +
43 + // walk up to find the root dir, from which we can locate the binary
44 + wd, err := os.Getwd()
45 + if err != nil {
46 + panic(err)
47 + }
48 + goMod := FindUp("go.mod", wd)
49 + if goMod == "" {
50 + panic("unable to find root dir")
51 + }
52 + rootDir := filepath.Dir(goMod)
53 + h.IPFSBin = filepath.Join(rootDir, "cmd", "ipfs", "ipfs")
54 +
55 + // setup working dir
56 + tmpDir, err := os.MkdirTemp("", "")
57 + if err != nil {
58 + log.Panicf("error creating temp dir: %s", err)
59 + }
60 + h.Dir = tmpDir
61 + h.Runner.Dir = h.Dir
62 +
63 + h.NodesRoot = filepath.Join(h.Dir, ".nodes")
64 +
65 + // apply any customizations
66 + // this should happen after all initialization
67 + for _, o := range options {
68 + o(h)
69 + }
70 +
71 + return h
72 +}
73 +
74 +func osEnviron() map[string]string {
75 + m := map[string]string{}
76 + for _, entry := range os.Environ() {
77 + split := strings.Split(entry, "=")
78 + m[split[0]] = split[1]
79 + }
80 + return m
81 +}
82 +
83 +func (h *Harness) NewNode() *Node {
84 + nodeID := len(h.Nodes)
85 + node := BuildNode(h.IPFSBin, h.NodesRoot, nodeID)
86 + h.Nodes = append(h.Nodes, node)
87 + return node
88 +}
89 +
90 +func (h *Harness) NewNodes(count int) Nodes {
91 + var newNodes []*Node
92 + for i := 0; i < count; i++ {
93 + newNodes = append(newNodes, h.NewNode())
94 + }
95 + return newNodes
96 +}
97 +
98 +// WriteToTemp writes the given contents to a guaranteed-unique temp file, returning its path.
99 +func (h *Harness) WriteToTemp(contents string) string {
100 + f := h.TempFile()
101 + _, err := f.WriteString(contents)
102 + if err != nil {
103 + log.Panicf("writing to temp file: %s", err.Error())
104 + }
105 + err = f.Close()
106 + if err != nil {
107 + log.Panicf("closing temp file: %s", err.Error())
108 + }
109 + return f.Name()
110 +}
111 +
112 +// TempFile creates a new unique temp file.
113 +func (h *Harness) TempFile() *os.File {
114 + f, err := os.CreateTemp(h.Dir, "")
115 + if err != nil {
116 + log.Panicf("creating temp file: %s", err.Error())
117 + }
118 + return f
119 +}
120 +
121 +// WriteFile writes a file given a filename and its contents.
122 +// The filename should be a relative path.
123 +func (h *Harness) WriteFile(filename, contents string) {
124 + if filepath.IsAbs(filename) {
125 + log.Panicf("%s must be a relative path", filename)
126 + }
127 + absPath := filepath.Join(h.Runner.Dir, filename)
128 + err := os.WriteFile(absPath, []byte(contents), 0644)
129 + if err != nil {
130 + log.Panicf("writing '%s' ('%s'): %s", filename, absPath, err.Error())
131 + }
132 +}
133 +
134 +func WaitForFile(path string, timeout time.Duration) error {
135 + start := time.Now()
136 + timer := time.NewTimer(timeout)
137 + ticker := time.NewTicker(1 * time.Millisecond)
138 + defer timer.Stop()
139 + defer ticker.Stop()
140 + for {
141 + select {
142 + case <-timer.C:
143 + end := time.Now()
144 + return fmt.Errorf("timeout waiting for %s after %v", path, end.Sub(start))
145 + case <-ticker.C:
146 + _, err := os.Stat(path)
147 + if err == nil {
148 + return nil
149 + }
150 + if errors.Is(err, os.ErrNotExist) {
151 + continue
152 + }
153 + return fmt.Errorf("error waiting for %s: %w", path, err)
154 + }
155 + }
156 +}
157 +
158 +func (h *Harness) Mkdirs(paths ...string) {
159 + for _, path := range paths {
160 + if filepath.IsAbs(path) {
161 + log.Panicf("%s must be a relative path when making dirs", path)
162 + }
163 + absPath := filepath.Join(h.Runner.Dir, path)
164 + err := os.MkdirAll(absPath, 0777)
165 + if err != nil {
166 + log.Panicf("recursively making dirs under %s: %s", absPath, err)
167 + }
168 + }
169 +}
170 +
171 +func (h *Harness) Sh(expr string) RunResult {
172 + return h.Runner.Run(RunRequest{
173 + Path: "bash",
174 + Args: []string{"-c", expr},
175 + })
176 +}
177 +
178 +func (h *Harness) Cleanup() {
179 + log.Debugf("cleaning up cluster")
180 + h.Nodes.StopDaemons()
181 + // TODO: don't do this if test fails, not sure how?
182 + log.Debugf("removing harness dir")
183 + err := os.RemoveAll(h.Dir)
184 + if err != nil {
185 + log.Panicf("removing temp dir %s: %s", h.Dir, err)
186 + }
187 +}
test/cli/harness/ipfs.go new
+80
@@ -0,0 +1,80 @@
1 +package harness
2 +
3 +import (
4 + "encoding/json"
5 + "fmt"
6 + "io"
7 + "reflect"
8 + "strings"
9 +
10 + . "github.com/ipfs/kubo/test/cli/testutils"
11 +)
12 +
13 +func (n *Node) IPFSCommands() []string {
14 + res := n.IPFS("commands").Stdout.String()
15 + res = strings.TrimSpace(res)
16 + split := SplitLines(res)
17 + var cmds []string
18 + for _, line := range split {
19 + trimmed := strings.TrimSpace(line)
20 + if trimmed == "ipfs" {
21 + continue
22 + }
23 + cmds = append(cmds, trimmed)
24 + }
25 + return cmds
26 +}
27 +
28 +func (n *Node) SetIPFSConfig(key string, val interface{}, flags ...string) {
29 + valBytes, err := json.Marshal(val)
30 + if err != nil {
31 + log.Panicf("marshling config for key '%s': %s", key, err)
32 + }
33 + valStr := string(valBytes)
34 +
35 + args := []string{"config", "--json"}
36 + args = append(args, flags...)
37 + args = append(args, key, valStr)
38 + n.IPFS(args...)
39 +
40 + // validate the config was set correctly
41 + var newVal string
42 + n.GetIPFSConfig(key, &newVal)
43 + if val != newVal {
44 + log.Panicf("key '%s' did not retain value '%s' after it was set, got '%s'", key, val, newVal)
45 + }
46 +}
47 +
48 +func (n *Node) GetIPFSConfig(key string, val interface{}) {
49 + res := n.IPFS("config", key)
50 + valStr := strings.TrimSpace(res.Stdout.String())
51 + // only when the result is a string is the result not well-formed JSON,
52 + // so check the value type and add quotes if it's expected to be a string
53 + reflectVal := reflect.ValueOf(val)
54 + if reflectVal.Kind() == reflect.Ptr && reflectVal.Elem().Kind() == reflect.String {
55 + valStr = fmt.Sprintf(`"%s"`, valStr)
56 + }
57 + err := json.Unmarshal([]byte(valStr), val)
58 + if err != nil {
59 + log.Fatalf("unmarshaling config for key '%s', value '%s': %s", key, valStr, err)
60 + }
61 +}
62 +
63 +func (n *Node) IPFSAddStr(content string, args ...string) string {
64 + log.Debugf("node %d adding content '%s' with args: %v", n.ID, PreviewStr(content), args)
65 + return n.IPFSAdd(strings.NewReader(content), args...)
66 +}
67 +
68 +func (n *Node) IPFSAdd(content io.Reader, args ...string) string {
69 + log.Debugf("node %d adding with args: %v", n.ID, args)
70 + fullArgs := []string{"add", "-q"}
71 + fullArgs = append(fullArgs, args...)
72 + res := n.Runner.MustRun(RunRequest{
73 + Path: n.IPFSBin,
74 + Args: fullArgs,
75 + CmdOpts: []CmdOpt{RunWithStdin(content)},
76 + })
77 + out := strings.TrimSpace(res.Stdout.String())
78 + log.Debugf("add result: %q", out)
79 + return out
80 +}
test/cli/harness/node.go new
+383
@@ -0,0 +1,383 @@
1 +package harness
2 +
3 +import (
4 + "encoding/json"
5 + "errors"
6 + "fmt"
7 + "io"
8 + "net/http"
9 + "os"
10 + "os/exec"
11 + "path/filepath"
12 + "strconv"
13 + "strings"
14 + "syscall"
15 + "time"
16 +
17 + logging "github.com/ipfs/go-log/v2"
18 + "github.com/ipfs/kubo/config"
19 + serial "github.com/ipfs/kubo/config/serialize"
20 + "github.com/libp2p/go-libp2p/core/peer"
21 + "github.com/multiformats/go-multiaddr"
22 +)
23 +
24 +var log = logging.Logger("testharness")
25 +
26 +// Node is a single Kubo node.
27 +// Each node has its own config and can run its own Kubo daemon.
28 +type Node struct {
29 + ID int
30 + Dir string
31 +
32 + APIListenAddr multiaddr.Multiaddr
33 + SwarmAddr multiaddr.Multiaddr
34 + EnableMDNS bool
35 +
36 + IPFSBin string
37 + Runner *Runner
38 +
39 + daemon *RunResult
40 +}
41 +
42 +func BuildNode(ipfsBin, baseDir string, id int) *Node {
43 + dir := filepath.Join(baseDir, strconv.Itoa(id))
44 + if err := os.MkdirAll(dir, 0755); err != nil {
45 + panic(err)
46 + }
47 +
48 + env := environToMap(os.Environ())
49 + env["IPFS_PATH"] = dir
50 +
51 + return &Node{
52 + ID: id,
53 + Dir: dir,
54 + IPFSBin: ipfsBin,
55 + Runner: &Runner{
56 + Env: env,
57 + Dir: dir,
58 + },
59 + }
60 +}
61 +
62 +func (n *Node) ReadConfig() *config.Config {
63 + cfg, err := serial.Load(filepath.Join(n.Dir, "config"))
64 + if err != nil {
65 + panic(err)
66 + }
67 + return cfg
68 +}
69 +
70 +func (n *Node) WriteConfig(c *config.Config) {
71 + err := serial.WriteConfigFile(filepath.Join(n.Dir, "config"), c)
72 + if err != nil {
73 + panic(err)
74 + }
75 +}
76 +
77 +func (n *Node) UpdateConfig(f func(cfg *config.Config)) {
78 + cfg := n.ReadConfig()
79 + f(cfg)
80 + n.WriteConfig(cfg)
81 +}
82 +
83 +func (n *Node) IPFS(args ...string) RunResult {
84 + res := n.RunIPFS(args...)
85 + n.Runner.AssertNoError(res)
86 + return res
87 +}
88 +
89 +func (n *Node) PipeStrToIPFS(s string, args ...string) RunResult {
90 + return n.PipeToIPFS(strings.NewReader(s), args...)
91 +}
92 +
93 +func (n *Node) PipeToIPFS(reader io.Reader, args ...string) RunResult {
94 + res := n.RunPipeToIPFS(reader, args...)
95 + n.Runner.AssertNoError(res)
96 + return res
97 +}
98 +
99 +func (n *Node) RunPipeToIPFS(reader io.Reader, args ...string) RunResult {
100 + return n.Runner.Run(RunRequest{
101 + Path: n.IPFSBin,
102 + Args: args,
103 + CmdOpts: []CmdOpt{RunWithStdin(reader)},
104 + })
105 +}
106 +
107 +func (n *Node) RunIPFS(args ...string) RunResult {
108 + return n.Runner.Run(RunRequest{
109 + Path: n.IPFSBin,
110 + Args: args,
111 + })
112 +}
113 +
114 +// Init initializes and configures the IPFS node, after which it is ready to run.
115 +func (n *Node) Init(ipfsArgs ...string) *Node {
116 + n.Runner.MustRun(RunRequest{
117 + Path: n.IPFSBin,
118 + Args: append([]string{"init"}, ipfsArgs...),
119 + })
120 +
121 + if n.SwarmAddr == nil {
122 + swarmAddr, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/0")
123 + if err != nil {
124 + panic(err)
125 + }
126 + n.SwarmAddr = swarmAddr
127 + }
128 +
129 + if n.APIListenAddr == nil {
130 + apiAddr, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/0")
131 + if err != nil {
132 + panic(err)
133 + }
134 + n.APIListenAddr = apiAddr
135 + }
136 +
137 + n.UpdateConfig(func(cfg *config.Config) {
138 + cfg.Bootstrap = []string{}
139 + cfg.Addresses.Swarm = []string{n.SwarmAddr.String()}
140 + cfg.Addresses.API = []string{n.APIListenAddr.String()}
141 + cfg.Addresses.Gateway = []string{""}
142 + cfg.Swarm.DisableNatPortMap = true
143 + cfg.Discovery.MDNS.Enabled = n.EnableMDNS
144 + })
145 + return n
146 +}
147 +
148 +func (n *Node) StartDaemon(ipfsArgs ...string) *Node {
149 + alive := n.IsAlive()
150 + if alive {
151 + log.Panicf("node %d is already running", n.ID)
152 + }
153 +
154 + daemonArgs := append([]string{"daemon"}, ipfsArgs...)
155 + log.Debugf("starting node %d", n.ID)
156 + res := n.Runner.MustRun(RunRequest{
157 + Path: n.IPFSBin,
158 + Args: daemonArgs,
159 + RunFunc: (*exec.Cmd).Start,
160 + })
161 +
162 + n.daemon = &res
163 +
164 + log.Debugf("node %d started, checking API", n.ID)
165 + n.WaitOnAPI()
166 + return n
167 +}
168 +
169 +func (n *Node) signalAndWait(watch <-chan struct{}, signal os.Signal, t time.Duration) bool {
170 + err := n.daemon.Cmd.Process.Signal(signal)
171 + if err != nil {
172 + if errors.Is(err, os.ErrProcessDone) {
173 + log.Debugf("process for node %d has already finished", n.ID)
174 + return true
175 + }
176 + log.Panicf("error killing daemon for node %d with peer ID %s: %s", n.ID, n.PeerID(), err.Error())
177 + }
178 + timer := time.NewTimer(t)
179 + defer timer.Stop()
180 + select {
181 + case <-watch:
182 + return true
183 + case <-timer.C:
184 + return false
185 + }
186 +}
187 +
188 +func (n *Node) StopDaemon() *Node {
189 + log.Debugf("stopping node %d", n.ID)
190 + if n.daemon == nil {
191 + log.Debugf("didn't stop node %d since no daemon present", n.ID)
192 + return n
193 + }
194 + watch := make(chan struct{}, 1)
195 + go func() {
196 + _, _ = n.daemon.Cmd.Process.Wait()
197 + watch <- struct{}{}
198 + }()
199 + log.Debugf("signaling node %d with SIGTERM", n.ID)
200 + if n.signalAndWait(watch, syscall.SIGTERM, 1*time.Second) {
201 + return n
202 + }
203 + log.Debugf("signaling node %d with SIGTERM", n.ID)
204 + if n.signalAndWait(watch, syscall.SIGTERM, 2*time.Second) {
205 + return n
206 + }
207 + log.Debugf("signaling node %d with SIGQUIT", n.ID)
208 + if n.signalAndWait(watch, syscall.SIGQUIT, 5*time.Second) {
209 + return n
210 + }
211 + log.Debugf("signaling node %d with SIGKILL", n.ID)
212 + if n.signalAndWait(watch, syscall.SIGKILL, 5*time.Second) {
213 + return n
214 + }
215 + log.Panicf("timed out stopping node %d with peer ID %s", n.ID, n.PeerID())
216 + return n
217 +}
218 +
219 +func (n *Node) APIAddr() multiaddr.Multiaddr {
220 + ma, err := n.TryAPIAddr()
221 + if err != nil {
222 + panic(err)
223 + }
224 + return ma
225 +}
226 +
227 +func (n *Node) TryAPIAddr() (multiaddr.Multiaddr, error) {
228 + b, err := os.ReadFile(filepath.Join(n.Dir, "api"))
229 + if err != nil {
230 + return nil, err
231 + }
232 + ma, err := multiaddr.NewMultiaddr(string(b))
233 + if err != nil {
234 + return nil, err
235 + }
236 + return ma, nil
237 +}
238 +
239 +func (n *Node) checkAPI() bool {
240 + apiAddr, err := n.TryAPIAddr()
241 + if err != nil {
242 + log.Debugf("node %d API addr not available yet: %s", n.ID, err.Error())
243 + return false
244 + }
245 + ip, err := apiAddr.ValueForProtocol(multiaddr.P_IP4)
246 + if err != nil {
247 + panic(err)
248 + }
249 + port, err := apiAddr.ValueForProtocol(multiaddr.P_TCP)
250 + if err != nil {
251 + panic(err)
252 + }
253 + url := fmt.Sprintf("http://%s:%s/api/v0/id", ip, port)
254 + log.Debugf("checking API for node %d at %s", n.ID, url)
255 + httpResp, err := http.Post(url, "", nil)
256 + if err != nil {
257 + log.Debugf("node %d API check error: %s", err.Error())
258 + return false
259 + }
260 + defer httpResp.Body.Close()
261 + resp := struct {
262 + ID string
263 + }{}
264 +
265 + respBytes, err := io.ReadAll(httpResp.Body)
266 + if err != nil {
267 + log.Debugf("error reading API check response for node %d: %s", n.ID, err.Error())
268 + return false
269 + }
270 + log.Debugf("got API check response for node %d: %s", n.ID, string(respBytes))
271 +
272 + err = json.Unmarshal(respBytes, &resp)
273 + if err != nil {
274 + log.Debugf("error decoding API check response for node %d: %s", n.ID, err.Error())
275 + return false
276 + }
277 + if resp.ID == "" {
278 + log.Debugf("API check response for node %d did not contain a Peer ID", n.ID)
279 + return false
280 + }
281 + respPeerID, err := peer.Decode(resp.ID)
282 + if err != nil {
283 + panic(err)
284 + }
285 +
286 + peerID := n.PeerID()
287 + if respPeerID != peerID {
288 + log.Panicf("expected peer ID %s but got %s", peerID, resp.ID)
289 + }
290 +
291 + log.Debugf("API check for node %d successful", n.ID)
292 + return true
293 +}
294 +
295 +func (n *Node) PeerID() peer.ID {
296 + cfg := n.ReadConfig()
297 + id, err := peer.Decode(cfg.Identity.PeerID)
298 + if err != nil {
299 + panic(err)
300 + }
301 + return id
302 +}
303 +
304 +func (n *Node) WaitOnAPI() *Node {
305 + log.Debugf("waiting on API for node %d", n.ID)
306 + for i := 0; i < 50; i++ {
307 + if n.checkAPI() {
308 + return n
309 + }
310 + time.Sleep(400 * time.Millisecond)
311 + }
312 + log.Panicf("node %d with peer ID %s failed to come online: \n%s\n\n%s", n.ID, n.PeerID(), n.daemon.Stderr.String(), n.daemon.Stdout.String())
313 + return n
314 +}
315 +
316 +func (n *Node) IsAlive() bool {
317 + if n.daemon == nil || n.daemon.Cmd == nil || n.daemon.Cmd.Process == nil {
318 + return false
319 + }
320 + log.Debugf("signaling node %d daemon process for liveness check", n.ID)
321 + err := n.daemon.Cmd.Process.Signal(syscall.Signal(0))
322 + if err == nil {
323 + log.Debugf("node %d daemon is alive", n.ID)
324 + return true
325 + }
326 + log.Debugf("node %d daemon not alive: %s", err.Error())
327 + return false
328 +}
329 +
330 +func (n *Node) SwarmAddrs() []multiaddr.Multiaddr {
331 + res := n.Runner.MustRun(RunRequest{
332 + Path: n.IPFSBin,
333 + Args: []string{"swarm", "addrs", "local"},
334 + })
335 + ipfsProtocol := multiaddr.ProtocolWithCode(multiaddr.P_IPFS).Name
336 + peerID := n.PeerID()
337 + out := strings.TrimSpace(res.Stdout.String())
338 + outLines := strings.Split(out, "\n")
339 + var addrs []multiaddr.Multiaddr
340 + for _, addrStr := range outLines {
341 + ma, err := multiaddr.NewMultiaddr(addrStr)
342 + if err != nil {
343 + panic(err)
344 + }
345 +
346 + // add the peer ID to the multiaddr if it doesn't have it
347 + _, err = ma.ValueForProtocol(multiaddr.P_IPFS)
348 + if errors.Is(err, multiaddr.ErrProtocolNotFound) {
349 + comp, err := multiaddr.NewComponent(ipfsProtocol, peerID.String())
350 + if err != nil {
351 + panic(err)
352 + }
353 + ma = ma.Encapsulate(comp)
354 + }
355 + addrs = append(addrs, ma)
356 + }
357 + return addrs
358 +}
359 +
360 +func (n *Node) Connect(other *Node) *Node {
361 + n.Runner.MustRun(RunRequest{
362 + Path: n.IPFSBin,
363 + Args: []string{"swarm", "connect", other.SwarmAddrs()[0].String()},
364 + })
365 + return n
366 +}
367 +
368 +func (n *Node) Peers() []multiaddr.Multiaddr {
369 + res := n.Runner.MustRun(RunRequest{
370 + Path: n.IPFSBin,
371 + Args: []string{"swarm", "peers"},
372 + })
373 + lines := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n")
374 + var addrs []multiaddr.Multiaddr
375 + for _, line := range lines {
376 + ma, err := multiaddr.NewMultiaddr(line)
377 + if err != nil {
378 + panic(err)
379 + }
380 + addrs = append(addrs, ma)
381 + }
382 + return addrs
383 +}
test/cli/harness/nodes.go new
+47
@@ -0,0 +1,47 @@
1 +package harness
2 +
3 +import (
4 + "github.com/multiformats/go-multiaddr"
5 +)
6 +
7 +// Nodes is a collection of Kubo nodes along with operations on groups of nodes.
8 +type Nodes []*Node
9 +
10 +func (n Nodes) Init(args ...string) Nodes {
11 + for _, node := range n {
12 + node.Init()
13 + }
14 + return n
15 +}
16 +
17 +func (n Nodes) Connect() Nodes {
18 + for i, node := range n {
19 + for j, otherNode := range n {
20 + if i == j {
21 + continue
22 + }
23 + node.Connect(otherNode)
24 + }
25 + }
26 + for _, node := range n {
27 + firstPeer := node.Peers()[0]
28 + if _, err := firstPeer.ValueForProtocol(multiaddr.P_P2P); err != nil {
29 + log.Panicf("unexpected state for node %d with peer ID %s: %s", node.ID, node.PeerID(), err)
30 + }
31 + }
32 + return n
33 +}
34 +
35 +func (n Nodes) StartDaemons() Nodes {
36 + for _, node := range n {
37 + node.StartDaemon()
38 + }
39 + return n
40 +}
41 +
42 +func (n Nodes) StopDaemons() Nodes {
43 + for _, node := range n {
44 + node.StopDaemon()
45 + }
46 + return n
47 +}
test/cli/harness/run.go new
+140
@@ -0,0 +1,140 @@
1 +package harness
2 +
3 +import (
4 + "fmt"
5 + "io"
6 + "os/exec"
7 + "strings"
8 +)
9 +
10 +// Runner is a process runner which can run subprocesses and aggregate output.
11 +type Runner struct {
12 + Env map[string]string
13 + Dir string
14 + Verbose bool
15 +}
16 +
17 +type CmdOpt func(*exec.Cmd)
18 +type RunFunc func(*exec.Cmd) error
19 +
20 +var RunFuncStart = (*exec.Cmd).Start
21 +
22 +type RunRequest struct {
23 + Path string
24 + Args []string
25 + // Options that are applied to the exec.Cmd just before running it
26 + CmdOpts []CmdOpt
27 + // Function to use to run the command.
28 + // If not specified, defaults to cmd.Run
29 + RunFunc func(*exec.Cmd) error
30 + Verbose bool
31 +}
32 +
33 +type RunResult struct {
34 + Stdout *Buffer
35 + Stderr *Buffer
36 + Err error
37 + ExitErr *exec.ExitError
38 + Cmd *exec.Cmd
39 +}
40 +
41 +func environToMap(environ []string) map[string]string {
42 + m := map[string]string{}
43 + for _, e := range environ {
44 + kv := strings.Split(e, "=")
45 + m[kv[0]] = kv[1]
46 + }
47 + return m
48 +}
49 +
50 +func (r *Runner) Run(req RunRequest) RunResult {
51 + cmd := exec.Command(req.Path, req.Args...)
52 + stdout := &Buffer{}
53 + stderr := &Buffer{}
54 + cmd.Stdout = stdout
55 + cmd.Stderr = stderr
56 + cmd.Dir = r.Dir
57 +
58 + for k, v := range r.Env {
59 + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
60 + }
61 +
62 + for _, o := range req.CmdOpts {
63 + o(cmd)
64 + }
65 +
66 + if req.RunFunc == nil {
67 + req.RunFunc = (*exec.Cmd).Run
68 + }
69 +
70 + log.Debugf("running %v", cmd.Args)
71 +
72 + err := req.RunFunc(cmd)
73 +
74 + result := RunResult{
75 + Stdout: stdout,
76 + Stderr: stderr,
77 + Cmd: cmd,
78 + Err: err,
79 + }
80 +
81 + if exitErr, ok := err.(*exec.ExitError); ok {
82 + result.ExitErr = exitErr
83 + }
84 +
85 + return result
86 +}
87 +
88 +// MustRun runs the command and fails the test if the command fails.
89 +func (r *Runner) MustRun(req RunRequest) RunResult {
90 + result := r.Run(req)
91 + r.AssertNoError(result)
92 + return result
93 +}
94 +
95 +func (r *Runner) AssertNoError(result RunResult) {
96 + if result.ExitErr != nil {
97 + log.Panicf("'%s' returned error, code: %d, err: %s\nstdout:%s\nstderr:%s\n",
98 + result.Cmd.Args, result.ExitErr.ExitCode(), result.ExitErr.Error(), result.Stdout.String(), result.Stderr.String())
99 +
100 + }
101 + if result.Err != nil {
102 + log.Panicf("unable to run %s: %s", result.Cmd.Path, result.Err)
103 +
104 + }
105 +}
106 +
107 +func RunWithEnv(env map[string]string) CmdOpt {
108 + return func(cmd *exec.Cmd) {
109 + for k, v := range env {
110 + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
111 + }
112 + }
113 +}
114 +
115 +func RunWithPath(path string) CmdOpt {
116 + return func(cmd *exec.Cmd) {
117 + var newEnv []string
118 + for _, env := range cmd.Env {
119 + e := strings.Split(env, "=")
120 + if e[0] == "PATH" {
121 + paths := strings.Split(e[1], ":")
122 + paths = append(paths, path)
123 + e[1] = strings.Join(paths, ":")
124 + fmt.Printf("path: %s\n", strings.Join(e, "="))
125 + }
126 + newEnv = append(newEnv, strings.Join(e, "="))
127 + }
128 + cmd.Env = newEnv
129 + }
130 +}
131 +
132 +func RunWithStdin(reader io.Reader) CmdOpt {
133 + return func(cmd *exec.Cmd) {
134 + cmd.Stdin = reader
135 + }
136 +}
137 +
138 +func RunWithStdinStr(s string) CmdOpt {
139 + return RunWithStdin(strings.NewReader(s))
140 +}
test/cli/init_test.go new
+164
@@ -0,0 +1,164 @@
1 +package cli
2 +
3 +import (
4 + "fmt"
5 + "os"
6 + fp "path/filepath"
7 + "strings"
8 + "testing"
9 +
10 + "github.com/ipfs/kubo/test/cli/harness"
11 + . "github.com/ipfs/kubo/test/cli/testutils"
12 + pb "github.com/libp2p/go-libp2p/core/crypto/pb"
13 + "github.com/libp2p/go-libp2p/core/peer"
14 + "github.com/stretchr/testify/assert"
15 + "github.com/stretchr/testify/require"
16 +)
17 +
18 +func validatePeerID(t *testing.T, peerID peer.ID, expErr error, expAlgo pb.KeyType) {
19 + assert.NoError(t, peerID.Validate())
20 + pub, err := peerID.ExtractPublicKey()
21 + assert.ErrorIs(t, expErr, err)
22 + if expAlgo != 0 {
23 + assert.Equal(t, expAlgo, pub.Type())
24 + }
25 +}
26 +
27 +func testInitAlgo(t *testing.T, initFlags []string, expOutputName string, expPeerIDPubKeyErr error, expPeerIDPubKeyType pb.KeyType) {
28 + t.Run("init", func(t *testing.T) {
29 + t.Parallel()
30 + node := harness.NewT(t).NewNode()
31 + initRes := node.IPFS(StrCat("init", initFlags)...)
32 +
33 + lines := []string{
34 + fmt.Sprintf("generating %s keypair...done", expOutputName),
35 + fmt.Sprintf("peer identity: %s", node.PeerID().String()),
36 + fmt.Sprintf("initializing IPFS node at %s", node.Dir),
37 + "to get started, enter:",
38 + fmt.Sprintf("\n\tipfs cat /ipfs/%s/readme\n\n", CIDWelcomeDocs),
39 + }
40 + expectedInitOutput := strings.Join(lines, "\n")
41 + assert.Equal(t, expectedInitOutput, initRes.Stdout.String())
42 +
43 + assert.DirExists(t, node.Dir)
44 + assert.FileExists(t, fp.Join(node.Dir, "config"))
45 + assert.DirExists(t, fp.Join(node.Dir, "datastore"))
46 + assert.DirExists(t, fp.Join(node.Dir, "blocks"))
47 + assert.NoFileExists(t, fp.Join(node.Dir, "._check_writeable"))
48 +
49 + _, err := os.ReadDir(node.Dir)
50 + assert.NoError(t, err, "ipfs dir should be listable")
51 +
52 + validatePeerID(t, node.PeerID(), expPeerIDPubKeyErr, expPeerIDPubKeyType)
53 +
54 + res := node.IPFS("config", "Mounts.IPFS")
55 + assert.Equal(t, "/ipfs", res.Stdout.Trimmed())
56 +
57 + node.IPFS("cat", fmt.Sprintf("/ipfs/%s/readme", CIDWelcomeDocs))
58 + })
59 +
60 + t.Run("init empty repo", func(t *testing.T) {
61 + t.Parallel()
62 + node := harness.NewT(t).NewNode()
63 + initRes := node.IPFS(StrCat("init", "--empty-repo", initFlags)...)
64 +
65 + validatePeerID(t, node.PeerID(), expPeerIDPubKeyErr, expPeerIDPubKeyType)
66 +
67 + lines := []string{
68 + fmt.Sprintf("generating %s keypair...done", expOutputName),
69 + fmt.Sprintf("peer identity: %s", node.PeerID().String()),
70 + fmt.Sprintf("initializing IPFS node at %s\n", node.Dir),
71 + }
72 + expectedEmptyInitOutput := strings.Join(lines, "\n")
73 + assert.Equal(t, expectedEmptyInitOutput, initRes.Stdout.String())
74 +
75 + catRes := node.RunIPFS("cat", fmt.Sprintf("/ipfs/%s/readme", CIDWelcomeDocs))
76 + assert.NotEqual(t, 0, catRes.ExitErr.ExitCode(), "welcome readme doesn't exist")
77 +
78 + idRes := node.IPFS("id", "-f", "<aver>")
79 + version := node.IPFS("version", "-n").Stdout.Trimmed()
80 + assert.Contains(t, idRes.Stdout.String(), version)
81 + })
82 +}
83 +
84 +func TestInit(t *testing.T) {
85 + t.Parallel()
86 +
87 + t.Run("init fails if the repo dir has no perms", func(t *testing.T) {
88 + t.Parallel()
89 + node := harness.NewT(t).NewNode()
90 + badDir := fp.Join(node.Dir, ".badipfs")
91 + err := os.Mkdir(badDir, 0000)
92 + require.NoError(t, err)
93 +
94 + res := node.RunIPFS("init", "--repo-dir", badDir)
95 + assert.NotEqual(t, 0, res.Cmd.ProcessState.ExitCode())
96 + assert.Contains(t, res.Stderr.String(), "permission denied")
97 +
98 + })
99 +
100 + t.Run("init with ed25519", func(t *testing.T) {
101 + t.Parallel()
102 + testInitAlgo(t, []string{"--algorithm=ed25519"}, "ED25519", nil, pb.KeyType_Ed25519)
103 + })
104 +
105 + t.Run("init with rsa", func(t *testing.T) {
106 + t.Parallel()
107 + testInitAlgo(t, []string{"--bits=2048", "--algorithm=rsa"}, "2048-bit RSA", peer.ErrNoPublicKey, 0)
108 + })
109 +
110 + t.Run("init with default algorithm", func(t *testing.T) {
111 + t.Parallel()
112 + testInitAlgo(t, []string{}, "ED25519", nil, pb.KeyType_Ed25519)
113 + })
114 +
115 + t.Run("ipfs init --profile with invalid profile fails", func(t *testing.T) {
116 + t.Parallel()
117 + node := harness.NewT(t).NewNode()
118 + res := node.RunIPFS("init", "--profile=invalid_profile")
119 + assert.NotEqual(t, 0, res.ExitErr.ExitCode())
120 + assert.Equal(t, "Error: invalid configuration profile: invalid_profile", res.Stderr.Trimmed())
121 + })
122 +
123 + t.Run("ipfs init --profile with valid profile succeeds", func(t *testing.T) {
124 + t.Parallel()
125 + node := harness.NewT(t).NewNode()
126 + node.IPFS("init", "--profile=server")
127 + })
128 +
129 + t.Run("ipfs config looks good", func(t *testing.T) {
130 + t.Parallel()
131 + node := harness.NewT(t).NewNode().Init("--profile=server")
132 +
133 + lines := node.IPFS("config", "Swarm.AddrFilters").Stdout.Lines()
134 + assert.Len(t, lines, 18)
135 +
136 + out := node.IPFS("config", "Bootstrap").Stdout.Trimmed()
137 + assert.Equal(t, "[]", out)
138 +
139 + out = node.IPFS("config", "Addresses.API").Stdout.Trimmed()
140 + assert.Equal(t, "/ip4/127.0.0.1/tcp/0", out)
141 + })
142 +
143 + t.Run("ipfs init from existing config succeeds", func(t *testing.T) {
144 + t.Parallel()
145 + nodes := harness.NewT(t).NewNodes(2)
146 + node1 := nodes[0]
147 + node2 := nodes[1]
148 +
149 + node1.Init("--profile=server")
150 +
151 + node2.IPFS("init", fp.Join(node1.Dir, "config"))
152 + out := node2.IPFS("config", "Addresses.API").Stdout.Trimmed()
153 + assert.Equal(t, "/ip4/127.0.0.1/tcp/0", out)
154 + })
155 +
156 + t.Run("ipfs init should not run while daemon is running", func(t *testing.T) {
157 + t.Parallel()
158 + node := harness.NewT(t).NewNode().Init().StartDaemon()
159 + res := node.RunIPFS("init")
160 + assert.NotEqual(t, 0, res.ExitErr.ExitCode())
161 + assert.Contains(t, res.Stderr.String(), "Error: ipfs daemon is running. please stop it to run this command")
162 + })
163 +
164 +}
test/cli/ping_test.go new
+73
@@ -0,0 +1,73 @@
1 +package cli
2 +
3 +import (
4 + "fmt"
5 + "testing"
6 +
7 + "github.com/ipfs/kubo/test/cli/harness"
8 + "github.com/stretchr/testify/assert"
9 +)
10 +
11 +func TestPing(t *testing.T) {
12 + t.Parallel()
13 +
14 + t.Run("other", func(t *testing.T) {
15 + t.Parallel()
16 + nodes := harness.NewT(t).NewNodes(2).Init().StartDaemons().Connect()
17 + node1 := nodes[0]
18 + node2 := nodes[1]
19 +
20 + node1.IPFS("ping", "-n", "2", "--", node2.PeerID().String())
21 + node2.IPFS("ping", "-n", "2", "--", node1.PeerID().String())
22 + })
23 +
24 + t.Run("ping unreachable peer", func(t *testing.T) {
25 + t.Parallel()
26 + nodes := harness.NewT(t).NewNodes(2).Init().StartDaemons().Connect()
27 + node1 := nodes[0]
28 +
29 + badPeer := "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJx"
30 + res := node1.RunIPFS("ping", "-n", "2", "--", badPeer)
31 + assert.Contains(t, res.Stdout.String(), fmt.Sprintf("Looking up peer %s", badPeer))
32 + assert.Contains(t, res.Stderr.String(), "Error: ping failed")
33 + })
34 +
35 + t.Run("self", func(t *testing.T) {
36 + t.Parallel()
37 + nodes := harness.NewT(t).NewNodes(2).Init().StartDaemons()
38 + node1 := nodes[0]
39 + node2 := nodes[1]
40 +
41 + res := node1.RunIPFS("ping", "-n", "2", "--", node1.PeerID().String())
42 + assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
43 + assert.Contains(t, res.Stderr.String(), "can't ping self")
44 +
45 + res = node2.RunIPFS("ping", "-n", "2", "--", node2.PeerID().String())
46 + assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
47 + assert.Contains(t, res.Stderr.String(), "can't ping self")
48 + })
49 +
50 + t.Run("0", func(t *testing.T) {
51 + t.Parallel()
52 + nodes := harness.NewT(t).NewNodes(2).Init().StartDaemons().Connect()
53 + node1 := nodes[0]
54 + node2 := nodes[1]
55 +
56 + res := node1.RunIPFS("ping", "-n", "0", "--", node2.PeerID().String())
57 + assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
58 + assert.Contains(t, res.Stderr.String(), "ping count must be greater than 0")
59 + })
60 +
61 + t.Run("offline", func(t *testing.T) {
62 + t.Parallel()
63 + nodes := harness.NewT(t).NewNodes(2).Init().StartDaemons().Connect()
64 + node1 := nodes[0]
65 + node2 := nodes[1]
66 +
67 + node2.StopDaemon()
68 +
69 + res := node1.RunIPFS("ping", "-n", "2", "--", node2.PeerID().String())
70 + assert.Equal(t, 1, res.Cmd.ProcessState.ExitCode())
71 + assert.Contains(t, res.Stderr.String(), "ping failed")
72 + })
73 +}
test/cli/testutils/cids.go new
+6
@@ -0,0 +1,6 @@
1 +package testutils
2 +
3 +const (
4 + CIDWelcomeDocs = "QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc"
5 + CIDEmptyDir = "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"
6 +)
test/cli/testutils/requires.go new
+37
@@ -0,0 +1,37 @@
1 +package testutils
2 +
3 +import (
4 + "os"
5 + "runtime"
6 + "testing"
7 +)
8 +
9 +func RequiresDocker(t *testing.T) {
10 + if os.Getenv("TEST_NO_DOCKER") == "1" {
11 + t.SkipNow()
12 + }
13 +}
14 +
15 +func RequiresFUSE(t *testing.T) {
16 + if os.Getenv("TEST_NO_FUSE") == "1" {
17 + t.SkipNow()
18 + }
19 +}
20 +
21 +func RequiresExpensive(t *testing.T) {
22 + if os.Getenv("TEST_EXPENSIVE") == "1" || testing.Short() {
23 + t.SkipNow()
24 + }
25 +}
26 +
27 +func RequiresPlugins(t *testing.T) {
28 + if os.Getenv("TEST_NO_PLUGIN") == "1" {
29 + t.SkipNow()
30 + }
31 +}
32 +
33 +func RequiresLinux(t *testing.T) {
34 + if runtime.GOOS != "linux" {
35 + t.SkipNow()
36 + }
37 +}
test/cli/testutils/util.go new
+97
@@ -0,0 +1,97 @@
1 +package testutils
2 +
3 +import (
4 + "bufio"
5 + "encoding/json"
6 + "fmt"
7 + "log"
8 + "os"
9 + "path/filepath"
10 + "strings"
11 +)
12 +
13 +func SplitLines(s string) []string {
14 + var lines []string
15 + scanner := bufio.NewScanner(strings.NewReader(s))
16 + for scanner.Scan() {
17 + lines = append(lines, scanner.Text())
18 + }
19 + return lines
20 +}
21 +
22 +func MustOpen(name string) *os.File {
23 + f, err := os.Open(name)
24 + if err != nil {
25 + log.Panicf("opening %s: %s", name, err)
26 + }
27 + return f
28 +}
29 +
30 +// StrCat takes a bunch of strings or string slices
31 +// and concats them all together into one string slice.
32 +// If an arg is not one of those types, this panics.
33 +// If an arg is an empty string, it is dropped.
34 +func StrCat(args ...interface{}) []string {
35 + res := make([]string, 0)
36 + for _, a := range args {
37 + if s, ok := a.(string); ok {
38 + if s != "" {
39 + res = append(res, s)
40 + }
41 + continue
42 + }
43 + if ss, ok := a.([]string); ok {
44 + for _, s := range ss {
45 + if s != "" {
46 + res = append(res, s)
47 + }
48 + }
49 + continue
50 + }
51 + panic(fmt.Sprintf("arg '%v' must be a string or string slice, but is '%T'", a, a))
52 + }
53 + return res
54 +}
55 +
56 +// PreviewStr returns a preview of s, which is a prefix for logging that avoids dumping a huge string to logs.
57 +func PreviewStr(s string) string {
58 + suffix := "..."
59 + previewLength := 10
60 + if len(s) < previewLength {
61 + previewLength = len(s)
62 + suffix = ""
63 + }
64 + return s[0:previewLength] + suffix
65 +}
66 +
67 +type JSONObj map[string]interface{}
68 +
69 +func ToJSONStr(m JSONObj) string {
70 + b, err := json.Marshal(m)
71 + if err != nil {
72 + panic(err)
73 + }
74 + return string(b)
75 +}
76 +
77 +// Searches for a file in a dir, then the parent dir, etc.
78 +// If the file is not found, an empty string is returned.
79 +func FindUp(name, dir string) string {
80 + curDir := dir
81 + for {
82 + entries, err := os.ReadDir(curDir)
83 + if err != nil {
84 + panic(err)
85 + }
86 + for _, e := range entries {
87 + if name == e.Name() {
88 + return filepath.Join(curDir, name)
89 + }
90 + }
91 + newDir := filepath.Dir(curDir)
92 + if newDir == curDir {
93 + return ""
94 + }
95 + curDir = newDir
96 + }
97 +}
test/sharness/t0010-basic-commands.sh deleted
-149
@@ -1,149 +0,0 @@
1 -#!/usr/bin/env bash
2 -#
3 -# Copyright (c) 2014 Christian Couder
4 -# MIT Licensed; see the LICENSE file in this repository.
5 -#
6 -
7 -test_description="Test installation and some basic commands"
8 -
9 -. lib/test-lib.sh
10 -
11 -test_expect_success "current dir is writable" '
12 - echo "It works!" >test.txt
13 -'
14 -
15 -test_expect_success "ipfs version succeeds" '
16 - ipfs version >version.txt
17 -'
18 -
19 -test_expect_success "ipfs --version success" '
20 - ipfs --version
21 -'
22 -
23 -test_expect_success "ipfs version output looks good" '
24 - egrep "^ipfs version [0-9]+\.[0-9]+\.[0-9]" version.txt >/dev/null ||
25 - test_fsh cat version.txt
26 -'
27 -
28 -test_expect_success "ipfs versions matches ipfs --version" '
29 - ipfs version > version.txt &&
30 - ipfs --version > version2.txt &&
31 - diff version2.txt version.txt ||
32 - test_fsh ipfs --version
33 -
34 -'
35 -
36 -test_expect_success "ipfs version --all has all required fields" '
37 - ipfs version --all > version_all.txt &&
38 - grep "Kubo version" version_all.txt &&
39 - grep "Repo version" version_all.txt &&
40 - grep "System version" version_all.txt &&
41 - grep "Golang version" version_all.txt
42 -'
43 -
44 -test_expect_success "ipfs version deps succeeds" '
45 - ipfs version deps >deps.txt
46 -'
47 -
48 -test_expect_success "ipfs version deps output looks good ( set \$GOIPFSTEST_SKIP_LOCAL_DEVTREE_DEPS_CHECK to skip this test )" '
49 - head -1 deps.txt | grep "go-ipfs@(devel)" &&
50 - [[ "$GOIPFSTEST_SKIP_LOCAL_DEVTREE_DEPS_CHECK" == "1" ]] ||
51 - [[ $(tail -n +2 deps.txt | egrep -v -c "^[^ @]+@v[^ @]+( => [^ @]+@v[^ @]+)?$") -eq 0 ]] ||
52 - test_fsh cat deps.txt
53 -'
54 -
55 -test_expect_success "'ipfs commands' succeeds" '
56 - ipfs commands >commands.txt
57 -'
58 -
59 -test_expect_success "'ipfs commands' output looks good" '
60 - grep "ipfs add" commands.txt &&
61 - grep "ipfs daemon" commands.txt &&
62 - grep "ipfs update" commands.txt
63 -'
64 -
65 -test_expect_success "All sub-commands accept help" '
66 - echo 0 > fail
67 - while read -r cmd
68 - do
69 - ${cmd:0:4} help ${cmd:5} >/dev/null ||
70 - { echo "$cmd does not accept --help"; echo 1 > fail; }
71 - echo stuff | $cmd --help >/dev/null ||
72 - { echo "$cmd does not accept --help when using stdin"; echo 1 > fail; }
73 - done <commands.txt
74 -
75 - if [ $(cat fail) = 1 ]; then
76 - return 1
77 - fi
78 -'
79 -
80 -test_expect_success "All commands accept --help" '
81 - echo 0 > fail
82 - while read -r cmd
83 - do
84 - $cmd --help >/dev/null ||
85 - { echo "$cmd does not accept --help"; echo 1 > fail; }
86 - echo stuff | $cmd --help >/dev/null ||
87 - { echo "$cmd does not accept --help when using stdin"; echo 1 > fail; }
88 - done <commands.txt
89 -
90 - if [ $(cat fail) = 1 ]; then
91 - return 1
92 - fi
93 -'
94 -
95 -test_expect_failure "All ipfs root commands are mentioned in base helptext" '
96 - echo 0 > fail
97 - ipfs --help > help.txt
98 - cut -d" " -f 2 commands.txt | grep -v ipfs | sort -u | \
99 - while read cmd
100 - do
101 - grep " $cmd" help.txt > /dev/null ||
102 - { echo "missing $cmd from helptext"; echo 1 > fail; }
103 - done
104 -
105 - if [ $(cat fail) = 1 ]; then
106 - return 1
107 - fi
108 -'
109 -
110 -test_expect_failure "All ipfs commands docs are 80 columns or less" '
111 - echo 0 > fail
112 - while read cmd
113 - do
114 - LENGTH="$($cmd --help | awk "{ print length }" | sort -nr | head -1)"
115 - [ $LENGTH -gt 80 ] &&
116 - { echo "$cmd help text is longer than 79 chars ($LENGTH)"; echo 1 > fail; }
117 - done <commands.txt
118 -
119 - if [ $(cat fail) = 1 ]; then
120 - return 1
121 - fi
122 -'
123 -
124 -test_expect_success "All ipfs commands fail when passed a bad flag" '
125 - echo 0 > fail
126 - while read -r cmd
127 - do
128 - test_must_fail $cmd --badflag >/dev/null 2>&1 ||
129 - { echo "$cmd exit with code 0 when passed --badflag"; echo 1 > fail; }
130 - done <commands.txt
131 -
132 - if [ $(cat fail) = 1 ]; then
133 - return 1
134 - fi
135 -'
136 -
137 -test_expect_success "'ipfs commands --flags' succeeds" '
138 - ipfs commands --flags >commands.txt
139 -'
140 -
141 -test_expect_success "'ipfs commands --flags' output looks good" '
142 - grep "ipfs pin add --recursive / ipfs pin add -r" commands.txt &&
143 - grep "ipfs id --format / ipfs id -f" commands.txt &&
144 - grep "ipfs repo gc --quiet / ipfs repo gc -q" commands.txt
145 -'
146 -
147 -
148 -
149 -test_done
test/sharness/t0011-completion.sh deleted
-15
@@ -1,15 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -test_description="Test generated bash completions"
4 -
5 -. lib/test-lib.sh
6 -
7 -test_expect_success "'ipfs commands completion bash' succeeds" '
8 - ipfs commands completion bash > completions.bash
9 -'
10 -
11 -test_expect_success "generated completions defines '_ipfs'" '
12 - bash -c "source completions.bash && type -t _ipfs"
13 -'
14 -
15 -test_done
test/sharness/t0020-init.sh deleted
-271
@@ -1,271 +0,0 @@
1 -#!/usr/bin/env bash
2 -#
3 -# Copyright (c) 2014 Christian Couder
4 -# MIT Licensed; see the LICENSE file in this repository.
5 -#
6 -
7 -test_description="Test init command"
8 -
9 -. lib/test-lib.sh
10 -
11 -# test that ipfs fails to init with BAD_IPFS_DIR that isn't writeable
12 -test_expect_success "create dir and change perms succeeds" '
13 - export BAD_IPFS_DIR="$(pwd)/.badipfs" &&
14 - mkdir "$BAD_IPFS_DIR" &&
15 - chmod 000 "$BAD_IPFS_DIR"
16 -'
17 -
18 -test_expect_success "ipfs init fails" '
19 - test_must_fail ipfs init --repo-dir "$BAD_IPFS_DIR" 2> init_fail_out
20 -'
21 -
22 -# Under Windows/Cygwin the error message is different,
23 -# so we use the STD_ERR_MSG prereq.
24 -if test_have_prereq STD_ERR_MSG; then
25 - init_err_msg="Error: error loading plugins: open $BAD_IPFS_DIR/config: permission denied"
26 -else
27 - init_err_msg="Error: error loading plugins: open $BAD_IPFS_DIR/config: The system cannot find the path specified."
28 -fi
29 -
30 -test_expect_success "ipfs init output looks good" '
31 - echo "$init_err_msg" >init_fail_exp &&
32 - test_cmp init_fail_exp init_fail_out
33 -'
34 -
35 -test_expect_success "cleanup dir with bad perms" '
36 - chmod 775 "$BAD_IPFS_DIR" &&
37 - rmdir "$BAD_IPFS_DIR"
38 -'
39 -
40 -# test no repo error message
41 -# this applies to `ipfs add sth`, `ipfs refs <hash>`
42 -test_expect_success "ipfs cat fails" '
43 - export IPFS_DIR="$(pwd)/.ipfs" &&
44 - test_must_fail ipfs cat --repo-dir "$IPFS_DIR" Qmaa4Rw81a3a1VEx4LxB7HADUAXvZFhCoRdBzsMZyZmqHD 2> cat_fail_out
45 -'
46 -
47 -test_expect_success "ipfs cat no repo message looks good" '
48 - echo "Error: no IPFS repo found in $IPFS_DIR." > cat_fail_exp &&
49 - echo "please run: '"'"'ipfs init'"'"'" >> cat_fail_exp &&
50 - test_path_cmp cat_fail_exp cat_fail_out
51 -'
52 -
53 -# $1 must be one of 'rsa', 'ed25519' or '' (for default key algorithm).
54 -test_ipfs_init_flags() {
55 - TEST_ALG=$1
56 -
57 - # test that init succeeds
58 - test_expect_success "ipfs init succeeds" '
59 - export IPFS_DIR="$(pwd)/.ipfs" &&
60 - echo "IPFS_DIR: \"$IPFS_DIR\"" &&
61 - RSA_BITS="2048" &&
62 - case $TEST_ALG in
63 - "rsa")
64 - ipfs init --repo-dir "$IPFS_DIR" --algorithm=rsa --bits="$RSA_BITS" >actual_init || test_fsh cat actual_init
65 - ;;
66 - "ed25519")
67 - ipfs init --repo-dir "$IPFS_DIR" --algorithm=ed25519 >actual_init || test_fsh cat actual_init
68 - ;;
69 - *)
70 - ipfs init --repo-dir "$IPFS_DIR" --algorithm=rsa --bits="$RSA_BITS" >actual_init || test_fsh cat actual_init
71 - ;;
72 - esac
73 - '
74 -
75 - test_expect_success ".ipfs/ has been created" '
76 - test -d "$IPFS_DIR" &&
77 - test -f "$IPFS_DIR/config" &&
78 - test -d "$IPFS_DIR/datastore" &&
79 - test -d "$IPFS_DIR/blocks" &&
80 - test ! -f ._check_writeable ||
81 - test_fsh ls -al $IPFS_DIR
82 - '
83 -
84 - test_expect_success "ipfs config succeeds" '
85 - echo /ipfs >expected_config &&
86 - ipfs config --repo-dir "$IPFS_DIR" Mounts.IPFS >actual_config &&
87 - test_cmp expected_config actual_config
88 - '
89 -
90 - test_expect_success "ipfs peer id looks good" '
91 - PEERID=$(ipfs config --repo-dir "$IPFS_DIR" Identity.PeerID) &&
92 - test_check_peerid "$PEERID"
93 - '
94 -
95 - test_expect_success "ipfs init output looks good" '
96 - STARTFILE="ipfs cat /ipfs/$HASH_WELCOME_DOCS/readme" &&
97 -
98 - echo "generating $RSA_BITS-bit RSA keypair...done" >rsa_expected &&
99 - echo "peer identity: $PEERID" >>rsa_expected &&
100 - echo "initializing IPFS node at $IPFS_DIR" >>rsa_expected &&
101 - echo "to get started, enter:" >>rsa_expected &&
102 - printf "\\n\\t$STARTFILE\\n\\n" >>rsa_expected &&
103 -
104 - echo "generating ED25519 keypair...done" >ed25519_expected &&
105 - echo "peer identity: $PEERID" >>ed25519_expected &&
106 - echo "initializing IPFS node at $IPFS_DIR" >>ed25519_expected &&
107 - echo "to get started, enter:" >>ed25519_expected &&
108 - printf "\\n\\t$STARTFILE\\n\\n" >>ed25519_expected &&
109 -
110 - case $TEST_ALG in
111 - rsa)
112 - test_cmp rsa_expected actual_init
113 - ;;
114 - ed25519)
115 - test_cmp ed25519_expected actual_init
116 - ;;
117 - *)
118 - test_cmp rsa_expected actual_init
119 - ;;
120 - esac
121 - '
122 -
123 - test_expect_success "Welcome readme exists" '
124 - ipfs cat /ipfs/$HASH_WELCOME_DOCS/readme
125 - '
126 -
127 - test_expect_success "clean up ipfs dir" '
128 - rm -rf "$IPFS_DIR"
129 - '
130 -
131 - test_expect_success "'ipfs init --empty-repo' succeeds" '
132 - RSA_BITS="2048" &&
133 - case $TEST_ALG in
134 - rsa)
135 - ipfs init --repo-dir "$IPFS_DIR" --algorithm=rsa --bits="$RSA_BITS" --empty-repo >actual_init
136 - ;;
137 - ed25519)
138 - ipfs init --repo-dir "$IPFS_DIR" --algorithm=ed25519 --empty-repo >actual_init
139 - ;;
140 - *)
141 - ipfs init --repo-dir "$IPFS_DIR" --empty-repo >actual_init
142 - ;;
143 - esac
144 - '
145 -
146 - test_expect_success "ipfs peer id looks good" '
147 - PEERID=$(ipfs config --repo-dir "$IPFS_DIR" Identity.PeerID) &&
148 - test_check_peerid "$PEERID"
149 - '
150 -
151 - test_expect_success "'ipfs init --empty-repo' output looks good" '
152 -
153 - echo "generating $RSA_BITS-bit RSA keypair...done" >rsa_expected &&
154 - echo "peer identity: $PEERID" >>rsa_expected &&
155 - echo "initializing IPFS node at $IPFS_DIR" >>rsa_expected &&
156 -
157 - echo "generating ED25519 keypair...done" >ed25519_expected &&
158 - echo "peer identity: $PEERID" >>ed25519_expected &&
159 - echo "initializing IPFS node at $IPFS_DIR" >>ed25519_expected &&
160 -
161 - case $TEST_ALG in
162 - rsa)
163 - test_cmp rsa_expected actual_init
164 - ;;
165 - ed25519)
166 - test_cmp ed25519_expected actual_init
167 - ;;
168 - *)
169 - test_cmp ed25519_expected actual_init
170 - ;;
171 - esac
172 - '
173 -
174 - test_expect_success "Welcome readme doesn't exist" '
175 - test_must_fail ipfs cat /ipfs/$HASH_WELCOME_DOCS/readme
176 - '
177 -
178 - test_expect_success "ipfs id agent string contains correct version" '
179 - ipfs id -f "<aver>" | grep $(ipfs version -n)
180 - '
181 -
182 - test_expect_success "clean up ipfs dir" '
183 - rm -rf "$IPFS_DIR"
184 - '
185 -}
186 -test_ipfs_init_flags 'ed25519'
187 -test_ipfs_init_flags 'rsa'
188 -test_ipfs_init_flags ''
189 -
190 -# test init profiles
191 -test_expect_success "'ipfs init --profile' with invalid profile fails" '
192 - RSA_BITS="2048" &&
193 - test_must_fail ipfs init --repo-dir "$IPFS_DIR" --profile=nonexistent_profile 2> invalid_profile_out
194 - EXPECT="Error: invalid configuration profile: nonexistent_profile" &&
195 - grep "$EXPECT" invalid_profile_out
196 -'
197 -
198 -test_expect_success "'ipfs init --profile' succeeds" '
199 - RSA_BITS="2048" &&
200 - ipfs init --repo-dir "$IPFS_DIR" --profile=server
201 -'
202 -
203 -test_expect_success "'ipfs config Swarm.AddrFilters' looks good" '
204 - ipfs config --repo-dir "$IPFS_DIR" Swarm.AddrFilters > actual_config &&
205 - test $(cat actual_config | wc -l) = 18
206 -'
207 -
208 -test_expect_success "clean up ipfs dir" '
209 - rm -rf "$IPFS_DIR"
210 -'
211 -
212 -test_expect_success "'ipfs init --profile=test' succeeds" '
213 - RSA_BITS="2048" &&
214 - ipfs init --repo-dir "$IPFS_DIR" --profile=test
215 -'
216 -
217 -test_expect_success "'ipfs config Bootstrap' looks good" '
218 - ipfs config --repo-dir "$IPFS_DIR" Bootstrap > actual_config &&
219 - test $(cat actual_config) = "[]"
220 -'
221 -
222 -test_expect_success "'ipfs config Addresses.API' looks good" '
223 - ipfs config --repo-dir "$IPFS_DIR" Addresses.API > actual_config &&
224 - test $(cat actual_config) = "/ip4/127.0.0.1/tcp/0"
225 -'
226 -
227 -test_expect_success "ipfs init from existing config succeeds" '
228 - export ORIG_PATH=$IPFS_DIR
229 - export IPFS_DIR=$(pwd)/.ipfs-clone
230 -
231 - ipfs init --repo-dir "$IPFS_DIR" "$ORIG_PATH/config" &&
232 - ipfs config --repo-dir "$IPFS_DIR" Addresses.API > actual_config &&
233 - test $(cat actual_config) = "/ip4/127.0.0.1/tcp/0"
234 -'
235 -
236 -test_expect_success "clean up ipfs clone dir and reset IPFS_DIR" '
237 - rm -rf "$IPFS_DIR" &&
238 - export IPFS_DIR=$ORIG_PATH
239 -'
240 -
241 -test_expect_success "clean up ipfs dir" '
242 - rm -rf "$IPFS_DIR"
243 -'
244 -
245 -test_expect_success "'ipfs init --profile=lowpower' succeeds" '
246 - RSA_BITS="2048" &&
247 - ipfs init --repo-dir "$IPFS_DIR" --profile=lowpower
248 -'
249 -
250 -test_expect_success "'ipfs config Discovery.Routing' looks good" '
251 - ipfs config --repo-dir "$IPFS_DIR" Routing.Type > actual_config &&
252 - test $(cat actual_config) = "dhtclient"
253 -'
254 -
255 -test_expect_success "clean up ipfs dir" '
256 - rm -rf "$IPFS_DIR"
257 -'
258 -
259 -test_init_ipfs
260 -
261 -test_launch_ipfs_daemon
262 -
263 -test_expect_success "ipfs init should not run while daemon is running" '
264 - test_must_fail ipfs init --repo-dir "$IPFS_DIR" 2> daemon_running_err &&
265 - EXPECT="Error: ipfs daemon is running. please stop it to run this command" &&
266 - grep "$EXPECT" daemon_running_err
267 -'
268 -
269 -test_kill_ipfs_daemon
270 -
271 -test_done
test/sharness/t0702-delegated-routing-http.sh deleted
-171
@@ -1,171 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -test_description="Test delegated routing via HTTP endpoint"
4 -
5 -. lib/test-lib.sh
6 -
7 -if ! test_have_prereq SOCAT; then
8 - skip_all="skipping '$test_description': socat is not available"
9 - test_done
10 -fi
11 -
12 -# simple http routing server mock
13 -# local endpoint responds with deterministic application/vnd.ipfs.rpc+dag-json; version=1
14 -HTTP_ROUTING_PORT=5098
15 -function start_http_routing_mock_endpoint() {
16 - REMOTE_SERVER_LOG="http-routing-server.log"
17 - rm -f $REMOTE_SERVER_LOG
18 -
19 - touch response
20 - socat tcp-listen:$HTTP_ROUTING_PORT,fork,bind=127.0.0.1,reuseaddr 'SYSTEM:cat response'!!CREATE:$REMOTE_SERVER_LOG &
21 - REMOTE_SERVER_PID=$!
22 -
23 - socat /dev/null tcp:127.0.0.1:$HTTP_ROUTING_PORT,retry=10
24 - return $?
25 -}
26 -function serve_http_routing_response() {
27 - local body=$1
28 - local status_code=${2:-"200 OK"}
29 - local length=$((1 + ${#body}))
30 - echo -e "HTTP/1.1 $status_code\nContent-Length: $length\nContent-Type: application/json\n\n$body" > response
31 -}
32 -function stop_http_routing_mock_endpoint() {
33 - exec 7<&-
34 - kill $REMOTE_SERVER_PID > /dev/null 2>&1
35 - wait $REMOTE_SERVER_PID || true
36 -}
37 -
38 -# daemon running in online mode to ensure Pin.origins/PinStatus.delegates work
39 -test_init_ipfs
40 -
41 -# based on static, synthetic http routing messages:
42 -# t0702-delegated-routing-http/FindProvidersRequest
43 -# t0702-delegated-routing-http/FindProvidersResponse
44 -FINDPROV_CID="baeabep4vu3ceru7nerjjbk37sxb7wmftteve4hcosmyolsbsiubw2vr6pqzj6mw7kv6tbn6nqkkldnklbjgm5tzbi4hkpkled4xlcr7xz4bq"
45 -EXPECTED_PROV="12D3KooWARYacCc6eoCqvsS9RW9MA2vo51CV75deoiqssx3YgyYJ"
46 -
47 -test_expect_success "default Routing config has no Routers defined" '
48 - echo null > expected &&
49 - ipfs config show | jq .Routing.Routers > actual &&
50 - test_cmp expected actual
51 -'
52 -
53 -# turn off all implicit routers
54 -ipfs config Routing.Type none || exit 1
55 -test_launch_ipfs_daemon
56 -test_expect_success "disabling default router (dht) works" '
57 - ipfs config Routing.Type > actual &&
58 - echo none > expected &&
59 - test_cmp expected actual
60 -'
61 -test_expect_success "no routers means findprovs returns no results" '
62 - ipfs routing findprovs "$FINDPROV_CID" > actual &&
63 - echo -n > expected &&
64 - test_cmp expected actual
65 -'
66 -
67 -test_kill_ipfs_daemon
68 -
69 -ipfs config Routing.Type --json '"custom"' || exit 1
70 -ipfs config Routing.Methods --json '{
71 - "find-peers": {
72 - "RouterName": "TestDelegatedRouter"
73 - },
74 - "find-providers": {
75 - "RouterName": "TestDelegatedRouter"
76 - },
77 - "get-ipns": {
78 - "RouterName": "TestDelegatedRouter"
79 - },
80 - "provide": {
81 - "RouterName": "TestDelegatedRouter"
82 - }
83 - }' || exit 1
84 -
85 -test_expect_success "missing method params makes daemon fails" '
86 - echo "Error: constructing the node (see log for full detail): method name \"put-ipns\" is missing from Routing.Methods config param" > expected_error &&
87 - GOLOG_LOG_LEVEL=fatal ipfs daemon 2> actual_error || exit 0 &&
88 - test_cmp expected_error actual_error
89 -'
90 -
91 -ipfs config Routing.Methods --json '{
92 - "find-peers": {
93 - "RouterName": "TestDelegatedRouter"
94 - },
95 - "find-providers": {
96 - "RouterName": "TestDelegatedRouter"
97 - },
98 - "get-ipns": {
99 - "RouterName": "TestDelegatedRouter"
100 - },
101 - "provide": {
102 - "RouterName": "TestDelegatedRouter"
103 - },
104 - "put-ipns": {
105 - "RouterName": "TestDelegatedRouter"
106 - },
107 - "NOT_SUPPORTED": {
108 - "RouterName": "TestDelegatedRouter"
109 - }
110 - }' || exit 1
111 -
112 -test_expect_success "having wrong methods makes daemon fails" '
113 - echo "Error: constructing the node (see log for full detail): method name \"NOT_SUPPORTED\" is not a supported method on Routing.Methods config param" > expected_error &&
114 - GOLOG_LOG_LEVEL=fatal ipfs daemon 2> actual_error || exit 0 &&
115 - test_cmp expected_error actual_error
116 -'
117 -
118 -# set Routing config to only use delegated routing via mocked http routing endpoint
119 -
120 -ipfs config Routing.Type --json '"custom"' || exit 1
121 -ipfs config Routing.Routers.TestDelegatedRouter --json '{
122 - "Type": "http",
123 - "Parameters": {
124 - "Endpoint": "http://127.0.0.1:5098/routing/v1"
125 - }
126 -}' || exit 1
127 -ipfs config Routing.Methods --json '{
128 - "find-peers": {
129 - "RouterName": "TestDelegatedRouter"
130 - },
131 - "find-providers": {
132 - "RouterName": "TestDelegatedRouter"
133 - },
134 - "get-ipns": {
135 - "RouterName": "TestDelegatedRouter"
136 - },
137 - "provide": {
138 - "RouterName": "TestDelegatedRouter"
139 - },
140 - "put-ipns": {
141 - "RouterName": "TestDelegatedRouter"
142 - }
143 - }' || exit 1
144 -
145 -test_expect_success "adding http delegated routing endpoint to Routing.Routers config works" '
146 - echo "http://127.0.0.1:5098/routing/v1" > expected &&
147 - ipfs config Routing.Routers.TestDelegatedRouter.Parameters.Endpoint > actual &&
148 - test_cmp expected actual
149 -'
150 -
151 -test_launch_ipfs_daemon
152 -
153 -test_expect_success "start_http_routing_mock_endpoint" '
154 - start_http_routing_mock_endpoint
155 -'
156 -
157 -test_expect_success "'ipfs routing findprovs' returns result from delegated http router" '
158 - serve_http_routing_response "$(<../t0702-delegated-routing-http/FindProvidersResponse)" &&
159 - echo "$EXPECTED_PROV" > expected &&
160 - ipfs routing findprovs "$FINDPROV_CID" > actual &&
161 - test_cmp expected actual
162 -'
163 -
164 -test_expect_success "stop_http_routing_mock_endpoint" '
165 - stop_http_routing_mock_endpoint
166 -'
167 -
168 -
169 -test_kill_ipfs_daemon
170 -test_done
171 -# vim: ts=2 sw=2 sts=2 et:
test/unit/Rules.mk
+1 -1
@@ -2,7 +2,7 @@ include mk/header.mk
2
3 CLEAN += $(d)/gotest.json $(d)/gotest.junit.xml
4
5 -$(d)/gotest.junit.xml: clean test/bin/gotestsum coverage/unit_tests.coverprofile
5 +$(d)/gotest.junit.xml: test/bin/gotestsum coverage/unit_tests.coverprofile
6 gotestsum --no-color --junitfile $@ --raw-command cat $(@D)/gotest.json
7
8 include mk/footer.mk