1 module fluentasserts.vibe.request; 2 3 version(Have_vibe_d_http): 4 5 import vibe.inet.url; 6 import vibe.http.router; 7 import vibe.http.form; 8 import vibe.data.json; 9 10 import vibe.stream.memory; 11 12 import std.conv, std.string, std.array; 13 import std.algorithm, std.conv; 14 import std.stdio; 15 import std.format; 16 import std.exception; 17 18 import fluentasserts.core.base; 19 import fluentasserts.core.results; 20 21 //@safe: 22 23 RequestRouter request(URLRouter router) 24 { 25 return new RequestRouter(router); 26 } 27 28 /// 29 final class RequestRouter { 30 private { 31 alias ExpectedCallback = void delegate(Response res); 32 ExpectedCallback[] expected; 33 URLRouter router; 34 HTTPServerRequest preparedRequest; 35 36 string[string] headers; 37 38 string responseBody; 39 string requestBody; 40 } 41 42 /// 43 this(URLRouter router) { 44 this.router = router; 45 } 46 47 /// Send a string[string] to the server as x-www-form-urlencoded data 48 RequestRouter send(string[string] data) { 49 auto dst = appender!string; 50 51 dst.writeFormData(data); 52 header("Content-Type", "application/x-www-form-urlencoded"); 53 54 return send(dst.data); 55 } 56 57 /// Send data to the server. You can send strings, Json or any other object 58 /// which will be serialized to Json 59 RequestRouter send(T)(T data) { 60 static if (is(T == string)) 61 { 62 requestBody = data; 63 return this; 64 } 65 else static if (is(T == Json)) 66 { 67 requestBody = data.toPrettyString; 68 () @trusted { preparedRequest.bodyReader = createMemoryStream(cast(ubyte[]) requestBody); }(); 69 preparedRequest.json = data; 70 return this; 71 } 72 else 73 { 74 return send(data.serializeToJson()); 75 } 76 } 77 78 /// Add a header to the server request 79 RequestRouter header(string name, string value) { 80 if(preparedRequest is null) { 81 headers[name] = value; 82 } else { 83 preparedRequest.headers[name] = value; 84 } 85 return this; 86 } 87 88 /// Send a POST request 89 RequestRouter post(string host = "localhost", ushort port = 80)(string path) { 90 return customMethod!(HTTPMethod.POST, host, port)(path); 91 } 92 93 /// Send a PATCH request 94 RequestRouter patch(string host = "localhost", ushort port = 80)(string path) { 95 return customMethod!(HTTPMethod.PATCH, host, port)(path); 96 } 97 98 /// Send a PUT request 99 RequestRouter put(string host = "localhost", ushort port = 80)(string path) { 100 return customMethod!(HTTPMethod.PUT, host, port)(path); 101 } 102 103 /// Send a DELETE request 104 RequestRouter delete_(string host = "localhost", ushort port = 80)(string path) { 105 return customMethod!(HTTPMethod.DELETE, host, port)(path); 106 } 107 108 /// Send a GET request 109 RequestRouter get(string host = "localhost", ushort port = 80)(string path) { 110 return customMethod!(HTTPMethod.GET, host, port)(path); 111 } 112 113 /// Send a custom method request 114 RequestRouter customMethod(HTTPMethod method, string host = "localhost", ushort port = 80)(string path) { 115 return customMethod!method(URL("http://" ~ host ~ ":" ~ port.to!string ~ path)); 116 } 117 118 /// ditto 119 RequestRouter customMethod(HTTPMethod method)(URL url) { 120 preparedRequest = createTestHTTPServerRequest(url, method); 121 preparedRequest.host = url.host; 122 123 foreach(name, value; headers) { 124 preparedRequest.headers[name] = value; 125 } 126 127 return this; 128 } 129 130 RequestRouter expectHeaderExist(string name, const string file = __FILE__, const size_t line = __LINE__) { 131 void localExpectHeaderExist(Response res) { 132 auto result = expect(res.headers.keys, file, line).to.contain(name); 133 result.message = new MessageResult("Response header `" ~ name ~ "` is missing."); 134 } 135 136 expected ~= &localExpectHeaderExist; 137 138 return this; 139 } 140 141 RequestRouter expectHeader(string name, string value, const string file = __FILE__, const size_t line = __LINE__) { 142 expectHeaderExist(name, file, line); 143 144 void localExpectedHeader(Response res) { 145 auto result = expect(res.headers[name], file, line).to.equal(value); 146 result.message = new MessageResult("Response header `" ~ name ~ "` has an unexpected value. Expected `" 147 ~ value ~ "` != `" ~ res.headers[name].to!string ~ "`"); 148 } 149 150 expected ~= &localExpectedHeader; 151 152 return this; 153 } 154 155 RequestRouter expectHeaderContains(string name, string value, const string file = __FILE__, const size_t line = __LINE__) { 156 expectHeaderExist(name, file, line); 157 158 void expectHeaderContains(Response res) { 159 auto result = expect(res.headers[name], file, line).contain(value); 160 result.message = new MessageResult("Response header `" ~ name ~ "` has an unexpected value. Expected `" 161 ~ value ~ "` not found in `" ~ res.headers[name].to!string ~ "`"); 162 } 163 164 expected ~= &expectHeaderContains; 165 166 return this; 167 } 168 169 RequestRouter expectStatusCode(int code, const string file = __FILE__, const size_t line = __LINE__) { 170 void localExpectStatusCode(Response res) { 171 if(code != 404 && res.statusCode == 404) { 172 writeln("\n\nIs your route defined here?"); 173 router.getAllRoutes.map!(a => a.method.to!string ~ " " ~ a.pattern).each!writeln; 174 } 175 176 if(code != res.statusCode) { 177 IResult[] results = [ cast(IResult) new MessageResult("Invalid status code."), 178 cast(IResult) new ExpectedActualResult(code.to!string ~ " - " ~ httpStatusText(code), 179 res.statusCode.to!string ~ " - " ~ httpStatusText(res.statusCode)), 180 cast(IResult) new SourceResult(file, line) ]; 181 182 throw new TestException(results, file, line); 183 } 184 } 185 186 expected ~= &localExpectStatusCode; 187 188 return this; 189 } 190 191 private void performExpected(Response res) { 192 foreach(func; expected) { 193 func(res); 194 } 195 } 196 197 void end() { 198 end((Response response) => { }); 199 } 200 201 void end(T)(T callback) @trusted { 202 import vibe.stream.operations : readAllUTF8; 203 import vibe.inet.webform; 204 import vibe.stream.memory; 205 206 auto data = new ubyte[50000]; 207 208 static if(__traits(compiles, createMemoryStream(data) )) { 209 MemoryStream stream = createMemoryStream(data); 210 } else { 211 MemoryStream stream = new MemoryStream(data); 212 } 213 214 HTTPServerResponse res = createTestHTTPServerResponse(stream); 215 res.statusCode = 404; 216 217 static if(__traits(compiles, createMemoryStream(data) )) { 218 preparedRequest.bodyReader = createMemoryStream(cast(ubyte[]) requestBody); 219 } else { 220 preparedRequest.bodyReader = new MemoryStream(cast(ubyte[]) requestBody); 221 } 222 223 router.handleRequest(preparedRequest, res); 224 225 if(res.bytesWritten == 0 && data[0] == 0) { 226 enum notFound = "HTTP/1.1 404 No Content\r\n\r\n"; 227 data = cast(ubyte[]) notFound; 228 } 229 230 auto response = new Response(data, res.bytesWritten); 231 232 callback(response)(); 233 234 performExpected(response); 235 } 236 } 237 238 /// 239 class Response { 240 ubyte[] bodyRaw; 241 242 private { 243 Json _bodyJson; 244 string responseLine; 245 string originalStringData; 246 } 247 248 /// 249 string[string] headers; 250 251 /// 252 int statusCode; 253 254 /// Instantiate the Response 255 this(ubyte[] data, ulong len) { 256 this.originalStringData = (cast(char[])data).toStringz.to!string.dup; 257 258 auto bodyIndex = originalStringData.indexOf("\r\n\r\n"); 259 260 assert(bodyIndex != -1, "Invalid response data: \n" ~ originalStringData ~ "\n\n"); 261 262 auto headers = originalStringData[0 .. bodyIndex].split("\r\n").array; 263 264 responseLine = headers[0]; 265 statusCode = headers[0].split(" ")[1].to!int; 266 267 foreach (i; 1 .. headers.length) { 268 auto header = headers[i].split(": "); 269 this.headers[header[0]] = header[1]; 270 } 271 272 size_t start = bodyIndex + 4; 273 size_t end = bodyIndex + 4 + len; 274 275 if("Transfer-Encoding" in this.headers && this.headers["Transfer-Encoding"] == "chunked") { 276 277 while(start < end) { 278 size_t pos = data[start..end].assumeUTF.indexOf("\r\n").to!size_t; 279 if(pos == -1) { 280 break; 281 } 282 283 auto ln = data[start..start+pos].assumeUTF; 284 auto chunkSize = parse!size_t(ln, 16u); 285 286 if(chunkSize == 0) { 287 break; 288 } 289 290 start += pos + 2; 291 bodyRaw ~= data[start..start+chunkSize]; 292 start += chunkSize + 2; 293 } 294 return; 295 } 296 297 bodyRaw = data[start .. end]; 298 } 299 300 /// get the body as a string 301 string bodyString() { 302 return (cast(char[])bodyRaw).toStringz.to!string.dup; 303 } 304 305 /// get the body as a json object 306 Json bodyJson() { 307 if (_bodyJson.type == Json.Type.undefined) 308 { 309 string str = this.bodyString(); 310 311 try { 312 _bodyJson = str.parseJson; 313 } catch(Exception e) { 314 writeln("`" ~ str ~ "` is not a json string"); 315 } 316 } 317 318 return _bodyJson; 319 } 320 321 /// get the request as a string 322 override string toString() const { 323 return originalStringData; 324 } 325 } 326 327 @("Mocking a GET Request") 328 unittest { 329 auto router = new URLRouter(); 330 331 void sayHello(HTTPServerRequest req, HTTPServerResponse res) 332 { 333 res.writeBody("hello"); 334 } 335 336 router.get("*", &sayHello); 337 request(router) 338 .get("/") 339 .end((Response response) => { 340 response.bodyString.should.equal("hello"); 341 }); 342 343 request(router) 344 .post("/") 345 .end((Response response) => { 346 response.bodyString.should.not.equal("hello"); 347 }); 348 } 349 350 @("Mocking a POST Request") 351 unittest { 352 auto router = new URLRouter(); 353 354 void sayHello(HTTPServerRequest req, HTTPServerResponse res) 355 { 356 res.writeBody("hello"); 357 } 358 359 router.post("*", &sayHello); 360 request(router) 361 .post("/") 362 .end((Response response) => { 363 response.bodyString.should.equal("hello"); 364 }); 365 366 request(router) 367 .get("/") 368 .end((Response response) => { 369 response.bodyString.should.not.equal("hello"); 370 }); 371 } 372 373 @("Mocking a PATCH Request") 374 unittest { 375 auto router = new URLRouter(); 376 377 void sayHello(HTTPServerRequest req, HTTPServerResponse res) 378 { 379 res.writeBody("hello"); 380 } 381 382 router.patch("*", &sayHello); 383 request(router) 384 .patch("/") 385 .end((Response response) => { 386 response.bodyString.should.equal("hello"); 387 }); 388 389 request(router) 390 .get("/") 391 .end((Response response) => { 392 response.bodyString.should.not.equal("hello"); 393 }); 394 } 395 396 @("Mocking a PUT Request") 397 unittest { 398 auto router = new URLRouter(); 399 400 void sayHello(HTTPServerRequest req, HTTPServerResponse res) 401 { 402 res.writeBody("hello"); 403 } 404 405 router.put("*", &sayHello); 406 request(router) 407 .put("/") 408 .end((Response response) => { 409 response.bodyString.should.equal("hello"); 410 }); 411 412 request(router) 413 .get("/") 414 .end((Response response) => { 415 response.bodyString.should.not.equal("hello"); 416 }); 417 } 418 419 @("Mocking a DELETE Request") 420 unittest { 421 auto router = new URLRouter(); 422 423 void sayHello(HTTPServerRequest req, HTTPServerResponse res) 424 { 425 res.writeBody("hello"); 426 } 427 428 router.delete_("*", &sayHello); 429 request(router) 430 .delete_("/") 431 .end((Response response) => { 432 response.bodyString.should.equal("hello"); 433 }); 434 435 request(router) 436 .get("/") 437 .end((Response response) => { 438 response.bodyString.should.not.equal("hello"); 439 }); 440 } 441 442 @("Mocking a ACL Request") 443 unittest { 444 auto router = new URLRouter(); 445 446 void sayHello(HTTPServerRequest, HTTPServerResponse res) 447 { 448 res.writeBody("hello"); 449 } 450 451 router.match(HTTPMethod.ACL, "*", &sayHello); 452 453 request(router) 454 .customMethod!(HTTPMethod.ACL)("/") 455 .end((Response response) => { 456 response.bodyString.should.equal("hello"); 457 }); 458 459 request(router) 460 .get("/") 461 .end((Response response) => { 462 response.bodyString.should.not.equal("hello"); 463 }); 464 } 465 466 @("Sending headers") 467 unittest { 468 auto router = new URLRouter(); 469 470 void checkHeaders(HTTPServerRequest req, HTTPServerResponse) 471 { 472 req.headers["Accept"].should.equal("application/json"); 473 } 474 475 router.any("*", &checkHeaders); 476 477 request(router) 478 .get("/") 479 .header("Accept", "application/json") 480 .end(); 481 } 482 483 @("Sending raw string") 484 unittest { 485 import std.string; 486 487 auto router = new URLRouter(); 488 489 void checkStringData(HTTPServerRequest req, HTTPServerResponse) 490 { 491 req.bodyReader.peek.assumeUTF.to!string.should.equal("raw string"); 492 } 493 494 router.any("*", &checkStringData); 495 496 request(router) 497 .post("/") 498 .send("raw string") 499 .end(); 500 } 501 502 @("Receiving raw binary") 503 unittest { 504 import std.string; 505 506 auto router = new URLRouter(); 507 508 void checkStringData(HTTPServerRequest req, HTTPServerResponse res) 509 { 510 res.writeBody(cast(ubyte[]) [0, 1, 2], 200, "application/binary"); 511 } 512 513 router.any("*", &checkStringData); 514 515 request(router) 516 .post("/") 517 .end((Response response) => { 518 response.bodyRaw.should.equal(cast(ubyte[]) [0,1,2]); 519 }); 520 } 521 522 @("Sending form data") 523 unittest { 524 auto router = new URLRouter(); 525 526 void checkFormData(HTTPServerRequest req, HTTPServerResponse) 527 { 528 req.headers["content-type"].should.equal("application/x-www-form-urlencoded"); 529 530 req.form["key1"].should.equal("value1"); 531 req.form["key2"].should.equal("value2"); 532 } 533 534 router.any("*", &checkFormData); 535 536 request(router) 537 .post("/") 538 .send(["key1": "value1", "key2": "value2"]) 539 .end(); 540 } 541 542 @("Sending json data") 543 unittest { 544 auto router = new URLRouter(); 545 546 void checkJsonData(HTTPServerRequest req, HTTPServerResponse) 547 { 548 req.json["key"].to!string.should.equal("value"); 549 } 550 551 router.any("*", &checkJsonData); 552 553 request(router) 554 .post("/") 555 .send(`{ "key": "value" }`.parseJsonString) 556 .end(); 557 } 558 559 @("Receive json data") 560 unittest { 561 auto router = new URLRouter(); 562 563 void respondJsonData(HTTPServerRequest, HTTPServerResponse res) 564 { 565 res.writeJsonBody(`{ "key": "value"}`.parseJsonString); 566 } 567 568 router.any("*", &respondJsonData); 569 570 request(router) 571 .get("/") 572 .end((Response response) => { 573 response.bodyJson["key"].to!string.should.equal("value"); 574 }); 575 } 576 577 @("Expect status code") 578 unittest { 579 auto router = new URLRouter(); 580 581 void respondStatus(HTTPServerRequest, HTTPServerResponse res) 582 { 583 res.statusCode = 200; 584 res.writeBody(""); 585 } 586 587 router.get("*", &respondStatus); 588 589 request(router) 590 .get("/") 591 .expectStatusCode(200) 592 .end(); 593 594 595 ({ 596 request(router) 597 .post("/") 598 .expectStatusCode(200) 599 .end(); 600 }).should.throwException!TestException.msg.should.startWith("Invalid status code."); 601 } 602 603 604 /// Expect header 605 unittest { 606 auto router = new URLRouter(); 607 608 void respondHeader(HTTPServerRequest, HTTPServerResponse res) 609 { 610 res.headers["some-header"] = "some-value"; 611 res.writeBody(""); 612 } 613 614 router.get("*", &respondHeader); 615 616 617 // Check for the exact header value: 618 request(router) 619 .get("/") 620 .expectHeader("some-header", "some-value") 621 .end(); 622 623 624 ({ 625 request(router) 626 .post("/") 627 .expectHeader("some-header", "some-value") 628 .end(); 629 }).should.throwAnyException.msg.should.contain("Response header `some-header` is missing."); 630 631 ({ 632 request(router) 633 .get("/") 634 .expectHeader("some-header", "other-value") 635 .end(); 636 }).should.throwAnyException.msg.should.contain("Response header `some-header` has an unexpected value"); 637 638 // Check if a header exists 639 request(router) 640 .get("/") 641 .expectHeaderExist("some-header") 642 .end(); 643 644 645 ({ 646 request(router) 647 .post("/") 648 .expectHeaderExist("some-header") 649 .end(); 650 }).should.throwAnyException.msg.should.contain("Response header `some-header` is missing."); 651 652 // Check if a header contains a string 653 request(router) 654 .get("/") 655 .expectHeaderContains("some-header", "value") 656 .end(); 657 658 ({ 659 request(router) 660 .get("/") 661 .expectHeaderContains("some-header", "other") 662 .end(); 663 }).should.throwAnyException.msg.should.contain("Response header `some-header` has an unexpected value."); 664 }