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