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 }