1package rest 2 3import ( 4 "bytes" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "strings" 10 "testing" 11 "time" 12) 13 14type Data struct { 15 Version int64 `json:"version"` 16 Msg string `json:"msg"` 17} 18 19var goldenCases = []struct { 20 Name string 21 Method string 22 Path string 23 Body string 24 Headers map[string]string 25 WantStatusCode int 26 WantBody string 27 WantHeaders map[string]string 28 Operation interface{} 29}{ 30 31 {"create", 32 "POST", "/", `{"msg": "hello"}`, map[string]string{"Content-Type": "application/json"}, 33 201, "", map[string]string{ 34 "Location": "/1001", 35 "ETag": `"1456260879956532222"`, 36 }, 37 func(d *Data) (int64, error) { 38 d.Version = 1456260879956532222 39 return 1001, nil 40 }, 41 }, 42 {"create fail & verify version untouched", 43 "POST", "/", `{"version": 3, "msg": "hello"}`, map[string]string{"Content-Type": "application/json"}, 44 500, "error v3\n", 45 map[string]string{ 46 "Location": "", 47 "ETag": "", 48 }, 49 func(d *Data) (int64, error) { 50 return 0, fmt.Errorf("error v%d", d.Version) 51 }, 52 }, 53 54 // Verify call arguments: 55 {"read latest fail", 56 "GET", "/99", "", nil, 57 500, "error 99 v0\n", nil, 58 func(id, version int64) (*Data, error) { 59 return nil, fmt.Errorf("error %d v%d", id, version) 60 }, 61 }, 62 {"read version fail", 63 "GET", "/99?v=2", "", nil, 64 500, "error 99 v2\n", nil, 65 func(id, version int64) (*Data, error) { 66 return nil, fmt.Errorf("error %d v%d", id, version) 67 }, 68 }, 69 70 // Verify return arguments: 71 {"read not found", 72 "GET", "/99?v=2", "", nil, 73 404, "ID 99 not found\n", map[string]string{ 74 "Content-Location": "", 75 "ETag": "", 76 "Last-Modified": "", 77 }, 78 func(id, version int64) (*Data, error) { 79 return nil, ErrNotFound 80 }, 81 }, 82 {"read version not found", 83 "GET", "/99?v=2", "", nil, 84 404, "version 2 not found (latest is 1)\n", map[string]string{ 85 "Content-Location": "", 86 "ETag": "", 87 "Last-Modified": "", 88 }, 89 func(id, version int64) (*Data, error) { 90 return &Data{Version: 1}, nil 91 }, 92 }, 93 94 // Caching: 95 {"read cache miss", 96 "GET", "/99", "", map[string]string{"If-None-Match": `"1456249153812139289"`, "If-Modified-Since": "Tue, 23 Feb 2016 20:54:39 UTC"}, 97 200, "{\n\t\"version\": 1456260879956532222,\n\t\"msg\": \"hello 99 v0\"\n}\n", map[string]string{ 98 "Content-Location": "/99?v=1456260879956532222", 99 "Allow": "OPTIONS, GET, HEAD", 100 "ETag": `"1456260879956532222"`, 101 "Last-Modified": "Tue, 23 Feb 2016 20:54:39 UTC", 102 "Content-Type": "application/json;charset=UTF-8", 103 }, 104 func(id, version int64) (*Data, error) { 105 return &Data{1456260879956532222, fmt.Sprintf("hello %d v%d", id, version)}, nil 106 }, 107 }, 108 {"read ETag cache hit", 109 "GET", "/99", "", map[string]string{"If-None-Match": `"1456260879956532222"`}, 110 304, "", map[string]string{ 111 "Content-Location": "/99?v=1456260879956532222", 112 "Allow": "OPTIONS, GET, HEAD", 113 "ETag": `"1456260879956532222"`, 114 "Last-Modified": "", // forbidden by status code 115 }, 116 func(id, version int64) (*Data, error) { 117 return &Data{Version: 1456260879956532222}, nil 118 }, 119 }, 120 {"read timestamp cache hit", 121 "GET", "/99", "", map[string]string{"If-Modified-Since": "Tue, 23 Feb 2016 20:54:40 UTC"}, 122 304, "", map[string]string{ 123 "Content-Location": "/99?v=1456260879956532222", 124 "Allow": "OPTIONS, GET, HEAD", 125 "ETag": `"1456260879956532222"`, 126 "Last-Modified": "", // forbidden by status code 127 }, 128 func(id, version int64) (*Data, error) { 129 return &Data{Version: 1456260879956532222}, nil 130 }, 131 }, 132 133 {"update fail", 134 "PUT", "/99", `{"version": 2, "msg": "hello"}`, map[string]string{"Content-Type": "application/json"}, 135 500, "hello 99 v2\n", nil, 136 func(id int64, d *Data) error { 137 return fmt.Errorf("%s %d v%d", d.Msg, id, d.Version) 138 }, 139 }, 140 {"update version match", 141 "PUT", "/99", `{"version": 2, "msg": "hello"}`, map[string]string{"Content-Type": "application/json", "If-Match": `"1456260879956532222"`}, 142 200, "{\n\t\"version\": 1456264479956532222,\n\t\"msg\": \"hello\"\n}\n", map[string]string{ 143 "Content-Location": "/99?v=1456264479956532222", 144 "Allow": "OPTIONS, PUT", 145 "Content-Type": "application/json;charset=UTF-8", 146 "ETag": `"1456264479956532222"`, 147 "Last-Modified": "Tue, 23 Feb 2016 21:54:39 UTC", 148 }, 149 func(id int64, d *Data) error { 150 d.Version += int64(time.Hour) 151 return nil 152 }, 153 }, 154 155 {"delete fail", 156 "DELETE", "/666", "", nil, 157 500, "operation rejected\n", nil, 158 func(id, version int64) error { 159 if id != 666 || version != 0 { 160 return fmt.Errorf("got ID %d v%d", id, version) 161 } 162 return fmt.Errorf("operation rejected") 163 }, 164 }, 165 {"delete version match not found", 166 "DELETE", "/666", "", map[string]string{"If-Match": `"1456260879956532222"`}, 167 404, "", nil, 168 func(id, version int64) error { 169 if id != 666 || version != 1456260879956532222 { 170 return fmt.Errorf("got ID %d v%d", id, version) 171 } 172 return ErrNotFound 173 }, 174 }, 175 {"delete version match optimistic lock lost", 176 "DELETE", "/666", "", map[string]string{"If-Match": `"1456260879956532222"`}, 177 412, "lost optimistic lock\n", nil, 178 func(id, version int64) error { 179 if id != 666 || version != 1456260879956532222 { 180 return fmt.Errorf("got ID %d v%d", id, version) 181 } 182 return ErrOptimisticLock 183 }, 184 }, 185 {"delete version query optimistic lock lost", 186 "DELETE", "/666?v=1456260879956532222", "", nil, 187 405, "not the latest version\n", nil, 188 func(id, version int64) error { 189 if id != 666 || version != 1456260879956532222 { 190 return fmt.Errorf("got ID %d v%d", id, version) 191 } 192 return ErrOptimisticLock 193 }, 194 }, 195} 196 197func TestGolden(t *testing.T) { 198 for _, gold := range goldenCases { 199 repo := NewCRUD("/", "/Version") 200 switch gold.Method { 201 case "POST": 202 repo.SetCreateFunc(gold.Operation) 203 case "GET": 204 repo.SetReadFunc(gold.Operation) 205 case "PUT": 206 repo.SetUpdateFunc(gold.Operation) 207 case "DELETE": 208 repo.SetDeleteFunc(gold.Operation) 209 default: 210 t.Fatalf("%s: unknown HTTP method %q", gold.Name, gold.Method) 211 } 212 server := httptest.NewServer(repo) 213 defer server.Close() 214 215 var reqBody io.Reader 216 if gold.Body != "" { 217 reqBody = strings.NewReader(gold.Body) 218 } 219 req, err := http.NewRequest(gold.Method, server.URL+gold.Path, reqBody) 220 if err != nil { 221 t.Fatalf("%s: malformed request: %s", gold.Name, err) 222 } 223 for name, value := range gold.Headers { 224 req.Header.Set(name, value) 225 } 226 227 res, err := http.DefaultClient.Do(req) 228 if err != nil { 229 t.Fatalf("%s: HTTP exchange: %s", gold.Name, err) 230 } 231 232 if res.StatusCode != gold.WantStatusCode { 233 buf := new(bytes.Buffer) 234 if err := res.Write(buf); err != nil { 235 t.Fatalf("%s: Read response: %s", gold.Name, err) 236 } 237 t.Logf("%s: response: %s", gold.Name, buf.String()) 238 239 t.Errorf("%s: Got HTTP %q, want HTTP %d", gold.Name, res.Status, gold.WantStatusCode) 240 } 241 242 for name, want := range gold.WantHeaders { 243 if got := res.Header.Get(name); got != want { 244 t.Errorf("%s: Got header %s value %q, want %q", gold.Name, name, got, want) 245 } 246 } 247 248 if want := gold.WantBody; want != "" { 249 buf := new(bytes.Buffer) 250 if _, err := buf.ReadFrom(res.Body); err != nil { 251 t.Fatalf("%s: Read response body: %s", gold.Name, err) 252 } 253 254 if got := buf.String(); got != want { 255 t.Errorf("%s: Got body %q, want %q", gold.Name, got, want) 256 } 257 } 258 } 259} 260