test_ldp.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452
  1. import pdb
  2. import pytest
  3. from base64 import b64encode
  4. from hashlib import sha1
  5. from uuid import uuid4
  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(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(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(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(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 TestDigestHeaders:
  548. """
  549. Test Digest and ETag headers.
  550. """
  551. def test_etag_digest(self):
  552. """
  553. Verify ETag and Digest headers on creation.
  554. The headers must correspond to the SHA1 checksum of the binary content.
  555. """
  556. uid = '/test_etag1'
  557. path = '/ldp' + uid
  558. content = uuid4().bytes
  559. content_cksum = sha1(content)
  560. put_rsp = self.client.put(
  561. path, data=content, headers={'content-type': 'text/plain'})
  562. assert content_cksum.hexdigest() in \
  563. put_rsp.headers.get('etag').split(',')
  564. assert put_rsp.headers.get('digest') == \
  565. 'SHA1=' + b64encode(content_cksum.digest()).decode()
  566. get_rsp = self.client.get(path)
  567. assert content_cksum.hexdigest() in \
  568. put_rsp.headers.get('etag').split(',')
  569. assert get_rsp.headers.get('digest') == \
  570. 'SHA1=' + b64encode(content_cksum.digest()).decode()
  571. def test_etag_ident(self):
  572. """
  573. Verify that two resources with the same content yield identical ETags.
  574. """
  575. path1 = f'/ldp/{uuid4()}'
  576. path2 = f'/ldp/{uuid4()}'
  577. content = uuid4().bytes
  578. content_cksum = sha1(content)
  579. self.client.put(
  580. path1, data=content, headers={'content-type': 'text/plain'})
  581. self.client.put(
  582. path2, data=content, headers={'content-type': 'text/plain'})
  583. get_rsp1 = self.client.get(path1)
  584. get_rsp2 = self.client.get(path2)
  585. assert get_rsp1.headers.get('etag') == get_rsp2.headers.get('etag')
  586. assert get_rsp1.headers.get('digest') == get_rsp2.headers.get('digest')
  587. def test_etag_diff(self):
  588. """
  589. Verify that two resources with different content yield different ETags.
  590. """
  591. path1 = f'/ldp/{uuid4()}'
  592. path2 = f'/ldp/{uuid4()}'
  593. content1 = b'some interesting content.'
  594. content_cksum1 = sha1(content1)
  595. content2 = b'Some great content.'
  596. content_cksum2 = sha1(content2)
  597. self.client.put(
  598. path1, data=content1, headers={'content-type': 'text/plain'})
  599. self.client.put(
  600. path2, data=content2, headers={'content-type': 'text/plain'})
  601. get_rsp1 = self.client.get(path1)
  602. get_rsp2 = self.client.get(path2)
  603. assert get_rsp1.headers.get('etag') != get_rsp2.headers.get('etag')
  604. assert get_rsp1.headers.get('digest') != get_rsp2.headers.get('digest')
  605. def test_etag_update(self):
  606. """
  607. Verify that ETag and digest change when the resource is updated.
  608. The headers should NOT change if the same binary content is
  609. re-submitted.
  610. """
  611. path = f'/ldp/{uuid4()}'
  612. content1 = uuid4().bytes
  613. content_cksum1 = sha1(content1)
  614. content2 = uuid4().bytes
  615. content_cksum2 = sha1(content2)
  616. self.client.put(
  617. path, data=content1, headers={'content-type': 'text/plain'})
  618. get_rsp = self.client.get(path)
  619. assert content_cksum1.hexdigest() == \
  620. get_rsp.headers.get('etag').strip('"')
  621. assert get_rsp.headers.get('digest') == \
  622. 'SHA1=' + b64encode(content_cksum1.digest()).decode()
  623. put_rsp = self.client.put(
  624. path, data=content2, headers={'content-type': 'text/plain'})
  625. assert content_cksum2.hexdigest() == \
  626. put_rsp.headers.get('etag').strip('"')
  627. assert put_rsp.headers.get('digest') == \
  628. 'SHA1=' + b64encode(content_cksum2.digest()).decode()
  629. get_rsp = self.client.get(path)
  630. assert content_cksum2.hexdigest() == \
  631. get_rsp.headers.get('etag').strip('"')
  632. assert get_rsp.headers.get('digest') == \
  633. 'SHA1=' + b64encode(content_cksum2.digest()).decode()
  634. def test_etag_rdf(self):
  635. """
  636. Verify that LDP-RS resources don't get an ETag.
  637. TODO This is by design for now; when a reliable hashing method
  638. for a graph is devised, this test should change.
  639. """
  640. path = '/ldp/test_etag_rdf1'
  641. put_rsp = self.client.put(path)
  642. assert not put_rsp.headers.get('etag')
  643. assert not put_rsp.headers.get('digest')
  644. get_rsp = self.client.get(path)
  645. assert not get_rsp.headers.get('etag')
  646. assert not get_rsp.headers.get('digest')
  647. def test_if_match_get(self):
  648. """
  649. Test the If-Match header on GET requests.
  650. Test providing single and multiple ETags.
  651. """
  652. path = '/ldp/test_if_match1'
  653. content = uuid4().bytes
  654. content_cksum = sha1(content).hexdigest()
  655. bogus_cksum = uuid4().hex
  656. self.client.put(
  657. path, data=content, headers={'content-type': 'text/plain'})
  658. get_rsp1 = self.client.get(path, headers={
  659. 'if-match': f'"{content_cksum}"'})
  660. assert get_rsp1.status_code == 200
  661. get_rsp2 = self.client.get(path, headers={
  662. 'if-match': f'"{bogus_cksum}"'})
  663. assert get_rsp2.status_code == 412
  664. get_rsp3 = self.client.get(path, headers={
  665. 'if-match': f'"{content_cksum}", "{bogus_cksum}"'})
  666. assert get_rsp3.status_code == 200
  667. def test_if_match_put(self):
  668. """
  669. Test the If-Match header on PUT requests.
  670. Test providing single and multiple ETags.
  671. """
  672. path = '/ldp/test_if_match1'
  673. content = uuid4().bytes
  674. content_cksum = sha1(content).hexdigest()
  675. bogus_cksum = uuid4().hex
  676. get_rsp = self.client.get(path)
  677. old_cksum = get_rsp.headers.get('etag')
  678. put_rsp1 = self.client.put(path, data=content, headers={
  679. 'if-match': f'"{content_cksum}"'})
  680. assert put_rsp1.status_code == 412
  681. put_rsp2 = self.client.put(path, data=content, headers={
  682. 'if-match': f'"{content_cksum}", "{bogus_cksum}"'})
  683. assert put_rsp2.status_code == 412
  684. put_rsp3 = self.client.put(path, data=content, headers={
  685. 'if-match': f'"{old_cksum}", "{bogus_cksum}"'})
  686. assert put_rsp3.status_code == 204
  687. # Now contents have changed.
  688. put_rsp4 = self.client.put(path, data=content, headers={
  689. 'if-match': f'"{old_cksum}"'})
  690. assert put_rsp4.status_code == 412
  691. put_rsp5 = self.client.put(path, data=content, headers={
  692. 'if-match': f'"{content_cksum}"'})
  693. assert put_rsp5.status_code == 204
  694. # Exactly the same content was uploaded, so the ETag should not have
  695. # changed.
  696. put_rsp6 = self.client.put(path, data=content, headers={
  697. 'if-match': f'"{content_cksum}"'})
  698. assert put_rsp6.status_code == 204
  699. # Catch-all: Proceed if resource exists at the given location.
  700. put_rsp7 = self.client.put(path, data=content, headers={
  701. 'if-match': '*'})
  702. assert put_rsp7.status_code == 204
  703. # This is wrong syntax. It will not update because the literal asterisk
  704. # won't match.
  705. put_rsp8 = self.client.put(path, data=content, headers={
  706. 'if-match': '"*"'})
  707. assert put_rsp8.status_code == 412
  708. self.client.delete(path, headers={'Prefer': 'no-tombstone'})
  709. put_rsp9 = self.client.put(path, data=content, headers={
  710. 'if-match': '*'})
  711. assert put_rsp9.status_code == 412
  712. def test_if_none_match_get(self):
  713. """
  714. Test the If-None-Match header on GET requests.
  715. Test providing single and multiple ETags.
  716. """
  717. path = '/ldp/test_if_none_match1'
  718. content = uuid4().bytes
  719. content_cksum = sha1(content).hexdigest()
  720. bogus_cksum = uuid4().hex
  721. self.client.put(
  722. path, data=content, headers={'content-type': 'text/plain'})
  723. get_rsp1 = self.client.get(path, headers={
  724. 'if-none-match': f'"{content_cksum}"'})
  725. assert get_rsp1.status_code == 304
  726. get_rsp2 = self.client.get(path, headers={
  727. 'if-none-match': f'"{bogus_cksum}"'})
  728. assert get_rsp2.status_code == 200
  729. get_rsp3 = self.client.get(path, headers={
  730. 'if-none-match': f'"{content_cksum}", "{bogus_cksum}"'})
  731. assert get_rsp3.status_code == 304
  732. def test_if_none_match_put(self):
  733. """
  734. Test the If-None-Match header on PUT requests.
  735. Test providing single and multiple ETags.
  736. Uses a previously created resource.
  737. """
  738. path = '/ldp/test_if_none_match1'
  739. content = uuid4().bytes
  740. content_cksum = sha1(content).hexdigest()
  741. bogus_cksum = uuid4().hex
  742. get_rsp = self.client.get(path)
  743. old_cksum = get_rsp.headers.get('etag')
  744. put_rsp1 = self.client.put(path, data=content, headers={
  745. 'if-none-match': f'"{old_cksum}"'})
  746. assert put_rsp1.status_code == 412
  747. put_rsp2 = self.client.put(path, data=content, headers={
  748. 'if-none-match': f'"{old_cksum}", "{bogus_cksum}"'})
  749. assert put_rsp2.status_code == 412
  750. put_rsp3 = self.client.put(path, data=content, headers={
  751. 'if-none-match': f'"{bogus_cksum}"'})
  752. assert put_rsp3.status_code == 204
  753. # Now contents have changed.
  754. put_rsp4 = self.client.put(path, data=content, headers={
  755. 'if-none-match': f'"{content_cksum}"'})
  756. assert put_rsp4.status_code == 412
  757. put_rsp5 = self.client.put(path, data=content, headers={
  758. 'if-none-match': f'"{old_cksum}"'})
  759. assert put_rsp5.status_code == 204
  760. # Catch-all: fail if any resource exists at the given location.
  761. put_rsp6 = self.client.put(path, data=content, headers={
  762. 'if-none-match': '*'})
  763. assert put_rsp6.status_code == 412
  764. self.client.delete(path, headers={'Prefer': 'no-tombstone'})
  765. put_rsp7 = self.client.put(path, data=content, headers={
  766. 'if-none-match': '*'})
  767. assert put_rsp7.status_code == 201
  768. # This is wrong syntax. It will update because the literal asterisk
  769. # won't match.
  770. put_rsp8 = self.client.put(path, data=content, headers={
  771. 'if-none-match': '"*"'})
  772. assert put_rsp8.status_code == 204
  773. @pytest.mark.skip(reason='Not yet implemented.')
  774. def test_if_modified_since(self):
  775. """
  776. Test various uses of the If-Modified-Since header.
  777. """
  778. pass
  779. @pytest.mark.skip(reason='Not yet implemented.')
  780. def test_if_unmodified_since(self):
  781. """
  782. Test various uses of the If-Unmodified-Since header.
  783. """
  784. pass
  785. @pytest.mark.usefixtures('client_class')
  786. @pytest.mark.usefixtures('db')
  787. class TestPrefHeader:
  788. """
  789. Test various combinations of `Prefer` header.
  790. """
  791. @pytest.fixture(scope='class')
  792. def cont_structure(self):
  793. """
  794. Create a container structure to be used for subsequent requests.
  795. """
  796. parent_path = '/ldp/test_parent'
  797. self.client.put(parent_path)
  798. self.client.put(parent_path + '/child1')
  799. self.client.put(parent_path + '/child2')
  800. self.client.put(parent_path + '/child3')
  801. return {
  802. 'path' : parent_path,
  803. 'response' : self.client.get(parent_path),
  804. }
  805. def test_put_prefer_handling(self, random_uuid):
  806. """
  807. Trying to PUT an existing resource should:
  808. - Return a 204 if the payload is empty
  809. - Return a 204 if the payload is RDF, server-managed triples are
  810. included and the 'Prefer' header is set to 'handling=lenient'
  811. - Return a 412 (ServerManagedTermError) if the payload is RDF,
  812. server-managed triples are included and handling is set to 'strict',
  813. or not set.
  814. """
  815. path = '/ldp/put_pref_header01'
  816. assert self.client.put(path).status_code == 201
  817. assert self.client.get(path).status_code == 200
  818. assert self.client.put(path).status_code == 204
  819. # Default handling is strict.
  820. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  821. rsp_default = self.client.put(
  822. path,
  823. headers={
  824. 'Content-Type' : 'text/turtle',
  825. },
  826. data=f
  827. )
  828. assert rsp_default.status_code == 412
  829. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  830. rsp_len = self.client.put(
  831. path,
  832. headers={
  833. 'Prefer' : 'handling=lenient',
  834. 'Content-Type' : 'text/turtle',
  835. },
  836. data=f
  837. )
  838. assert rsp_len.status_code == 204
  839. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  840. rsp_strict = self.client.put(
  841. path,
  842. headers={
  843. 'Prefer' : 'handling=strict',
  844. 'Content-Type' : 'text/turtle',
  845. },
  846. data=f
  847. )
  848. assert rsp_strict.status_code == 412
  849. # @HOLD Embed children is debated.
  850. def _disabled_test_embed_children(self, cont_structure):
  851. """
  852. verify the "embed children" prefer header.
  853. """
  854. self.client.get('/ldp')
  855. parent_path = cont_structure['path']
  856. cont_resp = cont_structure['response']
  857. cont_subject = URIRef(g.webroot + '/test_parent')
  858. #minimal_resp = self.client.get(parent_path, headers={
  859. # 'Prefer' : 'return=minimal',
  860. #})
  861. incl_embed_children_resp = self.client.get(parent_path, headers={
  862. 'Prefer' : 'return=representation; include={}'\
  863. .format(Ldpr.EMBED_CHILD_RES_URI),
  864. })
  865. omit_embed_children_resp = self.client.get(parent_path, headers={
  866. 'Prefer' : 'return=representation; omit={}'\
  867. .format(Ldpr.EMBED_CHILD_RES_URI),
  868. })
  869. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  870. incl_gr = Graph().parse(
  871. data=incl_embed_children_resp.data, format='turtle')
  872. omit_gr = Graph().parse(
  873. data=omit_embed_children_resp.data, format='turtle')
  874. assert isomorphic(omit_gr, default_gr)
  875. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  876. assert len(children) == 3
  877. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  878. for child_uri in children:
  879. assert set(incl_gr[ child_uri : : ])
  880. assert not set(omit_gr[ child_uri : : ])
  881. def test_return_children(self, cont_structure):
  882. """
  883. verify the "return children" prefer header.
  884. """
  885. self.client.get('/ldp')
  886. parent_path = cont_structure['path']
  887. cont_resp = cont_structure['response']
  888. cont_subject = URIRef(g.webroot + '/test_parent')
  889. incl_children_resp = self.client.get(parent_path, headers={
  890. 'Prefer' : 'return=representation; include={}'\
  891. .format(Ldpr.RETURN_CHILD_RES_URI),
  892. })
  893. omit_children_resp = self.client.get(parent_path, headers={
  894. 'Prefer' : 'return=representation; omit={}'\
  895. .format(Ldpr.RETURN_CHILD_RES_URI),
  896. })
  897. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  898. incl_gr = Graph().parse(data=incl_children_resp.data, format='turtle')
  899. omit_gr = Graph().parse(data=omit_children_resp.data, format='turtle')
  900. assert isomorphic(incl_gr, default_gr)
  901. children = incl_gr[cont_subject : nsc['ldp'].contains]
  902. for child_uri in children:
  903. assert not omit_gr[cont_subject : nsc['ldp'].contains : child_uri]
  904. def test_inbound_rel(self, cont_structure):
  905. """
  906. verify the "inbound relationships" prefer header.
  907. """
  908. self.client.put('/ldp/test_target')
  909. data = '<> <http://ex.org/ns#shoots> <{}> .'.format(
  910. g.webroot + '/test_target')
  911. self.client.put('/ldp/test_shooter', data=data,
  912. headers={'Content-Type': 'text/turtle'})
  913. cont_resp = self.client.get('/ldp/test_target')
  914. incl_inbound_resp = self.client.get('/ldp/test_target', headers={
  915. 'Prefer' : 'return=representation; include="{}"'\
  916. .format(Ldpr.RETURN_INBOUND_REF_URI),
  917. })
  918. omit_inbound_resp = self.client.get('/ldp/test_target', headers={
  919. 'Prefer' : 'return=representation; omit="{}"'\
  920. .format(Ldpr.RETURN_INBOUND_REF_URI),
  921. })
  922. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  923. incl_gr = Graph().parse(data=incl_inbound_resp.data, format='turtle')
  924. omit_gr = Graph().parse(data=omit_inbound_resp.data, format='turtle')
  925. subject = URIRef(g.webroot + '/test_target')
  926. inbd_subject = URIRef(g.webroot + '/test_shooter')
  927. assert isomorphic(omit_gr, default_gr)
  928. assert len(set(incl_gr[inbd_subject : : ])) == 1
  929. assert incl_gr[
  930. inbd_subject : URIRef('http://ex.org/ns#shoots') : subject]
  931. assert not len(set(omit_gr[inbd_subject : :]))
  932. def test_srv_mgd_triples(self, cont_structure):
  933. """
  934. verify the "server managed triples" prefer header.
  935. """
  936. self.client.get('/ldp')
  937. parent_path = cont_structure['path']
  938. cont_resp = cont_structure['response']
  939. cont_subject = URIRef(g.webroot + '/test_parent')
  940. incl_srv_mgd_resp = self.client.get(parent_path, headers={
  941. 'Prefer' : 'return=representation; include={}'\
  942. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  943. })
  944. omit_srv_mgd_resp = self.client.get(parent_path, headers={
  945. 'Prefer' : 'return=representation; omit={}'\
  946. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  947. })
  948. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  949. incl_gr = Graph().parse(data=incl_srv_mgd_resp.data, format='turtle')
  950. omit_gr = Graph().parse(data=omit_srv_mgd_resp.data, format='turtle')
  951. assert isomorphic(incl_gr, default_gr)
  952. for pred in {
  953. nsc['fcrepo'].created,
  954. nsc['fcrepo'].createdBy,
  955. nsc['fcrepo'].lastModified,
  956. nsc['fcrepo'].lastModifiedBy,
  957. nsc['ldp'].contains,
  958. }:
  959. assert set(incl_gr[ cont_subject : pred : ])
  960. assert not set(omit_gr[ cont_subject : pred : ])
  961. for type in {
  962. nsc['fcrepo'].Resource,
  963. nsc['ldp'].Container,
  964. nsc['ldp'].Resource,
  965. }:
  966. assert incl_gr[ cont_subject : RDF.type : type ]
  967. assert not omit_gr[ cont_subject : RDF.type : type ]
  968. def test_delete_no_tstone(self):
  969. """
  970. Test the `no-tombstone` Prefer option.
  971. """
  972. self.client.put('/ldp/test_delete_no_tstone01')
  973. self.client.put('/ldp/test_delete_no_tstone01/a')
  974. self.client.delete('/ldp/test_delete_no_tstone01', headers={
  975. 'prefer' : 'no-tombstone'})
  976. resp = self.client.get('/ldp/test_delete_no_tstone01')
  977. assert resp.status_code == 404
  978. child_resp = self.client.get('/ldp/test_delete_no_tstone01/a')
  979. assert child_resp.status_code == 404
  980. #@pytest.mark.usefixtures('client_class')
  981. #@pytest.mark.usefixtures('db')
  982. #class TestDigest:
  983. # """
  984. # Test digest and ETag handling.
  985. # """
  986. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  987. # def test_digest_post(self):
  988. # """
  989. # Test ``Digest`` and ``ETag`` headers on resource POST.
  990. # """
  991. # resp = self.client.post('/ldp/')
  992. # assert 'Digest' in resp.headers
  993. # assert 'ETag' in resp.headers
  994. # assert (
  995. # b64encode(bytes.fromhex(
  996. # resp.headers['ETag'].replace('W/', '')
  997. # )).decode('ascii') ==
  998. # resp.headers['Digest'].replace('SHA256=', ''))
  999. #
  1000. #
  1001. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  1002. # def test_digest_put(self):
  1003. # """
  1004. # Test ``Digest`` and ``ETag`` headers on resource PUT.
  1005. # """
  1006. # resp_put = self.client.put('/ldp/test_digest_put')
  1007. # assert 'Digest' in resp_put.headers
  1008. # assert 'ETag' in resp_put.headers
  1009. # assert (
  1010. # b64encode(bytes.fromhex(
  1011. # resp_put.headers['ETag'].replace('W/', '')
  1012. # )).decode('ascii') ==
  1013. # resp_put.headers['Digest'].replace('SHA256=', ''))
  1014. #
  1015. # resp_get = self.client.get('/ldp/test_digest_put')
  1016. # assert 'Digest' in resp_get.headers
  1017. # assert 'ETag' in resp_get.headers
  1018. # assert (
  1019. # b64encode(bytes.fromhex(
  1020. # resp_get.headers['ETag'].replace('W/', '')
  1021. # )).decode('ascii') ==
  1022. # resp_get.headers['Digest'].replace('SHA256=', ''))
  1023. #
  1024. #
  1025. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  1026. # def test_digest_patch(self):
  1027. # """
  1028. # Verify that the digest and ETag change on resource change.
  1029. # """
  1030. # path = '/ldp/test_digest_patch'
  1031. # self.client.put(path)
  1032. # rsp1 = self.client.get(path)
  1033. #
  1034. # self.client.patch(
  1035. # path, data=b'DELETE {} INSERT {<> a <http://ex.org/Test> .} '
  1036. # b'WHERE {}',
  1037. # headers={'Content-Type': 'application/sparql-update'})
  1038. # rsp2 = self.client.get(path)
  1039. #
  1040. # assert rsp1.headers['ETag'] != rsp2.headers['ETag']
  1041. # assert rsp1.headers['Digest'] != rsp2.headers['Digest']
  1042. @pytest.mark.usefixtures('client_class')
  1043. @pytest.mark.usefixtures('db')
  1044. class TestVersion:
  1045. """
  1046. Test version creation, retrieval and deletion.
  1047. """
  1048. def test_create_versions(self):
  1049. """
  1050. Test that POSTing multiple times to fcr:versions creates the
  1051. 'hasVersions' triple and yields multiple version snapshots.
  1052. """
  1053. self.client.put('/ldp/test_version')
  1054. create_rsp = self.client.post('/ldp/test_version/fcr:versions')
  1055. assert create_rsp.status_code == 201
  1056. rsrc_rsp = self.client.get('/ldp/test_version')
  1057. rsrc_gr = Graph().parse(data=rsrc_rsp.data, format='turtle')
  1058. assert len(set(rsrc_gr[: nsc['fcrepo'].hasVersions :])) == 1
  1059. info_rsp = self.client.get('/ldp/test_version/fcr:versions')
  1060. assert info_rsp.status_code == 200
  1061. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  1062. assert len(set(info_gr[: nsc['fcrepo'].hasVersion :])) == 1
  1063. self.client.post('/ldp/test_version/fcr:versions')
  1064. info2_rsp = self.client.get('/ldp/test_version/fcr:versions')
  1065. info2_gr = Graph().parse(data=info2_rsp.data, format='turtle')
  1066. assert len(set(info2_gr[: nsc['fcrepo'].hasVersion :])) == 2
  1067. def test_version_with_slug(self):
  1068. """
  1069. Test a version with a slug.
  1070. """
  1071. self.client.put('/ldp/test_version_slug')
  1072. create_rsp = self.client.post('/ldp/test_version_slug/fcr:versions',
  1073. headers={'slug' : 'v1'})
  1074. new_ver_uri = create_rsp.headers['Location']
  1075. assert new_ver_uri == g.webroot + '/test_version_slug/fcr:versions/v1'
  1076. info_rsp = self.client.get('/ldp/test_version_slug/fcr:versions')
  1077. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  1078. assert info_gr[
  1079. URIRef(new_ver_uri) :
  1080. nsc['fcrepo'].hasVersionLabel :
  1081. Literal('v1')]
  1082. def test_dupl_version(self):
  1083. """
  1084. Make sure that two POSTs with the same slug result in two different
  1085. versions.
  1086. """
  1087. path = '/ldp/test_duplicate_slug'
  1088. self.client.put(path)
  1089. v1_rsp = self.client.post(path + '/fcr:versions',
  1090. headers={'slug' : 'v1'})
  1091. v1_uri = v1_rsp.headers['Location']
  1092. dup_rsp = self.client.post(path + '/fcr:versions',
  1093. headers={'slug' : 'v1'})
  1094. dup_uri = dup_rsp.headers['Location']
  1095. assert v1_uri != dup_uri
  1096. @pytest.mark.skip(
  1097. reason='TODO Reverting from version and resurrecting is not fully '
  1098. 'functional.')
  1099. def test_revert_version(self):
  1100. """
  1101. Take a version snapshot, update a resource, and then revert to the
  1102. previous vresion.
  1103. """
  1104. rsrc_path = '/ldp/test_revert_version'
  1105. payload1 = '<> <urn:demo:p1> <urn:demo:o1> .'
  1106. payload2 = '<> <urn:demo:p1> <urn:demo:o2> .'
  1107. self.client.put(rsrc_path, headers={
  1108. 'content-type': 'text/turtle'}, data=payload1)
  1109. self.client.post(
  1110. rsrc_path + '/fcr:versions', headers={'slug': 'v1'})
  1111. v1_rsp = self.client.get(rsrc_path)
  1112. v1_gr = Graph().parse(data=v1_rsp.data, format='turtle')
  1113. assert v1_gr[
  1114. URIRef(g.webroot + '/test_revert_version')
  1115. : URIRef('urn:demo:p1')
  1116. : URIRef('urn:demo:o1')
  1117. ]
  1118. self.client.put(rsrc_path, headers={
  1119. 'content-type': 'text/turtle'}, data=payload2)
  1120. v2_rsp = self.client.get(rsrc_path)
  1121. v2_gr = Graph().parse(data=v2_rsp.data, format='turtle')
  1122. assert v2_gr[
  1123. URIRef(g.webroot + '/test_revert_version')
  1124. : URIRef('urn:demo:p1')
  1125. : URIRef('urn:demo:o2')
  1126. ]
  1127. self.client.patch(rsrc_path + '/fcr:versions/v1')
  1128. revert_rsp = self.client.get(rsrc_path)
  1129. revert_gr = Graph().parse(data=revert_rsp.data, format='turtle')
  1130. assert revert_gr[
  1131. URIRef(g.webroot + '/test_revert_version')
  1132. : URIRef('urn:demo:p1')
  1133. : URIRef('urn:demo:o1')
  1134. ]
  1135. def test_resurrection(self):
  1136. """
  1137. Delete and then resurrect a resource.
  1138. Make sure that the resource is resurrected to the latest version.
  1139. """
  1140. path = '/ldp/test_lazarus'
  1141. self.client.put(path)
  1142. self.client.post(path + '/fcr:versions', headers={'slug': 'v1'})
  1143. self.client.put(
  1144. path, headers={'content-type': 'text/turtle'},
  1145. data=b'<> <urn:demo:p1> <urn:demo:o1> .')
  1146. self.client.post(path + '/fcr:versions', headers={'slug': 'v2'})
  1147. self.client.put(
  1148. path, headers={'content-type': 'text/turtle'},
  1149. data=b'<> <urn:demo:p1> <urn:demo:o2> .')
  1150. self.client.delete(path)
  1151. assert self.client.get(path).status_code == 410
  1152. self.client.post(path + '/fcr:tombstone')
  1153. laz_data = self.client.get(path).data
  1154. laz_gr = Graph().parse(data=laz_data, format='turtle')
  1155. assert laz_gr[
  1156. URIRef(g.webroot + '/test_lazarus')
  1157. : URIRef('urn:demo:p1')
  1158. : URIRef('urn:demo:o2')
  1159. ]