1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "strings" 12 "testing" 13 ) 14 15 type response struct { 16 Action, ID, Status, Message string 17 } 18 19 func TestUpload(t *testing.T) { 20 dir, err := os.MkdirTemp("", "justification") 21 if err != nil { 22 t.Errorf("unexepected error creating Schema: %s", err) 23 return 24 } 25 defer os.RemoveAll(dir) 26 s, err := NewSchema(dir) 27 if err != nil { 28 t.Errorf("unexepected error creating Schema: %s", err) 29 return 30 } 31 server := httptest.NewServer(s) 32 defer server.Close() 33 for n, test := range [...]struct { 34 ID, JSON, Status, Message string 35 Code int 36 }{ 37 { 38 ID: "TEST", 39 JSON: ``, 40 Code: http.StatusBadRequest, 41 Status: "error", 42 Message: "Invalid JSON", 43 }, 44 { 45 ID: "TEST", 46 JSON: `{}`, 47 Code: http.StatusCreated, 48 Status: "success", 49 }, 50 { 51 ID: "TEST", 52 JSON: `{}`, 53 Code: http.StatusMethodNotAllowed, 54 Status: "error", 55 Message: "Method Not Allowed", 56 }, 57 { 58 ID: "ANOTHER_TEST", 59 JSON: `{"$schema": "some invalid schema"}`, 60 Code: http.StatusBadRequest, 61 Status: "error", 62 Message: "jsonschema schema:///ANOTHER_TEST compilation failed: invalid $schema in schema:///ANOTHER_TEST", 63 }, 64 { 65 ID: "BAD ID", 66 JSON: `{}`, 67 Code: http.StatusBadRequest, 68 Status: "error", 69 Message: "Invalid ID", 70 }, 71 { 72 ID: "FULL-SCHEMA", 73 JSON: `{"$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": {"source": {"type": "string"}, "destination": {"type": "string"}, "timeout": {"type": "integer", "minimum": 0, "maximum": 32767}, "chunks": {"type": "object", "properties": {"size": {"type": "integer"}, "number": {"type": "integer"}}, "required": ["size"]}}, "required": ["source", "destination"]}`, 74 Code: http.StatusCreated, 75 Status: "success", 76 }, 77 } { 78 resp, err := http.Post(server.URL+"/schema/"+test.ID, "application/json", strings.NewReader(test.JSON)) 79 var r response 80 if err != nil { 81 t.Errorf("test %d.1: unexpected error: %s", n+1, err) 82 } else if resp.StatusCode != test.Code { 83 t.Errorf("test %d.1: expecting status code %d, got %d", n+1, test.Code, resp.StatusCode) 84 } else if ct := resp.Header.Get("Content-Type"); ct != "application/json" { 85 t.Errorf("test %d.1: expecting Content-Type of \"application/json\", got %s", n+1, ct) 86 } else if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 87 t.Errorf("test %d.1: unexpected error: %s", n+1, err) 88 } else if r.Action != "uploadSchema" { 89 t.Errorf("test %d.1: expecting Action \"uploadSchema\", got %q", n+1, r.Action) 90 } else if r.ID != test.ID { 91 t.Errorf("test %d.1: expecting ID %q, got %q", n+1, test.ID, r.ID) 92 } else if r.Status != test.Status { 93 t.Errorf("test %d.1: expecting Status %q, got %q", n+1, test.Status, r.Status) 94 } else if r.Message != test.Message { 95 t.Errorf("test %d.1: expecting Message %q, got %q", n+1, test.Message, r.Message) 96 } else if test.Message != "Invalid ID" { 97 resp, err := http.Get(server.URL + "/schema/" + test.ID) 98 var ( 99 expectingCode int 100 expectingJSON string 101 expectingContentType string 102 ) 103 if test.Code == http.StatusCreated || test.Code == http.StatusMethodNotAllowed { 104 expectingCode = http.StatusOK 105 expectingJSON = test.JSON 106 expectingContentType = "application/schema+json" 107 } else { 108 expectingCode = http.StatusMethodNotAllowed 109 expectingJSON = fmt.Sprintf(`{"action": "uploadSchema", "id": %q, "status": "error", "message": "Method Not Allowed"}`, test.ID) 110 expectingContentType = "application/json" 111 } 112 var b bytes.Buffer 113 if err != nil { 114 t.Errorf("test %d.2: unexpected error grabbing Scheme JSON: %s", n+1, err) 115 } else if ct := resp.Header.Get("Content-Type"); ct != expectingContentType { 116 t.Errorf("test %d.2: expecting Content-Type of %s, got %s", n+1, expectingContentType, ct) 117 } else if resp.StatusCode != expectingCode { 118 t.Errorf("test %d.2: expecting status code %d, got %d", n+1, expectingCode, resp.StatusCode) 119 } else if _, err := io.Copy(&b, resp.Body); err != nil { 120 t.Errorf("test %d.2: unexpected error reading Scheme JSON: %s", n+1, err) 121 } else if str := b.String(); str != expectingJSON { 122 t.Errorf("test %d.2: expecting to read JSON %q, got %q", n+1, expectingJSON, str) 123 } 124 } 125 } 126 } 127 128 type IDSchema struct { 129 ID, JSON string 130 } 131 132 var schemaTestJSON = []IDSchema{ 133 { 134 ID: "SimpleBoolean", 135 JSON: "true", 136 }, 137 { 138 ID: "SimpleObject", 139 JSON: "{}", 140 }, 141 { 142 ID: "Complex", 143 JSON: `{"$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": {"source": {"type": "string"}, "destination": {"type": "string"}, "timeout": {"type": "integer", "minimum": 0, "maximum": 32767}, "chunks": {"type": "object", "properties": {"size": {"type": "integer"}, "number": {"type": "integer"}}, "required": ["size"]}}, "required": ["source", "destination"]}`, 144 }, 145 } 146 147 func insertSchemas(url string, schemas []IDSchema) error { 148 for n, test := range schemas { 149 resp, err := http.Post(url+"/schema/"+test.ID, "application/json", strings.NewReader(test.JSON)) 150 var r response 151 if err != nil { 152 return fmt.Errorf("test %d: unexpected error: %w", n+1, err) 153 } else if resp.StatusCode != http.StatusCreated { 154 return fmt.Errorf("test %d: unexpected error, expecting status code 201, got %d", n+1, resp.StatusCode) 155 } else if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 156 return fmt.Errorf("test %d: unexpected error: %w", n+1, err) 157 } else if r.Status != "success" { 158 return fmt.Errorf("test %d: unexpected error, expecting Message \"success\", got %q", n+1, r.Status) 159 } 160 } 161 return nil 162 } 163 164 func TestLoad(t *testing.T) { 165 dir, err := os.MkdirTemp("", "justification") 166 if err != nil { 167 t.Errorf("unexepected error creating Schema: %s", err) 168 return 169 } 170 defer os.RemoveAll(dir) 171 s, err := NewSchema(dir) 172 if err != nil { 173 t.Errorf("unexepected error creating Schema: %s", err) 174 return 175 } 176 server := httptest.NewServer(s) 177 defer server.Close() 178 if err = insertSchemas(server.URL, schemaTestJSON); err != nil { 179 t.Error(err) 180 return 181 } 182 s, err = NewSchema(dir) 183 if err != nil { 184 t.Errorf("unexepected error loading Schema: %s", err) 185 return 186 } 187 server = httptest.NewServer(s) 188 defer server.Close() 189 for n, test := range schemaTestJSON { 190 resp, err := http.Get(server.URL + "/schema/" + test.ID) 191 var b bytes.Buffer 192 if err != nil { 193 t.Errorf("test %d: unexpected error grabbing Scheme JSON: %s", n+1, err) 194 } else if ct := resp.Header.Get("Content-Type"); ct != "application/schema+json" { 195 t.Errorf("test %d: expecting Content-Type of \"application/schema+json\", got %s", n+1, ct) 196 } else if resp.StatusCode != http.StatusOK { 197 t.Errorf("test %d: expecting status code 200, got %d", n+1, resp.StatusCode) 198 } else if _, err := io.Copy(&b, resp.Body); err != nil { 199 t.Errorf("test %d: unexpected error reading Scheme JSON: %s", n+1, err) 200 } else if str := b.String(); str != test.JSON { 201 t.Errorf("test %d: expecting to read JSON %q, got %q", n+1, test.JSON, str) 202 } 203 } 204 } 205 206 func TestValidate(t *testing.T) { 207 dir, err := os.MkdirTemp("", "justification") 208 if err != nil { 209 t.Errorf("unexepected error creating Schema: %s", err) 210 return 211 } 212 defer os.RemoveAll(dir) 213 s, err := NewSchema(dir) 214 if err != nil { 215 t.Errorf("unexepected error creating Schema: %s", err) 216 return 217 } 218 server := httptest.NewServer(s) 219 defer server.Close() 220 if err = insertSchemas(server.URL, schemaTestJSON); err != nil { 221 t.Error(err) 222 return 223 } 224 for n, test := range [...]struct { 225 ID, JSON, Status, Message string 226 Code int 227 }{ 228 { 229 ID: "SimpleBoolean", 230 JSON: "{}", 231 Status: "success", 232 Code: http.StatusOK, 233 }, 234 { 235 ID: "Bad ID", 236 JSON: "{}", 237 Status: "error", 238 Message: "Invalid ID", 239 Code: http.StatusBadRequest, 240 }, 241 { 242 ID: "Unknown", 243 JSON: "{}", 244 Status: "error", 245 Message: "Unknown ID", 246 Code: http.StatusNotFound, 247 }, 248 { 249 ID: "SimpleBoolean", 250 JSON: "{", 251 Status: "error", 252 Message: "Invalid JSON", 253 Code: http.StatusBadRequest, 254 }, 255 { 256 ID: "SimpleObject", 257 JSON: "{}", 258 Status: "success", 259 Code: http.StatusOK, 260 }, 261 { 262 ID: "Complex", 263 JSON: "{}", 264 Status: "error", 265 Message: "jsonschema: '' does not validate with schema:///Complex#/required: missing properties: 'source', 'destination'", 266 Code: http.StatusOK, 267 }, 268 { 269 ID: "Complex", 270 JSON: `{"source": "/home/alice/image.iso", "destination": "/mnt/storage", "chunks": {"size": 1024}}`, 271 Status: "success", 272 Code: http.StatusOK, 273 }, 274 { 275 ID: "Complex", 276 JSON: `{"source": "/home/alice/image.iso", "destination": "/mnt/storage", "timeout": null, "chunks": {"size": 1024, "number": null}}`, 277 Status: "success", 278 Code: http.StatusOK, 279 }, 280 { 281 ID: "Complex", 282 JSON: `{"source": "/home/alice/image.iso", "destination": null}`, 283 Status: "error", 284 Message: "jsonschema: '' does not validate with schema:///Complex#/required: missing properties: 'destination'", 285 Code: http.StatusOK, 286 }, 287 } { 288 resp, err := http.Post(server.URL+"/validate/"+test.ID, "application/json", strings.NewReader(test.JSON)) 289 var r response 290 if err != nil { 291 t.Errorf("test %d: unexpected error: %s", n+1, err) 292 } else if resp.StatusCode != test.Code { 293 t.Errorf("test %d: expecting status code %d, got %d", n+1, test.Code, resp.StatusCode) 294 } else if ct := resp.Header.Get("Content-Type"); ct != "application/json" { 295 t.Errorf("test %d: expecting Content-Type of \"application/json\", %s", n+1, ct) 296 } else if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 297 t.Errorf("test %d: unexpected error: %s", n+1, err) 298 } else if r.Action != "validateDocument" { 299 t.Errorf("test %d: expecting Action \"validateDocument\", got %q", n+1, r.Action) 300 } else if r.ID != test.ID { 301 t.Errorf("test %d: expecting ID %q, got %q", n+1, test.ID, r.ID) 302 } else if r.Status != test.Status { 303 t.Errorf("test %d: expecting Status %q, got %q", n+1, test.Status, r.Status) 304 } else if r.Message != test.Message { 305 t.Errorf("test %d: expecting Message %q, got %q", n+1, test.Message, r.Message) 306 } 307 } 308 } 309 310 func TestOptions(t *testing.T) { 311 dir, err := os.MkdirTemp("", "justification") 312 if err != nil { 313 t.Errorf("unexepected error creating Schema: %s", err) 314 return 315 } 316 defer os.RemoveAll(dir) 317 s, err := NewSchema(dir) 318 if err != nil { 319 t.Errorf("unexepected error creating Schema: %s", err) 320 return 321 } 322 server := httptest.NewServer(s) 323 defer server.Close() 324 if err = insertSchemas(server.URL, schemaTestJSON); err != nil { 325 t.Error(err) 326 return 327 } 328 var c http.Client 329 for n, test := range [...]struct { 330 Method, Endpoint, Options string 331 Code int 332 }{ 333 { 334 Method: http.MethodOptions, 335 Endpoint: "/schema/Unknown", 336 Options: optionsPost, 337 Code: http.StatusNoContent, 338 }, 339 { 340 Method: http.MethodOptions, 341 Endpoint: "/schema/Complex", 342 Options: optionsGetHead, 343 Code: http.StatusNoContent, 344 }, 345 { 346 Method: http.MethodOptions, 347 Endpoint: "/validate/Unknown", 348 Options: "", 349 Code: http.StatusNotFound, 350 }, 351 { 352 Method: http.MethodOptions, 353 Endpoint: "/validate/Complex", 354 Options: optionsPost, 355 Code: http.StatusNoContent, 356 }, 357 { 358 Method: http.MethodOptions, 359 Endpoint: "/other-endpoint", 360 Options: "", 361 Code: http.StatusNotFound, 362 }, 363 { 364 Method: http.MethodPut, 365 Endpoint: "/schema/Unknown", 366 Options: optionsPost, 367 Code: http.StatusMethodNotAllowed, 368 }, 369 { 370 Method: http.MethodPut, 371 Endpoint: "/schema/Complex", 372 Options: optionsGetHead, 373 Code: http.StatusMethodNotAllowed, 374 }, 375 { 376 Method: http.MethodPut, 377 Endpoint: "/validate/Unknown", 378 Options: "", 379 Code: http.StatusNotFound, 380 }, 381 { 382 Method: http.MethodPut, 383 Endpoint: "/validate/Complex", 384 Options: optionsPost, 385 Code: http.StatusMethodNotAllowed, 386 }, 387 { 388 Method: http.MethodPut, 389 Endpoint: "/other-endpoint", 390 Options: "", 391 Code: http.StatusNotFound, 392 }, 393 } { 394 req, _ := http.NewRequest(test.Method, server.URL+test.Endpoint, nil) 395 resp, err := c.Do(req) 396 if err != nil { 397 t.Errorf("test %d: unexpected error: %s", n+1, err) 398 } else if resp.StatusCode != test.Code { 399 t.Errorf("test %d: expecting status code %d, got %d", n+1, test.Code, resp.StatusCode) 400 } else if options := resp.Header.Get("Allow"); options != test.Options { 401 t.Errorf("test %d: expecting options %q, got %q", n+1, test.Options, options) 402 } 403 } 404 } 405