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?