##// END OF EJS Templates
TEST: Add test for handling of last_modified....
Scott Sanderson -
Show More
@@ -1,497 +1,521 b''
1 1 # coding: utf-8
2 2 """Tests for the notebook manager."""
3 3 from __future__ import print_function
4 4
5 5 import os
6 6 import sys
7 7 import time
8 8 from contextlib import contextmanager
9 9
10 10 from nose import SkipTest
11 11 from tornado.web import HTTPError
12 12 from unittest import TestCase
13 13 from tempfile import NamedTemporaryFile
14 14
15 15 from IPython.nbformat import v4 as nbformat
16 16
17 17 from IPython.utils.tempdir import TemporaryDirectory
18 18 from IPython.utils.traitlets import TraitError
19 19 from IPython.testing import decorators as dec
20 20
21 21 from ..filemanager import FileContentsManager
22 22
23 23
24 24 def _make_dir(contents_manager, api_path):
25 25 """
26 26 Make a directory.
27 27 """
28 28 os_path = contents_manager._get_os_path(api_path)
29 29 try:
30 30 os.makedirs(os_path)
31 31 except OSError:
32 32 print("Directory already exists: %r" % os_path)
33 33
34 34
35 35 class TestFileContentsManager(TestCase):
36 36
37 37 @contextmanager
38 38 def assertRaisesHTTPError(self, status, msg=None):
39 39 msg = msg or "Should have raised HTTPError(%i)" % status
40 40 try:
41 41 yield
42 42 except HTTPError as e:
43 43 self.assertEqual(e.status_code, status)
44 44 else:
45 45 self.fail(msg)
46 46
47 47 def symlink(self, contents_manager, src, dst):
48 48 """Make a symlink to src from dst
49 49
50 50 src and dst are api_paths
51 51 """
52 52 src_os_path = contents_manager._get_os_path(src)
53 53 dst_os_path = contents_manager._get_os_path(dst)
54 54 print(src_os_path, dst_os_path, os.path.isfile(src_os_path))
55 55 os.symlink(src_os_path, dst_os_path)
56 56
57 57 def test_root_dir(self):
58 58 with TemporaryDirectory() as td:
59 59 fm = FileContentsManager(root_dir=td)
60 60 self.assertEqual(fm.root_dir, td)
61 61
62 62 def test_missing_root_dir(self):
63 63 with TemporaryDirectory() as td:
64 64 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
65 65 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
66 66
67 67 def test_invalid_root_dir(self):
68 68 with NamedTemporaryFile() as tf:
69 69 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
70 70
71 71 def test_get_os_path(self):
72 72 # full filesystem path should be returned with correct operating system
73 73 # separators.
74 74 with TemporaryDirectory() as td:
75 75 root = td
76 76 fm = FileContentsManager(root_dir=root)
77 77 path = fm._get_os_path('/path/to/notebook/test.ipynb')
78 78 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
79 79 fs_path = os.path.join(fm.root_dir, *rel_path_list)
80 80 self.assertEqual(path, fs_path)
81 81
82 82 fm = FileContentsManager(root_dir=root)
83 83 path = fm._get_os_path('test.ipynb')
84 84 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
85 85 self.assertEqual(path, fs_path)
86 86
87 87 fm = FileContentsManager(root_dir=root)
88 88 path = fm._get_os_path('////test.ipynb')
89 89 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
90 90 self.assertEqual(path, fs_path)
91 91
92 92 def test_checkpoint_subdir(self):
93 93 subd = u'sub βˆ‚ir'
94 94 cp_name = 'test-cp.ipynb'
95 95 with TemporaryDirectory() as td:
96 96 root = td
97 97 os.mkdir(os.path.join(td, subd))
98 98 fm = FileContentsManager(root_dir=root)
99 99 cpm = fm.checkpoints
100 100 cp_dir = cpm.checkpoint_path(
101 101 'cp', 'test.ipynb'
102 102 )
103 103 cp_subdir = cpm.checkpoint_path(
104 104 'cp', '/%s/test.ipynb' % subd
105 105 )
106 106 self.assertNotEqual(cp_dir, cp_subdir)
107 107 self.assertEqual(cp_dir, os.path.join(root, cpm.checkpoint_dir, cp_name))
108 108 self.assertEqual(cp_subdir, os.path.join(root, subd, cpm.checkpoint_dir, cp_name))
109 109
110 110 @dec.skip_win32
111 111 def test_bad_symlink(self):
112 112 with TemporaryDirectory() as td:
113 113 cm = FileContentsManager(root_dir=td)
114 114 path = 'test bad symlink'
115 115 _make_dir(cm, path)
116 116
117 117 file_model = cm.new_untitled(path=path, ext='.txt')
118 118
119 119 # create a broken symlink
120 120 self.symlink(cm, "target", '%s/%s' % (path, 'bad symlink'))
121 121 model = cm.get(path)
122 122 self.assertEqual(model['content'], [file_model])
123 123
124 124 @dec.skip_win32
125 125 def test_good_symlink(self):
126 126 with TemporaryDirectory() as td:
127 127 cm = FileContentsManager(root_dir=td)
128 128 parent = 'test good symlink'
129 129 name = 'good symlink'
130 130 path = '{0}/{1}'.format(parent, name)
131 131 _make_dir(cm, parent)
132 132
133 133 file_model = cm.new(path=parent + '/zfoo.txt')
134 134
135 135 # create a good symlink
136 136 self.symlink(cm, file_model['path'], path)
137 137 symlink_model = cm.get(path, content=False)
138 138 dir_model = cm.get(parent)
139 139 self.assertEqual(
140 140 sorted(dir_model['content'], key=lambda x: x['name']),
141 141 [symlink_model, file_model],
142 142 )
143 143
144 144 def test_403(self):
145 145 if hasattr(os, 'getuid'):
146 146 if os.getuid() == 0:
147 147 raise SkipTest("Can't test permissions as root")
148 148 if sys.platform.startswith('win'):
149 149 raise SkipTest("Can't test permissions on Windows")
150 150
151 151 with TemporaryDirectory() as td:
152 152 cm = FileContentsManager(root_dir=td)
153 153 model = cm.new_untitled(type='file')
154 154 os_path = cm._get_os_path(model['path'])
155 155
156 156 os.chmod(os_path, 0o400)
157 157 try:
158 158 with cm.open(os_path, 'w') as f:
159 159 f.write(u"don't care")
160 160 except HTTPError as e:
161 161 self.assertEqual(e.status_code, 403)
162 162 else:
163 163 self.fail("Should have raised HTTPError(403)")
164 164
165 165 def test_escape_root(self):
166 166 with TemporaryDirectory() as td:
167 167 cm = FileContentsManager(root_dir=td)
168 168 # make foo, bar next to root
169 169 with open(os.path.join(cm.root_dir, '..', 'foo'), 'w') as f:
170 170 f.write('foo')
171 171 with open(os.path.join(cm.root_dir, '..', 'bar'), 'w') as f:
172 172 f.write('bar')
173 173
174 174 with self.assertRaisesHTTPError(404):
175 175 cm.get('..')
176 176 with self.assertRaisesHTTPError(404):
177 177 cm.get('foo/../../../bar')
178 178 with self.assertRaisesHTTPError(404):
179 179 cm.delete('../foo')
180 180 with self.assertRaisesHTTPError(404):
181 181 cm.rename('../foo', '../bar')
182 182 with self.assertRaisesHTTPError(404):
183 183 cm.save(model={
184 184 'type': 'file',
185 185 'content': u'',
186 186 'format': 'text',
187 187 }, path='../foo')
188 188
189 189
190 190 class TestContentsManager(TestCase):
191 191
192 192 def setUp(self):
193 193 self._temp_dir = TemporaryDirectory()
194 194 self.td = self._temp_dir.name
195 195 self.contents_manager = FileContentsManager(
196 196 root_dir=self.td,
197 197 )
198 198
199 199 def tearDown(self):
200 200 self._temp_dir.cleanup()
201 201
202 202 def make_dir(self, api_path):
203 203 """make a subdirectory at api_path
204 204
205 205 override in subclasses if contents are not on the filesystem.
206 206 """
207 207 _make_dir(self.contents_manager, api_path)
208 208
209 209 def add_code_cell(self, nb):
210 210 output = nbformat.new_output("display_data", {'application/javascript': "alert('hi');"})
211 211 cell = nbformat.new_code_cell("print('hi')", outputs=[output])
212 212 nb.cells.append(cell)
213 213
214 214 def new_notebook(self):
215 215 cm = self.contents_manager
216 216 model = cm.new_untitled(type='notebook')
217 217 name = model['name']
218 218 path = model['path']
219 219
220 220 full_model = cm.get(path)
221 221 nb = full_model['content']
222 222 nb['metadata']['counter'] = int(1e6 * time.time())
223 223 self.add_code_cell(nb)
224 224
225 225 cm.save(full_model, path)
226 226 return nb, name, path
227 227
228 228 def test_new_untitled(self):
229 229 cm = self.contents_manager
230 230 # Test in root directory
231 231 model = cm.new_untitled(type='notebook')
232 232 assert isinstance(model, dict)
233 233 self.assertIn('name', model)
234 234 self.assertIn('path', model)
235 235 self.assertIn('type', model)
236 236 self.assertEqual(model['type'], 'notebook')
237 237 self.assertEqual(model['name'], 'Untitled.ipynb')
238 238 self.assertEqual(model['path'], 'Untitled.ipynb')
239 239
240 240 # Test in sub-directory
241 241 model = cm.new_untitled(type='directory')
242 242 assert isinstance(model, dict)
243 243 self.assertIn('name', model)
244 244 self.assertIn('path', model)
245 245 self.assertIn('type', model)
246 246 self.assertEqual(model['type'], 'directory')
247 247 self.assertEqual(model['name'], 'Untitled Folder')
248 248 self.assertEqual(model['path'], 'Untitled Folder')
249 249 sub_dir = model['path']
250 250
251 251 model = cm.new_untitled(path=sub_dir)
252 252 assert isinstance(model, dict)
253 253 self.assertIn('name', model)
254 254 self.assertIn('path', model)
255 255 self.assertIn('type', model)
256 256 self.assertEqual(model['type'], 'file')
257 257 self.assertEqual(model['name'], 'untitled')
258 258 self.assertEqual(model['path'], '%s/untitled' % sub_dir)
259 259
260 def test_modified_date(self):
261
262 cm = self.contents_manager
263
264 # Create a new notebook.
265 nb, name, path = self.new_notebook()
266 model = cm.get(path)
267
268 # Add a cell and save.
269 self.add_code_cell(model['content'])
270 cm.save(model, path)
271
272 # Reload notebook and verify that last_modified incremented.
273 saved = cm.get(path)
274 self.assertGreater(saved['last_modified'], model['last_modified'])
275
276 # Move the notebook and verify that last_modified stayed the same.
277 # (The frontend fires a warning if last_modified increases on the
278 # renamed file.)
279 new_path = 'renamed.ipynb'
280 cm.rename(path, new_path)
281 renamed = cm.get(new_path)
282 self.assertEqual(renamed['last_modified'], saved['last_modified'])
283
260 284 def test_get(self):
261 285 cm = self.contents_manager
262 286 # Create a notebook
263 287 model = cm.new_untitled(type='notebook')
264 288 name = model['name']
265 289 path = model['path']
266 290
267 291 # Check that we 'get' on the notebook we just created
268 292 model2 = cm.get(path)
269 293 assert isinstance(model2, dict)
270 294 self.assertIn('name', model2)
271 295 self.assertIn('path', model2)
272 296 self.assertEqual(model['name'], name)
273 297 self.assertEqual(model['path'], path)
274 298
275 299 nb_as_file = cm.get(path, content=True, type='file')
276 300 self.assertEqual(nb_as_file['path'], path)
277 301 self.assertEqual(nb_as_file['type'], 'file')
278 302 self.assertEqual(nb_as_file['format'], 'text')
279 303 self.assertNotIsInstance(nb_as_file['content'], dict)
280 304
281 305 nb_as_bin_file = cm.get(path, content=True, type='file', format='base64')
282 306 self.assertEqual(nb_as_bin_file['format'], 'base64')
283 307
284 308 # Test in sub-directory
285 309 sub_dir = '/foo/'
286 310 self.make_dir('foo')
287 311 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
288 312 model2 = cm.get(sub_dir + name)
289 313 assert isinstance(model2, dict)
290 314 self.assertIn('name', model2)
291 315 self.assertIn('path', model2)
292 316 self.assertIn('content', model2)
293 317 self.assertEqual(model2['name'], 'Untitled.ipynb')
294 318 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
295 319
296 320 # Test with a regular file.
297 321 file_model_path = cm.new_untitled(path=sub_dir, ext='.txt')['path']
298 322 file_model = cm.get(file_model_path)
299 323 self.assertDictContainsSubset(
300 324 {
301 325 'content': u'',
302 326 'format': u'text',
303 327 'mimetype': u'text/plain',
304 328 'name': u'untitled.txt',
305 329 'path': u'foo/untitled.txt',
306 330 'type': u'file',
307 331 'writable': True,
308 332 },
309 333 file_model,
310 334 )
311 335 self.assertIn('created', file_model)
312 336 self.assertIn('last_modified', file_model)
313 337
314 338 # Test getting directory model
315 339
316 340 # Create a sub-sub directory to test getting directory contents with a
317 341 # subdir.
318 342 self.make_dir('foo/bar')
319 343 dirmodel = cm.get('foo')
320 344 self.assertEqual(dirmodel['type'], 'directory')
321 345 self.assertIsInstance(dirmodel['content'], list)
322 346 self.assertEqual(len(dirmodel['content']), 3)
323 347 self.assertEqual(dirmodel['path'], 'foo')
324 348 self.assertEqual(dirmodel['name'], 'foo')
325 349
326 350 # Directory contents should match the contents of each individual entry
327 351 # when requested with content=False.
328 352 model2_no_content = cm.get(sub_dir + name, content=False)
329 353 file_model_no_content = cm.get(u'foo/untitled.txt', content=False)
330 354 sub_sub_dir_no_content = cm.get('foo/bar', content=False)
331 355 self.assertEqual(sub_sub_dir_no_content['path'], 'foo/bar')
332 356 self.assertEqual(sub_sub_dir_no_content['name'], 'bar')
333 357
334 358 for entry in dirmodel['content']:
335 359 # Order isn't guaranteed by the spec, so this is a hacky way of
336 360 # verifying that all entries are matched.
337 361 if entry['path'] == sub_sub_dir_no_content['path']:
338 362 self.assertEqual(entry, sub_sub_dir_no_content)
339 363 elif entry['path'] == model2_no_content['path']:
340 364 self.assertEqual(entry, model2_no_content)
341 365 elif entry['path'] == file_model_no_content['path']:
342 366 self.assertEqual(entry, file_model_no_content)
343 367 else:
344 368 self.fail("Unexpected directory entry: %s" % entry())
345 369
346 370 with self.assertRaises(HTTPError):
347 371 cm.get('foo', type='file')
348 372
349 373 def test_update(self):
350 374 cm = self.contents_manager
351 375 # Create a notebook
352 376 model = cm.new_untitled(type='notebook')
353 377 name = model['name']
354 378 path = model['path']
355 379
356 380 # Change the name in the model for rename
357 381 model['path'] = 'test.ipynb'
358 382 model = cm.update(model, path)
359 383 assert isinstance(model, dict)
360 384 self.assertIn('name', model)
361 385 self.assertIn('path', model)
362 386 self.assertEqual(model['name'], 'test.ipynb')
363 387
364 388 # Make sure the old name is gone
365 389 self.assertRaises(HTTPError, cm.get, path)
366 390
367 391 # Test in sub-directory
368 392 # Create a directory and notebook in that directory
369 393 sub_dir = '/foo/'
370 394 self.make_dir('foo')
371 395 model = cm.new_untitled(path=sub_dir, type='notebook')
372 396 path = model['path']
373 397
374 398 # Change the name in the model for rename
375 399 d = path.rsplit('/', 1)[0]
376 400 new_path = model['path'] = d + '/test_in_sub.ipynb'
377 401 model = cm.update(model, path)
378 402 assert isinstance(model, dict)
379 403 self.assertIn('name', model)
380 404 self.assertIn('path', model)
381 405 self.assertEqual(model['name'], 'test_in_sub.ipynb')
382 406 self.assertEqual(model['path'], new_path)
383 407
384 408 # Make sure the old name is gone
385 409 self.assertRaises(HTTPError, cm.get, path)
386 410
387 411 def test_save(self):
388 412 cm = self.contents_manager
389 413 # Create a notebook
390 414 model = cm.new_untitled(type='notebook')
391 415 name = model['name']
392 416 path = model['path']
393 417
394 418 # Get the model with 'content'
395 419 full_model = cm.get(path)
396 420
397 421 # Save the notebook
398 422 model = cm.save(full_model, path)
399 423 assert isinstance(model, dict)
400 424 self.assertIn('name', model)
401 425 self.assertIn('path', model)
402 426 self.assertEqual(model['name'], name)
403 427 self.assertEqual(model['path'], path)
404 428
405 429 # Test in sub-directory
406 430 # Create a directory and notebook in that directory
407 431 sub_dir = '/foo/'
408 432 self.make_dir('foo')
409 433 model = cm.new_untitled(path=sub_dir, type='notebook')
410 434 name = model['name']
411 435 path = model['path']
412 436 model = cm.get(path)
413 437
414 438 # Change the name in the model for rename
415 439 model = cm.save(model, path)
416 440 assert isinstance(model, dict)
417 441 self.assertIn('name', model)
418 442 self.assertIn('path', model)
419 443 self.assertEqual(model['name'], 'Untitled.ipynb')
420 444 self.assertEqual(model['path'], 'foo/Untitled.ipynb')
421 445
422 446 def test_delete(self):
423 447 cm = self.contents_manager
424 448 # Create a notebook
425 449 nb, name, path = self.new_notebook()
426 450
427 451 # Delete the notebook
428 452 cm.delete(path)
429 453
430 454 # Check that deleting a non-existent path raises an error.
431 455 self.assertRaises(HTTPError, cm.delete, path)
432 456
433 457 # Check that a 'get' on the deleted notebook raises and error
434 458 self.assertRaises(HTTPError, cm.get, path)
435 459
436 460 def test_copy(self):
437 461 cm = self.contents_manager
438 462 parent = u'Γ₯ b'
439 463 name = u'nb √.ipynb'
440 464 path = u'{0}/{1}'.format(parent, name)
441 465 self.make_dir(parent)
442 466
443 467 orig = cm.new(path=path)
444 468 # copy with unspecified name
445 469 copy = cm.copy(path)
446 470 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
447 471
448 472 # copy with specified name
449 473 copy2 = cm.copy(path, u'Γ₯ b/copy 2.ipynb')
450 474 self.assertEqual(copy2['name'], u'copy 2.ipynb')
451 475 self.assertEqual(copy2['path'], u'Γ₯ b/copy 2.ipynb')
452 476 # copy with specified path
453 477 copy2 = cm.copy(path, u'/')
454 478 self.assertEqual(copy2['name'], name)
455 479 self.assertEqual(copy2['path'], name)
456 480
457 481 def test_trust_notebook(self):
458 482 cm = self.contents_manager
459 483 nb, name, path = self.new_notebook()
460 484
461 485 untrusted = cm.get(path)['content']
462 486 assert not cm.notary.check_cells(untrusted)
463 487
464 488 # print(untrusted)
465 489 cm.trust_notebook(path)
466 490 trusted = cm.get(path)['content']
467 491 # print(trusted)
468 492 assert cm.notary.check_cells(trusted)
469 493
470 494 def test_mark_trusted_cells(self):
471 495 cm = self.contents_manager
472 496 nb, name, path = self.new_notebook()
473 497
474 498 cm.mark_trusted_cells(nb, path)
475 499 for cell in nb.cells:
476 500 if cell.cell_type == 'code':
477 501 assert not cell.metadata.trusted
478 502
479 503 cm.trust_notebook(path)
480 504 nb = cm.get(path)['content']
481 505 for cell in nb.cells:
482 506 if cell.cell_type == 'code':
483 507 assert cell.metadata.trusted
484 508
485 509 def test_check_and_sign(self):
486 510 cm = self.contents_manager
487 511 nb, name, path = self.new_notebook()
488 512
489 513 cm.mark_trusted_cells(nb, path)
490 514 cm.check_and_sign(nb, path)
491 515 assert not cm.notary.check_signature(nb)
492 516
493 517 cm.trust_notebook(path)
494 518 nb = cm.get(path)['content']
495 519 cm.mark_trusted_cells(nb, path)
496 520 cm.check_and_sign(nb, path)
497 521 assert cm.notary.check_signature(nb)
General Comments 0
You need to be logged in to leave comments. Login now