test_ldp.py 39 KB

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