##// END OF EJS Templates
TEST: Allow greater or equal modified_date on rename.
Scott Sanderson -
Show More
@@ -1,521 +1,524
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 260 def test_modified_date(self):
261 261
262 262 cm = self.contents_manager
263 263
264 264 # Create a new notebook.
265 265 nb, name, path = self.new_notebook()
266 266 model = cm.get(path)
267 267
268 268 # Add a cell and save.
269 269 self.add_code_cell(model['content'])
270 270 cm.save(model, path)
271 271
272 272 # Reload notebook and verify that last_modified incremented.
273 273 saved = cm.get(path)
274 274 self.assertGreater(saved['last_modified'], model['last_modified'])
275 275
276 276 # Move the notebook and verify that last_modified stayed the same.
277 277 # (The frontend fires a warning if last_modified increases on the
278 278 # renamed file.)
279 279 new_path = 'renamed.ipynb'
280 280 cm.rename(path, new_path)
281 281 renamed = cm.get(new_path)
282 self.assertEqual(renamed['last_modified'], saved['last_modified'])
282 self.assertGreaterEqual(
283 renamed['last_modified'],
284 saved['last_modified'],
285 )
283 286
284 287 def test_get(self):
285 288 cm = self.contents_manager
286 289 # Create a notebook
287 290 model = cm.new_untitled(type='notebook')
288 291 name = model['name']
289 292 path = model['path']
290 293
291 294 # Check that we 'get' on the notebook we just created
292 295 model2 = cm.get(path)
293 296 assert isinstance(model2, dict)
294 297 self.assertIn('name', model2)
295 298 self.assertIn('path', model2)
296 299 self.assertEqual(model['name'], name)
297 300 self.assertEqual(model['path'], path)
298 301
299 302 nb_as_file = cm.get(path, content=True, type='file')
300 303 self.assertEqual(nb_as_file['path'], path)
301 304 self.assertEqual(nb_as_file['type'], 'file')
302 305 self.assertEqual(nb_as_file['format'], 'text')
303 306 self.assertNotIsInstance(nb_as_file['content'], dict)
304 307
305 308 nb_as_bin_file = cm.get(path, content=True, type='file', format='base64')
306 309 self.assertEqual(nb_as_bin_file['format'], 'base64')
307 310
308 311 # Test in sub-directory
309 312 sub_dir = '/foo/'
310 313 self.make_dir('foo')
311 314 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
312 315 model2 = cm.get(sub_dir + name)
313 316 assert isinstance(model2, dict)
314 317 self.assertIn('name', model2)
315 318 self.assertIn('path', model2)
316 319 self.assertIn('content', model2)
317 320 self.assertEqual(model2['name'], 'Untitled.ipynb')
318 321 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
319 322
320 323 # Test with a regular file.
321 324 file_model_path = cm.new_untitled(path=sub_dir, ext='.txt')['path']
322 325 file_model = cm.get(file_model_path)
323 326 self.assertDictContainsSubset(
324 327 {
325 328 'content': u'',
326 329 'format': u'text',
327 330 'mimetype': u'text/plain',
328 331 'name': u'untitled.txt',
329 332 'path': u'foo/untitled.txt',
330 333 'type': u'file',
331 334 'writable': True,
332 335 },
333 336 file_model,
334 337 )
335 338 self.assertIn('created', file_model)
336 339 self.assertIn('last_modified', file_model)
337 340
338 341 # Test getting directory model
339 342
340 343 # Create a sub-sub directory to test getting directory contents with a
341 344 # subdir.
342 345 self.make_dir('foo/bar')
343 346 dirmodel = cm.get('foo')
344 347 self.assertEqual(dirmodel['type'], 'directory')
345 348 self.assertIsInstance(dirmodel['content'], list)
346 349 self.assertEqual(len(dirmodel['content']), 3)
347 350 self.assertEqual(dirmodel['path'], 'foo')
348 351 self.assertEqual(dirmodel['name'], 'foo')
349 352
350 353 # Directory contents should match the contents of each individual entry
351 354 # when requested with content=False.
352 355 model2_no_content = cm.get(sub_dir + name, content=False)
353 356 file_model_no_content = cm.get(u'foo/untitled.txt', content=False)
354 357 sub_sub_dir_no_content = cm.get('foo/bar', content=False)
355 358 self.assertEqual(sub_sub_dir_no_content['path'], 'foo/bar')
356 359 self.assertEqual(sub_sub_dir_no_content['name'], 'bar')
357 360
358 361 for entry in dirmodel['content']:
359 362 # Order isn't guaranteed by the spec, so this is a hacky way of
360 363 # verifying that all entries are matched.
361 364 if entry['path'] == sub_sub_dir_no_content['path']:
362 365 self.assertEqual(entry, sub_sub_dir_no_content)
363 366 elif entry['path'] == model2_no_content['path']:
364 367 self.assertEqual(entry, model2_no_content)
365 368 elif entry['path'] == file_model_no_content['path']:
366 369 self.assertEqual(entry, file_model_no_content)
367 370 else:
368 371 self.fail("Unexpected directory entry: %s" % entry())
369 372
370 373 with self.assertRaises(HTTPError):
371 374 cm.get('foo', type='file')
372 375
373 376 def test_update(self):
374 377 cm = self.contents_manager
375 378 # Create a notebook
376 379 model = cm.new_untitled(type='notebook')
377 380 name = model['name']
378 381 path = model['path']
379 382
380 383 # Change the name in the model for rename
381 384 model['path'] = 'test.ipynb'
382 385 model = cm.update(model, path)
383 386 assert isinstance(model, dict)
384 387 self.assertIn('name', model)
385 388 self.assertIn('path', model)
386 389 self.assertEqual(model['name'], 'test.ipynb')
387 390
388 391 # Make sure the old name is gone
389 392 self.assertRaises(HTTPError, cm.get, path)
390 393
391 394 # Test in sub-directory
392 395 # Create a directory and notebook in that directory
393 396 sub_dir = '/foo/'
394 397 self.make_dir('foo')
395 398 model = cm.new_untitled(path=sub_dir, type='notebook')
396 399 path = model['path']
397 400
398 401 # Change the name in the model for rename
399 402 d = path.rsplit('/', 1)[0]
400 403 new_path = model['path'] = d + '/test_in_sub.ipynb'
401 404 model = cm.update(model, path)
402 405 assert isinstance(model, dict)
403 406 self.assertIn('name', model)
404 407 self.assertIn('path', model)
405 408 self.assertEqual(model['name'], 'test_in_sub.ipynb')
406 409 self.assertEqual(model['path'], new_path)
407 410
408 411 # Make sure the old name is gone
409 412 self.assertRaises(HTTPError, cm.get, path)
410 413
411 414 def test_save(self):
412 415 cm = self.contents_manager
413 416 # Create a notebook
414 417 model = cm.new_untitled(type='notebook')
415 418 name = model['name']
416 419 path = model['path']
417 420
418 421 # Get the model with 'content'
419 422 full_model = cm.get(path)
420 423
421 424 # Save the notebook
422 425 model = cm.save(full_model, path)
423 426 assert isinstance(model, dict)
424 427 self.assertIn('name', model)
425 428 self.assertIn('path', model)
426 429 self.assertEqual(model['name'], name)
427 430 self.assertEqual(model['path'], path)
428 431
429 432 # Test in sub-directory
430 433 # Create a directory and notebook in that directory
431 434 sub_dir = '/foo/'
432 435 self.make_dir('foo')
433 436 model = cm.new_untitled(path=sub_dir, type='notebook')
434 437 name = model['name']
435 438 path = model['path']
436 439 model = cm.get(path)
437 440
438 441 # Change the name in the model for rename
439 442 model = cm.save(model, path)
440 443 assert isinstance(model, dict)
441 444 self.assertIn('name', model)
442 445 self.assertIn('path', model)
443 446 self.assertEqual(model['name'], 'Untitled.ipynb')
444 447 self.assertEqual(model['path'], 'foo/Untitled.ipynb')
445 448
446 449 def test_delete(self):
447 450 cm = self.contents_manager
448 451 # Create a notebook
449 452 nb, name, path = self.new_notebook()
450 453
451 454 # Delete the notebook
452 455 cm.delete(path)
453 456
454 457 # Check that deleting a non-existent path raises an error.
455 458 self.assertRaises(HTTPError, cm.delete, path)
456 459
457 460 # Check that a 'get' on the deleted notebook raises and error
458 461 self.assertRaises(HTTPError, cm.get, path)
459 462
460 463 def test_copy(self):
461 464 cm = self.contents_manager
462 465 parent = u'Γ₯ b'
463 466 name = u'nb √.ipynb'
464 467 path = u'{0}/{1}'.format(parent, name)
465 468 self.make_dir(parent)
466 469
467 470 orig = cm.new(path=path)
468 471 # copy with unspecified name
469 472 copy = cm.copy(path)
470 473 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb'))
471 474
472 475 # copy with specified name
473 476 copy2 = cm.copy(path, u'Γ₯ b/copy 2.ipynb')
474 477 self.assertEqual(copy2['name'], u'copy 2.ipynb')
475 478 self.assertEqual(copy2['path'], u'Γ₯ b/copy 2.ipynb')
476 479 # copy with specified path
477 480 copy2 = cm.copy(path, u'/')
478 481 self.assertEqual(copy2['name'], name)
479 482 self.assertEqual(copy2['path'], name)
480 483
481 484 def test_trust_notebook(self):
482 485 cm = self.contents_manager
483 486 nb, name, path = self.new_notebook()
484 487
485 488 untrusted = cm.get(path)['content']
486 489 assert not cm.notary.check_cells(untrusted)
487 490
488 491 # print(untrusted)
489 492 cm.trust_notebook(path)
490 493 trusted = cm.get(path)['content']
491 494 # print(trusted)
492 495 assert cm.notary.check_cells(trusted)
493 496
494 497 def test_mark_trusted_cells(self):
495 498 cm = self.contents_manager
496 499 nb, name, path = self.new_notebook()
497 500
498 501 cm.mark_trusted_cells(nb, path)
499 502 for cell in nb.cells:
500 503 if cell.cell_type == 'code':
501 504 assert not cell.metadata.trusted
502 505
503 506 cm.trust_notebook(path)
504 507 nb = cm.get(path)['content']
505 508 for cell in nb.cells:
506 509 if cell.cell_type == 'code':
507 510 assert cell.metadata.trusted
508 511
509 512 def test_check_and_sign(self):
510 513 cm = self.contents_manager
511 514 nb, name, path = self.new_notebook()
512 515
513 516 cm.mark_trusted_cells(nb, path)
514 517 cm.check_and_sign(nb, path)
515 518 assert not cm.notary.check_signature(nb)
516 519
517 520 cm.trust_notebook(path)
518 521 nb = cm.get(path)['content']
519 522 cm.mark_trusted_cells(nb, path)
520 523 cm.check_and_sign(nb, path)
521 524 assert cm.notary.check_signature(nb)
General Comments 0
You need to be logged in to leave comments. Login now