github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/api/api_test.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime/multipart" 11 "net/http" 12 "net/http/httptest" 13 "os" 14 "path/filepath" 15 "testing" 16 "time" 17 18 "github.com/beme/abide" 19 "github.com/gorilla/mux" 20 golog "github.com/ipfs/go-log" 21 ma "github.com/multiformats/go-multiaddr" 22 manet "github.com/multiformats/go-multiaddr/net" 23 apispec "github.com/qri-io/qri/api/spec" 24 "github.com/qri-io/qri/automation" 25 "github.com/qri-io/qri/base/dsfs" 26 "github.com/qri-io/qri/config" 27 testcfg "github.com/qri-io/qri/config/test" 28 "github.com/qri-io/qri/event" 29 "github.com/qri-io/qri/lib" 30 "github.com/qri-io/qri/logbook" 31 "github.com/qri-io/qri/p2p" 32 "github.com/qri-io/qri/repo" 33 "github.com/qri-io/qri/repo/test" 34 ) 35 36 func init() { 37 abide.SnapshotsDir = "testdata" 38 } 39 40 func TestMain(m *testing.M) { 41 exit := m.Run() 42 abide.Cleanup() 43 os.Exit(exit) 44 } 45 46 func TestApiSpec(t *testing.T) { 47 if err := confirmQriNotRunning(); err != nil { 48 t.Fatal(err.Error()) 49 } 50 51 tr := NewAPITestRunner(t) 52 defer tr.Delete() 53 54 ts := tr.MustTestServer(t) 55 defer ts.Close() 56 57 apispec.AssertHTTPAPISpec(t, ts.URL, "./spec") 58 } 59 60 func TestConnectNoP2P(t *testing.T) { 61 ctx, cancel := context.WithCancel(context.Background()) 62 defer cancel() 63 64 node, teardown := newTestNode(t) 65 defer teardown() 66 67 inst := newTestInstanceWithProfileFromNode(ctx, node) 68 cfg := inst.GetConfig() 69 cfg.P2P.Enabled = false 70 if err := inst.ChangeConfig(cfg); err != nil { 71 t.Fatal(err) 72 } 73 74 s := New(inst) 75 ctx, cancel2 := context.WithTimeout(ctx, time.Millisecond*15) 76 defer cancel2() 77 78 if err := s.Serve(ctx); !errors.Is(err, http.ErrServerClosed) { 79 t.Fatal(err) 80 } 81 } 82 83 func newTestRepo(t *testing.T) (r repo.Repo, teardown func()) { 84 var err error 85 if err = confirmQriNotRunning(); err != nil { 86 t.Fatal(err.Error()) 87 } 88 89 // bump up log level to keep test output clean 90 golog.SetLogLevel("qriapi", "error") 91 92 // to keep hashes consistent, artificially specify the timestamp by overriding 93 // the dsfs.Timestamp func 94 prevTs := dsfs.Timestamp 95 dsfs.Timestamp = func() time.Time { return time.Date(2001, 01, 01, 01, 01, 01, 01, time.UTC) } 96 97 logbookTsSec := 0 98 prevLogbookTs := logbook.NewTimestamp 99 logbook.NewTimestamp = func() int64 { 100 logbookTsSec++ 101 return time.Date(2001, 01, 01, 01, 01, logbookTsSec, 01, time.UTC).Unix() 102 } 103 104 if r, err = test.NewTestRepo(); err != nil { 105 t.Fatalf("error allocating test repo: %s", err.Error()) 106 } 107 108 teardown = func() { 109 golog.SetLogLevel("qriapi", "info") 110 // lib.SaveConfig = prevSaveConfig 111 dsfs.Timestamp = prevTs 112 logbook.NewTimestamp = prevLogbookTs 113 } 114 115 return 116 } 117 118 func newTestNode(t *testing.T) (node *p2p.QriNode, teardown func()) { 119 t.Helper() 120 121 var r repo.Repo 122 r, teardown = newTestRepo(t) 123 node, err := p2p.NewQriNode(r, testcfg.DefaultP2PForTesting(), event.NilBus, nil) 124 if err != nil { 125 t.Fatal(err.Error()) 126 } 127 return node, teardown 128 } 129 130 func testConfigAndSetter() (cfg *config.Config, setCfg func(*config.Config) error) { 131 cfg = testcfg.DefaultConfigForTesting() 132 cfg.Profile = test.ProfileConfig() 133 134 setCfg = func(*config.Config) error { return nil } 135 return 136 } 137 138 func newTestInstanceWithProfileFromNode(ctx context.Context, node *p2p.QriNode) *lib.Instance { 139 return newTestInstanceWithProfileFromNodeAndOrchestratorOpts(ctx, node, nil) 140 } 141 142 func newTestInstanceWithProfileFromNodeAndOrchestratorOpts(ctx context.Context, node *p2p.QriNode, o *automation.OrchestratorOptions) *lib.Instance { 143 cfg := testcfg.DefaultConfigForTesting() 144 cfg.Profile, _ = node.Repo.Profiles().Owner(ctx).Encode() 145 return lib.NewInstanceFromConfigAndNodeAndBusAndOrchestratorOpts(ctx, cfg, node, event.NilBus, o) 146 } 147 148 type handlerTestCase struct { 149 method, endpoint string 150 body []byte 151 muxVars map[string]string 152 } 153 154 // runHandlerTestCases executes a slice of handlerTestCase against a handler 155 func runHandlerTestCases(t *testing.T, name string, h http.HandlerFunc, cases []handlerTestCase, jsonHeader bool) { 156 for i, c := range cases { 157 name := fmt.Sprintf("%s %s case %d: %s %s", t.Name(), name, i, c.method, c.endpoint) 158 req := httptest.NewRequest(c.method, c.endpoint, bytes.NewBuffer(c.body)) 159 if c.muxVars != nil { 160 req = mux.SetURLVars(req, c.muxVars) 161 } 162 setRefStringFromMuxVars(req) 163 if jsonHeader { 164 req.Header.Set("Content-Type", "application/json") 165 } 166 w := httptest.NewRecorder() 167 168 h(w, req) 169 170 res := w.Result() 171 abide.AssertHTTPResponse(t, name, res) 172 } 173 } 174 175 // runHandlerZipPostTestCases executes a slice of handlerTestCase against a handler using zip content-type 176 func runHandlerZipPostTestCases(t *testing.T, name string, h http.HandlerFunc, cases []handlerTestCase) { 177 for i, c := range cases { 178 name := fmt.Sprintf("%s %s case %d: %s %s", t.Name(), name, i, c.method, c.endpoint) 179 req := httptest.NewRequest(c.method, c.endpoint, bytes.NewBuffer(c.body)) 180 req.Header.Set("Content-Type", "application/zip") 181 w := httptest.NewRecorder() 182 183 h(w, req) 184 185 res := w.Result() 186 abide.AssertHTTPResponse(t, name, res) 187 } 188 } 189 190 // mustFile reads file bytes, calling t.Fatalf if the file doesn't exist 191 func mustFile(t *testing.T, filename string) []byte { 192 data, err := ioutil.ReadFile(filename) 193 if err != nil { 194 t.Fatalf("error opening test file: %s: %s", filename, err.Error()) 195 } 196 return data 197 } 198 199 func confirmQriNotRunning() error { 200 addr, err := ma.NewMultiaddr(config.DefaultAPIAddress) 201 if err != nil { 202 return fmt.Errorf(err.Error()) 203 } 204 l, err := manet.Listen(addr) 205 if err != nil { 206 return fmt.Errorf("it looks like a qri server is already running on address %s, please close before running tests", config.DefaultAPIAddress) 207 } 208 209 l.Close() 210 return nil 211 } 212 213 func TestHealthCheck(t *testing.T) { 214 prevAPIVer := APIVersion 215 APIVersion = "test_version" 216 defer func() { 217 APIVersion = prevAPIVer 218 }() 219 220 healthCheckCases := []handlerTestCase{ 221 {"GET", "/", nil, nil}, 222 } 223 runHandlerTestCases(t, "health check", HealthCheckHandler, healthCheckCases, true) 224 } 225 226 type handlerMimeMultipartTestCase struct { 227 method string 228 endpoint string 229 filePaths map[string]string 230 params map[string]string 231 muxVars map[string]string 232 } 233 234 func runMimeMultipartHandlerTestCases(t *testing.T, name string, h http.HandlerFunc, cases []handlerMimeMultipartTestCase) { 235 for i, c := range cases { 236 body := &bytes.Buffer{} 237 writer := multipart.NewWriter(body) 238 name := fmt.Sprintf("%s %s case %d: %s %s", t.Name(), name, i, c.method, c.endpoint) 239 240 for name, path := range c.filePaths { 241 data, err := os.Open(path) 242 if err != nil { 243 t.Fatalf("error opening datafile: %s %s", name, err) 244 } 245 dataPart, err := writer.CreateFormFile(name, filepath.Base(path)) 246 if err != nil { 247 t.Fatalf("error adding data file to form: %s %s", name, err) 248 } 249 250 if _, err := io.Copy(dataPart, data); err != nil { 251 t.Fatalf("error copying data: %s %s %s", c.method, c.endpoint, err) 252 } 253 } 254 for key, val := range c.params { 255 if err := writer.WriteField(key, val); err != nil { 256 t.Fatalf("error adding field to writer: %s %s", name, err) 257 } 258 } 259 260 if err := writer.Close(); err != nil { 261 t.Fatalf("error closing writer: %s", err) 262 } 263 264 req := httptest.NewRequest(c.method, c.endpoint, body) 265 req.Header.Add("Content-Type", writer.FormDataContentType()) 266 if c.muxVars != nil { 267 req = mux.SetURLVars(req, c.muxVars) 268 } 269 setRefStringFromMuxVars(req) 270 271 w := httptest.NewRecorder() 272 273 h(w, req) 274 275 res := w.Result() 276 abide.AssertHTTPResponse(t, name, res) 277 } 278 } 279 280 // NewFilesRequest creates a mime/multipart http.Request with files specified by a map of param : filepath, 281 // and form values specified by a map, params 282 func NewFilesRequest(method, endpoint, url string, filePaths, params map[string]string) (*http.Request, error) { 283 body := &bytes.Buffer{} 284 writer := multipart.NewWriter(body) 285 286 for name, path := range filePaths { 287 data, err := os.Open(path) 288 if err != nil { 289 return nil, fmt.Errorf("error opening datafile: %s %s %s", method, endpoint, err) 290 } 291 dataPart, err := writer.CreateFormFile(name, filepath.Base(path)) 292 if err != nil { 293 return nil, fmt.Errorf("error adding data file to form: %s %s %s", method, endpoint, err) 294 } 295 296 if _, err := io.Copy(dataPart, data); err != nil { 297 return nil, fmt.Errorf("error copying data: %s %s %s", method, endpoint, err) 298 } 299 } 300 for key, val := range params { 301 if err := writer.WriteField(key, val); err != nil { 302 return nil, fmt.Errorf("error adding field to writer: %s %s %s", method, endpoint, err) 303 } 304 } 305 306 if err := writer.Close(); err != nil { 307 return nil, fmt.Errorf("error closing writer: %s", err) 308 } 309 310 req, err := http.NewRequest(method, url, body) 311 if err != nil { 312 return nil, fmt.Errorf("error creating request: %s %s %s", method, endpoint, err) 313 } 314 315 req.Header.Add("Content-Type", writer.FormDataContentType()) 316 317 return req, nil 318 }