##// END OF EJS Templates
Improve tests for notebook REST API
Thomas Kluyver -
Show More
@@ -1,113 +1,160 b''
1 1 """Test the notebooks webservice API."""
2 2
3
3 import io
4 4 import os
5 import sys
6 import json
5 import shutil
7 6 from zmq.utils import jsonapi
8 7
8 pjoin = os.path.join
9
9 10 import requests
10 11
11 12 from IPython.html.utils import url_path_join
12 13 from IPython.html.tests.launchnotebook import NotebookTestBase
14 from IPython.nbformat.current import new_notebook, write
15 from IPython.utils.data import uniq_stable
16
17 class NBAPI(object):
18 """Wrapper for notebook API calls."""
19 def __init__(self, base_url):
20 self.base_url = base_url
21
22 @property
23 def nb_url(self):
24 return url_path_join(self.base_url, 'api/notebooks')
25
26 def _req(self, verb, path, body=None):
27 response = requests.request(verb,
28 url_path_join(self.base_url, 'api/notebooks', path), data=body)
29 response.raise_for_status()
30 return response
31
32 def list(self, path='/'):
33 return self._req('GET', path)
34
35 def read(self, name, path='/'):
36 return self._req('GET', url_path_join(path, name))
37
38 def create_untitled(self, path='/'):
39 return self._req('POST', path)
40
41 def delete(self, name, path='/'):
42 return self._req('DELETE', url_path_join(path, name))
43
44 def rename(self, name, path, new_name):
45 body = jsonapi.dumps({'name': new_name})
46 return self._req('PATCH', url_path_join(path, name), body)
13 47
14 48 class APITest(NotebookTestBase):
15 49 """Test the kernels web service API"""
16
17 def notebook_url(self):
18 return url_path_join(super(APITest,self).base_url(), 'api/notebooks')
19
20 def mknb(self, name='', path='/'):
21 url = self.notebook_url() + path
22 return url, requests.post(url)
23
24 def delnb(self, name, path='/'):
25 url = self.notebook_url() + path + name
26 r = requests.delete(url)
27 return r.status_code
28
29 def test_notebook_handler(self):
30 # POST a notebook and test the dict thats returned.
31 #url, nb = self.mknb()
32 url = self.notebook_url()
33 nb = requests.post(url+'/')
34 data = nb.json()
35 status = nb.status_code
36 assert isinstance(data, dict)
37 self.assertIn('name', data)
38 self.assertIn('path', data)
39 self.assertEqual(data['name'], u'Untitled0.ipynb')
40 self.assertEqual(data['path'], u'/')
41
42 # GET list of notebooks in directory.
43 r = requests.get(url)
44 assert isinstance(r.json(), list)
45 assert isinstance(r.json()[0], dict)
46
47 self.delnb('Untitled0.ipynb')
48
49 # GET with a notebook name.
50 url, nb = self.mknb()
51 data = nb.json()
52 url = self.notebook_url() + '/Untitled0.ipynb'
53 r = requests.get(url)
54 assert isinstance(data, dict)
55
56 # PATCH (rename) request.
57 new_name = {'name':'test.ipynb'}
58 r = requests.patch(url, data=jsonapi.dumps(new_name))
59 data = r.json()
60 assert isinstance(data, dict)
61
62 # make sure the patch worked.
63 new_url = self.notebook_url() + '/test.ipynb'
64 r = requests.get(new_url)
65 assert isinstance(r.json(), dict)
66
67 # GET bad (old) notebook name.
68 r = requests.get(url)
69 self.assertEqual(r.status_code, 404)
70
71 # POST notebooks to folders one and two levels down.
72 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
73 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
74 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
75 url, nb = self.mknb(path='/foo/')
76 url2, nb2 = self.mknb(path='/foo/bar/')
77 data = nb.json()
78 data2 = nb2.json()
79 assert isinstance(data, dict)
80 assert isinstance(data2, dict)
81 self.assertIn('name', data)
82 self.assertIn('path', data)
83 self.assertEqual(data['name'], u'Untitled0.ipynb')
84 self.assertEqual(data['path'], u'/foo/')
85 self.assertIn('name', data2)
86 self.assertIn('path', data2)
87 self.assertEqual(data2['name'], u'Untitled0.ipynb')
88 self.assertEqual(data2['path'], u'/foo/bar/')
89
90 # GET request on notebooks one and two levels down.
91 r = requests.get(url+'/Untitled0.ipynb')
92 r2 = requests.get(url2+'/Untitled0.ipynb')
93 assert isinstance(r.json(), dict)
94 assert isinstance(r2.json(), dict)
95
96 # PATCH notebooks that are one and two levels down.
97 new_name = {'name': 'testfoo.ipynb'}
98 r = requests.patch(url+'/Untitled0.ipynb', data=jsonapi.dumps(new_name))
99 r = requests.get(url+'/testfoo.ipynb')
100 data = r.json()
101 assert isinstance(data, dict)
102 self.assertIn('name', data)
103 self.assertEqual(data['name'], 'testfoo.ipynb')
104 r = requests.get(url+'/Untitled0.ipynb')
105 self.assertEqual(r.status_code, 404)
106
107 # DELETE notebooks
108 r0 = self.delnb('test.ipynb')
109 r1 = self.delnb('testfoo.ipynb', '/foo/')
110 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
111 self.assertEqual(r0, 204)
112 self.assertEqual(r1, 204)
113 self.assertEqual(r2, 204)
50 dirs_nbs = [('', 'inroot'),
51 ('Directory with spaces in', 'inspace'),
52 (u'unicodΓ©', 'innonascii'),
53 ('foo', 'a'),
54 ('foo', 'b'),
55 ('foo', 'name with spaces'),
56 ('foo', u'unicodΓ©'),
57 ('foo/bar', 'baz'),
58 ]
59
60 dirs = uniq_stable([d for (d,n) in dirs_nbs])
61 del dirs[0] # remove ''
62
63 def setUp(self):
64 nbdir = self.notebook_dir.name
65 for d in self.dirs:
66 os.mkdir(pjoin(nbdir, d))
67
68 for d, name in self.dirs_nbs:
69 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
70 nb = new_notebook(name=name)
71 write(nb, f, format='ipynb')
72
73 self.nb_api = NBAPI(self.base_url())
74
75 def tearDown(self):
76 nbdir = self.notebook_dir.name
77
78 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©']:
79 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
80
81 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
82 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
83
84 def test_list_notebooks(self):
85 nbs = self.nb_api.list().json()
86 self.assertEqual(len(nbs), 1)
87 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
88
89 nbs = self.nb_api.list('/Directory with spaces in/').json()
90 self.assertEqual(len(nbs), 1)
91 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
92
93 nbs = self.nb_api.list(u'/unicodΓ©/').json()
94 self.assertEqual(len(nbs), 1)
95 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
96
97 nbs = self.nb_api.list('/foo/bar/').json()
98 self.assertEqual(len(nbs), 1)
99 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
100
101 nbs = self.nb_api.list('foo').json()
102 self.assertEqual(len(nbs), 4)
103 nbnames = set(n['name'] for n in nbs)
104 self.assertEqual(nbnames, {'a.ipynb', 'b.ipynb',
105 'name with spaces.ipynb', u'unicodΓ©.ipynb'})
106
107 def test_get_contents(self):
108 for d, name in self.dirs_nbs:
109 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
110 self.assertEqual(nb['name'], '%s.ipynb' % name)
111 self.assertIn('content', nb)
112 self.assertIn('metadata', nb['content'])
113 self.assertIsInstance(nb['content']['metadata'], dict)
114
115 # Name that doesn't exist - should be a 404
116 try:
117 self.nb_api.read('q.ipynb', 'foo')
118 except requests.HTTPError as e:
119 self.assertEqual(e.response.status_code, 404)
120 else:
121 assert False, "Reading a non-existent notebook should fail"
122
123 def _check_nb_created(self, resp, name, path):
124 self.assertEqual(resp.headers['Location'].split('/')[-1], name)
125 self.assertEqual(resp.json()['name'], name)
126 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
127
128 def test_create_untitled(self):
129 resp = self.nb_api.create_untitled(path='foo')
130 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo')
131
132 # Second time
133 resp = self.nb_api.create_untitled(path='foo')
134 self._check_nb_created(resp, 'Untitled1.ipynb', 'foo')
135
136 # And two directories down
137 resp = self.nb_api.create_untitled(path='foo/bar')
138 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
139
140 def test_delete(self):
141 for d, name in self.dirs_nbs:
142 resp = self.nb_api.delete('%s.ipynb' % name, d)
143 self.assertEqual(resp.status_code, 204)
144
145 for d in self.dirs + ['/']:
146 nbs = self.nb_api.list(d).json()
147 self.assertEqual(len(nbs), 0)
148
149 def test_rename(self):
150 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
151 if False:
152 # XXX: Spec says this should be set, but it isn't
153 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
154 self.assertEqual(resp.json()['name'], 'z.ipynb')
155 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
156
157 nbs = self.nb_api.list('foo').json()
158 nbnames = set(n['name'] for n in nbs)
159 self.assertIn('z.ipynb', nbnames)
160 self.assertNotIn('a.ipynb', nbnames)
General Comments 0
You need to be logged in to leave comments. Login now