541822a4cc9c0bf4a228ca1285a8ff453dd93665
[darkhttpd] / devel / test.py
1 #!/usr/bin/env python
2 import unittest
3 import socket
4 import signal
5 import re
6 import os
7 import random
8
9 WWWROOT = "tmp.httpd.tests"
10
11 class Conn:
12 def __init__(self):
13 self.port = 12346
14 self.s = socket.socket()
15 self.s.connect(("0.0.0.0", self.port))
16 # connect throws socket.error on connection refused
17
18 def get(self, url, http_ver="1.0", endl="\n", req_hdrs={}, method="GET"):
19 req = method+" "+url
20 if http_ver is not None:
21 req += " HTTP/"+http_ver
22 req += endl
23 if http_ver is not None:
24 req_hdrs["User-Agent"] = "test.py"
25 req_hdrs["Connection"] = "close"
26 for k,v in req_hdrs.items():
27 req += k+": "+v+endl
28 req += endl # end of request
29 self.s.send(req)
30 ret = ""
31 while True:
32 signal.alarm(1) # don't wait forever
33 r = self.s.recv(65536)
34 signal.alarm(0)
35 if r == "":
36 break
37 else:
38 ret += r
39 return ret
40
41 def parse(resp):
42 """
43 Parse response into status line, headers and body.
44 """
45 pos = resp.index("\r\n\r\n") # throws exception on failure
46 head = resp[:pos]
47 body = resp[pos+4:]
48 status,head = head.split("\r\n", 1)
49 hdrs = {}
50 for line in head.split("\r\n"):
51 k, v = line.split(": ", 1)
52 hdrs[k] = v
53 return (status, hdrs, body)
54
55 class TestHelper(unittest.TestCase):
56 def assertContains(self, body, *strings):
57 for s in strings:
58 self.assertTrue(s in body,
59 msg="expected %s in %s"%(repr(s), repr(body)))
60
61 def assertIsIndex(self, body, path):
62 self.assertContains(body,
63 "<title>%s</title>\n"%path,
64 "<h1>%s</h1>\n"%path,
65 '<a href="..">..</a>/',
66 'Generated by darkhttpd')
67
68 def assertIsInvalid(self, body, path):
69 self.assertContains(body,
70 "<title>400 Bad Request</title>",
71 "<h1>Bad Request</h1>\n",
72 "You requested an invalid URL: %s\n"%path,
73 'Generated by darkhttpd')
74
75 class TestDirList(TestHelper):
76 def setUp(self):
77 self.fn = WWWROOT+"/escape#this"
78 open(self.fn, "w").write("x"*12345)
79
80 def tearDown(self):
81 os.unlink(self.fn)
82
83 def test_dirlist_escape(self):
84 resp = Conn().get("/")
85 status, hdrs, body = parse(resp)
86 self.assertEquals(ord("#"), 0x23)
87 self.assertContains(body, "escape%23this", "12345")
88
89 class TestCases(TestHelper):
90 pass # these get autogenerated in setUpModule()
91
92 def nerf(s):
93 return re.sub("[^a-zA-Z0-9]", "_", s)
94
95 def makeCase(name, url, hdr_checker=None, body_checker=None,
96 req_hdrs={"User-Agent": "test.py"},
97 http_ver=None, endl="\n"):
98 def do_test(self):
99 resp = Conn().get(url, http_ver, endl, req_hdrs)
100 if http_ver is None:
101 status = ""
102 hdrs = {}
103 body = resp
104 else:
105 status, hdrs, body = parse(resp)
106
107 if hdr_checker is not None and http_ver is not None:
108 hdr_checker(self, hdrs)
109
110 if body_checker is not None:
111 body_checker(self, body)
112
113 # FIXME: check status
114 if http_ver is not None:
115 prefix = "HTTP/1.1 " # should 1.0 stay 1.0?
116 self.assertTrue(status.startswith(prefix),
117 msg="%s at start of %s"%(repr(prefix), repr(status)))
118
119 v = http_ver
120 if v is None:
121 v = "0.9"
122 test_name = "_".join([
123 "test",
124 nerf(name),
125 nerf("HTTP"+v),
126 {"\n":"LF", "\r\n":"CRLF"}[endl],
127 ])
128 do_test.__name__ = test_name # hax
129 setattr(TestCases, test_name, do_test)
130
131 def makeCases(name, url, hdr_checker=None, body_checker=None,
132 req_hdrs={"User-Agent": "test.py"}):
133 for http_ver in [None, "1.0", "1.1"]:
134 for endl in ["\n", "\r\n"]:
135 makeCase(name, url, hdr_checker, body_checker,
136 req_hdrs, http_ver, endl)
137
138 def makeSimpleCases(name, url, assert_name):
139 makeCases(name, url, None,
140 lambda self,body: getattr(self, assert_name)(body, url))
141
142 def setUpModule():
143 for args in [
144 ["index", "/", "assertIsIndex"],
145 ["up dir", "/dir/../", "assertIsIndex"],
146 ["extra slashes", "//dir///..////", "assertIsIndex"],
147 ["no trailing slash", "/dir/..", "assertIsIndex"],
148 ["no leading slash", "dir/../", "assertIsInvalid"],
149 ["invalid up dir", "/../", "assertIsInvalid"],
150 ["fancy invalid up dir", "/./dir/./../../", "assertIsInvalid"],
151 ]:
152 makeSimpleCases(*args)
153
154 class TestDirRedirect(TestHelper):
155 def setUp(self):
156 self.url = "/mydir"
157 self.fn = WWWROOT + self.url
158 os.mkdir(self.fn)
159
160 def tearDown(self):
161 os.rmdir(self.fn)
162
163 def test_dir_redirect(self):
164 resp = Conn().get(self.url)
165 status, hdrs, body = parse(resp)
166 self.assertContains(status, "301 Moved Permanently")
167 self.assertEquals(hdrs["Location"], self.url+"/") # trailing slash
168
169 class TestFileGet(TestHelper):
170 def setUp(self):
171 self.datalen = 2345
172 self.data = "".join(
173 [chr(random.randint(0,255)) for _ in xrange(self.datalen)])
174 self.url = "/data.jpeg"
175 self.fn = WWWROOT + self.url
176 open(self.fn, "w").write(self.data)
177
178 def tearDown(self):
179 os.unlink(self.fn)
180
181 def test_file_get(self):
182 resp = Conn().get(self.url)
183 status, hdrs, body = parse(resp)
184 self.assertContains(status, "200 OK")
185 self.assertEquals(hdrs["Accept-Ranges"], "bytes")
186 self.assertEquals(hdrs["Content-Length"], str(self.datalen))
187 self.assertEquals(hdrs["Content-Type"], "image/jpeg")
188 self.assertEquals(body, self.data)
189
190 def test_file_head(self):
191 resp = Conn().get(self.url, method="HEAD")
192 status, hdrs, body = parse(resp)
193 self.assertContains(status, "200 OK")
194 self.assertEquals(hdrs["Accept-Ranges"], "bytes")
195 self.assertEquals(hdrs["Content-Length"], str(self.datalen))
196 self.assertEquals(hdrs["Content-Type"], "image/jpeg")
197
198 def test_if_modified_since(self):
199 resp1 = Conn().get(self.url, method="HEAD")
200 status, hdrs, body = parse(resp1)
201 lastmod = hdrs["Last-Modified"]
202
203 resp2 = Conn().get(self.url, method="GET", req_hdrs =
204 {"If-Modified-Since": lastmod })
205 status, hdrs, body = parse(resp2)
206 self.assertContains(status, "304 Not Modified")
207 self.assertEquals(hdrs["Accept-Ranges"], "bytes")
208 self.assertFalse(hdrs.has_key("Last-Modified"))
209 self.assertFalse(hdrs.has_key("Content-Length"))
210 self.assertFalse(hdrs.has_key("Content-Type"))
211
212 def drive_range(self, range_in, range_out, len_out, data_out,
213 status_out = "206 Partial Content"):
214 resp = Conn().get(self.url, req_hdrs = {"Range": "bytes="+range_in})
215 status, hdrs, body = parse(resp)
216 self.assertContains(status, status_out)
217 self.assertEquals(hdrs["Accept-Ranges"], "bytes")
218 self.assertEquals(hdrs["Content-Range"], "bytes "+range_out)
219 self.assertEquals(hdrs["Content-Length"], str(len_out))
220 self.assertEquals(body, data_out)
221
222 def test_range_single(self):
223 self.drive_range("5-5", "5-5/%d" % self.datalen,
224 1, self.data[5])
225
226 def test_range_reasonable(self):
227 self.drive_range("10-20", "10-20/%d" % self.datalen,
228 20-10+1, self.data[10:20+1])
229
230 def test_range_start_given(self):
231 self.drive_range("10-", "10-%d/%d" % (self.datalen-1, self.datalen),
232 self.datalen-10, self.data[10:])
233
234 def test_range_end_given(self):
235 self.drive_range("-25",
236 "%d-%d/%d"%(self.datalen-25, self.datalen-1, self.datalen),
237 25, self.data[-25:])
238
239 def test_range_beyond_end(self):
240 # expecting same result as test_range_end_given
241 self.drive_range("%d-%d"%(self.datalen-25, self.datalen*2),
242 "%d-%d/%d"%(self.datalen-25, self.datalen-1, self.datalen),
243 25, self.data[-25:])
244
245 def test_range_end_given_oversize(self):
246 # expecting full file
247 self.drive_range("-%d"%(self.datalen*3),
248 "0-%d/%d"%(self.datalen-1, self.datalen),
249 self.datalen, self.data)
250
251 def test_range_bad_start(self):
252 resp = Conn().get(self.url, req_hdrs = {"Range": "bytes=%d-"%(
253 self.datalen*2)})
254 status, hdrs, body = parse(resp)
255 self.assertContains(status, "416 Requested Range Not Satisfiable")
256
257 def test_range_backwards(self):
258 resp = Conn().get(self.url, req_hdrs = {"Range": "bytes=20-10"})
259 status, hdrs, body = parse(resp)
260 self.assertContains(status, "416 Requested Range Not Satisfiable")
261
262 if __name__ == '__main__':
263 setUpModule()
264 unittest.main()
265
266 # vim:set ts=4 sw=4 et: