github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/api/spec/spec.go (about) 1 package spec 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net" 10 "net/http" 11 "net/http/httptest" 12 "net/url" 13 "os" 14 "path/filepath" 15 "strings" 16 "testing" 17 ) 18 19 // AssertHTTPAPISpec runs a test suite of HTTP requests against the given base URL 20 // to assert it conforms to the qri core API specification. Spec is defined in 21 // the "open_api_3.yaml" file in the api package 22 func AssertHTTPAPISpec(t *testing.T, baseURL, specPackagePath string) { 23 t.Helper() 24 25 // Create a mock data server. Can't move this into the testRunner, because we need to 26 // ensure only this test is using the server's port "55556". 27 s := newMockDataServer(t) 28 defer s.Close() 29 30 base, err := url.Parse(baseURL) 31 if err != nil { 32 t.Fatalf("invalid base url: %s", err) 33 } 34 35 testFiles := []string{ 36 "testdata/access.json", 37 "testdata/aggregate.json", 38 "testdata/automation.json", 39 "testdata/dataset.json", 40 "testdata/misc.json", 41 "testdata/peer.json", 42 "testdata/profile.json", 43 "testdata/remote.json", 44 // sync.json is intentionally left out 45 // as it's more a protocol that doesn't belong 46 // in the RPC API 47 // "testdata/sync.json", 48 } 49 50 sl := mustLoadSkipList(t, filepath.Join(specPackagePath, "testdata/skip.json")) 51 52 for _, path := range testFiles { 53 t.Run(filepath.Base(path), func(t *testing.T) { 54 ts := mustLoadTestSuite(t, filepath.Join(specPackagePath, path)) 55 for i, c := range ts { 56 if isInSkipList(sl, c.Endpoint) { 57 continue 58 } 59 if err := c.do(base); err != nil { 60 t.Errorf("case %d %s %s:\n%s", i, c.Method, c.Endpoint, err) 61 } 62 } 63 }) 64 } 65 } 66 67 func isInSkipList(sl SkipList, endpoint string) bool { 68 skip := false 69 for _, s := range sl { 70 if s == endpoint { 71 skip = true 72 break 73 } 74 } 75 return skip 76 } 77 78 func newMockDataServer(t *testing.T) *httptest.Server { 79 t.Helper() 80 81 mockData := []byte(`Parent Identifier,Student Identifier 82 1001,1002 83 1010,1020 84 `) 85 mockDataServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 w.Write(mockData) 87 })) 88 l, err := net.Listen("tcp", ":55556") 89 if err != nil { 90 t.Fatal(err.Error()) 91 } 92 mockDataServer.Listener = l 93 mockDataServer.Start() 94 return mockDataServer 95 } 96 97 // TestCase is a single request-response round trip to the API with parameters 98 // for constructing the request & expectations for assessing the response. 99 type TestCase struct { 100 Endpoint string // API endpoint to test 101 Method string // HTTP request method, defaults to "GET" 102 Params map[string]string // Request query or path parameters 103 Headers map[string]string // Request HTTP headers 104 Body interface{} // request body 105 Expect *Response // Assertions about the response 106 } 107 108 func (c *TestCase) do(u *url.URL) error { 109 if c.Method == "" { 110 c.Method = http.MethodGet 111 } 112 113 u.Path = c.Endpoint 114 115 qvars := u.Query() 116 for k, v := range c.Params { 117 qvars.Set(k, v) 118 } 119 u.RawQuery = qvars.Encode() 120 121 body, err := c.reqBodyReader() 122 if err != nil { 123 return err 124 } 125 126 req, err := http.NewRequest(c.Method, u.String(), body) 127 if err != nil { 128 return err 129 } 130 for k, v := range c.Headers { 131 req.Header.Set(k, v) 132 } 133 134 res, err := http.DefaultClient.Do(req) 135 if err != nil { 136 return err 137 } 138 139 if exp := c.Expect; exp != nil { 140 if exp.Code != 0 && exp.Code != res.StatusCode { 141 return fmt.Errorf("response status code mismatch. want: %d got: %d\nresponse body: %s", exp.Code, res.StatusCode, c.resBodyErrString(res)) 142 } 143 144 for key, expVal := range exp.Headers { 145 got := res.Header.Get(key) 146 if expVal != got { 147 return fmt.Errorf("repsonse header %q mismatch.\nwant: %q\ngot: %q", key, expVal, got) 148 } 149 } 150 } 151 152 return nil 153 } 154 155 func (c *TestCase) reqBodyReader() (io.Reader, error) { 156 switch b := c.Body.(type) { 157 case map[string]interface{}: 158 buf := &bytes.Buffer{} 159 if err := json.NewEncoder(buf).Encode(b); err != nil { 160 return nil, err 161 } 162 return buf, nil 163 case string: 164 return strings.NewReader(b), nil 165 case nil: 166 return nil, nil 167 default: 168 return nil, fmt.Errorf("unrecognized type for request body %T", c.Body) 169 } 170 } 171 172 func (c *TestCase) decodeResponseBody(res *http.Response) (body interface{}, contentType string, err error) { 173 defer res.Body.Close() 174 contentType = res.Header.Get("Content-Type") 175 switch contentType { 176 case "application/json": 177 err = json.NewDecoder(res.Body).Decode(&body) 178 default: 179 body, err = ioutil.ReadAll(res.Body) 180 } 181 return body, contentType, err 182 } 183 184 func (c *TestCase) resBodyErrString(res *http.Response) string { 185 bd, ct, err := c.decodeResponseBody(res) 186 if err != nil { 187 return err.Error() 188 } 189 if ct == "application/json" { 190 enc, _ := json.MarshalIndent(bd, "", " ") 191 return string(enc) 192 } 193 194 if data, ok := bd.([]byte); ok { 195 return string(data) 196 } 197 198 return fmt.Sprintf("<unexpected response body. Content-Type: %q DataType: %T>", ct, bd) 199 } 200 201 // Response holds the expected HTTP response 202 type Response struct { 203 Code int 204 Headers map[string]string 205 } 206 207 func mustLoadTestSuite(t *testing.T, filePath string) []*TestCase { 208 f, err := os.Open(filePath) 209 if err != nil { 210 t.Fatalf("opening test suite file %q: %s", filePath, err) 211 } 212 defer f.Close() 213 suite := []*TestCase{} 214 if err := json.NewDecoder(f).Decode(&suite); err != nil { 215 t.Fatalf("deserializing test suite file %q: %s", filePath, err) 216 } 217 218 return suite 219 } 220 221 // SkipList holds a list of endpoints for which to skip testing 222 type SkipList []string 223 224 func mustLoadSkipList(t *testing.T, filePath string) SkipList { 225 f, err := os.Open(filePath) 226 if err != nil { 227 t.Fatalf("opening test skip list file %q: %s", filePath, err) 228 } 229 defer f.Close() 230 skipList := SkipList{} 231 if err := json.NewDecoder(f).Decode(&skipList); err != nil { 232 t.Fatalf("deserializing test skip list file %q: %s", filePath, err) 233 } 234 235 return skipList 236 }