index.spec.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. import { Contents, ContentsManager, Drive, ServerConnection } from '../../src';
  4. import { expectFailure, JupyterServer } from '@jupyterlab/testutils';
  5. import { DEFAULT_FILE, makeSettings, handleRequest } from '../utils';
  6. const DEFAULT_DIR: Contents.IModel = {
  7. name: 'bar',
  8. path: 'foo/bar',
  9. type: 'directory',
  10. created: 'yesterday',
  11. last_modified: 'today',
  12. writable: false,
  13. mimetype: '',
  14. content: [
  15. { name: 'buzz.txt', path: 'foo/bar/buzz.txt' },
  16. { name: 'bazz.py', path: 'foo/bar/bazz.py' }
  17. ],
  18. format: 'json'
  19. };
  20. const DEFAULT_CP: Contents.ICheckpointModel = {
  21. id: '1234',
  22. last_modified: 'yesterday'
  23. };
  24. const server = new JupyterServer();
  25. beforeAll(async () => {
  26. await server.start();
  27. });
  28. afterAll(async () => {
  29. await server.shutdown();
  30. });
  31. describe('contents', () => {
  32. let contents: ContentsManager;
  33. let serverSettings: ServerConnection.ISettings;
  34. beforeEach(() => {
  35. serverSettings = makeSettings();
  36. contents = new ContentsManager({ serverSettings });
  37. });
  38. afterEach(() => {
  39. contents.dispose();
  40. });
  41. describe('#constructor()', () => {
  42. it('should accept no options', () => {
  43. const contents = new ContentsManager();
  44. expect(contents).toBeInstanceOf(ContentsManager);
  45. });
  46. it('should accept options', () => {
  47. const contents = new ContentsManager({
  48. defaultDrive: new Drive()
  49. });
  50. expect(contents).toBeInstanceOf(ContentsManager);
  51. });
  52. });
  53. describe('#fileChanged', () => {
  54. it('should be emitted when a file changes', async () => {
  55. handleRequest(contents, 201, DEFAULT_FILE);
  56. let called = false;
  57. contents.fileChanged.connect((sender, args) => {
  58. expect(sender).toBe(contents);
  59. expect(args.type).toBe('new');
  60. expect(args.oldValue).toBeNull();
  61. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  62. called = true;
  63. });
  64. await contents.newUntitled();
  65. expect(called).toBe(true);
  66. });
  67. it('should include the full path for additional drives', async () => {
  68. const drive = new Drive({ name: 'other', serverSettings });
  69. contents.addDrive(drive);
  70. handleRequest(drive, 201, DEFAULT_FILE);
  71. let called = false;
  72. contents.fileChanged.connect((sender, args) => {
  73. expect(args.newValue!.path).toBe('other:' + DEFAULT_FILE.path);
  74. called = true;
  75. });
  76. await contents.newUntitled({ path: 'other:' });
  77. expect(called).toBe(true);
  78. });
  79. });
  80. describe('#isDisposed', () => {
  81. it('should test whether the manager is disposed', () => {
  82. expect(contents.isDisposed).toBe(false);
  83. contents.dispose();
  84. expect(contents.isDisposed).toBe(true);
  85. });
  86. });
  87. describe('#dispose()', () => {
  88. it('should dispose of the resources used by the manager', () => {
  89. expect(contents.isDisposed).toBe(false);
  90. contents.dispose();
  91. expect(contents.isDisposed).toBe(true);
  92. contents.dispose();
  93. expect(contents.isDisposed).toBe(true);
  94. });
  95. });
  96. describe('#addDrive()', () => {
  97. it('should add a new drive to the manager', () => {
  98. contents.addDrive(new Drive({ name: 'other' }));
  99. handleRequest(contents, 200, DEFAULT_FILE);
  100. return contents.get('other:');
  101. });
  102. });
  103. describe('#localPath()', () => {
  104. it('should parse the local part of a path', () => {
  105. contents.addDrive(new Drive({ name: 'other' }));
  106. contents.addDrive(new Drive({ name: 'alternative' }));
  107. expect(contents.localPath('other:foo/bar/example.txt')).toBe(
  108. 'foo/bar/example.txt'
  109. );
  110. expect(contents.localPath('alternative:/foo/bar/example.txt')).toBe(
  111. 'foo/bar/example.txt'
  112. );
  113. });
  114. it('should allow the ":" character in other parts of the path', () => {
  115. contents.addDrive(new Drive({ name: 'other' }));
  116. expect(
  117. contents.localPath('other:foo/odd:directory/example:file.txt')
  118. ).toBe('foo/odd:directory/example:file.txt');
  119. });
  120. it('should leave alone names with ":" that are not drive names', () => {
  121. contents.addDrive(new Drive({ name: 'other' }));
  122. expect(
  123. contents.localPath('which:foo/odd:directory/example:file.txt')
  124. ).toBe('which:foo/odd:directory/example:file.txt');
  125. });
  126. });
  127. describe('.driveName()', () => {
  128. it('should parse the drive name a path', () => {
  129. contents.addDrive(new Drive({ name: 'other' }));
  130. contents.addDrive(new Drive({ name: 'alternative' }));
  131. expect(contents.driveName('other:foo/bar/example.txt')).toBe('other');
  132. expect(contents.driveName('alternative:/foo/bar/example.txt')).toBe(
  133. 'alternative'
  134. );
  135. });
  136. it('should allow the ":" character in other parts of the path', () => {
  137. contents.addDrive(new Drive({ name: 'other' }));
  138. expect(
  139. contents.driveName('other:foo/odd:directory/example:file.txt')
  140. ).toBe('other');
  141. });
  142. it('should leave alone names with ":" that are not drive names', () => {
  143. contents.addDrive(new Drive({ name: 'other' }));
  144. expect(
  145. contents.driveName('which:foo/odd:directory/example:file.txt')
  146. ).toBe('');
  147. });
  148. });
  149. describe('#get()', () => {
  150. it('should get a file', async () => {
  151. handleRequest(contents, 200, DEFAULT_FILE);
  152. const options: Contents.IFetchOptions = { type: 'file' };
  153. const model = await contents.get('/foo', options);
  154. expect(model.path).toBe('foo');
  155. });
  156. it('should get a directory', async () => {
  157. handleRequest(contents, 200, DEFAULT_DIR);
  158. const options: Contents.IFetchOptions = { type: 'directory' };
  159. const model = await contents.get('/foo', options);
  160. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  161. });
  162. it('should get a file from an additional drive', async () => {
  163. const drive = new Drive({ name: 'other', serverSettings });
  164. contents.addDrive(drive);
  165. handleRequest(drive, 200, DEFAULT_FILE);
  166. const options: Contents.IFetchOptions = { type: 'file' };
  167. const model = await contents.get('other:/foo', options);
  168. expect(model.path).toBe('other:foo');
  169. });
  170. it('should get a directory from an additional drive', async () => {
  171. const drive = new Drive({ name: 'other', serverSettings });
  172. contents.addDrive(drive);
  173. handleRequest(drive, 200, DEFAULT_DIR);
  174. const options: Contents.IFetchOptions = { type: 'directory' };
  175. const model = await contents.get('other:/foo', options);
  176. expect(model.content[0].path).toBe('other:foo/bar/buzz.txt');
  177. });
  178. it('should fail for an incorrect response', async () => {
  179. handleRequest(contents, 201, DEFAULT_DIR);
  180. const get = contents.get('/foo');
  181. await expectFailure(get, 'Invalid response: 201 Created');
  182. });
  183. });
  184. describe('#getDownloadUrl()', () => {
  185. const settings = ServerConnection.makeSettings({
  186. baseUrl: 'http://foo'
  187. });
  188. it('should get the url of a file', async () => {
  189. const drive = new Drive({ serverSettings: settings });
  190. const contents = new ContentsManager({ defaultDrive: drive });
  191. const test1 = contents.getDownloadUrl('bar.txt');
  192. const test2 = contents.getDownloadUrl('fizz/buzz/bar.txt');
  193. const test3 = contents.getDownloadUrl('/bar.txt');
  194. const urls = await Promise.all([test1, test2, test3]);
  195. expect(urls[0]).toBe('http://foo/files/bar.txt');
  196. expect(urls[1]).toBe('http://foo/files/fizz/buzz/bar.txt');
  197. expect(urls[2]).toBe('http://foo/files/bar.txt');
  198. });
  199. it('should encode characters', async () => {
  200. const drive = new Drive({ serverSettings: settings });
  201. const contents = new ContentsManager({ defaultDrive: drive });
  202. const url = await contents.getDownloadUrl('b ar?3.txt');
  203. expect(url).toBe('http://foo/files/b%20ar%3F3.txt');
  204. });
  205. it('should not handle relative paths', async () => {
  206. const drive = new Drive({ serverSettings: settings });
  207. const contents = new ContentsManager({ defaultDrive: drive });
  208. const url = await contents.getDownloadUrl('fizz/../bar.txt');
  209. expect(url).toBe('http://foo/files/fizz/../bar.txt');
  210. });
  211. it('should get the url of a file from an additional drive', async () => {
  212. const contents = new ContentsManager();
  213. const other = new Drive({ name: 'other', serverSettings: settings });
  214. contents.addDrive(other);
  215. const test1 = contents.getDownloadUrl('other:bar.txt');
  216. const test2 = contents.getDownloadUrl('other:fizz/buzz/bar.txt');
  217. const test3 = contents.getDownloadUrl('other:/bar.txt');
  218. const urls = await Promise.all([test1, test2, test3]);
  219. expect(urls[0]).toBe('http://foo/files/bar.txt');
  220. expect(urls[1]).toBe('http://foo/files/fizz/buzz/bar.txt');
  221. expect(urls[2]).toBe('http://foo/files/bar.txt');
  222. });
  223. });
  224. describe('#newUntitled()', () => {
  225. it('should create a file', async () => {
  226. handleRequest(contents, 201, DEFAULT_FILE);
  227. const model = await contents.newUntitled({ path: '/foo' });
  228. expect(model.path).toBe('foo/test');
  229. });
  230. it('should create a directory', async () => {
  231. handleRequest(contents, 201, DEFAULT_DIR);
  232. const options: Contents.ICreateOptions = {
  233. path: '/foo',
  234. type: 'directory'
  235. };
  236. const model = await contents.newUntitled(options);
  237. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  238. });
  239. it('should create a file on an additional drive', async () => {
  240. const other = new Drive({ name: 'other', serverSettings });
  241. contents.addDrive(other);
  242. handleRequest(other, 201, DEFAULT_FILE);
  243. const model = await contents.newUntitled({ path: 'other:/foo' });
  244. expect(model.path).toBe('other:foo/test');
  245. });
  246. it('should create a directory on an additional drive', async () => {
  247. const other = new Drive({ name: 'other', serverSettings });
  248. contents.addDrive(other);
  249. handleRequest(other, 201, DEFAULT_DIR);
  250. const options: Contents.ICreateOptions = {
  251. path: 'other:/foo',
  252. type: 'directory'
  253. };
  254. const model = await contents.newUntitled(options);
  255. expect(model.path).toBe('other:' + DEFAULT_DIR.path);
  256. });
  257. it('should emit the fileChanged signal', async () => {
  258. handleRequest(contents, 201, DEFAULT_FILE);
  259. let called = false;
  260. contents.fileChanged.connect((sender, args) => {
  261. expect(args.type).toBe('new');
  262. expect(args.oldValue).toBeNull();
  263. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  264. called = true;
  265. });
  266. await contents.newUntitled({ type: 'file', ext: 'test' });
  267. expect(called).toBe(true);
  268. });
  269. it('should fail for an incorrect model', async () => {
  270. const dir = JSON.parse(JSON.stringify(DEFAULT_DIR));
  271. dir.name = 1;
  272. handleRequest(contents, 201, dir);
  273. const options: Contents.ICreateOptions = {
  274. path: '/foo',
  275. type: 'file',
  276. ext: 'py'
  277. };
  278. const newFile = contents.newUntitled(options);
  279. await expectFailure(newFile);
  280. });
  281. it('should fail for an incorrect response', async () => {
  282. handleRequest(contents, 200, DEFAULT_DIR);
  283. const newDir = contents.newUntitled();
  284. await expectFailure(newDir, 'Invalid response: 200 OK');
  285. });
  286. });
  287. describe('#delete()', () => {
  288. it('should delete a file', () => {
  289. handleRequest(contents, 204, {});
  290. return contents.delete('/foo/bar.txt');
  291. });
  292. it('should delete a file on an additional drive', () => {
  293. const other = new Drive({ name: 'other', serverSettings });
  294. contents.addDrive(other);
  295. handleRequest(other, 204, {});
  296. return contents.delete('other:/foo/bar.txt');
  297. });
  298. it('should emit the fileChanged signal', async () => {
  299. const path = '/foo/bar.txt';
  300. handleRequest(contents, 204, { path });
  301. let called = false;
  302. contents.fileChanged.connect((sender, args) => {
  303. expect(args.type).toBe('delete');
  304. expect(args.oldValue!.path).toBe('foo/bar.txt');
  305. called = true;
  306. });
  307. await contents.delete(path);
  308. expect(called).toBe(true);
  309. });
  310. it('should fail for an incorrect response', async () => {
  311. handleRequest(contents, 200, {});
  312. const del = contents.delete('/foo/bar.txt');
  313. await expectFailure(del, 'Invalid response: 200 OK');
  314. });
  315. it('should throw a specific error', async () => {
  316. handleRequest(contents, 400, {});
  317. const del = contents.delete('/foo/');
  318. await expectFailure(del, '');
  319. });
  320. it('should throw a general error', async () => {
  321. handleRequest(contents, 500, {});
  322. const del = contents.delete('/foo/');
  323. await expectFailure(del, '');
  324. });
  325. });
  326. describe('#rename()', () => {
  327. it('should rename a file', async () => {
  328. handleRequest(contents, 200, DEFAULT_FILE);
  329. const rename = contents.rename('/foo/bar.txt', '/foo/baz.txt');
  330. const model = await rename;
  331. expect(model.created).toBe(DEFAULT_FILE.created);
  332. });
  333. it('should rename a file on an additional drive', async () => {
  334. const other = new Drive({ name: 'other', serverSettings });
  335. contents.addDrive(other);
  336. handleRequest(other, 200, DEFAULT_FILE);
  337. const rename = contents.rename(
  338. 'other:/foo/bar.txt',
  339. 'other:/foo/baz.txt'
  340. );
  341. const model = await rename;
  342. expect(model.created).toBe(DEFAULT_FILE.created);
  343. });
  344. it('should emit the fileChanged signal', async () => {
  345. handleRequest(contents, 200, DEFAULT_FILE);
  346. let called = false;
  347. contents.fileChanged.connect((sender, args) => {
  348. expect(args.type).toBe('rename');
  349. expect(args.oldValue!.path).toBe('foo/bar.txt');
  350. expect(args.newValue!.path).toBe('foo/test');
  351. called = true;
  352. });
  353. await contents.rename('/foo/bar.txt', '/foo/baz.txt');
  354. expect(called).toBe(true);
  355. });
  356. it('should fail for an incorrect model', async () => {
  357. const dir = JSON.parse(JSON.stringify(DEFAULT_FILE));
  358. delete dir.path;
  359. handleRequest(contents, 200, dir);
  360. const rename = contents.rename('/foo/bar.txt', '/foo/baz.txt');
  361. await expectFailure(rename);
  362. });
  363. it('should fail for an incorrect response', async () => {
  364. handleRequest(contents, 201, DEFAULT_FILE);
  365. const rename = contents.rename('/foo/bar.txt', '/foo/baz.txt');
  366. await expectFailure(rename, 'Invalid response: 201 Created');
  367. });
  368. });
  369. describe('#save()', () => {
  370. it('should save a file', async () => {
  371. handleRequest(contents, 200, DEFAULT_FILE);
  372. const save = contents.save('/foo', { type: 'file', name: 'test' });
  373. const model = await save;
  374. expect(model.created).toBe(DEFAULT_FILE.created);
  375. });
  376. it('should save a file on an additional drive', async () => {
  377. const other = new Drive({ name: 'other', serverSettings });
  378. contents.addDrive(other);
  379. handleRequest(contents, 200, DEFAULT_FILE);
  380. const save = contents.save('other:/foo', { type: 'file', name: 'test' });
  381. const model = await save;
  382. expect(model.path).toBe('other:foo');
  383. });
  384. it('should create a new file', async () => {
  385. handleRequest(contents, 201, DEFAULT_FILE);
  386. const save = contents.save('/foo', { type: 'file', name: 'test' });
  387. const model = await save;
  388. expect(model.created).toBe(DEFAULT_FILE.created);
  389. });
  390. it('should emit the fileChanged signal', async () => {
  391. handleRequest(contents, 201, DEFAULT_FILE);
  392. let called = false;
  393. contents.fileChanged.connect((sender, args) => {
  394. expect(args.type).toBe('save');
  395. expect(args.oldValue).toBeNull();
  396. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  397. called = true;
  398. });
  399. await contents.save('/foo', { type: 'file', name: 'test' });
  400. expect(called).toBe(true);
  401. });
  402. it('should fail for an incorrect model', async () => {
  403. const file = JSON.parse(JSON.stringify(DEFAULT_FILE));
  404. delete file.format;
  405. handleRequest(contents, 200, file);
  406. const save = contents.save('/foo', { type: 'file', name: 'test' });
  407. await expectFailure(save);
  408. });
  409. it('should fail for an incorrect response', async () => {
  410. handleRequest(contents, 204, DEFAULT_FILE);
  411. const save = contents.save('/foo', { type: 'file', name: 'test' });
  412. await expectFailure(save, 'Invalid response: 204 No Content');
  413. });
  414. });
  415. describe('#copy()', () => {
  416. it('should copy a file', async () => {
  417. handleRequest(contents, 201, DEFAULT_FILE);
  418. const model = await contents.copy('/foo/bar.txt', '/baz');
  419. expect(model.created).toBe(DEFAULT_FILE.created);
  420. });
  421. it('should copy a file on an additional drive', async () => {
  422. const other = new Drive({ serverSettings, name: 'other' });
  423. contents.addDrive(other);
  424. handleRequest(other, 201, DEFAULT_FILE);
  425. const model = await contents.copy('other:/foo/test', 'other:/baz');
  426. expect(model.path).toBe('other:foo/test');
  427. });
  428. it('should emit the fileChanged signal', async () => {
  429. handleRequest(contents, 201, DEFAULT_FILE);
  430. let called = false;
  431. contents.fileChanged.connect((sender, args) => {
  432. expect(args.type).toBe('new');
  433. expect(args.oldValue).toBeNull();
  434. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  435. called = true;
  436. });
  437. await contents.copy('/foo/bar.txt', '/baz');
  438. expect(called).toBe(true);
  439. });
  440. it('should fail for an incorrect model', async () => {
  441. const file = JSON.parse(JSON.stringify(DEFAULT_FILE));
  442. delete file.type;
  443. handleRequest(contents, 201, file);
  444. const copy = contents.copy('/foo/bar.txt', '/baz');
  445. await expectFailure(copy);
  446. });
  447. it('should fail for an incorrect response', async () => {
  448. handleRequest(contents, 200, DEFAULT_FILE);
  449. const copy = contents.copy('/foo/bar.txt', '/baz');
  450. await expectFailure(copy, 'Invalid response: 200 OK');
  451. });
  452. });
  453. describe('#createCheckpoint()', () => {
  454. it('should create a checkpoint', async () => {
  455. handleRequest(contents, 201, DEFAULT_CP);
  456. const checkpoint = contents.createCheckpoint('/foo/bar.txt');
  457. const model = await checkpoint;
  458. expect(model.last_modified).toBe(DEFAULT_CP.last_modified);
  459. });
  460. it('should create a checkpoint on an additional drive', async () => {
  461. const other = new Drive({ name: 'other', serverSettings });
  462. contents.addDrive(other);
  463. handleRequest(other, 201, DEFAULT_CP);
  464. const checkpoint = contents.createCheckpoint('other:/foo/bar.txt');
  465. const model = await checkpoint;
  466. expect(model.last_modified).toBe(DEFAULT_CP.last_modified);
  467. });
  468. it('should fail for an incorrect model', async () => {
  469. const cp = JSON.parse(JSON.stringify(DEFAULT_CP));
  470. delete cp.last_modified;
  471. handleRequest(contents, 201, cp);
  472. const checkpoint = contents.createCheckpoint('/foo/bar.txt');
  473. await expectFailure(checkpoint);
  474. });
  475. it('should fail for an incorrect response', async () => {
  476. handleRequest(contents, 200, DEFAULT_CP);
  477. const checkpoint = contents.createCheckpoint('/foo/bar.txt');
  478. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  479. });
  480. });
  481. describe('#listCheckpoints()', () => {
  482. it('should list the checkpoints', async () => {
  483. handleRequest(contents, 200, [DEFAULT_CP, DEFAULT_CP]);
  484. const checkpoints = contents.listCheckpoints('/foo/bar.txt');
  485. const models = await checkpoints;
  486. expect(models[0].last_modified).toBe(DEFAULT_CP.last_modified);
  487. });
  488. it('should list the checkpoints on an additional drive', async () => {
  489. const other = new Drive({ name: 'other', serverSettings });
  490. contents.addDrive(other);
  491. handleRequest(other, 200, [DEFAULT_CP, DEFAULT_CP]);
  492. const checkpoints = contents.listCheckpoints('other:/foo/bar.txt');
  493. const models = await checkpoints;
  494. expect(models[0].last_modified).toBe(DEFAULT_CP.last_modified);
  495. });
  496. it('should fail for an incorrect model', async () => {
  497. const cp = JSON.parse(JSON.stringify(DEFAULT_CP));
  498. delete cp.id;
  499. handleRequest(contents, 200, [cp, DEFAULT_CP]);
  500. const checkpoints = contents.listCheckpoints('/foo/bar.txt');
  501. await expectFailure(checkpoints);
  502. handleRequest(contents, 200, DEFAULT_CP);
  503. const newCheckpoints = contents.listCheckpoints('/foo/bar.txt');
  504. await expectFailure(newCheckpoints, 'Invalid Checkpoint list');
  505. });
  506. it('should fail for an incorrect response', async () => {
  507. handleRequest(contents, 201, {});
  508. const checkpoints = contents.listCheckpoints('/foo/bar.txt');
  509. await expectFailure(checkpoints, 'Invalid response: 201 Created');
  510. });
  511. });
  512. describe('#restoreCheckpoint()', () => {
  513. it('should restore a checkpoint', () => {
  514. handleRequest(contents, 204, {});
  515. const checkpoint = contents.restoreCheckpoint(
  516. '/foo/bar.txt',
  517. DEFAULT_CP.id
  518. );
  519. return checkpoint;
  520. });
  521. it('should restore a checkpoint on an additional drive', () => {
  522. const other = new Drive({ name: 'other', serverSettings });
  523. contents.addDrive(other);
  524. handleRequest(other, 204, {});
  525. const checkpoint = contents.restoreCheckpoint(
  526. 'other:/foo/bar.txt',
  527. DEFAULT_CP.id
  528. );
  529. return checkpoint;
  530. });
  531. it('should fail for an incorrect response', async () => {
  532. handleRequest(contents, 200, {});
  533. const checkpoint = contents.restoreCheckpoint(
  534. '/foo/bar.txt',
  535. DEFAULT_CP.id
  536. );
  537. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  538. });
  539. });
  540. describe('#deleteCheckpoint()', () => {
  541. it('should delete a checkpoint', () => {
  542. handleRequest(contents, 204, {});
  543. return contents.deleteCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  544. });
  545. it('should delete a checkpoint on an additional drive', () => {
  546. const other = new Drive({ name: 'other', serverSettings });
  547. contents.addDrive(other);
  548. handleRequest(other, 204, {});
  549. return contents.deleteCheckpoint('other:/foo/bar.txt', DEFAULT_CP.id);
  550. });
  551. it('should fail for an incorrect response', async () => {
  552. handleRequest(contents, 200, {});
  553. const checkpoint = contents.deleteCheckpoint(
  554. '/foo/bar.txt',
  555. DEFAULT_CP.id
  556. );
  557. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  558. });
  559. });
  560. });
  561. describe('drive', () => {
  562. let serverSettings: ServerConnection.ISettings;
  563. let contents: ContentsManager;
  564. beforeEach(() => {
  565. serverSettings = makeSettings();
  566. contents = new ContentsManager({ serverSettings });
  567. });
  568. afterEach(() => {
  569. contents.dispose();
  570. });
  571. describe('#constructor()', () => {
  572. it('should accept no options', () => {
  573. const drive = new Drive();
  574. expect(drive).toBeInstanceOf(Drive);
  575. });
  576. it('should accept options', () => {
  577. const drive = new Drive({
  578. name: 'name'
  579. });
  580. expect(drive).toBeInstanceOf(Drive);
  581. });
  582. });
  583. describe('#name', () => {
  584. it('should return the name of the drive', () => {
  585. const drive = new Drive({
  586. name: 'name'
  587. });
  588. expect(drive.name).toBe('name');
  589. });
  590. });
  591. describe('#fileChanged', () => {
  592. it('should be emitted when a file changes', async () => {
  593. const drive = new Drive();
  594. handleRequest(drive, 201, DEFAULT_FILE);
  595. let called = false;
  596. drive.fileChanged.connect((sender, args) => {
  597. expect(sender).toBe(drive);
  598. expect(args.type).toBe('new');
  599. expect(args.oldValue).toBeNull();
  600. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  601. called = true;
  602. });
  603. await drive.newUntitled();
  604. expect(called).toBe(true);
  605. });
  606. });
  607. describe('#isDisposed', () => {
  608. it('should test whether the drive is disposed', () => {
  609. const drive = new Drive();
  610. expect(drive.isDisposed).toBe(false);
  611. drive.dispose();
  612. expect(drive.isDisposed).toBe(true);
  613. });
  614. });
  615. describe('#dispose()', () => {
  616. it('should dispose of the resources used by the drive', () => {
  617. const drive = new Drive();
  618. expect(drive.isDisposed).toBe(false);
  619. drive.dispose();
  620. expect(drive.isDisposed).toBe(true);
  621. drive.dispose();
  622. expect(drive.isDisposed).toBe(true);
  623. });
  624. });
  625. describe('#get()', () => {
  626. it('should get a file', async () => {
  627. const drive = new Drive();
  628. handleRequest(drive, 200, DEFAULT_FILE);
  629. const options: Contents.IFetchOptions = { type: 'file' };
  630. const get = drive.get('/foo', options);
  631. const model = await get;
  632. expect(model.path).toBe(DEFAULT_FILE.path);
  633. });
  634. it('should get a directory', async () => {
  635. const drive = new Drive();
  636. handleRequest(drive, 200, DEFAULT_DIR);
  637. const options: Contents.IFetchOptions = { type: 'directory' };
  638. const get = drive.get('/foo', options);
  639. const model = await get;
  640. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  641. });
  642. it('should accept server settings', async () => {
  643. const drive = new Drive({ serverSettings });
  644. handleRequest(drive, 200, DEFAULT_DIR);
  645. const options: Contents.IFetchOptions = { type: 'directory' };
  646. const get = drive.get('/foo', options);
  647. const model = await get;
  648. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  649. });
  650. it('should fail for an incorrect response', async () => {
  651. const drive = new Drive();
  652. handleRequest(drive, 201, DEFAULT_DIR);
  653. const get = drive.get('/foo');
  654. await expectFailure(get, 'Invalid response: 201 Created');
  655. });
  656. });
  657. describe('#getDownloadUrl()', () => {
  658. const settings = ServerConnection.makeSettings({
  659. baseUrl: 'http://foo'
  660. });
  661. it('should get the url of a file', async () => {
  662. const drive = new Drive({ serverSettings: settings });
  663. const test1 = drive.getDownloadUrl('bar.txt');
  664. const test2 = drive.getDownloadUrl('fizz/buzz/bar.txt');
  665. const test3 = drive.getDownloadUrl('/bar.txt');
  666. const urls = await Promise.all([test1, test2, test3]);
  667. expect(urls[0]).toBe('http://foo/files/bar.txt');
  668. expect(urls[1]).toBe('http://foo/files/fizz/buzz/bar.txt');
  669. expect(urls[2]).toBe('http://foo/files/bar.txt');
  670. });
  671. it('should encode characters', async () => {
  672. const drive = new Drive({ serverSettings: settings });
  673. const url = await drive.getDownloadUrl('b ar?3.txt');
  674. expect(url).toBe('http://foo/files/b%20ar%3F3.txt');
  675. });
  676. it('should not handle relative paths', async () => {
  677. const drive = new Drive({ serverSettings: settings });
  678. const url = await drive.getDownloadUrl('fizz/../bar.txt');
  679. expect(url).toBe('http://foo/files/fizz/../bar.txt');
  680. });
  681. });
  682. describe('#newUntitled()', () => {
  683. it('should create a file', async () => {
  684. const drive = new Drive();
  685. handleRequest(drive, 201, DEFAULT_FILE);
  686. const model = await drive.newUntitled({ path: '/foo' });
  687. expect(model.path).toBe(DEFAULT_FILE.path);
  688. });
  689. it('should create a directory', async () => {
  690. const drive = new Drive();
  691. handleRequest(drive, 201, DEFAULT_DIR);
  692. const options: Contents.ICreateOptions = {
  693. path: '/foo',
  694. type: 'directory'
  695. };
  696. const newDir = drive.newUntitled(options);
  697. const model = await newDir;
  698. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  699. });
  700. it('should emit the fileChanged signal', async () => {
  701. const drive = new Drive();
  702. handleRequest(drive, 201, DEFAULT_FILE);
  703. let called = false;
  704. drive.fileChanged.connect((sender, args) => {
  705. expect(args.type).toBe('new');
  706. expect(args.oldValue).toBeNull();
  707. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  708. called = true;
  709. });
  710. await drive.newUntitled({ type: 'file', ext: 'test' });
  711. expect(called).toBe(true);
  712. });
  713. it('should accept server settings', async () => {
  714. const drive = new Drive({ serverSettings });
  715. handleRequest(drive, 201, DEFAULT_DIR);
  716. const options: Contents.ICreateOptions = {
  717. path: '/foo',
  718. type: 'file',
  719. ext: 'txt'
  720. };
  721. const model = await drive.newUntitled(options);
  722. expect(model.content[0].path).toBe(DEFAULT_DIR.content[0].path);
  723. });
  724. it('should fail for an incorrect model', async () => {
  725. const drive = new Drive();
  726. const dir = JSON.parse(JSON.stringify(DEFAULT_DIR));
  727. dir.name = 1;
  728. handleRequest(drive, 201, dir);
  729. const options: Contents.ICreateOptions = {
  730. path: '/foo',
  731. type: 'file',
  732. ext: 'py'
  733. };
  734. const newFile = drive.newUntitled(options);
  735. await expectFailure(newFile);
  736. });
  737. it('should fail for an incorrect response', async () => {
  738. const drive = new Drive();
  739. handleRequest(drive, 200, DEFAULT_DIR);
  740. const newDir = drive.newUntitled();
  741. await expectFailure(newDir, 'Invalid response: 200 OK');
  742. });
  743. });
  744. describe('#delete()', () => {
  745. it('should delete a file', async () => {
  746. const drive = new Drive();
  747. handleRequest(drive, 204, {});
  748. await drive.delete('/foo/bar.txt');
  749. });
  750. it('should emit the fileChanged signal', async () => {
  751. const drive = new Drive();
  752. const path = '/foo/bar.txt';
  753. handleRequest(drive, 204, { path });
  754. let called = false;
  755. drive.fileChanged.connect((sender, args) => {
  756. expect(args.type).toBe('delete');
  757. expect(args.oldValue!.path).toBe('/foo/bar.txt');
  758. called = true;
  759. });
  760. await drive.delete(path);
  761. expect(called).toBe(true);
  762. });
  763. it('should accept server settings', async () => {
  764. const drive = new Drive({ serverSettings });
  765. handleRequest(drive, 204, {});
  766. await drive.delete('/foo/bar.txt');
  767. });
  768. it('should fail for an incorrect response', async () => {
  769. const drive = new Drive();
  770. handleRequest(drive, 200, {});
  771. const del = drive.delete('/foo/bar.txt');
  772. await expectFailure(del, 'Invalid response: 200 OK');
  773. });
  774. it('should throw a specific error', async () => {
  775. const drive = new Drive();
  776. handleRequest(drive, 400, {});
  777. const del = drive.delete('/foo/');
  778. await expectFailure(del, '');
  779. });
  780. it('should throw a general error', async () => {
  781. const drive = new Drive();
  782. handleRequest(drive, 500, {});
  783. const del = drive.delete('/foo/');
  784. await expectFailure(del, '');
  785. });
  786. });
  787. describe('#rename()', () => {
  788. it('should rename a file', async () => {
  789. const drive = new Drive();
  790. handleRequest(drive, 200, DEFAULT_FILE);
  791. const rename = drive.rename('/foo/bar.txt', '/foo/baz.txt');
  792. const model = await rename;
  793. expect(model.created).toBe(DEFAULT_FILE.created);
  794. });
  795. it('should emit the fileChanged signal', async () => {
  796. const drive = new Drive();
  797. handleRequest(drive, 200, DEFAULT_FILE);
  798. let called = false;
  799. drive.fileChanged.connect((sender, args) => {
  800. expect(args.type).toBe('rename');
  801. expect(args.oldValue!.path).toBe('/foo/bar.txt');
  802. expect(args.newValue!.path).toBe('foo/test');
  803. called = true;
  804. });
  805. await drive.rename('/foo/bar.txt', '/foo/baz.txt');
  806. expect(called).toBe(true);
  807. });
  808. it('should accept server settings', async () => {
  809. const drive = new Drive({ serverSettings });
  810. handleRequest(drive, 200, DEFAULT_FILE);
  811. const rename = drive.rename('/foo/bar.txt', '/foo/baz.txt');
  812. const model = await rename;
  813. expect(model.created).toBe(DEFAULT_FILE.created);
  814. });
  815. it('should fail for an incorrect model', async () => {
  816. const drive = new Drive();
  817. const dir = JSON.parse(JSON.stringify(DEFAULT_FILE));
  818. delete dir.path;
  819. handleRequest(drive, 200, dir);
  820. const rename = drive.rename('/foo/bar.txt', '/foo/baz.txt');
  821. await expectFailure(rename);
  822. });
  823. it('should fail for an incorrect response', async () => {
  824. const drive = new Drive();
  825. handleRequest(drive, 201, DEFAULT_FILE);
  826. const rename = drive.rename('/foo/bar.txt', '/foo/baz.txt');
  827. await expectFailure(rename, 'Invalid response: 201 Created');
  828. });
  829. });
  830. describe('#save()', () => {
  831. it('should save a file', async () => {
  832. const drive = new Drive();
  833. handleRequest(drive, 200, DEFAULT_FILE);
  834. const save = drive.save('/foo', { type: 'file', name: 'test' });
  835. const model = await save;
  836. expect(model.created).toBe(DEFAULT_FILE.created);
  837. });
  838. it('should create a new file', async () => {
  839. const drive = new Drive();
  840. handleRequest(drive, 201, DEFAULT_FILE);
  841. const save = drive.save('/foo', { type: 'file', name: 'test' });
  842. const model = await save;
  843. expect(model.created).toBe(DEFAULT_FILE.created);
  844. });
  845. it('should emit the fileChanged signal', async () => {
  846. const drive = new Drive();
  847. handleRequest(drive, 201, DEFAULT_FILE);
  848. let called = false;
  849. drive.fileChanged.connect((sender, args) => {
  850. expect(args.type).toBe('save');
  851. expect(args.oldValue).toBeNull();
  852. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  853. called = true;
  854. });
  855. await drive.save('/foo', { type: 'file', name: 'test' });
  856. expect(called).toBe(true);
  857. });
  858. it('should accept server settings', async () => {
  859. const drive = new Drive({ serverSettings });
  860. handleRequest(drive, 200, DEFAULT_FILE);
  861. const save = drive.save('/foo', { type: 'file', name: 'test' });
  862. const model = await save;
  863. expect(model.created).toBe(DEFAULT_FILE.created);
  864. });
  865. it('should fail for an incorrect model', async () => {
  866. const drive = new Drive();
  867. const file = JSON.parse(JSON.stringify(DEFAULT_FILE));
  868. delete file.format;
  869. handleRequest(drive, 200, file);
  870. const save = drive.save('/foo', { type: 'file', name: 'test' });
  871. await expectFailure(save);
  872. });
  873. it('should fail for an incorrect response', async () => {
  874. const drive = new Drive();
  875. handleRequest(drive, 204, DEFAULT_FILE);
  876. const save = drive.save('/foo', { type: 'file', name: 'test' });
  877. await expectFailure(save, 'Invalid response: 204 No Content');
  878. });
  879. });
  880. describe('#copy()', () => {
  881. it('should copy a file', async () => {
  882. const drive = new Drive();
  883. handleRequest(drive, 201, DEFAULT_FILE);
  884. const model = await drive.copy('/foo/bar.txt', '/baz');
  885. expect(model.created).toBe(DEFAULT_FILE.created);
  886. });
  887. it('should emit the fileChanged signal', async () => {
  888. const drive = new Drive();
  889. handleRequest(drive, 201, DEFAULT_FILE);
  890. let called = false;
  891. drive.fileChanged.connect((sender, args) => {
  892. expect(args.type).toBe('new');
  893. expect(args.oldValue).toBeNull();
  894. expect(args.newValue!.path).toBe(DEFAULT_FILE.path);
  895. called = true;
  896. });
  897. await drive.copy('/foo/bar.txt', '/baz');
  898. expect(called).toBe(true);
  899. });
  900. it('should accept server settings', async () => {
  901. const drive = new Drive({ serverSettings });
  902. handleRequest(drive, 201, DEFAULT_FILE);
  903. const model = await drive.copy('/foo/bar.txt', '/baz');
  904. expect(model.created).toBe(DEFAULT_FILE.created);
  905. });
  906. it('should fail for an incorrect model', async () => {
  907. const drive = new Drive();
  908. const file = JSON.parse(JSON.stringify(DEFAULT_FILE));
  909. delete file.type;
  910. handleRequest(drive, 201, file);
  911. const copy = drive.copy('/foo/bar.txt', '/baz');
  912. await expectFailure(copy);
  913. });
  914. it('should fail for an incorrect response', async () => {
  915. const drive = new Drive();
  916. handleRequest(drive, 200, DEFAULT_FILE);
  917. const copy = drive.copy('/foo/bar.txt', '/baz');
  918. await expectFailure(copy, 'Invalid response: 200 OK');
  919. });
  920. });
  921. describe('#createCheckpoint()', () => {
  922. it('should create a checkpoint', async () => {
  923. const drive = new Drive();
  924. handleRequest(drive, 201, DEFAULT_CP);
  925. const checkpoint = drive.createCheckpoint('/foo/bar.txt');
  926. const model = await checkpoint;
  927. expect(model.last_modified).toBe(DEFAULT_CP.last_modified);
  928. });
  929. it('should accept server settings', async () => {
  930. const drive = new Drive({ serverSettings });
  931. handleRequest(drive, 201, DEFAULT_CP);
  932. const checkpoint = drive.createCheckpoint('/foo/bar.txt');
  933. const model = await checkpoint;
  934. expect(model.last_modified).toBe(DEFAULT_CP.last_modified);
  935. });
  936. it('should fail for an incorrect model', async () => {
  937. const drive = new Drive();
  938. const cp = JSON.parse(JSON.stringify(DEFAULT_CP));
  939. delete cp.last_modified;
  940. handleRequest(drive, 201, cp);
  941. const checkpoint = drive.createCheckpoint('/foo/bar.txt');
  942. await expectFailure(checkpoint);
  943. });
  944. it('should fail for an incorrect response', async () => {
  945. const drive = new Drive();
  946. handleRequest(drive, 200, DEFAULT_CP);
  947. const checkpoint = drive.createCheckpoint('/foo/bar.txt');
  948. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  949. });
  950. });
  951. describe('#listCheckpoints()', () => {
  952. it('should list the checkpoints', async () => {
  953. const drive = new Drive();
  954. handleRequest(drive, 200, [DEFAULT_CP, DEFAULT_CP]);
  955. const checkpoints = drive.listCheckpoints('/foo/bar.txt');
  956. const models = await checkpoints;
  957. expect(models[0].last_modified).toBe(DEFAULT_CP.last_modified);
  958. });
  959. it('should accept server settings', async () => {
  960. const drive = new Drive({ serverSettings });
  961. handleRequest(drive, 200, [DEFAULT_CP, DEFAULT_CP]);
  962. const checkpoints = drive.listCheckpoints('/foo/bar.txt');
  963. const models = await checkpoints;
  964. expect(models[0].last_modified).toBe(DEFAULT_CP.last_modified);
  965. });
  966. it('should fail for an incorrect model', async () => {
  967. const drive = new Drive();
  968. const cp = JSON.parse(JSON.stringify(DEFAULT_CP));
  969. delete cp.id;
  970. handleRequest(drive, 200, [cp, DEFAULT_CP]);
  971. const checkpoints = drive.listCheckpoints('/foo/bar.txt');
  972. await expectFailure(checkpoints);
  973. handleRequest(drive, 200, DEFAULT_CP);
  974. const newCheckpoints = drive.listCheckpoints('/foo/bar.txt');
  975. await expectFailure(newCheckpoints, 'Invalid Checkpoint list');
  976. });
  977. it('should fail for an incorrect response', async () => {
  978. const drive = new Drive();
  979. handleRequest(drive, 201, {});
  980. const checkpoints = drive.listCheckpoints('/foo/bar.txt');
  981. await expectFailure(checkpoints, 'Invalid response: 201 Created');
  982. });
  983. });
  984. describe('#restoreCheckpoint()', () => {
  985. it('should restore a checkpoint', () => {
  986. const drive = new Drive();
  987. handleRequest(drive, 204, {});
  988. const checkpoint = drive.restoreCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  989. return checkpoint;
  990. });
  991. it('should accept server settings', () => {
  992. const drive = new Drive({ serverSettings });
  993. handleRequest(drive, 204, {});
  994. const checkpoint = drive.restoreCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  995. return checkpoint;
  996. });
  997. it('should fail for an incorrect response', async () => {
  998. const drive = new Drive();
  999. handleRequest(drive, 200, {});
  1000. const checkpoint = drive.restoreCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  1001. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  1002. });
  1003. });
  1004. describe('#deleteCheckpoint()', () => {
  1005. it('should delete a checkpoint', () => {
  1006. const drive = new Drive();
  1007. handleRequest(drive, 204, {});
  1008. return drive.deleteCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  1009. });
  1010. it('should accept server settings', () => {
  1011. const drive = new Drive({ serverSettings });
  1012. handleRequest(drive, 204, {});
  1013. return drive.deleteCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  1014. });
  1015. it('should fail for an incorrect response', async () => {
  1016. const drive = new Drive();
  1017. handleRequest(drive, 200, {});
  1018. const checkpoint = drive.deleteCheckpoint('/foo/bar.txt', DEFAULT_CP.id);
  1019. await expectFailure(checkpoint, 'Invalid response: 200 OK');
  1020. });
  1021. });
  1022. describe('integration tests', () => {
  1023. it('should list a directory and get the file contents', async () => {
  1024. let content: Contents.IModel[];
  1025. let path = '';
  1026. const listing = await contents.get('src');
  1027. content = listing.content as Contents.IModel[];
  1028. let called = false;
  1029. for (let i = 0; i < content.length; i++) {
  1030. if (content[i].type === 'file') {
  1031. path = content[i].path;
  1032. const msg = await contents.get(path, { type: 'file' });
  1033. expect(msg.path).toBe(path);
  1034. called = true;
  1035. }
  1036. }
  1037. expect(called).toBe(true);
  1038. });
  1039. it('should create a new file, rename it, and delete it', async () => {
  1040. const options: Contents.ICreateOptions = { type: 'file', ext: '.ipynb' };
  1041. const model0 = await contents.newUntitled(options);
  1042. const model1 = await contents.rename(model0.path, 'foo.ipynb');
  1043. expect(model1.path).toBe('foo.ipynb');
  1044. return contents.delete('foo.ipynb');
  1045. });
  1046. it('should create a file by name and delete it', async () => {
  1047. const options: Partial<Contents.IModel> = {
  1048. type: 'file',
  1049. content: '',
  1050. format: 'text'
  1051. };
  1052. await contents.save('baz.txt', options);
  1053. await contents.delete('baz.txt');
  1054. });
  1055. it('should exercise the checkpoint API', async () => {
  1056. const options: Partial<Contents.IModel> = {
  1057. type: 'file',
  1058. format: 'text',
  1059. content: 'foo'
  1060. };
  1061. let checkpoint: Contents.ICheckpointModel;
  1062. const model0 = await contents.save('baz.txt', options);
  1063. expect(model0.name).toBe('baz.txt');
  1064. const value = await contents.createCheckpoint('baz.txt');
  1065. checkpoint = value;
  1066. const checkpoints = await contents.listCheckpoints('baz.txt');
  1067. expect(checkpoints[0]).toEqual(checkpoint);
  1068. await contents.restoreCheckpoint('baz.txt', checkpoint.id);
  1069. await contents.deleteCheckpoint('baz.txt', checkpoint.id);
  1070. await contents.delete('baz.txt');
  1071. });
  1072. });
  1073. });