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 }