Merge lp:~wgrant/loggerhead/bug-929275 into lp:loggerhead
- bug-929275
- Merge into trunk-rich
Proposed by
William Grant
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 468 | ||||
Proposed branch: | lp:~wgrant/loggerhead/bug-929275 | ||||
Merge into: | lp:loggerhead | ||||
Diff against target: |
308 lines (+97/-56) 4 files modified
NEWS (+3/-2) loggerhead/controllers/download_ui.py (+8/-2) loggerhead/controllers/view_ui.py (+19/-13) loggerhead/tests/test_controllers.py (+67/-39) |
||||
To merge this branch: | bzr merge lp:~wgrant/loggerhead/bug-929275 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Steve Kowalik (community) | code | Approve | |
Review via email: mp+92194@code.launchpad.net |
Commit message
Description of the change
Fix more non-existent fileid or revid crashes.
To post a comment you must log in.
lp:~wgrant/loggerhead/bug-929275
updated
- 469. By William Grant
-
blah
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'NEWS' | |||
2 | --- NEWS 2012-02-03 01:29:46 +0000 | |||
3 | +++ NEWS 2012-02-09 03:39:18 +0000 | |||
4 | @@ -55,8 +55,9 @@ | |||
5 | 55 | - Make tz calculations consistent and use UTC in the UI everywhere we show | 55 | - Make tz calculations consistent and use UTC in the UI everywhere we show |
6 | 56 | a precise timestamp. (Robert Collins, #594591) | 56 | a precise timestamp. (Robert Collins, #594591) |
7 | 57 | 57 | ||
10 | 58 | - Avoid crashing when viewing or annotating a non-existent file. | 58 | - Avoid crashing when viewing, annotating or downloading a |
11 | 59 | (William Grant, #728209) | 59 | non-existent file or revision. |
12 | 60 | (William Grant, #728209, #929275) | ||
13 | 60 | 61 | ||
14 | 61 | 62 | ||
15 | 62 | 1.18.1 [24Mar2011] | 63 | 1.18.1 [24Mar2011] |
16 | 63 | 64 | ||
17 | === modified file 'loggerhead/controllers/download_ui.py' | |||
18 | --- loggerhead/controllers/download_ui.py 2011-11-25 01:41:07 +0000 | |||
19 | +++ loggerhead/controllers/download_ui.py 2012-02-09 03:39:18 +0000 | |||
20 | @@ -19,9 +19,12 @@ | |||
21 | 19 | 19 | ||
22 | 20 | import logging | 20 | import logging |
23 | 21 | import mimetypes | 21 | import mimetypes |
24 | 22 | import os | ||
25 | 23 | import urllib | 22 | import urllib |
26 | 24 | 23 | ||
27 | 24 | from bzrlib.errors import ( | ||
28 | 25 | NoSuchId, | ||
29 | 26 | NoSuchRevision, | ||
30 | 27 | ) | ||
31 | 25 | from paste import httpexceptions | 28 | from paste import httpexceptions |
32 | 26 | from paste.request import path_info_pop | 29 | from paste.request import path_info_pop |
33 | 27 | 30 | ||
34 | @@ -55,7 +58,10 @@ | |||
35 | 55 | self._branch.absolute_url('/changes')) | 58 | self._branch.absolute_url('/changes')) |
36 | 56 | revid = h.fix_revid(args[0]) | 59 | revid = h.fix_revid(args[0]) |
37 | 57 | file_id = args[1] | 60 | file_id = args[1] |
39 | 58 | path, filename, content = h.get_file(file_id, revid) | 61 | try: |
40 | 62 | path, filename, content = h.get_file(file_id, revid) | ||
41 | 63 | except (NoSuchId, NoSuchRevision): | ||
42 | 64 | raise httpexceptions.HTTPNotFound() | ||
43 | 59 | mime_type, encoding = mimetypes.guess_type(filename) | 65 | mime_type, encoding = mimetypes.guess_type(filename) |
44 | 60 | if mime_type is None: | 66 | if mime_type is None: |
45 | 61 | mime_type = 'application/octet-stream' | 67 | mime_type = 'application/octet-stream' |
46 | 62 | 68 | ||
47 | === modified file 'loggerhead/controllers/view_ui.py' | |||
48 | --- loggerhead/controllers/view_ui.py 2012-02-03 01:25:58 +0000 | |||
49 | +++ loggerhead/controllers/view_ui.py 2012-02-09 03:39:18 +0000 | |||
50 | @@ -18,9 +18,12 @@ | |||
51 | 18 | # | 18 | # |
52 | 19 | 19 | ||
53 | 20 | import os | 20 | import os |
54 | 21 | import time | ||
55 | 22 | 21 | ||
57 | 23 | import bzrlib.errors | 22 | from bzrlib.errors import ( |
58 | 23 | BinaryFile, | ||
59 | 24 | NoSuchId, | ||
60 | 25 | NoSuchRevision, | ||
61 | 26 | ) | ||
62 | 24 | import bzrlib.textfile | 27 | import bzrlib.textfile |
63 | 25 | import bzrlib.osutils | 28 | import bzrlib.osutils |
64 | 26 | 29 | ||
65 | @@ -76,7 +79,7 @@ | |||
66 | 76 | def file_contents(self, file_id, revid): | 79 | def file_contents(self, file_id, revid): |
67 | 77 | try: | 80 | try: |
68 | 78 | file_lines = self.text_lines(file_id, revid) | 81 | file_lines = self.text_lines(file_id, revid) |
70 | 79 | except bzrlib.errors.BinaryFile: | 82 | except BinaryFile: |
71 | 80 | # bail out; this isn't displayable text | 83 | # bail out; this isn't displayable text |
72 | 81 | return ['(This is a binary file.)'] | 84 | return ['(This is a binary file.)'] |
73 | 82 | 85 | ||
74 | @@ -92,17 +95,17 @@ | |||
75 | 92 | raise HTTPBadRequest('No file_id or filename ' | 95 | raise HTTPBadRequest('No file_id or filename ' |
76 | 93 | 'provided to view') | 96 | 'provided to view') |
77 | 94 | 97 | ||
86 | 95 | if file_id is None: | 98 | try: |
87 | 96 | file_id = history.get_file_id(revid, path) | 99 | if file_id is None: |
88 | 97 | 100 | file_id = history.get_file_id(revid, path) | |
89 | 98 | # no navbar for revisions | 101 | if path is None: |
90 | 99 | navigation = util.Container() | 102 | path = history.get_path(revid, file_id) |
91 | 100 | 103 | except (NoSuchId, NoSuchRevision): | |
92 | 101 | if path is None: | 104 | raise HTTPNotFound() |
93 | 102 | path = history.get_path(revid, file_id) | 105 | |
94 | 103 | filename = os.path.basename(path) | 106 | filename = os.path.basename(path) |
95 | 104 | 107 | ||
97 | 105 | change = history.get_changes([ revid ])[0] | 108 | change = history.get_changes([revid])[0] |
98 | 106 | # If we're looking at the tip, use head: in the URL instead | 109 | # If we're looking at the tip, use head: in the URL instead |
99 | 107 | if revid == branch.last_revision(): | 110 | if revid == branch.last_revision(): |
100 | 108 | revno_url = 'head:' | 111 | revno_url = 'head:' |
101 | @@ -126,12 +129,15 @@ | |||
102 | 126 | 129 | ||
103 | 127 | try: | 130 | try: |
104 | 128 | file = inv[file_id] | 131 | file = inv[file_id] |
106 | 129 | except bzrlib.errors.NoSuchId: | 132 | except NoSuchId: |
107 | 130 | raise HTTPNotFound() | 133 | raise HTTPNotFound() |
108 | 131 | 134 | ||
109 | 132 | if file.kind == "directory": | 135 | if file.kind == "directory": |
110 | 133 | raise HTTPMovedPermanently(self._branch.context_url(['/files', revno_url, path])) | 136 | raise HTTPMovedPermanently(self._branch.context_url(['/files', revno_url, path])) |
111 | 134 | 137 | ||
112 | 138 | # no navbar for revisions | ||
113 | 139 | navigation = util.Container() | ||
114 | 140 | |||
115 | 135 | return { | 141 | return { |
116 | 136 | # In AnnotateUI, "annotated" is a dictionary mapping lines to changes. | 142 | # In AnnotateUI, "annotated" is a dictionary mapping lines to changes. |
117 | 137 | # We exploit the fact that bool({}) is False when checking whether | 143 | # We exploit the fact that bool({}) is False when checking whether |
118 | 138 | 144 | ||
119 | === modified file 'loggerhead/tests/test_controllers.py' | |||
120 | --- loggerhead/tests/test_controllers.py 2012-02-03 01:25:58 +0000 | |||
121 | +++ loggerhead/tests/test_controllers.py 2012-02-09 03:39:18 +0000 | |||
122 | @@ -14,27 +14,19 @@ | |||
123 | 14 | # along with this program; if not, write to the Free Software | 14 | # along with this program; if not, write to the Free Software |
124 | 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
125 | 16 | 16 | ||
126 | 17 | from cStringIO import StringIO | ||
127 | 18 | import logging | ||
128 | 19 | import tarfile | 17 | import tarfile |
129 | 20 | import tempfile | 18 | import tempfile |
130 | 21 | 19 | ||
131 | 22 | from paste.fixture import ( | 20 | from paste.fixture import ( |
132 | 23 | AppError, | 21 | AppError, |
133 | 24 | ) | 22 | ) |
138 | 25 | from paste.httpexceptions import ( | 23 | from paste.httpexceptions import HTTPNotFound |
135 | 26 | HTTPNotFound, | ||
136 | 27 | HTTPServerError, | ||
137 | 28 | ) | ||
139 | 29 | 24 | ||
140 | 30 | from testtools.matchers import ( | 25 | from testtools.matchers import ( |
141 | 31 | Matcher, | 26 | Matcher, |
142 | 32 | Mismatch, | 27 | Mismatch, |
143 | 33 | ) | 28 | ) |
144 | 34 | 29 | ||
145 | 35 | from bzrlib import errors | ||
146 | 36 | import simplejson | ||
147 | 37 | |||
148 | 38 | from loggerhead.apps.branch import BranchWSGIApp | 30 | from loggerhead.apps.branch import BranchWSGIApp |
149 | 39 | from loggerhead.controllers.annotate_ui import AnnotateUI | 31 | from loggerhead.controllers.annotate_ui import AnnotateUI |
150 | 40 | from loggerhead.controllers.inventory_ui import InventoryUI | 32 | from loggerhead.controllers.inventory_ui import InventoryUI |
151 | @@ -159,7 +151,6 @@ | |||
152 | 159 | self.assertOkJsonResponse(revision_ui, env) | 151 | self.assertOkJsonResponse(revision_ui, env) |
153 | 160 | 152 | ||
154 | 161 | 153 | ||
155 | 162 | |||
156 | 163 | class TestAnnotateUI(BasicTests): | 154 | class TestAnnotateUI(BasicTests): |
157 | 164 | 155 | ||
158 | 165 | def make_annotate_ui_for_file_history(self, file_id, rev_ids_texts): | 156 | def make_annotate_ui_for_file_history(self, file_id, rev_ids_texts): |
159 | @@ -192,12 +183,13 @@ | |||
160 | 192 | history = [('rev1', 'old\nold\n', '.'), ('rev2', 'new\nold\n', '')] | 183 | history = [('rev1', 'old\nold\n', '.'), ('rev2', 'new\nold\n', '')] |
161 | 193 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) | 184 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) |
162 | 194 | ann_ui.args = ['rev2'] | 185 | ann_ui.args = ['rev2'] |
165 | 195 | annotate_info = ann_ui.get_values('filename', | 186 | ann_ui.get_values( |
166 | 196 | kwargs={'file_id': 'file_id'}, headers={}) | 187 | 'filename', kwargs={'file_id': 'file_id'}, headers={}) |
167 | 197 | 188 | ||
168 | 198 | def test_annotate_file_zero_sized(self): | 189 | def test_annotate_file_zero_sized(self): |
171 | 199 | # Test against a zero-sized file without breaking. No annotation must be present. | 190 | # Test against a zero-sized file without breaking. No annotation |
172 | 200 | history = [('rev1', '' , '.')] | 191 | # must be present. |
173 | 192 | history = [('rev1', '', '.')] | ||
174 | 201 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) | 193 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) |
175 | 202 | ann_ui.args = ['rev1'] | 194 | ann_ui.args = ['rev1'] |
176 | 203 | annotate_info = ann_ui.get_values('filename', | 195 | annotate_info = ann_ui.get_values('filename', |
177 | @@ -206,12 +198,19 @@ | |||
178 | 206 | self.assertEqual(0, len(annotated)) | 198 | self.assertEqual(0, len(annotated)) |
179 | 207 | 199 | ||
180 | 208 | def test_annotate_nonexistent_file(self): | 200 | def test_annotate_nonexistent_file(self): |
182 | 209 | history = [('rev1', '' , '.')] | 201 | history = [('rev1', '', '.')] |
183 | 210 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) | 202 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) |
184 | 211 | ann_ui.args = ['rev1'] | 203 | ann_ui.args = ['rev1'] |
185 | 212 | self.assertRaises( | 204 | self.assertRaises( |
186 | 213 | HTTPNotFound, ann_ui.get_values, 'not-filename', {}, {}) | 205 | HTTPNotFound, ann_ui.get_values, 'not-filename', {}, {}) |
187 | 214 | 206 | ||
188 | 207 | def test_annotate_nonexistent_rev(self): | ||
189 | 208 | history = [('rev1', '', '.')] | ||
190 | 209 | ann_ui = self.make_annotate_ui_for_file_history('file_id', history) | ||
191 | 210 | ann_ui.args = ['norev'] | ||
192 | 211 | self.assertRaises( | ||
193 | 212 | HTTPNotFound, ann_ui.get_values, 'not-filename', {}, {}) | ||
194 | 213 | |||
195 | 215 | 214 | ||
196 | 216 | class TestFileDiffUI(BasicTests): | 215 | class TestFileDiffUI(BasicTests): |
197 | 217 | 216 | ||
198 | @@ -308,6 +307,55 @@ | |||
199 | 308 | self.assertEquals("I am hooked", app.lookup_app(env)) | 307 | self.assertEquals("I am hooked", app.lookup_app(env)) |
200 | 309 | 308 | ||
201 | 310 | 309 | ||
202 | 310 | class MatchesDownloadHeaders(Matcher): | ||
203 | 311 | |||
204 | 312 | def __init__(self, expect_filename, expect_mimetype): | ||
205 | 313 | self.expect_filename = expect_filename | ||
206 | 314 | self.expect_mimetype = expect_mimetype | ||
207 | 315 | |||
208 | 316 | def match(self, response): | ||
209 | 317 | # Maybe the c-t should be more specific, but this is probably good for | ||
210 | 318 | # making sure it gets saved without the client trying to decompress it | ||
211 | 319 | # or anything. | ||
212 | 320 | if (response.header('Content-Type') == self.expect_mimetype | ||
213 | 321 | and response.header('Content-Disposition') == | ||
214 | 322 | "attachment; filename*=utf-8''" + self.expect_filename): | ||
215 | 323 | pass | ||
216 | 324 | else: | ||
217 | 325 | return Mismatch("wrong response headers: %r" | ||
218 | 326 | % response.headers) | ||
219 | 327 | |||
220 | 328 | def __str__(self): | ||
221 | 329 | return 'MatchesDownloadHeaders(%r, %r)' % ( | ||
222 | 330 | self.expect_filename, self.expect_mimetype) | ||
223 | 331 | |||
224 | 332 | |||
225 | 333 | class TestDownloadUI(TestWithSimpleTree): | ||
226 | 334 | |||
227 | 335 | def test_download(self): | ||
228 | 336 | app = self.setUpLoggerhead() | ||
229 | 337 | response = app.get('/download/1/myfilename-id/myfilename') | ||
230 | 338 | self.assertEqual( | ||
231 | 339 | 'some\nmultiline\ndata\nwith<htmlspecialchars\n', response.body) | ||
232 | 340 | self.assertThat( | ||
233 | 341 | response, | ||
234 | 342 | MatchesDownloadHeaders('myfilename', 'application/octet-stream')) | ||
235 | 343 | |||
236 | 344 | def test_download_bad_revision(self): | ||
237 | 345 | app = self.setUpLoggerhead() | ||
238 | 346 | e = self.assertRaises( | ||
239 | 347 | AppError, | ||
240 | 348 | app.get, '/download/norev/myfilename-id/myfilename') | ||
241 | 349 | self.assertContainsRe(str(e), '404 Not Found') | ||
242 | 350 | |||
243 | 351 | def test_download_bad_fileid(self): | ||
244 | 352 | app = self.setUpLoggerhead() | ||
245 | 353 | e = self.assertRaises( | ||
246 | 354 | AppError, | ||
247 | 355 | app.get, '/download/1/myfilename-notid/myfilename') | ||
248 | 356 | self.assertContainsRe(str(e), '404 Not Found') | ||
249 | 357 | |||
250 | 358 | |||
251 | 311 | class IsTarfile(Matcher): | 359 | class IsTarfile(Matcher): |
252 | 312 | 360 | ||
253 | 313 | def __init__(self, compression): | 361 | def __init__(self, compression): |
254 | @@ -323,29 +371,8 @@ | |||
255 | 323 | f.close() | 371 | f.close() |
256 | 324 | 372 | ||
257 | 325 | 373 | ||
258 | 326 | class MatchesTarballHeaders(Matcher): | ||
259 | 327 | |||
260 | 328 | def __init__(self, expect_filename): | ||
261 | 329 | self.expect_filename = expect_filename | ||
262 | 330 | |||
263 | 331 | def match(self, response): | ||
264 | 332 | # Maybe the c-t should be more specific, but this is probably good for | ||
265 | 333 | # making sure it gets saved without the client trying to decompress it | ||
266 | 334 | # or anything. | ||
267 | 335 | if (response.header('Content-Type') == 'application/octet-stream' | ||
268 | 336 | and response.header('Content-Disposition') == | ||
269 | 337 | "attachment; filename*=utf-8''" + self.expect_filename): | ||
270 | 338 | pass | ||
271 | 339 | else: | ||
272 | 340 | return Mismatch("wrong response headers: %r" | ||
273 | 341 | % response.headers) | ||
274 | 342 | |||
275 | 343 | |||
276 | 344 | class TestDownloadTarballUI(TestWithSimpleTree): | 374 | class TestDownloadTarballUI(TestWithSimpleTree): |
277 | 345 | 375 | ||
278 | 346 | def setUp(self): | ||
279 | 347 | super(TestDownloadTarballUI, self).setUp() | ||
280 | 348 | |||
281 | 349 | def test_download_tarball(self): | 376 | def test_download_tarball(self): |
282 | 350 | # Tarball downloads are enabled by default. | 377 | # Tarball downloads are enabled by default. |
283 | 351 | app = self.setUpLoggerhead() | 378 | app = self.setUpLoggerhead() |
284 | @@ -355,7 +382,7 @@ | |||
285 | 355 | IsTarfile('gz')) | 382 | IsTarfile('gz')) |
286 | 356 | self.assertThat( | 383 | self.assertThat( |
287 | 357 | response, | 384 | response, |
289 | 358 | MatchesTarballHeaders('branch.tgz')) | 385 | MatchesDownloadHeaders('branch.tgz', 'application/octet-stream')) |
290 | 359 | 386 | ||
291 | 360 | def test_download_tarball_of_version(self): | 387 | def test_download_tarball_of_version(self): |
292 | 361 | app = self.setUpLoggerhead() | 388 | app = self.setUpLoggerhead() |
293 | @@ -365,12 +392,13 @@ | |||
294 | 365 | IsTarfile('gz')) | 392 | IsTarfile('gz')) |
295 | 366 | self.assertThat( | 393 | self.assertThat( |
296 | 367 | response, | 394 | response, |
298 | 368 | MatchesTarballHeaders('branch-r1.tgz')) | 395 | MatchesDownloadHeaders( |
299 | 396 | 'branch-r1.tgz', 'application/octet-stream')) | ||
300 | 369 | 397 | ||
301 | 370 | def test_download_tarball_forbidden(self): | 398 | def test_download_tarball_forbidden(self): |
302 | 371 | app = self.setUpLoggerhead(export_tarballs=False) | 399 | app = self.setUpLoggerhead(export_tarballs=False) |
303 | 372 | e = self.assertRaises( | 400 | e = self.assertRaises( |
305 | 373 | AppError, | 401 | AppError, |
306 | 374 | app.get, | 402 | app.get, |
307 | 375 | '/tarball') | 403 | '/tarball') |
308 | 376 | self.assertContainsRe( | 404 | self.assertContainsRe( |
This looks like excellent work. And thanks for fixing the import I mentioned on IRC.