1 /**
2  This module implements integration tests for InfluxDB. As such, they record in
3  code the assumptions made with regards to the HTTP API. Given that these tests
4  pass, the unit tests are sufficient to guarantee correct behaviour.
5 
6  These tests can be run with `dub run -c integration` and require a running
7  instance of InfluxDB on localhost:8086. On systems with systemd, install
8  InfluxDB (as appropriate for the Linux distribution) and start it with
9  `systemctl start influxdb`.
10 
11  If these tests fail, nothing else in this repository will work.
12 */
13 module integration.curl;
14 
15 import unit_threaded;
16 import integration.common: influxURL;
17 
18 
19 ///
20 @Serial
21 @("Create and drop")
22 unittest {
23     curlPostQuery("CREATE DATABASE testdb").shouldSucceed;
24     curlPostQuery("DROP DATABASE testdb").shouldSucceed;
25 }
26 
27 ///
28 @Serial
29 @("Nonsense query")
30 unittest {
31     curlPostQuery("FOO DATABASE testdb").shouldFail;
32 }
33 
34 ///
35 @Serial
36 @("Query empty database")
37 unittest {
38     import std.string: join;
39     import std.json: parseJSON;
40     import std.algorithm: find;
41 
42     // in case there's still data there, delete the DB
43     curlPostQuery("DROP DATABASE testdb").shouldSucceed;
44     curlPostQuery("CREATE DATABASE testdb").shouldSucceed;
45     scope(exit) curlPostQuery("DROP DATABASE testdb").shouldSucceed;
46 
47     const lines = curlGet("SELECT * from foo").shouldSucceed;
48     const json = lines.join(" ").find("{").parseJSON;
49     json.toString.shouldEqual(`{"results":[{"statement_id":0}]}`);
50 }
51 
52 
53 ///
54 @Serial
55 @("Query database with data")
56 unittest {
57     import std.string: join;
58     import std.json: parseJSON;
59     import std.algorithm: find, map;
60 
61     // in case there's still data there, delete the DB
62     curlPostQuery("DROP DATABASE testdb").shouldSucceed;
63     curlPostQuery("CREATE DATABASE testdb").shouldSucceed;
64     scope(exit) curlPostQuery("DROP DATABASE testdb").shouldSucceed;
65 
66     curlPostWrite("foo,tag1=letag,tag2=othertag value=1,othervalue=3").shouldSucceed;
67     curlPostWrite("foo,tag1=toto,tag2=titi value=2,othervalue=4 1434055562000000000").shouldSucceed;
68 
69     /*
70       Example of a response (prettified):
71       {
72         "results": [{
73                 "series": [{
74                         "columns": ["time", "othervalue", "tag1", "tag2", "value"],
75                         "name": "foo",
76                         "values": [
77                                 ["2015-06-11T20:46:02Z", 4, "toto", "titi", 2],
78                                 ["2017-03-14T23:15:01.06282785Z", 3, "letag", "othertag", 1]
79                         ]
80                 }],
81                 "statement_id": 0
82         }]
83      }
84     */
85 
86     {
87         const lines = curlGet("SELECT * from foo").shouldSucceed;
88         const json = lines.join(" ").find("{").parseJSON;
89         const result = json.object["results"].array[0].object;
90         const table = result["series"].array[0].object;
91         table["columns"].array.map!(a => a.str).shouldBeSameSetAs(
92             ["time", "othervalue", "tag1", "tag2", "value"]);
93         table["name"].str.shouldEqual("foo");
94         table["values"].array.length.shouldEqual(2);
95     }
96 
97     {
98         const lines = curlGet("SELECT value from foo WHERE value > 1").shouldSucceed;
99         const json = lines.join(" ").find("{").parseJSON;
100         const result = json.object["results"].array[0].object;
101         const table = result["series"].array[0].object;
102         table["values"].array.length.shouldEqual(1);
103     }
104 
105     {
106         const lines = curlGet("SELECT othervalue from foo WHERE othervalue > 42").shouldSucceed;
107         const json = lines.join(" ").find("{").parseJSON;
108         const result = json.object["results"].array[0];
109         // no result in this case, no data with othervalue > 42
110         json.object["results"].array[0].toString.shouldEqual(`{"statement_id":0}`);
111     }
112 }
113 
114 private string[] curlPostQuery(in string arg) {
115     return ["curl", "-i", "-XPOST", influxURL ~ `/query`, "--data-urlencode",
116             `q=` ~ arg];
117 }
118 
119 private string[] curlPostWrite(in string arg) {
120     return ["curl", "-i", "-XPOST", influxURL ~ `/write?db=testdb`, "--data-binary", arg];
121 }
122 
123 private string[] curlGet(in string arg) {
124     return ["curl", "-G", influxURL ~ "/query?pretty=true", "--data-urlencode", "db=testdb",
125             "--data-urlencode", `q=` ~ arg];
126 }
127 
128 
129 private string[] shouldSucceed(in string[] cmd, in string file = __FILE__, in size_t line = __LINE__) {
130     import std.process: execute;
131     import std.conv: text;
132     import std.string: splitLines, join;
133     import std.algorithm: find, canFind, startsWith, endsWith;
134     import std.array: empty;
135     import std.json: parseJSON;
136 
137     writelnUt(cmd.join(" "));
138 
139     const ret = execute(cmd);
140     if(ret.status != 0)
141         throw new UnitTestException([text("Could not execute '", cmd.join(" "), "':")] ~
142                                     ret.output.splitLines, file, line);
143 
144     if(!ret.output.splitLines.canFind!(a => a.canFind("HTTP/1.1 20")) &&
145        !ret.output.canFind(`"results"`))
146         throw new UnitTestException([text("Bad HTTP response for '", cmd.join(" "), "':")]
147                                     ~ ("first: " ~ ret.output[0] ~ " last: " ~ ret.output[$-1])
148                                     ~
149                                     ret.output.splitLines, file, line);
150 
151     return ret.output.splitLines;
152 }
153 
154 private void shouldFail(in string[] cmd, in string file = __FILE__, in size_t line = __LINE__) {
155 
156     import std.conv: text;
157 
158     try {
159         shouldSucceed(cmd, file, line);
160         fail(text("Command '", cmd, "' was expected to fail but did not:"), file, line);
161     } catch(Exception ex) {}
162 }