1 module irc.testing; 2 3 version(dirk_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 static 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 std.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 auto conn = new TestConnection(); 133 auto origin = "testserver"; 134 auto client = conn.client; 135 136 struct TestEvent(string eventName) 137 { 138 import std.traits; 139 alias HandlerType = typeof(mixin("IrcClient." ~ eventName)[0]); 140 alias Args = ParameterTypeTuple!HandlerType; 141 alias Ret = ReturnType!HandlerType; 142 143 Ret delegate(Args) handler; 144 bool prepared = false, ran = false; 145 146 @disable this(this); 147 148 static if(is(Ret == void)) 149 alias ExpectedRet = TypeTuple!(); 150 else 151 alias ExpectedRet = Ret; 152 153 void prepare(ExpectedRet expectedRet, Args expectedArgs) 154 { 155 handler = delegate Ret(Args args) { 156 ran = true; 157 assert(args == expectedArgs); 158 static if (!is(Ret == void)) 159 return expectedRet; 160 }; 161 162 mixin("client." ~ eventName) ~= handler; 163 prepared = true; 164 } 165 166 void check() 167 { 168 assert(prepared); 169 assert(ran); 170 mixin("client." ~ eventName).unsubscribeHandler(handler); 171 } 172 } 173 174 auto socketSet = new SocketSet(1); 175 socketSet.add(conn.clientSocket); 176 void handleClientEvents() 177 { 178 Socket.select(socketSet, null, null); 179 assert(socketSet.isSet(conn.clientSocket)); 180 assert(!client.read()); 181 } 182 183 conn.assertLine("NICK", testUser.nickName); 184 conn.assertLine("USER", testUser.userName, null, null, testRealName); 185 186 { 187 TestEvent!"onNickInUse" onNickInUse; 188 auto newNickName = testUser.nickName ~ "_"; 189 onNickInUse.prepare(newNickName, testUser.nickName); 190 conn.injectfln(":%s 433 %s :Nickname is already in use", origin, testUser.nickName); 191 handleClientEvents(); 192 onNickInUse.check(); 193 conn.assertLine("NICK", newNickName); 194 testUser.nickName = newNickName; 195 } 196 197 TestEvent!"onConnect" onConnect; 198 onConnect.prepare(); 199 conn.injectfln(":%s 001 %s :Welcome to the test server", origin, testUser.nickName); 200 handleClientEvents(); 201 onConnect.check(); 202 203 conn.injectfln(":%s PING :hello world", origin); 204 handleClientEvents(); 205 conn.assertLine("PONG", "hello world"); 206 207 client.join("#test"); 208 conn.assertLine("JOIN", "#test"); 209 210 TestEvent!"onSuccessfulJoin" onSuccessfulJoin; 211 onSuccessfulJoin.prepare("#test"); 212 conn.injectfln(":%s JOIN #test", testUser); 213 handleClientEvents(); 214 onSuccessfulJoin.check(); 215 216 TestEvent!"onNameList" onNameList; 217 onNameList.prepare("#test", ["a", "b", "c"]); 218 conn.injectfln(":%s 353 = #test :a +b @c", origin); 219 handleClientEvents(); 220 onNameList.check(); 221 222 TestEvent!"onNameListEnd" onNameListEnd; 223 onNameListEnd.prepare("#test"); 224 conn.injectfln(":%s 366 #test :End of NAMES list"); 225 handleClientEvents(); 226 onNameListEnd.check(); 227 228 TestEvent!"onMessage" onMessage; 229 onMessage.prepare(IrcUser("nick", "user", null), "#test", "hello world"); 230 conn.injectfln(":nick!user PRIVMSG #test :hello world"); 231 handleClientEvents(); 232 onMessage.check(); 233 234 onMessage = TestEvent!"onMessage"(); 235 onMessage.prepare(IrcUser("nick", "user", "host"), "#test", "hi"); 236 conn.injectfln(":nick!user@host PRIVMSG #test hi"); 237 handleClientEvents(); 238 onMessage.check(); 239 240 TestEvent!"onNotice" onNotice; 241 onNotice.prepare(IrcUser(origin), testUser.nickName, "foo bar"); 242 conn.injectfln(":%s NOTICE %s :foo bar", origin, testUser.nickName); 243 handleClientEvents(); 244 onNotice.check(); 245 246 TestEvent!"onNickChange" onNickChange; 247 onNickChange.prepare(testUser, "newNick"); 248 conn.injectfln(":%s NICK newNick", testUser); 249 testUser.nickName = "newNick"; 250 testUser.nickName = "newNick"; 251 handleClientEvents(); 252 onNickChange.check(); 253 254 auto otherUser = IrcUser("othernick", "other", "other.org"); 255 TestEvent!"onJoin" onJoin; 256 onJoin.prepare(otherUser, "#test"); 257 conn.injectfln(":%s JOIN #test", otherUser); 258 handleClientEvents(); 259 onJoin.check(); 260 261 TestEvent!"onPart" onPart; 262 onPart.prepare(otherUser, "#test"); 263 conn.injectfln(":%s PART #test", otherUser); 264 handleClientEvents(); 265 onPart.check(); 266 267 TestEvent!"onMePart" onMePart; 268 onMePart.prepare("#test"); 269 client.part("#test"); 270 conn.assertLine("PART", "#test"); 271 conn.injectfln(":%s PART #test", testUser); 272 handleClientEvents(); 273 onMePart.check(); 274 275 client.join("#test"); 276 conn.assertLine("JOIN", "#test"); 277 278 TestEvent!"onKick" onKick; 279 onKick.prepare(testUser, "#test", testUser.nickName, "test reason"); 280 client.kick("#test", testUser.nickName, "test reason"); 281 conn.assertLine("KICK", "#test", testUser.nickName, "test reason"); 282 conn.injectfln(":%s KICK #test %s :test reason", testUser, testUser.nickName); 283 handleClientEvents(); 284 onKick.check(); 285 286 auto quittingUser = IrcUser("iquit", "quitter", "quitting.org"); 287 TestEvent!"onQuit" onQuit; 288 onQuit.prepare(quittingUser, "Goodbye!"); 289 conn.injectfln(":%s QUIT :Goodbye!", quittingUser); 290 handleClientEvents(); 291 onQuit.check(); 292 293 client.quit("test"); 294 conn.assertLine("QUIT", "test"); 295 } 296 297 void main() {} // TODO: does VisualD support -main yet?