test_ldp.py 35 KB

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