test_ldp.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089
  1. import pdb
  2. import pytest
  3. import uuid
  4. from base64 import b64encode
  5. from hashlib import sha1
  6. from flask import g
  7. from rdflib import Graph
  8. from rdflib.compare import isomorphic
  9. from rdflib.namespace import RDF
  10. from rdflib.term import Literal, URIRef
  11. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  12. from lakesuperior.model.ldpr import Ldpr
  13. @pytest.fixture(scope='module')
  14. def random_uuid():
  15. return str(uuid.uuid4())
  16. @pytest.mark.usefixtures('client_class')
  17. @pytest.mark.usefixtures('db')
  18. class TestLdp:
  19. """
  20. Test HTTP interaction with LDP endpoint.
  21. """
  22. def test_get_root_node(self):
  23. """
  24. Get the root node from two different endpoints.
  25. The test triplestore must be initialized, hence the `db` fixture.
  26. """
  27. ldp_resp = self.client.get('/ldp')
  28. rest_resp = self.client.get('/rest')
  29. assert ldp_resp.status_code == 200
  30. assert rest_resp.status_code == 200
  31. def test_put_empty_resource(self, random_uuid):
  32. """
  33. Check response headers for a PUT operation with empty payload.
  34. """
  35. resp = self.client.put('/ldp/new_resource')
  36. assert resp.status_code == 201
  37. assert resp.data == bytes(
  38. '{}/new_resource'.format(g.webroot), 'utf-8')
  39. def test_put_existing_resource(self, random_uuid):
  40. """
  41. Trying to PUT an existing resource should return a 204 if the payload
  42. is empty.
  43. """
  44. path = '/ldp/nonidempotent01'
  45. put1_resp = self.client.put(path)
  46. assert put1_resp.status_code == 201
  47. assert self.client.get(path).status_code == 200
  48. put2_resp = self.client.put(path)
  49. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  50. put2_resp = self.client.put(
  51. path, data=f, content_type='text/turtle')
  52. assert put2_resp.status_code == 204
  53. put2_resp = self.client.put(path)
  54. assert put2_resp.status_code == 204
  55. def test_put_tree(self, client):
  56. """
  57. PUT a resource with several path segments.
  58. The test should create intermediate path segments that are LDPCs,
  59. accessible to PUT or POST.
  60. """
  61. path = '/ldp/test_tree/a/b/c/d/e/f/g'
  62. self.client.put(path)
  63. assert self.client.get(path).status_code == 200
  64. assert self.client.get('/ldp/test_tree/a/b/c').status_code == 200
  65. assert self.client.post('/ldp/test_tree/a/b').status_code == 201
  66. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  67. put_int_resp = self.client.put(
  68. 'ldp/test_tree/a', data=f, content_type='text/turtle')
  69. assert put_int_resp.status_code == 204
  70. # @TODO More thorough testing of contents
  71. def test_put_nested_tree(self, client):
  72. """
  73. Verify that containment is set correctly in nested hierarchies.
  74. First put a new hierarchy and verify that the root node is its
  75. container; then put another hierarchy under it and verify that the
  76. first hierarchy is the container of the second one.
  77. """
  78. uuid1 = 'test_nested_tree/a/b/c/d'
  79. uuid2 = uuid1 + '/e/f/g'
  80. path1 = '/ldp/' + uuid1
  81. path2 = '/ldp/' + uuid2
  82. self.client.put(path1)
  83. cont1_data = self.client.get('/ldp').data
  84. gr1 = Graph().parse(data=cont1_data, format='turtle')
  85. assert gr1[ URIRef(g.webroot + '/') : nsc['ldp'].contains : \
  86. URIRef(g.webroot + '/test_nested_tree') ]
  87. self.client.put(path2)
  88. cont2_data = self.client.get(path1).data
  89. gr2 = Graph().parse(data=cont2_data, format='turtle')
  90. assert gr2[ URIRef(g.webroot + '/' + uuid1) : \
  91. nsc['ldp'].contains : \
  92. URIRef(g.webroot + '/' + uuid1 + '/e') ]
  93. def test_put_ldp_rs(self, client):
  94. """
  95. PUT a resource with RDF payload and verify.
  96. """
  97. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  98. self.client.put('/ldp/ldprs01', data=f, content_type='text/turtle')
  99. resp = self.client.get('/ldp/ldprs01',
  100. headers={'accept' : 'text/turtle'})
  101. assert resp.status_code == 200
  102. gr = Graph().parse(data=resp.data, format='text/turtle')
  103. assert URIRef('http://vocab.getty.edu/ontology#Subject') in \
  104. gr.objects(None, RDF.type)
  105. def test_put_ldp_nr(self, rnd_img):
  106. """
  107. PUT a resource with binary payload and verify checksums.
  108. """
  109. rnd_img['content'].seek(0)
  110. resp = self.client.put('/ldp/ldpnr01', data=rnd_img['content'],
  111. headers={
  112. 'Content-Type': 'image/png',
  113. 'Content-Disposition' : 'attachment; filename={}'.format(
  114. rnd_img['filename'])})
  115. assert resp.status_code == 201
  116. resp = self.client.get(
  117. '/ldp/ldpnr01', headers={'accept' : 'image/png'})
  118. assert resp.status_code == 200
  119. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  120. def test_put_ldp_nr_multipart(self, rnd_img):
  121. """
  122. PUT a resource with a multipart/form-data payload.
  123. """
  124. rnd_img['content'].seek(0)
  125. resp = self.client.put(
  126. '/ldp/ldpnr02',
  127. data={
  128. 'file': (
  129. rnd_img['content'], rnd_img['filename'],
  130. 'image/png',
  131. )
  132. }
  133. )
  134. assert resp.status_code == 201
  135. resp = self.client.get(
  136. '/ldp/ldpnr02', headers={'accept' : 'image/png'})
  137. assert resp.status_code == 200
  138. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  139. def test_put_mismatched_ldp_rs(self, rnd_img):
  140. """
  141. Verify MIME type / LDP mismatch.
  142. PUT a LDP-RS, then PUT a LDP-NR on the same location and verify it
  143. fails.
  144. """
  145. path = '/ldp/' + str(uuid.uuid4())
  146. rnd_img['content'].seek(0)
  147. ldp_nr_resp = self.client.put(path, data=rnd_img['content'],
  148. headers={
  149. 'Content-Disposition' : 'attachment; filename={}'.format(
  150. rnd_img['filename'])})
  151. assert ldp_nr_resp.status_code == 201
  152. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  153. ldp_rs_resp = self.client.put(path, data=f,
  154. content_type='text/turtle')
  155. assert ldp_rs_resp.status_code == 415
  156. def test_put_mismatched_ldp_nr(self, rnd_img):
  157. """
  158. Verify MIME type / LDP mismatch.
  159. PUT a LDP-NR, then PUT a LDP-RS on the same location and verify it
  160. fails.
  161. """
  162. path = '/ldp/' + str(uuid.uuid4())
  163. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  164. ldp_rs_resp = self.client.put(path, data=f,
  165. content_type='text/turtle')
  166. assert ldp_rs_resp.status_code == 201
  167. rnd_img['content'].seek(0)
  168. ldp_nr_resp = self.client.put(path, data=rnd_img['content'],
  169. headers={
  170. 'Content-Disposition' : 'attachment; filename={}'.format(
  171. rnd_img['filename'])})
  172. assert ldp_nr_resp.status_code == 415
  173. def test_missing_reference(self, client):
  174. """
  175. PUT a resource with RDF payload referencing a non-existing in-repo
  176. resource.
  177. """
  178. self.client.get('/ldp')
  179. data = '''
  180. PREFIX ns: <http://example.org#>
  181. PREFIX res: <http://example-source.org/res/>
  182. <> ns:p1 res:bogus ;
  183. ns:p2 <{0}> ;
  184. ns:p3 <{0}/> ;
  185. ns:p4 <{0}/nonexistent> .
  186. '''.format(g.webroot)
  187. put_rsp = self.client.put('/ldp/test_missing_ref', data=data, headers={
  188. 'content-type': 'text/turtle'})
  189. assert put_rsp.status_code == 201
  190. resp = self.client.get('/ldp/test_missing_ref',
  191. headers={'accept' : 'text/turtle'})
  192. assert resp.status_code == 200
  193. gr = Graph().parse(data=resp.data, format='text/turtle')
  194. assert URIRef('http://example-source.org/res/bogus') in \
  195. gr.objects(None, URIRef('http://example.org#p1'))
  196. assert URIRef(g.webroot + '/') in (
  197. gr.objects(None, URIRef('http://example.org#p2')))
  198. assert URIRef(g.webroot + '/') in (
  199. gr.objects(None, URIRef('http://example.org#p3')))
  200. assert URIRef(g.webroot + '/nonexistent') not in (
  201. gr.objects(None, URIRef('http://example.org#p4')))
  202. def test_post_resource(self, client):
  203. """
  204. Check response headers for a POST operation with empty payload.
  205. """
  206. res = self.client.post('/ldp/')
  207. assert res.status_code == 201
  208. assert 'Location' in res.headers
  209. def test_post_ldp_nr(self, rnd_img):
  210. """
  211. POST a resource with binary payload and verify checksums.
  212. """
  213. rnd_img['content'].seek(0)
  214. resp = self.client.post('/ldp/', data=rnd_img['content'],
  215. headers={
  216. 'slug': 'ldpnr03',
  217. 'Content-Type': 'image/png',
  218. 'Content-Disposition' : 'attachment; filename={}'.format(
  219. rnd_img['filename'])})
  220. assert resp.status_code == 201
  221. resp = self.client.get(
  222. '/ldp/ldpnr03', headers={'accept' : 'image/png'})
  223. assert resp.status_code == 200
  224. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  225. def test_post_slug(self):
  226. """
  227. Verify that a POST with slug results in the expected URI only if the
  228. resource does not exist already.
  229. """
  230. slug01_resp = self.client.post('/ldp', headers={'slug' : 'slug01'})
  231. assert slug01_resp.status_code == 201
  232. assert slug01_resp.headers['location'] == \
  233. g.webroot + '/slug01'
  234. slug02_resp = self.client.post('/ldp', headers={'slug' : 'slug01'})
  235. assert slug02_resp.status_code == 201
  236. assert slug02_resp.headers['location'] != \
  237. g.webroot + '/slug01'
  238. def test_post_404(self):
  239. """
  240. Verify that a POST to a non-existing parent results in a 404.
  241. """
  242. assert self.client.post('/ldp/{}'.format(uuid.uuid4()))\
  243. .status_code == 404
  244. def test_post_409(self, rnd_img):
  245. """
  246. Verify that you cannot POST to a binary resource.
  247. """
  248. rnd_img['content'].seek(0)
  249. self.client.put('/ldp/post_409', data=rnd_img['content'], headers={
  250. 'Content-Disposition' : 'attachment; filename={}'.format(
  251. rnd_img['filename'])})
  252. assert self.client.post('/ldp/post_409').status_code == 409
  253. def test_patch_root(self):
  254. """
  255. Test patching root node.
  256. """
  257. path = '/ldp/'
  258. self.client.get(path)
  259. uri = g.webroot + '/'
  260. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  261. resp = self.client.patch(path,
  262. data=data,
  263. headers={'content-type' : 'application/sparql-update'})
  264. assert resp.status_code == 204
  265. resp = self.client.get(path)
  266. gr = Graph().parse(data=resp.data, format='text/turtle')
  267. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Hello') ]
  268. def test_patch(self):
  269. """
  270. Test patching a resource.
  271. """
  272. path = '/ldp/test_patch01'
  273. self.client.put(path)
  274. uri = g.webroot + '/test_patch01'
  275. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  276. resp = self.client.patch(path,
  277. data=data,
  278. headers={'content-type' : 'application/sparql-update'})
  279. assert resp.status_code == 204
  280. resp = self.client.get(path)
  281. gr = Graph().parse(data=resp.data, format='text/turtle')
  282. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Hello') ]
  283. self.client.patch(path,
  284. data=open('tests/data/sparql_update/delete+insert+where.sparql'),
  285. headers={'content-type' : 'application/sparql-update'})
  286. resp = self.client.get(path)
  287. gr = Graph().parse(data=resp.data, format='text/turtle')
  288. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Ciao') ]
  289. def test_patch_ssr(self):
  290. """
  291. Test patching a resource violating the single-subject rule.
  292. """
  293. path = '/ldp/test_patch_ssr'
  294. self.client.put(path)
  295. uri = g.webroot + '/test_patch_ssr'
  296. nossr_qry = 'INSERT { <http://bogus.org> a <urn:ns:A> . } WHERE {}'
  297. abs_qry = 'INSERT {{ <{}> a <urn:ns:A> . }} WHERE {{}}'.format(uri)
  298. frag_qry = 'INSERT {{ <{}#frag> a <urn:ns:A> . }} WHERE {{}}'\
  299. .format(uri)
  300. # @TODO Leave commented until a decision is made about SSR.
  301. assert self.client.patch(
  302. path, data=nossr_qry,
  303. headers={'content-type': 'application/sparql-update'}
  304. ).status_code == 204
  305. assert self.client.patch(
  306. path, data=abs_qry,
  307. headers={'content-type': 'application/sparql-update'}
  308. ).status_code == 204
  309. assert self.client.patch(
  310. path, data=frag_qry,
  311. headers={'content-type': 'application/sparql-update'}
  312. ).status_code == 204
  313. def test_patch_ldp_nr_metadata(self):
  314. """
  315. Test patching a LDP-NR metadata resource from the fcr:metadata URI.
  316. """
  317. path = '/ldp/ldpnr01'
  318. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  319. self.client.patch(path + '/fcr:metadata',
  320. data=data,
  321. headers={'content-type' : 'application/sparql-update'})
  322. resp = self.client.get(path + '/fcr:metadata')
  323. assert resp.status_code == 200
  324. uri = g.webroot + '/ldpnr01'
  325. gr = Graph().parse(data=resp.data, format='text/turtle')
  326. assert gr[URIRef(uri) : nsc['dc'].title : Literal('Hello')]
  327. with open(
  328. 'tests/data/sparql_update/delete+insert+where.sparql') as data:
  329. patch_resp = self.client.patch(path + '/fcr:metadata',
  330. data=data,
  331. headers={'content-type' : 'application/sparql-update'})
  332. assert patch_resp.status_code == 204
  333. resp = self.client.get(path + '/fcr:metadata')
  334. assert resp.status_code == 200
  335. gr = Graph().parse(data=resp.data, format='text/turtle')
  336. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Ciao') ]
  337. def test_patch_ldpnr(self):
  338. """
  339. Verify that a direct PATCH to a LDP-NR results in a 415.
  340. """
  341. with open(
  342. 'tests/data/sparql_update/delete+insert+where.sparql') as data:
  343. patch_resp = self.client.patch('/ldp/ldpnr01',
  344. data=data,
  345. headers={'content-type': 'application/sparql-update'})
  346. assert patch_resp.status_code == 415
  347. def test_patch_invalid_mimetype(self, rnd_img):
  348. """
  349. Verify that a PATCH using anything other than an
  350. `application/sparql-update` MIME type results in an error.
  351. """
  352. self.client.put('/ldp/test_patch_invalid_mimetype')
  353. rnd_img['content'].seek(0)
  354. ldpnr_resp = self.client.patch('/ldp/ldpnr01/fcr:metadata',
  355. data=rnd_img,
  356. headers={'content-type' : 'image/jpeg'})
  357. ldprs_resp = self.client.patch('/ldp/test_patch_invalid_mimetype',
  358. data=b'Hello, I\'m not a SPARQL update.',
  359. headers={'content-type' : 'text/plain'})
  360. assert ldprs_resp.status_code == ldpnr_resp.status_code == 415
  361. def test_delete(self):
  362. """
  363. Test delete response codes.
  364. """
  365. self.client.put('/ldp/test_delete01')
  366. delete_resp = self.client.delete('/ldp/test_delete01')
  367. assert delete_resp.status_code == 204
  368. bogus_delete_resp = self.client.delete('/ldp/test_delete101')
  369. assert bogus_delete_resp.status_code == 404
  370. def test_tombstone(self):
  371. """
  372. Test tombstone behaviors.
  373. For POST on a tombstone, check `test_resurrection`.
  374. """
  375. tstone_resp = self.client.get('/ldp/test_delete01')
  376. assert tstone_resp.status_code == 410
  377. assert tstone_resp.headers['Link'] == \
  378. '<{}/test_delete01/fcr:tombstone>; rel="hasTombstone"'\
  379. .format(g.webroot)
  380. tstone_path = '/ldp/test_delete01/fcr:tombstone'
  381. assert self.client.get(tstone_path).status_code == 405
  382. assert self.client.put(tstone_path).status_code == 405
  383. assert self.client.delete(tstone_path).status_code == 204
  384. assert self.client.get('/ldp/test_delete01').status_code == 404
  385. def test_delete_recursive(self):
  386. """
  387. Test response codes for resources deleted recursively and their
  388. tombstones.
  389. """
  390. child_suffixes = ('a', 'a/b', 'a/b/c', 'a1', 'a1/b1')
  391. self.client.put('/ldp/test_delete_recursive01')
  392. for cs in child_suffixes:
  393. self.client.put('/ldp/test_delete_recursive01/{}'.format(cs))
  394. assert self.client.delete(
  395. '/ldp/test_delete_recursive01').status_code == 204
  396. tstone_resp = self.client.get('/ldp/test_delete_recursive01')
  397. assert tstone_resp.status_code == 410
  398. assert tstone_resp.headers['Link'] == \
  399. '<{}/test_delete_recursive01/fcr:tombstone>; rel="hasTombstone"'\
  400. .format(g.webroot)
  401. for cs in child_suffixes:
  402. child_tstone_resp = self.client.get(
  403. '/ldp/test_delete_recursive01/{}'.format(cs))
  404. assert child_tstone_resp.status_code == tstone_resp.status_code
  405. assert 'Link' not in child_tstone_resp.headers.keys()
  406. def test_put_fragments(self):
  407. """
  408. Test the correct handling of fragment URIs on PUT and GET.
  409. """
  410. with open('tests/data/fragments.ttl', 'rb') as f:
  411. self.client.put(
  412. '/ldp/test_fragment01',
  413. headers={
  414. 'Content-Type' : 'text/turtle',
  415. },
  416. data=f
  417. )
  418. rsp = self.client.get('/ldp/test_fragment01')
  419. gr = Graph().parse(data=rsp.data, format='text/turtle')
  420. assert gr[
  421. URIRef(g.webroot + '/test_fragment01#hash1')
  422. : URIRef('http://ex.org/p2') : URIRef('http://ex.org/o2')]
  423. def test_patch_fragments(self):
  424. """
  425. Test the correct handling of fragment URIs on PATCH.
  426. """
  427. self.client.put('/ldp/test_fragment_patch')
  428. with open('tests/data/fragments_insert.sparql', 'rb') as f:
  429. self.client.patch(
  430. '/ldp/test_fragment_patch',
  431. headers={
  432. 'Content-Type' : 'application/sparql-update',
  433. },
  434. data=f
  435. )
  436. ins_rsp = self.client.get('/ldp/test_fragment_patch')
  437. ins_gr = Graph().parse(data=ins_rsp.data, format='text/turtle')
  438. assert ins_gr[
  439. URIRef(g.webroot + '/test_fragment_patch#hash1234')
  440. : URIRef('http://ex.org/p3') : URIRef('http://ex.org/o3')]
  441. with open('tests/data/fragments_delete.sparql', 'rb') as f:
  442. self.client.patch(
  443. '/ldp/test_fragment_patch',
  444. headers={
  445. 'Content-Type' : 'application/sparql-update',
  446. },
  447. data=f
  448. )
  449. del_rsp = self.client.get('/ldp/test_fragment_patch')
  450. del_gr = Graph().parse(data=del_rsp.data, format='text/turtle')
  451. assert not del_gr[
  452. URIRef(g.webroot + '/test_fragment_patch#hash1234')
  453. : URIRef('http://ex.org/p3') : URIRef('http://ex.org/o3')]
  454. @pytest.mark.usefixtures('client_class')
  455. @pytest.mark.usefixtures('db')
  456. class TestMimeType:
  457. """
  458. Test ``Accept`` headers and input & output formats.
  459. """
  460. def test_accept(self):
  461. """
  462. Verify the default serialization method.
  463. """
  464. accept_list = {
  465. ('', 'text/turtle'),
  466. ('text/turtle', 'text/turtle'),
  467. ('application/rdf+xml', 'application/rdf+xml'),
  468. ('application/n-triples', 'application/n-triples'),
  469. ('application/bogus', 'text/turtle'),
  470. (
  471. 'application/rdf+xml;q=0.5,application/n-triples;q=0.7',
  472. 'application/n-triples'),
  473. (
  474. 'application/rdf+xml;q=0.5,application/bogus;q=0.7',
  475. 'application/rdf+xml'),
  476. ('application/rdf+xml;q=0.5,text/n3;q=0.7', 'text/n3'),
  477. (
  478. 'application/rdf+xml;q=0.5,application/ld+json;q=0.7',
  479. 'application/ld+json'),
  480. }
  481. for mimetype, fmt in accept_list:
  482. rsp = self.client.get('/ldp', headers={'Accept': mimetype})
  483. assert rsp.mimetype == fmt
  484. gr = Graph(identifier=g.webroot + '/').parse(
  485. data=rsp.data, format=fmt)
  486. assert nsc['fcrepo'].RepositoryRoot in set(gr.objects())
  487. def test_provided_rdf(self):
  488. """
  489. Test several input RDF serialiation formats.
  490. """
  491. self.client.get('/ldp')
  492. gr = Graph()
  493. gr.add((
  494. URIRef(g.webroot + '/test_mimetype'),
  495. nsc['dcterms'].title, Literal('Test MIME type.')))
  496. test_list = {
  497. 'application/n-triples',
  498. 'application/rdf+xml',
  499. 'text/n3',
  500. 'text/turtle',
  501. 'application/ld+json',
  502. }
  503. for mimetype in test_list:
  504. rdf_data = gr.serialize(format=mimetype)
  505. self.client.put('/ldp/test_mimetype', data=rdf_data, headers={
  506. 'content-type': mimetype})
  507. rsp = self.client.get('/ldp/test_mimetype')
  508. rsp_gr = Graph(identifier=g.webroot + '/test_mimetype').parse(
  509. data=rsp.data, format='text/turtle')
  510. assert (
  511. URIRef(g.webroot + '/test_mimetype'),
  512. nsc['dcterms'].title, Literal('Test MIME type.')) in rsp_gr
  513. @pytest.mark.usefixtures('client_class')
  514. @pytest.mark.usefixtures('db')
  515. class TestPrefHeader:
  516. """
  517. Test various combinations of `Prefer` header.
  518. """
  519. @pytest.fixture(scope='class')
  520. def cont_structure(self):
  521. """
  522. Create a container structure to be used for subsequent requests.
  523. """
  524. parent_path = '/ldp/test_parent'
  525. self.client.put(parent_path)
  526. self.client.put(parent_path + '/child1')
  527. self.client.put(parent_path + '/child2')
  528. self.client.put(parent_path + '/child3')
  529. return {
  530. 'path' : parent_path,
  531. 'response' : self.client.get(parent_path),
  532. }
  533. def test_put_prefer_handling(self, random_uuid):
  534. """
  535. Trying to PUT an existing resource should:
  536. - Return a 204 if the payload is empty
  537. - Return a 204 if the payload is RDF, server-managed triples are
  538. included and the 'Prefer' header is set to 'handling=lenient'
  539. - Return a 412 (ServerManagedTermError) if the payload is RDF,
  540. server-managed triples are included and handling is set to 'strict',
  541. or not set.
  542. """
  543. path = '/ldp/put_pref_header01'
  544. assert self.client.put(path).status_code == 201
  545. assert self.client.get(path).status_code == 200
  546. assert self.client.put(path).status_code == 204
  547. # Default handling is strict.
  548. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  549. rsp_default = self.client.put(
  550. path,
  551. headers={
  552. 'Content-Type' : 'text/turtle',
  553. },
  554. data=f
  555. )
  556. assert rsp_default.status_code == 412
  557. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  558. rsp_len = self.client.put(
  559. path,
  560. headers={
  561. 'Prefer' : 'handling=lenient',
  562. 'Content-Type' : 'text/turtle',
  563. },
  564. data=f
  565. )
  566. assert rsp_len.status_code == 204
  567. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  568. rsp_strict = self.client.put(
  569. path,
  570. headers={
  571. 'Prefer' : 'handling=strict',
  572. 'Content-Type' : 'text/turtle',
  573. },
  574. data=f
  575. )
  576. assert rsp_strict.status_code == 412
  577. # @HOLD Embed children is debated.
  578. def _disabled_test_embed_children(self, cont_structure):
  579. """
  580. verify the "embed children" prefer header.
  581. """
  582. self.client.get('/ldp')
  583. parent_path = cont_structure['path']
  584. cont_resp = cont_structure['response']
  585. cont_subject = URIRef(g.webroot + '/test_parent')
  586. #minimal_resp = self.client.get(parent_path, headers={
  587. # 'Prefer' : 'return=minimal',
  588. #})
  589. incl_embed_children_resp = self.client.get(parent_path, headers={
  590. 'Prefer' : 'return=representation; include={}'\
  591. .format(Ldpr.EMBED_CHILD_RES_URI),
  592. })
  593. omit_embed_children_resp = self.client.get(parent_path, headers={
  594. 'Prefer' : 'return=representation; omit={}'\
  595. .format(Ldpr.EMBED_CHILD_RES_URI),
  596. })
  597. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  598. incl_gr = Graph().parse(
  599. data=incl_embed_children_resp.data, format='turtle')
  600. omit_gr = Graph().parse(
  601. data=omit_embed_children_resp.data, format='turtle')
  602. assert isomorphic(omit_gr, default_gr)
  603. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  604. assert len(children) == 3
  605. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  606. for child_uri in children:
  607. assert set(incl_gr[ child_uri : : ])
  608. assert not set(omit_gr[ child_uri : : ])
  609. def test_return_children(self, cont_structure):
  610. """
  611. verify the "return children" prefer header.
  612. """
  613. self.client.get('/ldp')
  614. parent_path = cont_structure['path']
  615. cont_resp = cont_structure['response']
  616. cont_subject = URIRef(g.webroot + '/test_parent')
  617. incl_children_resp = self.client.get(parent_path, headers={
  618. 'Prefer' : 'return=representation; include={}'\
  619. .format(Ldpr.RETURN_CHILD_RES_URI),
  620. })
  621. omit_children_resp = self.client.get(parent_path, headers={
  622. 'Prefer' : 'return=representation; omit={}'\
  623. .format(Ldpr.RETURN_CHILD_RES_URI),
  624. })
  625. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  626. incl_gr = Graph().parse(data=incl_children_resp.data, format='turtle')
  627. omit_gr = Graph().parse(data=omit_children_resp.data, format='turtle')
  628. assert isomorphic(incl_gr, default_gr)
  629. children = incl_gr[cont_subject : nsc['ldp'].contains]
  630. for child_uri in children:
  631. assert not omit_gr[cont_subject : nsc['ldp'].contains : child_uri]
  632. def test_inbound_rel(self, cont_structure):
  633. """
  634. verify the "inbound relationships" prefer header.
  635. """
  636. self.client.put('/ldp/test_target')
  637. data = '<> <http://ex.org/ns#shoots> <{}> .'.format(
  638. g.webroot + '/test_target')
  639. self.client.put('/ldp/test_shooter', data=data,
  640. headers={'Content-Type': 'text/turtle'})
  641. cont_resp = self.client.get('/ldp/test_target')
  642. incl_inbound_resp = self.client.get('/ldp/test_target', headers={
  643. 'Prefer' : 'return=representation; include="{}"'\
  644. .format(Ldpr.RETURN_INBOUND_REF_URI),
  645. })
  646. omit_inbound_resp = self.client.get('/ldp/test_target', headers={
  647. 'Prefer' : 'return=representation; omit="{}"'\
  648. .format(Ldpr.RETURN_INBOUND_REF_URI),
  649. })
  650. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  651. incl_gr = Graph().parse(data=incl_inbound_resp.data, format='turtle')
  652. omit_gr = Graph().parse(data=omit_inbound_resp.data, format='turtle')
  653. subject = URIRef(g.webroot + '/test_target')
  654. inbd_subject = URIRef(g.webroot + '/test_shooter')
  655. assert isomorphic(omit_gr, default_gr)
  656. assert len(set(incl_gr[inbd_subject : : ])) == 1
  657. assert incl_gr[
  658. inbd_subject : URIRef('http://ex.org/ns#shoots') : subject]
  659. assert not len(set(omit_gr[inbd_subject : :]))
  660. def test_srv_mgd_triples(self, cont_structure):
  661. """
  662. verify the "server managed triples" prefer header.
  663. """
  664. self.client.get('/ldp')
  665. parent_path = cont_structure['path']
  666. cont_resp = cont_structure['response']
  667. cont_subject = URIRef(g.webroot + '/test_parent')
  668. incl_srv_mgd_resp = self.client.get(parent_path, headers={
  669. 'Prefer' : 'return=representation; include={}'\
  670. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  671. })
  672. omit_srv_mgd_resp = self.client.get(parent_path, headers={
  673. 'Prefer' : 'return=representation; omit={}'\
  674. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  675. })
  676. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  677. incl_gr = Graph().parse(data=incl_srv_mgd_resp.data, format='turtle')
  678. omit_gr = Graph().parse(data=omit_srv_mgd_resp.data, format='turtle')
  679. assert isomorphic(incl_gr, default_gr)
  680. for pred in {
  681. nsc['fcrepo'].created,
  682. nsc['fcrepo'].createdBy,
  683. nsc['fcrepo'].lastModified,
  684. nsc['fcrepo'].lastModifiedBy,
  685. nsc['ldp'].contains,
  686. }:
  687. assert set(incl_gr[ cont_subject : pred : ])
  688. assert not set(omit_gr[ cont_subject : pred : ])
  689. for type in {
  690. nsc['fcrepo'].Resource,
  691. nsc['ldp'].Container,
  692. nsc['ldp'].Resource,
  693. }:
  694. assert incl_gr[ cont_subject : RDF.type : type ]
  695. assert not omit_gr[ cont_subject : RDF.type : type ]
  696. def test_delete_no_tstone(self):
  697. """
  698. Test the `no-tombstone` Prefer option.
  699. """
  700. self.client.put('/ldp/test_delete_no_tstone01')
  701. self.client.put('/ldp/test_delete_no_tstone01/a')
  702. self.client.delete('/ldp/test_delete_no_tstone01', headers={
  703. 'prefer' : 'no-tombstone'})
  704. resp = self.client.get('/ldp/test_delete_no_tstone01')
  705. assert resp.status_code == 404
  706. child_resp = self.client.get('/ldp/test_delete_no_tstone01/a')
  707. assert child_resp.status_code == 404
  708. #@pytest.mark.usefixtures('client_class')
  709. #@pytest.mark.usefixtures('db')
  710. #class TestDigest:
  711. # """
  712. # Test digest and ETag handling.
  713. # """
  714. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  715. # def test_digest_post(self):
  716. # """
  717. # Test ``Digest`` and ``ETag`` headers on resource POST.
  718. # """
  719. # resp = self.client.post('/ldp/')
  720. # assert 'Digest' in resp.headers
  721. # assert 'ETag' in resp.headers
  722. # assert (
  723. # b64encode(bytes.fromhex(
  724. # resp.headers['ETag'].replace('W/', '')
  725. # )).decode('ascii') ==
  726. # resp.headers['Digest'].replace('SHA256=', ''))
  727. #
  728. #
  729. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  730. # def test_digest_put(self):
  731. # """
  732. # Test ``Digest`` and ``ETag`` headers on resource PUT.
  733. # """
  734. # resp_put = self.client.put('/ldp/test_digest_put')
  735. # assert 'Digest' in resp_put.headers
  736. # assert 'ETag' in resp_put.headers
  737. # assert (
  738. # b64encode(bytes.fromhex(
  739. # resp_put.headers['ETag'].replace('W/', '')
  740. # )).decode('ascii') ==
  741. # resp_put.headers['Digest'].replace('SHA256=', ''))
  742. #
  743. # resp_get = self.client.get('/ldp/test_digest_put')
  744. # assert 'Digest' in resp_get.headers
  745. # assert 'ETag' in resp_get.headers
  746. # assert (
  747. # b64encode(bytes.fromhex(
  748. # resp_get.headers['ETag'].replace('W/', '')
  749. # )).decode('ascii') ==
  750. # resp_get.headers['Digest'].replace('SHA256=', ''))
  751. #
  752. #
  753. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  754. # def test_digest_patch(self):
  755. # """
  756. # Verify that the digest and ETag change on resource change.
  757. # """
  758. # path = '/ldp/test_digest_patch'
  759. # self.client.put(path)
  760. # rsp1 = self.client.get(path)
  761. #
  762. # self.client.patch(
  763. # path, data=b'DELETE {} INSERT {<> a <http://ex.org/Test> .} '
  764. # b'WHERE {}',
  765. # headers={'Content-Type': 'application/sparql-update'})
  766. # rsp2 = self.client.get(path)
  767. #
  768. # assert rsp1.headers['ETag'] != rsp2.headers['ETag']
  769. # assert rsp1.headers['Digest'] != rsp2.headers['Digest']
  770. @pytest.mark.usefixtures('client_class')
  771. @pytest.mark.usefixtures('db')
  772. class TestVersion:
  773. """
  774. Test version creation, retrieval and deletion.
  775. """
  776. def test_create_versions(self):
  777. """
  778. Test that POSTing multiple times to fcr:versions creates the
  779. 'hasVersions' triple and yields multiple version snapshots.
  780. """
  781. self.client.put('/ldp/test_version')
  782. create_rsp = self.client.post('/ldp/test_version/fcr:versions')
  783. assert create_rsp.status_code == 201
  784. rsrc_rsp = self.client.get('/ldp/test_version')
  785. rsrc_gr = Graph().parse(data=rsrc_rsp.data, format='turtle')
  786. assert len(set(rsrc_gr[: nsc['fcrepo'].hasVersions :])) == 1
  787. info_rsp = self.client.get('/ldp/test_version/fcr:versions')
  788. assert info_rsp.status_code == 200
  789. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  790. assert len(set(info_gr[: nsc['fcrepo'].hasVersion :])) == 1
  791. self.client.post('/ldp/test_version/fcr:versions')
  792. info2_rsp = self.client.get('/ldp/test_version/fcr:versions')
  793. info2_gr = Graph().parse(data=info2_rsp.data, format='turtle')
  794. assert len(set(info2_gr[: nsc['fcrepo'].hasVersion :])) == 2
  795. def test_version_with_slug(self):
  796. """
  797. Test a version with a slug.
  798. """
  799. self.client.put('/ldp/test_version_slug')
  800. create_rsp = self.client.post('/ldp/test_version_slug/fcr:versions',
  801. headers={'slug' : 'v1'})
  802. new_ver_uri = create_rsp.headers['Location']
  803. assert new_ver_uri == g.webroot + '/test_version_slug/fcr:versions/v1'
  804. info_rsp = self.client.get('/ldp/test_version_slug/fcr:versions')
  805. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  806. assert info_gr[
  807. URIRef(new_ver_uri) :
  808. nsc['fcrepo'].hasVersionLabel :
  809. Literal('v1')]
  810. def test_dupl_version(self):
  811. """
  812. Make sure that two POSTs with the same slug result in two different
  813. versions.
  814. """
  815. path = '/ldp/test_duplicate_slug'
  816. self.client.put(path)
  817. v1_rsp = self.client.post(path + '/fcr:versions',
  818. headers={'slug' : 'v1'})
  819. v1_uri = v1_rsp.headers['Location']
  820. dup_rsp = self.client.post(path + '/fcr:versions',
  821. headers={'slug' : 'v1'})
  822. dup_uri = dup_rsp.headers['Location']
  823. assert v1_uri != dup_uri
  824. @pytest.mark.skip(
  825. reason='TODO Reverting from version and resurrecting is not fully '
  826. 'functional.')
  827. def test_revert_version(self):
  828. """
  829. Take a version snapshot, update a resource, and then revert to the
  830. previous vresion.
  831. """
  832. rsrc_path = '/ldp/test_revert_version'
  833. payload1 = '<> <urn:demo:p1> <urn:demo:o1> .'
  834. payload2 = '<> <urn:demo:p1> <urn:demo:o2> .'
  835. self.client.put(rsrc_path, headers={
  836. 'content-type': 'text/turtle'}, data=payload1)
  837. self.client.post(
  838. rsrc_path + '/fcr:versions', headers={'slug': 'v1'})
  839. v1_rsp = self.client.get(rsrc_path)
  840. v1_gr = Graph().parse(data=v1_rsp.data, format='turtle')
  841. assert v1_gr[
  842. URIRef(g.webroot + '/test_revert_version')
  843. : URIRef('urn:demo:p1')
  844. : URIRef('urn:demo:o1')
  845. ]
  846. self.client.put(rsrc_path, headers={
  847. 'content-type': 'text/turtle'}, data=payload2)
  848. v2_rsp = self.client.get(rsrc_path)
  849. v2_gr = Graph().parse(data=v2_rsp.data, format='turtle')
  850. assert v2_gr[
  851. URIRef(g.webroot + '/test_revert_version')
  852. : URIRef('urn:demo:p1')
  853. : URIRef('urn:demo:o2')
  854. ]
  855. self.client.patch(rsrc_path + '/fcr:versions/v1')
  856. revert_rsp = self.client.get(rsrc_path)
  857. revert_gr = Graph().parse(data=revert_rsp.data, format='turtle')
  858. assert revert_gr[
  859. URIRef(g.webroot + '/test_revert_version')
  860. : URIRef('urn:demo:p1')
  861. : URIRef('urn:demo:o1')
  862. ]
  863. def test_resurrection(self):
  864. """
  865. Delete and then resurrect a resource.
  866. Make sure that the resource is resurrected to the latest version.
  867. """
  868. path = '/ldp/test_lazarus'
  869. self.client.put(path)
  870. self.client.post(path + '/fcr:versions', headers={'slug': 'v1'})
  871. self.client.put(
  872. path, headers={'content-type': 'text/turtle'},
  873. data=b'<> <urn:demo:p1> <urn:demo:o1> .')
  874. self.client.post(path + '/fcr:versions', headers={'slug': 'v2'})
  875. self.client.put(
  876. path, headers={'content-type': 'text/turtle'},
  877. data=b'<> <urn:demo:p1> <urn:demo:o2> .')
  878. self.client.delete(path)
  879. assert self.client.get(path).status_code == 410
  880. self.client.post(path + '/fcr:tombstone')
  881. laz_data = self.client.get(path).data
  882. laz_gr = Graph().parse(data=laz_data, format='turtle')
  883. assert laz_gr[
  884. URIRef(g.webroot + '/test_lazarus')
  885. : URIRef('urn:demo:p1')
  886. : URIRef('urn:demo:o2')
  887. ]