1 module irc.testing; 2 3 version(unittest): 4 5 import std.socket; 6 7 import irc.client; 8 import irc.protocol; 9 10 immutable testRealName = "Test Name"; 11 auto testUser = IrcUser("TestNick", "user", "test.org"); 12 13 class TestConnection 14 { 15 private: 16 Socket clientSocket, server; 17 char[512] _lineBuffer; 18 19 public: 20 IrcClient client; 21 22 this() 23 { 24 auto listener = new TcpSocket(); 25 scope(exit) listener.close(); 26 27 auto serverAddress = parseAddress("127.0.0.1", InternetAddress.PORT_ANY); 28 listener.bind(serverAddress); 29 listener.listen(1); 30 31 this.clientSocket = new TcpSocket(); 32 this.client = new IrcClient(clientSocket); 33 client.nickName = testUser.nickName; 34 client.userName = testUser.userName.idup; 35 client.realName = testRealName; 36 37 this.client.connect(listener.localAddress); 38 39 server = listener.accept(); 40 } 41 42 void injectfln(FmtArgs...)(const(char)[] fmt, FmtArgs fmtArgs) 43 { 44 import std.string : sformat; 45 46 enum doFormat = fmtArgs.length > 0; 47 48 static if(doFormat) 49 { 50 fmt = _lineBuffer[0 .. 510].sformat(fmt, fmtArgs); 51 _lineBuffer[fmt.length .. fmt.length + 2] = "\r\n"; 52 fmt = _lineBuffer[0 .. fmt.length + 2]; 53 } 54 55 server.send(fmt); 56 57 static if(!doFormat) 58 server.send("\r\n"); 59 } 60 61 // TODO: Write a proper implementation 62 IrcLine getLine() 63 { 64 char recvChar() 65 { 66 char c; 67 auto received = server.receive((&c)[0 .. 1]); 68 assert(received == 1); 69 return c; 70 } 71 72 size_t lineLength = 0; 73 74 for(;;) 75 { 76 auto c = recvChar(); 77 78 if(c == '\r') 79 break; 80 else 81 _lineBuffer[lineLength++] = c; 82 } 83 84 char lf = recvChar(); 85 assert(lf == '\n'); 86 87 auto rawLine = _lineBuffer[0 .. lineLength]; 88 89 IrcLine line; 90 rawLine.parse(line); 91 return line; 92 } 93 94 IrcLine assertLine(in char[] cmd, in char[][] args...) 95 { 96 import std.string : format; 97 98 auto line = getLine(); 99 100 void assertOriginator(IrcUser originator) 101 { 102 assert(originator.nickName == testUser.nickName, `expected nickname "%s", got "%s")`.format(testUser.nickName, originator.nickName)); 103 assert(originator.userName == null, `got username, expected none`); 104 assert(originator.hostName == null, `got hostname, expected none`); 105 } 106 107 if(line.prefix) 108 { 109 import core.exception : AssertError; 110 111 try assertOriginator(IrcUser.fromPrefix(line.prefix)); 112 catch(AssertError e) 113 throw new AssertError("the only valid origin a client can send is the client's nickname", __FILE__, __LINE__, e); 114 } 115 116 assert(line.command == cmd, `expected command "%s", got "%s"`.format(cmd, line.command)); 117 118 foreach(i, arg; args) 119 { 120 if(arg.ptr) 121 assert(line.arguments[i] == arg, 122 `argument #%d did not match expectations; got "%s", expected "%s"` 123 .format(i + 1, line.arguments[i], arg)); 124 } 125 126 return line; 127 } 128 } 129 130 unittest 131 { 132 import std.algorithm : joiner; 133 import std.range : chain, iota, only, repeat; 134 import std.typetuple : TypeTuple; 135 136 auto conn = new TestConnection(); 137 auto origin = "testserver"; 138 auto client = conn.client; 139 140 struct TestEvent(string eventName) 141 { 142 import std.traits; 143 alias HandlerType = typeof(mixin("IrcClient." ~ eventName)[0]); 144 alias Args = ParameterTypeTuple!HandlerType; 145 alias Ret = ReturnType!HandlerType; 146 147 Ret delegate(Args) handler; 148 bool prepared = false, ran = false; 149 150 @disable this(this); 151 152 static if(is(Ret == void)) 153 alias ExpectedRet = TypeTuple!(); 154 else 155 alias ExpectedRet = Ret; 156 157 void prepare(ExpectedRet expectedRet, Args expectedArgs) 158 { 159 handler = delegate Ret(Args args) { 160 ran = true; 161 assert(args == expectedArgs); 162 static if (!is(Ret == void)) 163 return expectedRet; 164 }; 165 166 mixin("client." ~ eventName) ~= handler; 167 prepared = true; 168 } 169 170 void check() 171 { 172 assert(prepared); 173 assert(ran); 174 mixin("client." ~ eventName).unsubscribeHandler(handler); 175 } 176 } 177 178 auto socketSet = new SocketSet(1); 179 socketSet.add(conn.clientSocket); 180 void handleClientEvents() 181 { 182 Socket.select(socketSet, null, null); 183 assert(socketSet.isSet(conn.clientSocket)); 184 assert(!client.read()); 185 } 186 187 conn.assertLine("NICK", testUser.nickName); 188 conn.assertLine("USER", testUser.userName, null, null, testRealName); 189 190 { 191 TestEvent!"onNickInUse" onNickInUse; 192 auto newNickName = testUser.nickName ~ "_"; 193 onNickInUse.prepare(newNickName, testUser.nickName); 194 conn.injectfln(":%s 433 * %s :Nickname is already in use", origin, testUser.nickName); 195 handleClientEvents(); 196 onNickInUse.check(); 197 conn.assertLine("NICK", newNickName); 198 testUser.nickName = newNickName; 199 } 200 201 TestEvent!"onConnect" onConnect; 202 onConnect.prepare(); 203 conn.injectfln(":%s 001 %s :Welcome to the test server", origin, testUser.nickName); 204 handleClientEvents(); 205 onConnect.check(); 206 207 conn.injectfln(":%s PING :hello world", origin); 208 handleClientEvents(); 209 conn.assertLine("PONG", "hello world"); 210 211 client.join("#test"); 212 conn.assertLine("JOIN", "#test"); 213 214 TestEvent!"onSuccessfulJoin" onSuccessfulJoin; 215 onSuccessfulJoin.prepare("#test"); 216 conn.injectfln(":%s JOIN #test", testUser); 217 handleClientEvents(); 218 onSuccessfulJoin.check(); 219 220 TestEvent!"onNameList" onNameList; 221 onNameList.prepare("#test", ["a", "b", "c"]); 222 conn.injectfln(":%s 353 %s = #test :a +b @c", origin, testUser.nickName); 223 handleClientEvents(); 224 onNameList.check(); 225 226 TestEvent!"onNameListEnd" onNameListEnd; 227 onNameListEnd.prepare("#test"); 228 conn.injectfln(":%s 366 %s #test :End of NAMES list", origin, testUser.nickName); 229 handleClientEvents(); 230 onNameListEnd.check(); 231 232 TestEvent!"onMessage" onMessage; 233 onMessage.prepare(IrcUser("nick", "user", null), "#test", "hello world"); 234 conn.injectfln(":nick!user PRIVMSG #test :hello world"); 235 handleClientEvents(); 236 onMessage.check(); 237 238 onMessage = TestEvent!"onMessage"(); 239 onMessage.prepare(IrcUser("nick", "user", "host"), "#test", "hi"); 240 conn.injectfln(":nick!user@host PRIVMSG #test hi"); 241 handleClientEvents(); 242 onMessage.check(); 243 244 TestEvent!"onNotice" onNotice; 245 onNotice.prepare(IrcUser(origin), testUser.nickName, "foo bar"); 246 conn.injectfln(":%s NOTICE %s :foo bar", origin, testUser.nickName); 247 handleClientEvents(); 248 onNotice.check(); 249 250 TestEvent!"onNickChange" onNickChange; 251 onNickChange.prepare(testUser, "newNick"); 252 conn.injectfln(":%s NICK newNick", testUser); 253 testUser.nickName = "newNick"; 254 testUser.nickName = "newNick"; 255 handleClientEvents(); 256 onNickChange.check(); 257 258 auto otherUser = IrcUser("othernick", "other", "other.org"); 259 TestEvent!"onJoin" onJoin; 260 onJoin.prepare(otherUser, "#test"); 261 conn.injectfln(":%s JOIN #test", otherUser); 262 handleClientEvents(); 263 onJoin.check(); 264 265 TestEvent!"onPart" onPart; 266 onPart.prepare(otherUser, "#test"); 267 conn.injectfln(":%s PART #test", otherUser); 268 handleClientEvents(); 269 onPart.check(); 270 271 TestEvent!"onPart" onMePart; 272 onMePart.prepare(testUser, "#test"); 273 client.part("#test"); 274 conn.assertLine("PART", "#test"); 275 conn.injectfln(":%s PART #test", testUser); 276 handleClientEvents(); 277 onMePart.check(); 278 279 client.join("#test"); 280 conn.assertLine("JOIN", "#test"); 281 282 TestEvent!"onKick" onKick; 283 onKick.prepare(testUser, "#test", testUser.nickName, "test reason"); 284 client.kick("#test", testUser.nickName, "test reason"); 285 conn.assertLine("KICK", "#test", testUser.nickName, "test reason"); 286 conn.injectfln(":%s KICK #test %s :test reason", testUser, testUser.nickName); 287 handleClientEvents(); 288 onKick.check(); 289 290 auto quittingUser = IrcUser("iquit", "quitter", "quitting.org"); 291 TestEvent!"onQuit" onQuit; 292 onQuit.prepare(quittingUser, "Goodbye!"); 293 conn.injectfln(":%s QUIT :Goodbye!", quittingUser); 294 handleClientEvents(); 295 onQuit.check(); 296 297 // Test line splitting 298 // Max message size in unit test mode: PRIVMSG #test :0123456789ABCDEF 299 void send(string msg) 300 { 301 client.send("#test", msg); 302 } 303 304 void sendFormatted(string msg) 305 { 306 client.sendf("#test", "%s", msg); 307 } 308 309 foreach(sender; TypeTuple!(send, sendFormatted)) 310 { 311 sender("hello world"); 312 conn.assertLine("PRIVMSG", "#test", "hello world"); 313 314 sender("0123456789ABCDEF"); 315 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 316 317 sender("0123456789ABCDEF0123456789ABCDEF"); 318 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 319 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 320 321 sender("0123456789ABCDEF0123456789ABCDEFhello"); 322 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 323 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 324 conn.assertLine("PRIVMSG", "#test", "hello"); 325 326 sender("hello\nworld"); 327 conn.assertLine("PRIVMSG", "#test", "hello"); 328 conn.assertLine("PRIVMSG", "#test", "world"); 329 330 sender("\nhello\r\n\rworld"); 331 conn.assertLine("PRIVMSG", "#test", "hello"); 332 conn.assertLine("PRIVMSG", "#test", "world"); 333 334 sender("hello world\r\n0123456789ABCDEF"); 335 conn.assertLine("PRIVMSG", "#test", "hello world"); 336 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 337 } 338 339 // Test message formatting 340 client.sendf("#test", "%01d %02d %03d", 1, 2, 3); 341 conn.assertLine("PRIVMSG", "#test", "1 02 003"); 342 343 client.sendf("#test", "012%s456789ABCDEF01234567%s9ABCDEF", 3, 8); 344 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 345 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 346 347 client.sendf("#test", "abc%sdef%s%sghi", "\r", "\n", "\r\n"); 348 conn.assertLine("PRIVMSG", "#test", "abc"); 349 conn.assertLine("PRIVMSG", "#test", "def"); 350 conn.assertLine("PRIVMSG", "#test", "ghi"); 351 352 client.sendf("#test", "0123456789ABC%sEF\nhello\n\r\n", "\n"); 353 conn.assertLine("PRIVMSG", "#test", "0123456789ABC"); 354 conn.assertLine("PRIVMSG", "#test", "EF"); 355 conn.assertLine("PRIVMSG", "#test", "hello"); 356 357 // Test range messages 358 client.send("#test", only('h', 'e', 'l', 'l', 'o')); 359 conn.assertLine("PRIVMSG", "#test", "hello"); 360 361 client.send("#test", "日本語"w); 362 conn.assertLine("PRIVMSG", "#test", "日本語"); 363 364 client.send("#test", "日本語"d); 365 conn.assertLine("PRIVMSG", "#test", "日本語"); 366 367 client.send("#test", "0123456789".chain("ABCDEF")); 368 conn.assertLine("PRIVMSG", "#test", "0123456789ABCDEF"); 369 370 client.send("#test", chain("hello", "\r\n", "world")); 371 conn.assertLine("PRIVMSG", "#test", "hello"); 372 conn.assertLine("PRIVMSG", "#test", "world"); 373 374 client.send("#test", "hello".repeat(2).joiner); 375 conn.assertLine("PRIVMSG", "#test", "hellohello"); 376 377 client.quit("test"); 378 conn.assertLine("QUIT", "test"); 379 } 380