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