test_3_0_ldp.py 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931
  1. import hashlib
  2. import pdb
  3. import pytest
  4. from base64 import b64encode
  5. from datetime import timedelta
  6. from hashlib import sha1, sha256, blake2b
  7. from uuid import uuid4
  8. from werkzeug.http import http_date
  9. import arrow
  10. from flask import g
  11. from rdflib import Graph
  12. from rdflib.compare import isomorphic
  13. from rdflib.namespace import RDF
  14. from rdflib.term import Literal, URIRef
  15. from lakesuperior import env
  16. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  17. from lakesuperior.model.ldp.ldpr import Ldpr
  18. digest_algo = env.app_globals.config['application']['uuid']['algo']
  19. @pytest.fixture(scope='module')
  20. def random_uuid():
  21. return str(uuid4())
  22. @pytest.mark.usefixtures('client_class')
  23. @pytest.mark.usefixtures('db')
  24. class TestLdp:
  25. """
  26. Test HTTP interaction with LDP endpoint.
  27. """
  28. def test_get_root_node(self):
  29. """
  30. Get the root node from two different endpoints.
  31. The test triplestore must be initialized, hence the `db` fixture.
  32. """
  33. ldp_resp = self.client.get('/ldp')
  34. rest_resp = self.client.get('/rest')
  35. assert ldp_resp.status_code == 200
  36. assert rest_resp.status_code == 200
  37. def test_put_empty_resource(self, random_uuid):
  38. """
  39. Check response headers for a PUT operation with empty payload.
  40. """
  41. resp = self.client.put('/ldp/new_resource')
  42. assert resp.status_code == 201
  43. assert resp.data == bytes(
  44. '{}/new_resource'.format(g.webroot), 'utf-8')
  45. def test_put_existing_resource(self, random_uuid):
  46. """
  47. Trying to PUT an existing resource should return a 204 if the payload
  48. is empty.
  49. """
  50. path = '/ldp/nonidempotent01'
  51. put1_resp = self.client.put(path)
  52. assert put1_resp.status_code == 201
  53. assert self.client.get(path).status_code == 200
  54. put2_resp = self.client.put(path)
  55. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  56. put2_resp = self.client.put(
  57. path, data=f, content_type='text/turtle')
  58. assert put2_resp.status_code == 204
  59. put2_resp = self.client.put(path)
  60. assert put2_resp.status_code == 204
  61. def test_put_tree(self, client):
  62. """
  63. PUT a resource with several path segments.
  64. The test should create intermediate path segments that are LDPCs,
  65. accessible to PUT or POST.
  66. """
  67. path = '/ldp/test_tree/a/b/c/d/e/f/g'
  68. self.client.put(path)
  69. assert self.client.get(path).status_code == 200
  70. assert self.client.get('/ldp/test_tree/a/b/c').status_code == 200
  71. assert self.client.post('/ldp/test_tree/a/b').status_code == 201
  72. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  73. put_int_resp = self.client.put(
  74. 'ldp/test_tree/a', data=f, content_type='text/turtle')
  75. assert put_int_resp.status_code == 204
  76. # @TODO More thorough testing of contents
  77. def test_put_nested_tree(self, client):
  78. """
  79. Verify that containment is set correctly in nested hierarchies.
  80. First put a new hierarchy and verify that the root node is its
  81. container; then put another hierarchy under it and verify that the
  82. first hierarchy is the container of the second one.
  83. """
  84. uuid1 = 'test_nested_tree/a/b/c/d'
  85. uuid2 = uuid1 + '/e/f/g'
  86. path1 = '/ldp/' + uuid1
  87. path2 = '/ldp/' + uuid2
  88. self.client.put(path1)
  89. cont1_data = self.client.get('/ldp').data
  90. gr1 = Graph().parse(data=cont1_data, format='turtle')
  91. assert gr1[ URIRef(g.webroot + '/') : nsc['ldp'].contains : \
  92. URIRef(g.webroot + '/test_nested_tree') ]
  93. self.client.put(path2)
  94. cont2_data = self.client.get(path1).data
  95. gr2 = Graph().parse(data=cont2_data, format='turtle')
  96. assert gr2[ URIRef(g.webroot + '/' + uuid1) : \
  97. nsc['ldp'].contains : \
  98. URIRef(g.webroot + '/' + uuid1 + '/e') ]
  99. def test_put_ldp_rs(self, client):
  100. """
  101. PUT a resource with RDF payload and verify.
  102. """
  103. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  104. self.client.put('/ldp/ldprs01', data=f, content_type='text/turtle')
  105. resp = self.client.get('/ldp/ldprs01',
  106. headers={'accept' : 'text/turtle'})
  107. assert resp.status_code == 200
  108. gr = Graph().parse(data=resp.data, format='text/turtle')
  109. assert URIRef('http://vocab.getty.edu/ontology#Subject') in \
  110. gr.objects(None, RDF.type)
  111. def test_put_ldp_nr(self, rnd_img):
  112. """
  113. PUT a resource with binary payload and verify checksums.
  114. """
  115. rnd_img['content'].seek(0)
  116. resp = self.client.put('/ldp/ldpnr01', data=rnd_img['content'],
  117. headers={
  118. 'Content-Type': 'image/png',
  119. 'Content-Disposition' : 'attachment; filename={}'.format(
  120. rnd_img['filename'])})
  121. assert resp.status_code == 201
  122. resp = self.client.get(
  123. '/ldp/ldpnr01', headers={'accept' : 'image/png'})
  124. assert resp.status_code == 200
  125. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  126. def test_put_ldp_nr_multipart(self, rnd_img):
  127. """
  128. PUT a resource with a multipart/form-data payload.
  129. """
  130. rnd_img['content'].seek(0)
  131. resp = self.client.put(
  132. '/ldp/ldpnr02',
  133. data={
  134. 'file': (
  135. rnd_img['content'], rnd_img['filename'],
  136. 'image/png',
  137. )
  138. }
  139. )
  140. assert resp.status_code == 201
  141. resp = self.client.get(
  142. '/ldp/ldpnr02', headers={'accept' : 'image/png'})
  143. assert resp.status_code == 200
  144. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  145. def test_get_ldp_nr(self, rnd_img):
  146. """
  147. PUT a resource with binary payload and test various retieval methods.
  148. """
  149. uid = '/ldpnr03'
  150. path = '/ldp' + uid
  151. content = b'This is some exciting content.'
  152. resp = self.client.put(path, data=content,
  153. headers={
  154. 'Content-Type': 'text/plain',
  155. 'Content-Disposition' : 'attachment; filename=exciting.txt'})
  156. assert resp.status_code == 201
  157. uri = g.webroot + uid
  158. # Content retrieval methods.
  159. resp_bin1 = self.client.get(path)
  160. assert resp_bin1.status_code == 200
  161. assert resp_bin1.data == content
  162. resp_bin2 = self.client.get(path, headers={'accept' : 'text/plain'})
  163. assert resp_bin2.status_code == 200
  164. assert resp_bin2.data == content
  165. resp_bin3 = self.client.get(path + '/fcr:content')
  166. assert resp_bin3.status_code == 200
  167. assert resp_bin3.data == content
  168. # Metadata retrieval methods.
  169. resp_md1 = self.client.get(path, headers={'accept' : 'text/turtle'})
  170. assert resp_md1.status_code == 200
  171. gr1 = Graph().parse(data=resp_md1.data, format='text/turtle')
  172. assert gr1[ URIRef(uri) : nsc['rdf'].type : nsc['ldp'].Resource]
  173. resp_md2 = self.client.get(path + '/fcr:metadata')
  174. assert resp_md2.status_code == 200
  175. gr2 = Graph().parse(data=resp_md2.data, format='text/turtle')
  176. assert isomorphic(gr1, gr2)
  177. def test_put_mismatched_ldp_rs(self, rnd_img):
  178. """
  179. Verify MIME type / LDP mismatch.
  180. PUT a LDP-RS, then PUT a LDP-NR on the same location and verify it
  181. fails.
  182. """
  183. path = '/ldp/' + str(uuid4())
  184. rnd_img['content'].seek(0)
  185. ldp_nr_resp = self.client.put(path, data=rnd_img['content'],
  186. headers={
  187. 'Content-Disposition' : 'attachment; filename={}'.format(
  188. rnd_img['filename'])})
  189. assert ldp_nr_resp.status_code == 201
  190. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  191. ldp_rs_resp = self.client.put(path, data=f,
  192. content_type='text/turtle')
  193. assert ldp_rs_resp.status_code == 415
  194. def test_put_mismatched_ldp_nr(self, rnd_img):
  195. """
  196. Verify MIME type / LDP mismatch.
  197. PUT a LDP-NR, then PUT a LDP-RS on the same location and verify it
  198. fails.
  199. """
  200. path = '/ldp/' + str(uuid4())
  201. with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
  202. ldp_rs_resp = self.client.put(path, data=f,
  203. content_type='text/turtle')
  204. assert ldp_rs_resp.status_code == 201
  205. rnd_img['content'].seek(0)
  206. ldp_nr_resp = self.client.put(path, data=rnd_img['content'],
  207. headers={
  208. 'Content-Disposition' : 'attachment; filename={}'.format(
  209. rnd_img['filename'])})
  210. assert ldp_nr_resp.status_code == 415
  211. def test_missing_reference(self, client):
  212. """
  213. PUT a resource with RDF payload referencing a non-existing in-repo
  214. resource.
  215. """
  216. self.client.get('/ldp')
  217. data = '''
  218. PREFIX ns: <http://example.org#>
  219. PREFIX res: <http://example-source.org/res/>
  220. <> ns:p1 res:bogus ;
  221. ns:p2 <{0}> ;
  222. ns:p3 <{0}/> ;
  223. ns:p4 <{0}/nonexistent> .
  224. '''.format(g.webroot)
  225. put_rsp = self.client.put('/ldp/test_missing_ref', data=data, headers={
  226. 'content-type': 'text/turtle'})
  227. assert put_rsp.status_code == 201
  228. resp = self.client.get('/ldp/test_missing_ref',
  229. headers={'accept' : 'text/turtle'})
  230. assert resp.status_code == 200
  231. gr = Graph().parse(data=resp.data, format='text/turtle')
  232. assert URIRef('http://example-source.org/res/bogus') in \
  233. gr.objects(None, URIRef('http://example.org#p1'))
  234. assert URIRef(g.webroot + '/') in (
  235. gr.objects(None, URIRef('http://example.org#p2')))
  236. assert URIRef(g.webroot + '/') in (
  237. gr.objects(None, URIRef('http://example.org#p3')))
  238. assert URIRef(g.webroot + '/nonexistent') not in (
  239. gr.objects(None, URIRef('http://example.org#p4')))
  240. def test_post_resource(self, client):
  241. """
  242. Check response headers for a POST operation with empty payload.
  243. """
  244. res = self.client.post('/ldp/')
  245. assert res.status_code == 201
  246. assert 'Location' in res.headers
  247. def test_post_ldp_nr(self, rnd_img):
  248. """
  249. POST a resource with binary payload and verify checksums.
  250. """
  251. rnd_img['content'].seek(0)
  252. resp = self.client.post('/ldp/', data=rnd_img['content'],
  253. headers={
  254. 'slug': 'ldpnr04',
  255. 'Content-Type': 'image/png',
  256. 'Content-Disposition' : 'attachment; filename={}'.format(
  257. rnd_img['filename'])})
  258. assert resp.status_code == 201
  259. resp = self.client.get(
  260. '/ldp/ldpnr04', headers={'accept' : 'image/png'})
  261. assert resp.status_code == 200
  262. assert sha1(resp.data).hexdigest() == rnd_img['hash']
  263. def test_post_slug(self):
  264. """
  265. Verify that a POST with slug results in the expected URI only if the
  266. resource does not exist already.
  267. """
  268. slug01_resp = self.client.post('/ldp', headers={'slug' : 'slug01'})
  269. assert slug01_resp.status_code == 201
  270. assert slug01_resp.headers['location'] == \
  271. g.webroot + '/slug01'
  272. slug02_resp = self.client.post('/ldp', headers={'slug' : 'slug01'})
  273. assert slug02_resp.status_code == 201
  274. assert slug02_resp.headers['location'] != \
  275. g.webroot + '/slug01'
  276. def test_post_404(self):
  277. """
  278. Verify that a POST to a non-existing parent results in a 404.
  279. """
  280. assert self.client.post('/ldp/{}'.format(uuid4()))\
  281. .status_code == 404
  282. def test_post_409(self, rnd_img):
  283. """
  284. Verify that you cannot POST to a binary resource.
  285. """
  286. rnd_img['content'].seek(0)
  287. self.client.put('/ldp/post_409', data=rnd_img['content'], headers={
  288. 'Content-Disposition' : 'attachment; filename={}'.format(
  289. rnd_img['filename'])})
  290. assert self.client.post('/ldp/post_409').status_code == 409
  291. def test_patch_root(self):
  292. """
  293. Test patching root node.
  294. """
  295. path = '/ldp/'
  296. self.client.get(path)
  297. uri = g.webroot + '/'
  298. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  299. resp = self.client.patch(path,
  300. data=data,
  301. headers={'content-type' : 'application/sparql-update'})
  302. assert resp.status_code == 204
  303. resp = self.client.get(path)
  304. gr = Graph().parse(data=resp.data, format='text/turtle')
  305. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Hello') ]
  306. def test_patch(self):
  307. """
  308. Test patching a resource.
  309. """
  310. path = '/ldp/test_patch01'
  311. self.client.put(path)
  312. uri = g.webroot + '/test_patch01'
  313. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  314. resp = self.client.patch(path,
  315. data=data,
  316. headers={'content-type' : 'application/sparql-update'})
  317. assert resp.status_code == 204
  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('Hello') ]
  321. self.client.patch(path,
  322. data=open('tests/data/sparql_update/delete+insert+where.sparql'),
  323. headers={'content-type' : 'application/sparql-update'})
  324. resp = self.client.get(path)
  325. gr = Graph().parse(data=resp.data, format='text/turtle')
  326. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Ciao') ]
  327. def test_patch_no_single_subject(self):
  328. """
  329. Test patching a resource violating the single-subject rule.
  330. """
  331. path = '/ldp/test_patch_ssr'
  332. self.client.put(path)
  333. uri = g.webroot + '/test_patch_ssr'
  334. nossr_qry = 'INSERT { <http://bogus.org> a <urn:ns:A> . } WHERE {}'
  335. abs_qry = 'INSERT {{ <{}> a <urn:ns:A> . }} WHERE {{}}'.format(uri)
  336. frag_qry = 'INSERT {{ <{}#frag> a <urn:ns:A> . }} WHERE {{}}'\
  337. .format(uri)
  338. # @TODO Leave commented until a decision is made about SSR.
  339. assert self.client.patch(
  340. path, data=nossr_qry,
  341. headers={'content-type': 'application/sparql-update'}
  342. ).status_code == 204
  343. assert self.client.patch(
  344. path, data=abs_qry,
  345. headers={'content-type': 'application/sparql-update'}
  346. ).status_code == 204
  347. assert self.client.patch(
  348. path, data=frag_qry,
  349. headers={'content-type': 'application/sparql-update'}
  350. ).status_code == 204
  351. def test_patch_ldp_nr_metadata(self):
  352. """
  353. Test patching a LDP-NR metadata resource from the fcr:metadata URI.
  354. """
  355. path = '/ldp/ldpnr01'
  356. with open('tests/data/sparql_update/simple_insert.sparql') as data:
  357. self.client.patch(path + '/fcr:metadata',
  358. data=data,
  359. headers={'content-type' : 'application/sparql-update'})
  360. resp = self.client.get(path + '/fcr:metadata')
  361. assert resp.status_code == 200
  362. uri = g.webroot + '/ldpnr01'
  363. gr = Graph().parse(data=resp.data, format='text/turtle')
  364. assert gr[URIRef(uri) : nsc['dc'].title : Literal('Hello')]
  365. with open(
  366. 'tests/data/sparql_update/delete+insert+where.sparql') as data:
  367. patch_resp = self.client.patch(path + '/fcr:metadata',
  368. data=data,
  369. headers={'content-type' : 'application/sparql-update'})
  370. assert patch_resp.status_code == 204
  371. resp = self.client.get(path + '/fcr:metadata')
  372. assert resp.status_code == 200
  373. gr = Graph().parse(data=resp.data, format='text/turtle')
  374. assert gr[ URIRef(uri) : nsc['dc'].title : Literal('Ciao') ]
  375. def test_patch_ldpnr(self):
  376. """
  377. Verify that a direct PATCH to a LDP-NR results in a 415.
  378. """
  379. with open(
  380. 'tests/data/sparql_update/delete+insert+where.sparql') as data:
  381. patch_resp = self.client.patch('/ldp/ldpnr01',
  382. data=data,
  383. headers={'content-type': 'application/sparql-update'})
  384. assert patch_resp.status_code == 415
  385. def test_patch_invalid_mimetype(self, rnd_img):
  386. """
  387. Verify that a PATCH using anything other than an
  388. `application/sparql-update` MIME type results in an error.
  389. """
  390. self.client.put('/ldp/test_patch_invalid_mimetype')
  391. rnd_img['content'].seek(0)
  392. ldpnr_resp = self.client.patch('/ldp/ldpnr01/fcr:metadata',
  393. data=rnd_img,
  394. headers={'content-type' : 'image/jpeg'})
  395. ldprs_resp = self.client.patch('/ldp/test_patch_invalid_mimetype',
  396. data=b'Hello, I\'m not a SPARQL update.',
  397. headers={'content-type' : 'text/plain'})
  398. assert ldprs_resp.status_code == ldpnr_resp.status_code == 415
  399. def test_patch_srv_mgd_pred(self, rnd_img):
  400. """
  401. Verify that adding or removing a server-managed predicate fails.
  402. """
  403. uid = '/test_patch_sm_pred'
  404. path = f'/ldp{uid}'
  405. self.client.put(path)
  406. self.client.put(path + '/child1')
  407. uri = g.webroot + uid
  408. ins_qry1 = f'INSERT {{ <> <{nsc["ldp"].contains}> <http://bogus.com/ext1> . }} WHERE {{}}'
  409. ins_qry2 = (
  410. f'INSERT {{ <> <{nsc["fcrepo"].created}>'
  411. f'"2019-04-01T05:57:36.899033+00:00"^^<{nsc["xsd"].dateTime}> . }}'
  412. 'WHERE {}'
  413. )
  414. # The following won't change the graph so it does not raise an error.
  415. ins_qry3 = f'INSERT {{ <> a <{nsc["ldp"].Container}> . }} WHERE {{}}'
  416. del_qry1 = (
  417. f'DELETE {{ <> <{nsc["ldp"].contains}> ?o . }} '
  418. f'WHERE {{ <> <{nsc["ldp"].contains}> ?o . }}'
  419. )
  420. del_qry2 = f'DELETE {{ <> a <{nsc["ldp"].Container}> . }} WHERE {{}}'
  421. # No-op as ins_qry3
  422. del_qry3 = (
  423. f'DELETE {{ <> a <{nsc["ldp"].DirectContainer}> .}} '
  424. 'WHERE {}'
  425. )
  426. assert self.client.patch(
  427. path, data=ins_qry1,
  428. headers={'content-type': 'application/sparql-update'}
  429. ).status_code == 412
  430. assert self.client.patch(
  431. path, data=ins_qry1,
  432. headers={
  433. 'content-type': 'application/sparql-update',
  434. 'prefer': 'handling=lenient',
  435. }
  436. ).status_code == 204
  437. assert self.client.patch(
  438. path, data=ins_qry2,
  439. headers={'content-type': 'application/sparql-update'}
  440. ).status_code == 412
  441. assert self.client.patch(
  442. path, data=ins_qry2,
  443. headers={
  444. 'content-type': 'application/sparql-update',
  445. 'prefer': 'handling=lenient',
  446. }
  447. ).status_code == 204
  448. assert self.client.patch(
  449. path, data=ins_qry3,
  450. headers={'content-type': 'application/sparql-update'}
  451. ).status_code == 204
  452. assert self.client.patch(
  453. path, data=del_qry1,
  454. headers={'content-type': 'application/sparql-update'}
  455. ).status_code == 412
  456. assert self.client.patch(
  457. path, data=del_qry1,
  458. headers={
  459. 'content-type': 'application/sparql-update',
  460. 'prefer': 'handling=lenient',
  461. }
  462. ).status_code == 204
  463. assert self.client.patch(
  464. path, data=del_qry2,
  465. headers={'content-type': 'application/sparql-update'}
  466. ).status_code == 412
  467. assert self.client.patch(
  468. path, data=ins_qry2,
  469. headers={
  470. 'content-type': 'application/sparql-update',
  471. 'prefer': 'handling=lenient',
  472. }
  473. ).status_code == 204
  474. assert self.client.patch(
  475. path, data=del_qry3,
  476. headers={'content-type': 'application/sparql-update'}
  477. ).status_code == 204
  478. def test_delete(self):
  479. """
  480. Test delete response codes.
  481. """
  482. self.client.put('/ldp/test_delete01')
  483. delete_resp = self.client.delete('/ldp/test_delete01')
  484. assert delete_resp.status_code == 204
  485. bogus_delete_resp = self.client.delete('/ldp/test_delete101')
  486. assert bogus_delete_resp.status_code == 404
  487. def test_tombstone(self):
  488. """
  489. Test tombstone behaviors.
  490. For POST on a tombstone, check `test_resurrection`.
  491. """
  492. tstone_resp = self.client.get('/ldp/test_delete01')
  493. assert tstone_resp.status_code == 410
  494. assert tstone_resp.headers['Link'] == \
  495. '<{}/test_delete01/fcr:tombstone>; rel="hasTombstone"'\
  496. .format(g.webroot)
  497. tstone_path = '/ldp/test_delete01/fcr:tombstone'
  498. assert self.client.get(tstone_path).status_code == 405
  499. assert self.client.put(tstone_path).status_code == 405
  500. assert self.client.delete(tstone_path).status_code == 204
  501. assert self.client.get('/ldp/test_delete01').status_code == 404
  502. def test_delete_recursive(self):
  503. """
  504. Test response codes for resources deleted recursively and their
  505. tombstones.
  506. """
  507. child_suffixes = ('a', 'a/b', 'a/b/c', 'a1', 'a1/b1')
  508. self.client.put('/ldp/test_delete_recursive01')
  509. for cs in child_suffixes:
  510. self.client.put('/ldp/test_delete_recursive01/{}'.format(cs))
  511. assert self.client.delete(
  512. '/ldp/test_delete_recursive01').status_code == 204
  513. tstone_resp = self.client.get('/ldp/test_delete_recursive01')
  514. assert tstone_resp.status_code == 410
  515. assert tstone_resp.headers['Link'] == \
  516. '<{}/test_delete_recursive01/fcr:tombstone>; rel="hasTombstone"'\
  517. .format(g.webroot)
  518. for cs in child_suffixes:
  519. child_tstone_resp = self.client.get(
  520. '/ldp/test_delete_recursive01/{}'.format(cs))
  521. assert child_tstone_resp.status_code == tstone_resp.status_code
  522. assert 'Link' not in child_tstone_resp.headers.keys()
  523. def test_put_fragments(self):
  524. """
  525. Test the correct handling of fragment URIs on PUT and GET.
  526. """
  527. with open('tests/data/fragments.ttl', 'rb') as f:
  528. self.client.put(
  529. '/ldp/test_fragment01',
  530. headers={
  531. 'Content-Type' : 'text/turtle',
  532. },
  533. data=f
  534. )
  535. rsp = self.client.get('/ldp/test_fragment01')
  536. gr = Graph().parse(data=rsp.data, format='text/turtle')
  537. assert gr[
  538. URIRef(g.webroot + '/test_fragment01#hash1')
  539. : URIRef('http://ex.org/p2') : URIRef('http://ex.org/o2')]
  540. def test_patch_fragments(self):
  541. """
  542. Test the correct handling of fragment URIs on PATCH.
  543. """
  544. self.client.put('/ldp/test_fragment_patch')
  545. with open('tests/data/fragments_insert.sparql', 'rb') as f:
  546. self.client.patch(
  547. '/ldp/test_fragment_patch',
  548. headers={
  549. 'Content-Type' : 'application/sparql-update',
  550. },
  551. data=f
  552. )
  553. ins_rsp = self.client.get('/ldp/test_fragment_patch')
  554. ins_gr = Graph().parse(data=ins_rsp.data, format='text/turtle')
  555. assert ins_gr[
  556. URIRef(g.webroot + '/test_fragment_patch#hash1234')
  557. : URIRef('http://ex.org/p3') : URIRef('http://ex.org/o3')]
  558. with open('tests/data/fragments_delete.sparql', 'rb') as f:
  559. self.client.patch(
  560. '/ldp/test_fragment_patch',
  561. headers={
  562. 'Content-Type' : 'application/sparql-update',
  563. },
  564. data=f
  565. )
  566. del_rsp = self.client.get('/ldp/test_fragment_patch')
  567. del_gr = Graph().parse(data=del_rsp.data, format='text/turtle')
  568. assert not del_gr[
  569. URIRef(g.webroot + '/test_fragment_patch#hash1234')
  570. : URIRef('http://ex.org/p3') : URIRef('http://ex.org/o3')]
  571. @pytest.mark.usefixtures('client_class')
  572. @pytest.mark.usefixtures('db')
  573. class TestMimeType:
  574. """
  575. Test ``Accept`` headers and input & output formats.
  576. """
  577. def test_accept(self):
  578. """
  579. Verify the default serialization method.
  580. """
  581. accept_list = {
  582. ('', 'text/turtle'),
  583. ('text/turtle', 'text/turtle'),
  584. ('application/rdf+xml', 'application/rdf+xml'),
  585. ('application/n-triples', 'application/n-triples'),
  586. ('application/bogus', 'text/turtle'),
  587. (
  588. 'application/rdf+xml;q=0.5,application/n-triples;q=0.7',
  589. 'application/n-triples'),
  590. (
  591. 'application/rdf+xml;q=0.5,application/bogus;q=0.7',
  592. 'application/rdf+xml'),
  593. ('application/rdf+xml;q=0.5,text/n3;q=0.7', 'text/n3'),
  594. (
  595. 'application/rdf+xml;q=0.5,application/ld+json;q=0.7',
  596. 'application/ld+json'),
  597. }
  598. for mimetype, fmt in accept_list:
  599. rsp = self.client.get('/ldp', headers={'Accept': mimetype})
  600. assert rsp.mimetype == fmt
  601. gr = Graph(identifier=g.webroot + '/').parse(
  602. data=rsp.data, format=fmt)
  603. assert nsc['fcrepo'].RepositoryRoot in set(gr.objects())
  604. def test_provided_rdf(self):
  605. """
  606. Test several input RDF serialiation formats.
  607. """
  608. self.client.get('/ldp')
  609. gr = Graph()
  610. gr.add((
  611. URIRef(g.webroot + '/test_mimetype'),
  612. nsc['dcterms'].title, Literal('Test MIME type.')))
  613. test_list = {
  614. 'application/n-triples',
  615. 'application/rdf+xml',
  616. 'text/n3',
  617. 'text/turtle',
  618. 'application/ld+json',
  619. }
  620. for mimetype in test_list:
  621. rdf_data = gr.serialize(format=mimetype)
  622. self.client.put('/ldp/test_mimetype', data=rdf_data, headers={
  623. 'content-type': mimetype})
  624. rsp = self.client.get('/ldp/test_mimetype')
  625. rsp_gr = Graph(identifier=g.webroot + '/test_mimetype').parse(
  626. data=rsp.data, format='text/turtle')
  627. assert (
  628. URIRef(g.webroot + '/test_mimetype'),
  629. nsc['dcterms'].title, Literal('Test MIME type.')) in rsp_gr
  630. @pytest.mark.usefixtures('client_class')
  631. class TestDigestHeaders:
  632. """
  633. Test Digest and ETag headers.
  634. """
  635. def test_etag_digest(self):
  636. """
  637. Verify ETag and Digest headers on creation.
  638. The headers must correspond to the checksum of the binary content.
  639. """
  640. uid = '/test_etag1'
  641. path = '/ldp' + uid
  642. content = uuid4().bytes
  643. content_cksum = hashlib.new(digest_algo, content)
  644. put_rsp = self.client.put(
  645. path, data=content, headers={'content-type': 'text/plain'})
  646. assert content_cksum.hexdigest() in \
  647. put_rsp.headers.get('etag').split(',')
  648. assert put_rsp.headers.get('digest') == \
  649. f'{digest_algo.upper()}=' + b64encode(content_cksum.digest()).decode()
  650. get_rsp = self.client.get(path)
  651. assert content_cksum.hexdigest() in \
  652. put_rsp.headers.get('etag').split(',')
  653. assert get_rsp.headers.get('digest') == \
  654. f'{digest_algo.upper()}=' + b64encode(content_cksum.digest()).decode()
  655. def test_etag_ident(self):
  656. """
  657. Verify that two resources with the same content yield identical ETags.
  658. """
  659. path1 = f'/ldp/{uuid4()}'
  660. path2 = f'/ldp/{uuid4()}'
  661. content = uuid4().bytes
  662. content_cksum = hashlib.new(digest_algo, content)
  663. self.client.put(
  664. path1, data=content, headers={'content-type': 'text/plain'})
  665. self.client.put(
  666. path2, data=content, headers={'content-type': 'text/plain'})
  667. get_rsp1 = self.client.get(path1)
  668. get_rsp2 = self.client.get(path2)
  669. assert get_rsp1.headers.get('etag') == get_rsp2.headers.get('etag')
  670. assert get_rsp1.headers.get('digest') == get_rsp2.headers.get('digest')
  671. def test_etag_diff(self):
  672. """
  673. Verify that two resources with different content yield different ETags.
  674. """
  675. path1 = f'/ldp/{uuid4()}'
  676. path2 = f'/ldp/{uuid4()}'
  677. content1 = b'some interesting content.'
  678. content_cksum1 = hashlib.new(digest_algo, content1)
  679. content2 = b'Some great content.'
  680. content_cksum2 = hashlib.new(digest_algo, content2)
  681. self.client.put(
  682. path1, data=content1, headers={'content-type': 'text/plain'})
  683. self.client.put(
  684. path2, data=content2, headers={'content-type': 'text/plain'})
  685. get_rsp1 = self.client.get(path1)
  686. get_rsp2 = self.client.get(path2)
  687. assert get_rsp1.headers.get('etag') != get_rsp2.headers.get('etag')
  688. assert get_rsp1.headers.get('digest') != get_rsp2.headers.get('digest')
  689. def test_etag_update(self):
  690. """
  691. Verify that ETag and digest change when the resource is updated.
  692. The headers should NOT change if the same binary content is
  693. re-submitted.
  694. """
  695. path = f'/ldp/{uuid4()}'
  696. content1 = uuid4().bytes
  697. content_cksum1 = hashlib.new(digest_algo, content1)
  698. content2 = uuid4().bytes
  699. content_cksum2 = hashlib.new(digest_algo, content2)
  700. self.client.put(
  701. path, data=content1, headers={'content-type': 'text/plain'})
  702. get_rsp = self.client.get(path)
  703. assert content_cksum1.hexdigest() == \
  704. get_rsp.headers.get('etag').strip('"')
  705. assert get_rsp.headers.get('digest') == \
  706. f'{digest_algo.upper()}=' + b64encode(content_cksum1.digest()).decode()
  707. put_rsp = self.client.put(
  708. path, data=content2, headers={'content-type': 'text/plain'})
  709. assert content_cksum2.hexdigest() == \
  710. put_rsp.headers.get('etag').strip('"')
  711. assert put_rsp.headers.get('digest') == \
  712. f'{digest_algo.upper()}=' + b64encode(content_cksum2.digest()).decode()
  713. get_rsp = self.client.get(path)
  714. assert content_cksum2.hexdigest() == \
  715. get_rsp.headers.get('etag').strip('"')
  716. assert get_rsp.headers.get('digest') == \
  717. f'{digest_algo.upper()}=' + b64encode(content_cksum2.digest()).decode()
  718. def test_etag_rdf(self):
  719. """
  720. Verify that LDP-RS resources don't get an ETag.
  721. TODO This is by design for now; when a reliable hashing method
  722. for a graph is devised, this test should change.
  723. """
  724. path = f'/ldp/{uuid4()}'
  725. put_rsp = self.client.put(path)
  726. assert not put_rsp.headers.get('etag')
  727. assert not put_rsp.headers.get('digest')
  728. get_rsp = self.client.get(path)
  729. assert not get_rsp.headers.get('etag')
  730. assert not get_rsp.headers.get('digest')
  731. def test_digest_put(self):
  732. """
  733. Test the ``Digest`` header with PUT to verify content integrity.
  734. """
  735. path1 = f'/ldp/{uuid4()}'
  736. path2 = f'/ldp/{uuid4()}'
  737. path3 = f'/ldp/{uuid4()}'
  738. content = uuid4().bytes
  739. content_sha1 = sha1(content).hexdigest()
  740. content_sha256 = sha256(content).hexdigest()
  741. content_blake2b = blake2b(content).hexdigest()
  742. assert self.client.put(path1, data=content, headers={
  743. 'digest': 'sha1=abcd'}).status_code == 409
  744. assert self.client.put(path1, data=content, headers={
  745. 'digest': f'sha1={content_sha1}'}).status_code == 201
  746. assert self.client.put(path2, data=content, headers={
  747. 'digest': f'SHA1={content_sha1}'}).status_code == 201
  748. assert self.client.put(path3, data=content, headers={
  749. 'digest': f'SHA256={content_sha256}'}).status_code == 201
  750. assert self.client.put(path3, data=content, headers={
  751. 'digest': f'blake2b={content_blake2b}'}).status_code == 204
  752. def test_digest_post(self):
  753. """
  754. Test the ``Digest`` header with POST to verify content integrity.
  755. """
  756. path = '/ldp'
  757. content = uuid4().bytes
  758. content_sha1 = sha1(content).hexdigest()
  759. content_sha256 = sha256(content).hexdigest()
  760. content_blake2b = blake2b(content).hexdigest()
  761. assert self.client.post(path, data=content, headers={
  762. 'digest': 'sha1=abcd'}).status_code == 409
  763. assert self.client.post(path, data=content, headers={
  764. 'digest': f'sha1={content_sha1}'}).status_code == 201
  765. assert self.client.post(path, data=content, headers={
  766. 'digest': f'SHA1={content_sha1}'}).status_code == 201
  767. assert self.client.post(path, data=content, headers={
  768. 'digest': f'SHA256={content_sha256}'}).status_code == 201
  769. assert self.client.post(path, data=content, headers={
  770. 'digest': f'blake2b={content_blake2b}'}).status_code == 201
  771. assert self.client.post(path, data=content, headers={
  772. 'digest': f'bogusmd={content_blake2b}'}).status_code == 400
  773. bencoded = b64encode(content_blake2b.encode())
  774. assert self.client.post(
  775. path, data=content,
  776. headers={'digest': f'blake2b={bencoded}'}
  777. ).status_code == 400
  778. @pytest.mark.usefixtures('client_class')
  779. class TestETagCondHeaders:
  780. """
  781. Test Digest and ETag headers.
  782. """
  783. def test_if_match_get(self):
  784. """
  785. Test the If-Match header on GET requests.
  786. Test providing single and multiple ETags.
  787. """
  788. path = '/ldp/test_if_match1'
  789. content = uuid4().bytes
  790. content_cksum = hashlib.new(digest_algo, content).hexdigest()
  791. bogus_cksum = uuid4().hex
  792. self.client.put(
  793. path, data=content, headers={'content-type': 'text/plain'})
  794. get_rsp = self.client.get(path, headers={
  795. 'if-match': f'"{content_cksum}"'})
  796. assert get_rsp.status_code == 200
  797. get_rsp = self.client.get(path, headers={
  798. 'if-match': f'"{bogus_cksum}"'})
  799. assert get_rsp.status_code == 412
  800. get_rsp = self.client.get(path, headers={
  801. 'if-match': f'"{content_cksum}", "{bogus_cksum}"'})
  802. assert get_rsp.status_code == 200
  803. def test_if_match_put(self):
  804. """
  805. Test the If-Match header on PUT requests.
  806. Test providing single and multiple ETags.
  807. """
  808. path = '/ldp/test_if_match1'
  809. content = uuid4().bytes
  810. content_cksum = hashlib.new(digest_algo, content).hexdigest()
  811. bogus_cksum = uuid4().hex
  812. get_rsp = self.client.get(path)
  813. old_cksum = get_rsp.headers.get('etag')
  814. put_rsp = self.client.put(path, data=content, headers={
  815. 'if-match': f'"{content_cksum}"'})
  816. assert put_rsp.status_code == 412
  817. put_rsp = self.client.put(path, data=content, headers={
  818. 'if-match': f'"{content_cksum}", "{bogus_cksum}"'})
  819. assert put_rsp.status_code == 412
  820. put_rsp = self.client.put(path, data=content, headers={
  821. 'if-match': f'"{old_cksum}", "{bogus_cksum}"'})
  822. assert put_rsp.status_code == 204
  823. # Now contents have changed.
  824. put_rsp = self.client.put(path, data=content, headers={
  825. 'if-match': f'"{old_cksum}"'})
  826. assert put_rsp.status_code == 412
  827. put_rsp = self.client.put(path, data=content, headers={
  828. 'if-match': f'"{content_cksum}"'})
  829. assert put_rsp.status_code == 204
  830. # Exactly the same content was uploaded, so the ETag should not have
  831. # changed.
  832. put_rsp = self.client.put(path, data=content, headers={
  833. 'if-match': f'"{content_cksum}"'})
  834. assert put_rsp.status_code == 204
  835. # Catch-all: Proceed if resource exists at the given location.
  836. put_rsp = self.client.put(path, data=content, headers={
  837. 'if-match': '*'})
  838. assert put_rsp.status_code == 204
  839. # This is wrong syntax. It will not update because the literal asterisk
  840. # won't match.
  841. put_rsp = self.client.put(path, data=content, headers={
  842. 'if-match': '"*"'})
  843. assert put_rsp.status_code == 412
  844. # Test delete.
  845. del_rsp = self.client.delete(path, headers={
  846. 'if-match': f'"{old_cksum}"', 'Prefer': 'no-tombstone'})
  847. assert del_rsp.status_code == 412
  848. del_rsp = self.client.delete(path, headers={
  849. 'if-match': f'"{content_cksum}"', 'Prefer': 'no-tombstone'})
  850. assert del_rsp.status_code == 204
  851. put_rsp = self.client.put(path, data=content, headers={
  852. 'if-match': '*'})
  853. assert put_rsp.status_code == 412
  854. def test_if_none_match_get(self):
  855. """
  856. Test the If-None-Match header on GET requests.
  857. Test providing single and multiple ETags.
  858. """
  859. path = '/ldp/test_if_none_match1'
  860. content = uuid4().bytes
  861. content_cksum = hashlib.new(digest_algo, content).hexdigest()
  862. bogus_cksum = uuid4().hex
  863. self.client.put(
  864. path, data=content, headers={'content-type': 'text/plain'})
  865. get_rsp1 = self.client.get(path, headers={
  866. 'if-none-match': f'"{content_cksum}"'})
  867. assert get_rsp1.status_code == 304
  868. get_rsp2 = self.client.get(path, headers={
  869. 'if-none-match': f'"{bogus_cksum}"'})
  870. assert get_rsp2.status_code == 200
  871. get_rsp3 = self.client.get(path, headers={
  872. 'if-none-match': f'"{content_cksum}", "{bogus_cksum}"'})
  873. assert get_rsp3.status_code == 304
  874. # 404 has precedence on ETag handling.
  875. get_rsp = self.client.get('/ldp/bogus', headers={
  876. 'if-none-match': f'"{bogus_cksum}"'})
  877. assert get_rsp.status_code == 404
  878. get_rsp = self.client.get('/ldp/bogus', headers={
  879. 'if-none-match': f'"{content_cksum}"'})
  880. assert get_rsp.status_code == 404
  881. def test_if_none_match_put(self):
  882. """
  883. Test the If-None-Match header on PUT requests.
  884. Test providing single and multiple ETags.
  885. Uses a previously created resource.
  886. """
  887. path = '/ldp/test_if_none_match1'
  888. content = uuid4().bytes
  889. content_cksum = hashlib.new(digest_algo, content).hexdigest()
  890. bogus_cksum = uuid4().hex
  891. get_rsp = self.client.get(path)
  892. old_cksum = get_rsp.headers.get('etag')
  893. put_rsp = self.client.put(path, data=content, headers={
  894. 'if-none-match': f'"{old_cksum}"'})
  895. assert put_rsp.status_code == 412
  896. put_rsp = self.client.put(path, data=content, headers={
  897. 'if-none-match': f'"{old_cksum}", "{bogus_cksum}"'})
  898. assert put_rsp.status_code == 412
  899. put_rsp = self.client.put(path, data=content, headers={
  900. 'if-none-match': f'"{bogus_cksum}"'})
  901. assert put_rsp.status_code == 204
  902. # Now contents have changed.
  903. put_rsp = self.client.put(path, data=content, headers={
  904. 'if-none-match': f'"{content_cksum}"'})
  905. assert put_rsp.status_code == 412
  906. put_rsp = self.client.put(path, data=content, headers={
  907. 'if-none-match': f'"{old_cksum}"'})
  908. assert put_rsp.status_code == 204
  909. # Catch-all: fail if any resource exists at the given location.
  910. put_rsp = self.client.put(path, data=content, headers={
  911. 'if-none-match': '*'})
  912. assert put_rsp.status_code == 412
  913. # Test delete.
  914. del_rsp = self.client.delete(path, headers={
  915. 'if-none-match': f'"{content_cksum}"', 'Prefer': 'no-tombstone'})
  916. assert del_rsp.status_code == 412
  917. del_rsp = self.client.delete(path, headers={
  918. 'if-none-match': f'"{bogus_cksum}"', 'Prefer': 'no-tombstone'})
  919. assert del_rsp.status_code == 204
  920. put_rsp = self.client.put(path, data=content, headers={
  921. 'if-none-match': '*'})
  922. assert put_rsp.status_code == 201
  923. # This is wrong syntax. It will update because the literal asterisk
  924. # won't match.
  925. put_rsp = self.client.put(path, data=content, headers={
  926. 'if-none-match': '"*"'})
  927. assert put_rsp.status_code == 204
  928. def test_etag_notfound(self):
  929. """
  930. Verify that 404 and 410 have precedence on ETag handling.
  931. """
  932. path = f'/ldp/{uuid4()}'
  933. bogus_cksum = uuid4().hex
  934. get_rsp = self.client.get(path, headers={
  935. 'if-match': f'"{bogus_cksum}"'})
  936. assert get_rsp.status_code == 404
  937. get_rsp = self.client.get(path, headers={
  938. 'if-match': '*'})
  939. assert get_rsp.status_code == 404
  940. get_rsp = self.client.get(path, headers={
  941. 'if-none-match': f'"{bogus_cksum}"'})
  942. assert get_rsp.status_code == 404
  943. self.client.put(path)
  944. self.client.delete(path)
  945. get_rsp = self.client.get(path, headers={
  946. 'if-match': f'"{bogus_cksum}"'})
  947. assert get_rsp.status_code == 410
  948. get_rsp = self.client.get(path, headers={
  949. 'if-none-match': f'"{bogus_cksum}"'})
  950. assert get_rsp.status_code == 410
  951. get_rsp = self.client.get(path, headers={
  952. 'if-match': '*'})
  953. assert get_rsp.status_code == 410
  954. @pytest.mark.usefixtures('client_class')
  955. class TestModifyTimeCondHeaders:
  956. """
  957. Test time-related conditional headers.
  958. """
  959. @pytest.fixture(scope='class')
  960. def timeframe(self):
  961. """
  962. Times used in these tests: UTC midnight of today, yesterday, tomorrow.
  963. """
  964. today = arrow.utcnow().floor('day')
  965. yesterday = today.shift(days=-1)
  966. tomorrow = today.shift(days=1)
  967. path = f'/ldp/{uuid4()}'
  968. self.client.put(path)
  969. return path, today, yesterday, tomorrow
  970. def test_nothing(self):
  971. """
  972. For some reason, without this the fixture won't initialize properly.
  973. """
  974. self.client.get('/')
  975. def test_if_modified_since(self, timeframe):
  976. """
  977. Test various uses of the If-Modified-Since header.
  978. """
  979. path, today, yesterday, tomorrow = timeframe
  980. assert self.client.head(
  981. path, headers={'if-modified-since': http_date(today.timestamp)}
  982. ).status_code == 200
  983. assert self.client.get(
  984. path, headers={'if-modified-since': http_date(today.timestamp)}
  985. ).status_code == 200
  986. assert self.client.head(
  987. path, headers={'if-modified-since': http_date(yesterday.timestamp)}
  988. ).status_code == 200
  989. assert self.client.get(
  990. path, headers={'if-modified-since': http_date(yesterday.timestamp)}
  991. ).status_code == 200
  992. assert self.client.head(
  993. path, headers={'if-modified-since': http_date(tomorrow.timestamp)}
  994. ).status_code == 304
  995. assert self.client.get(
  996. path, headers={'if-modified-since': http_date(tomorrow.timestamp)}
  997. ).status_code == 304
  998. def test_if_unmodified_since(self, timeframe):
  999. """
  1000. Test various uses of the If-Unmodified-Since header.
  1001. """
  1002. path, today, yesterday, tomorrow = timeframe
  1003. assert self.client.head(
  1004. path, headers={'if-unmodified-since': http_date(today.timestamp)}
  1005. ).status_code == 304
  1006. assert self.client.get(
  1007. path, headers={'if-unmodified-since': http_date(today.timestamp)}
  1008. ).status_code == 304
  1009. assert self.client.head(
  1010. path, headers={'if-unmodified-since': http_date(yesterday.timestamp)}
  1011. ).status_code == 304
  1012. assert self.client.get(
  1013. path, headers={'if-unmodified-since': http_date(yesterday.timestamp)}
  1014. ).status_code == 304
  1015. assert self.client.head(
  1016. path, headers={'if-unmodified-since': http_date(tomorrow.timestamp)}
  1017. ).status_code == 200
  1018. assert self.client.get(
  1019. path, headers={'if-unmodified-since': http_date(tomorrow.timestamp)}
  1020. ).status_code == 200
  1021. def test_time_range(self, timeframe):
  1022. """
  1023. Test conditions inside and outside of a time range.
  1024. """
  1025. path, today, yesterday, tomorrow = timeframe
  1026. # Send me the resource if it has been modified between yesterday
  1027. # and tomorrow.
  1028. assert self.client.get(path, headers={
  1029. 'if-modified-since': http_date(yesterday.timestamp),
  1030. 'if-unmodified-since': http_date(tomorrow.timestamp),
  1031. }).status_code == 200
  1032. # Send me the resource if it has been modified between today
  1033. # and tomorrow.
  1034. assert self.client.get(path, headers={
  1035. 'if-modified-since': http_date(today.timestamp),
  1036. 'if-unmodified-since': http_date(tomorrow.timestamp),
  1037. }).status_code == 200
  1038. # Send me the resource if it has been modified between yesterday
  1039. # and today.
  1040. assert self.client.get(path, headers={
  1041. 'if-modified-since': http_date(yesterday.timestamp),
  1042. 'if-unmodified-since': http_date(today.timestamp),
  1043. }).status_code == 304
  1044. # Send me the resource if it has been modified between two days ago
  1045. # and yesterday.
  1046. assert self.client.get(path, headers={
  1047. 'if-modified-since': http_date(yesterday.shift(days=-1).timestamp),
  1048. 'if-unmodified-since': http_date(yesterday.timestamp),
  1049. }).status_code == 304
  1050. # Send me the resource if it has been modified between tomorrow
  1051. # and two days from today.
  1052. assert self.client.get(path, headers={
  1053. 'if-modified-since': http_date(tomorrow.timestamp),
  1054. 'if-unmodified-since': http_date(tomorrow.shift(days=1).timestamp),
  1055. }).status_code == 304
  1056. def test_time_etag_combo(self, timeframe):
  1057. """
  1058. Test evaluation priorities among ETag and time headers.
  1059. """
  1060. _, today, yesterday, tomorrow = timeframe
  1061. path = f'/ldp/{uuid4()}'
  1062. content = uuid4().bytes
  1063. content_cksum = hashlib.new(digest_algo, content).hexdigest()
  1064. bogus_cksum = uuid4().hex
  1065. self.client.put(
  1066. path, data=content, headers={'content-type': 'text/plain'})
  1067. # Negative ETag match wins.
  1068. assert self.client.get(path, headers={
  1069. 'if-match': f'"{bogus_cksum}"',
  1070. 'if-modified-since': http_date(yesterday.timestamp),
  1071. }).status_code == 412
  1072. assert self.client.get(path, headers={
  1073. 'if-match': f'"{bogus_cksum}"',
  1074. 'if-unmodified-since': http_date(tomorrow.timestamp),
  1075. }).status_code == 412
  1076. assert self.client.get(path, headers={
  1077. 'if-none-match': f'"{content_cksum}"',
  1078. 'if-modified-since': http_date(yesterday.timestamp),
  1079. }).status_code == 304
  1080. assert self.client.get(path, headers={
  1081. 'if-none-match': f'"{content_cksum}"',
  1082. 'if-unmodified-since': http_date(tomorrow.timestamp),
  1083. }).status_code == 304
  1084. # Positive ETag match wins.
  1085. assert self.client.get(path, headers={
  1086. 'if-match': f'"{content_cksum}"',
  1087. 'if-unmodified-since': http_date(yesterday.timestamp),
  1088. }).status_code == 200
  1089. assert self.client.get(path, headers={
  1090. 'if-match': f'"{content_cksum}"',
  1091. 'if-modified-since': http_date(tomorrow.timestamp),
  1092. }).status_code == 200
  1093. assert self.client.get(path, headers={
  1094. 'if-none-match': f'"{bogus_cksum}"',
  1095. 'if-unmodified-since': http_date(yesterday.timestamp),
  1096. }).status_code == 200
  1097. assert self.client.get(path, headers={
  1098. 'if-none-match': f'"{bogus_cksum}"',
  1099. 'if-modified-since': http_date(tomorrow.timestamp),
  1100. }).status_code == 200
  1101. @pytest.mark.usefixtures('client_class')
  1102. class TestRange:
  1103. """
  1104. Test byte range retrieval.
  1105. This should not need too deep testing since it's functionality implemented
  1106. in Werkzeug/Flask.
  1107. """
  1108. @pytest.fixture(scope='class')
  1109. def bytestream(self):
  1110. """
  1111. Create a sample bytestream with predictable (8x8 bytes) content.
  1112. """
  1113. return b''.join([bytes([n] * 8) for n in range(8)])
  1114. def test_get_range(self, bytestream):
  1115. """
  1116. Get different ranges of the bitstream.
  1117. """
  1118. path = '/ldp/test_range'
  1119. self.client.put(path, data=bytestream)
  1120. # First 8 bytes.
  1121. assert self.client.get(
  1122. path, headers={'range': 'bytes=0-7'}).data == b'\x00' * 8
  1123. # Last 4 bytes of first block, first 4 of second block.
  1124. assert self.client.get(
  1125. path, headers={'range': 'bytes=4-11'}
  1126. ).data == b'\x00' * 4 + b'\x01' * 4
  1127. # Last 8 bytes.
  1128. assert self.client.get(
  1129. path, headers={'range': 'bytes=56-'}).data == b'\x07' * 8
  1130. def test_fail_ranges(self, bytestream):
  1131. """
  1132. Test malformed or unsupported ranges.
  1133. """
  1134. path = '/ldp/test_range'
  1135. # TODO This shall be a 206 when multiple ranges are supported.
  1136. fail_rsp = self.client.get(path, headers={'range': 'bytes=0-1, 7-8'})
  1137. assert fail_rsp.status_code == 501
  1138. # Bad ranges will be ignored.
  1139. for rng in ((10, 4), ('', 3), (3600, 6400)):
  1140. bad_rsp = self.client.get(
  1141. path, headers={'range': 'bytes={rng[0]}-{rng[1]}'})
  1142. assert bad_rsp.status_code == 200
  1143. assert bad_rsp.data == bytestream
  1144. assert int(bad_rsp.headers['content-length']) == len(bytestream)
  1145. def test_range_rsp_headers(self, bytestream):
  1146. """
  1147. Test various headers for a ranged response.
  1148. """
  1149. path = '/ldp/test_range'
  1150. start_b = 0
  1151. end_b = 7
  1152. full_rsp = self.client.get(path)
  1153. part_rsp = self.client.get(path, headers={
  1154. 'range': f'bytes={start_b}-{end_b}'})
  1155. for hdr_name in ['etag', 'digest', 'content-type']:
  1156. assert part_rsp.headers[hdr_name] == full_rsp.headers[hdr_name]
  1157. for hdr in part_rsp.headers['link']:
  1158. assert hdr in full_rsp.headers['link']
  1159. assert int(part_rsp.headers['content-length']) == end_b - start_b + 1
  1160. assert part_rsp.headers['content-range'] == \
  1161. f'bytes {start_b}-{end_b} / {len(bytestream)}'
  1162. @pytest.mark.usefixtures('client_class')
  1163. class TestPrefHeader:
  1164. """
  1165. Test various combinations of `Prefer` header.
  1166. """
  1167. @pytest.fixture(scope='class')
  1168. def cont_structure(self):
  1169. """
  1170. Create a container structure to be used for subsequent requests.
  1171. """
  1172. parent_path = '/ldp/test_parent'
  1173. self.client.put(parent_path)
  1174. self.client.put(parent_path + '/child1')
  1175. self.client.put(parent_path + '/child2')
  1176. self.client.put(parent_path + '/child3')
  1177. return {
  1178. 'path' : parent_path,
  1179. 'response' : self.client.get(parent_path),
  1180. }
  1181. def test_put_prefer_handling(self, random_uuid):
  1182. """
  1183. Trying to PUT an existing resource should:
  1184. - Return a 204 if the payload is empty
  1185. - Return a 204 if the payload is RDF, server-managed triples are
  1186. included and the 'Prefer' header is set to 'handling=lenient'
  1187. - Return a 412 (ServerManagedTermError) if the payload is RDF,
  1188. server-managed triples are included and handling is set to 'strict',
  1189. or not set.
  1190. """
  1191. path = '/ldp/put_pref_header01'
  1192. assert self.client.put(path).status_code == 201
  1193. assert self.client.get(path).status_code == 200
  1194. assert self.client.put(path).status_code == 204
  1195. # Default handling is strict.
  1196. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  1197. rsp_default = self.client.put(
  1198. path,
  1199. headers={
  1200. 'Content-Type' : 'text/turtle',
  1201. },
  1202. data=f
  1203. )
  1204. assert rsp_default.status_code == 412
  1205. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  1206. rsp_len = self.client.put(
  1207. path,
  1208. headers={
  1209. 'Prefer' : 'handling=lenient',
  1210. 'Content-Type' : 'text/turtle',
  1211. },
  1212. data=f
  1213. )
  1214. assert rsp_len.status_code == 204
  1215. with open('tests/data/rdf_payload_w_srv_mgd_trp.ttl', 'rb') as f:
  1216. rsp_strict = self.client.put(
  1217. path,
  1218. headers={
  1219. 'Prefer' : 'handling=strict',
  1220. 'Content-Type' : 'text/turtle',
  1221. },
  1222. data=f
  1223. )
  1224. assert rsp_strict.status_code == 412
  1225. # @HOLD Embed children is debated.
  1226. def _disabled_test_embed_children(self, cont_structure):
  1227. """
  1228. verify the "embed children" prefer header.
  1229. """
  1230. self.client.get('/ldp')
  1231. parent_path = cont_structure['path']
  1232. cont_resp = cont_structure['response']
  1233. cont_subject = URIRef(g.webroot + '/test_parent')
  1234. #minimal_resp = self.client.get(parent_path, headers={
  1235. # 'Prefer' : 'return=minimal',
  1236. #})
  1237. incl_embed_children_resp = self.client.get(parent_path, headers={
  1238. 'Prefer' : 'return=representation; include={}'\
  1239. .format(Ldpr.EMBED_CHILD_RES_URI),
  1240. })
  1241. omit_embed_children_resp = self.client.get(parent_path, headers={
  1242. 'Prefer' : 'return=representation; omit={}'\
  1243. .format(Ldpr.EMBED_CHILD_RES_URI),
  1244. })
  1245. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  1246. incl_gr = Graph().parse(
  1247. data=incl_embed_children_resp.data, format='turtle')
  1248. omit_gr = Graph().parse(
  1249. data=omit_embed_children_resp.data, format='turtle')
  1250. assert isomorphic(omit_gr, default_gr)
  1251. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  1252. assert len(children) == 3
  1253. children = set(incl_gr[cont_subject : nsc['ldp'].contains])
  1254. for child_uri in children:
  1255. assert set(incl_gr[ child_uri : : ])
  1256. assert not set(omit_gr[ child_uri : : ])
  1257. def test_return_children(self, cont_structure):
  1258. """
  1259. verify the "return children" prefer header.
  1260. """
  1261. self.client.get('/ldp')
  1262. parent_path = cont_structure['path']
  1263. cont_resp = cont_structure['response']
  1264. cont_subject = URIRef(g.webroot + '/test_parent')
  1265. incl_children_resp = self.client.get(parent_path, headers={
  1266. 'Prefer' : 'return=representation; include={}'\
  1267. .format(Ldpr.RETURN_CHILD_RES_URI),
  1268. })
  1269. omit_children_resp = self.client.get(parent_path, headers={
  1270. 'Prefer' : 'return=representation; omit={}'\
  1271. .format(Ldpr.RETURN_CHILD_RES_URI),
  1272. })
  1273. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  1274. incl_gr = Graph().parse(data=incl_children_resp.data, format='turtle')
  1275. omit_gr = Graph().parse(data=omit_children_resp.data, format='turtle')
  1276. assert isomorphic(incl_gr, default_gr)
  1277. children = incl_gr[cont_subject : nsc['ldp'].contains]
  1278. for child_uri in children:
  1279. assert not omit_gr[cont_subject : nsc['ldp'].contains : child_uri]
  1280. def test_inbound_rel(self, cont_structure):
  1281. """
  1282. verify the "inbound relationships" prefer header.
  1283. """
  1284. self.client.put('/ldp/test_target')
  1285. data = '<> <http://ex.org/ns#shoots> <{}> .'.format(
  1286. g.webroot + '/test_target')
  1287. self.client.put('/ldp/test_shooter', data=data,
  1288. headers={'Content-Type': 'text/turtle'})
  1289. cont_resp = self.client.get('/ldp/test_target')
  1290. incl_inbound_resp = self.client.get('/ldp/test_target', headers={
  1291. 'Prefer' : 'return=representation; include="{}"'\
  1292. .format(Ldpr.RETURN_INBOUND_REF_URI),
  1293. })
  1294. omit_inbound_resp = self.client.get('/ldp/test_target', headers={
  1295. 'Prefer' : 'return=representation; omit="{}"'\
  1296. .format(Ldpr.RETURN_INBOUND_REF_URI),
  1297. })
  1298. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  1299. incl_gr = Graph().parse(data=incl_inbound_resp.data, format='turtle')
  1300. omit_gr = Graph().parse(data=omit_inbound_resp.data, format='turtle')
  1301. subject = URIRef(g.webroot + '/test_target')
  1302. inbd_subject = URIRef(g.webroot + '/test_shooter')
  1303. assert isomorphic(omit_gr, default_gr)
  1304. assert len(set(incl_gr[inbd_subject : : ])) == 1
  1305. assert incl_gr[
  1306. inbd_subject : URIRef('http://ex.org/ns#shoots') : subject]
  1307. assert not len(set(omit_gr[inbd_subject : :]))
  1308. def test_srv_mgd_triples(self, cont_structure):
  1309. """
  1310. verify the "server managed triples" prefer header.
  1311. """
  1312. self.client.get('/ldp')
  1313. parent_path = cont_structure['path']
  1314. cont_resp = cont_structure['response']
  1315. cont_subject = URIRef(g.webroot + '/test_parent')
  1316. incl_srv_mgd_resp = self.client.get(parent_path, headers={
  1317. 'Prefer' : 'return=representation; include={}'\
  1318. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  1319. })
  1320. omit_srv_mgd_resp = self.client.get(parent_path, headers={
  1321. 'Prefer' : 'return=representation; omit={}'\
  1322. .format(Ldpr.RETURN_SRV_MGD_RES_URI),
  1323. })
  1324. default_gr = Graph().parse(data=cont_resp.data, format='turtle')
  1325. incl_gr = Graph().parse(data=incl_srv_mgd_resp.data, format='turtle')
  1326. omit_gr = Graph().parse(data=omit_srv_mgd_resp.data, format='turtle')
  1327. assert isomorphic(incl_gr, default_gr)
  1328. for pred in {
  1329. nsc['fcrepo'].created,
  1330. nsc['fcrepo'].createdBy,
  1331. nsc['fcrepo'].lastModified,
  1332. nsc['fcrepo'].lastModifiedBy,
  1333. nsc['ldp'].contains,
  1334. }:
  1335. assert set(incl_gr[ cont_subject : pred : ])
  1336. assert not set(omit_gr[ cont_subject : pred : ])
  1337. for type in {
  1338. nsc['fcrepo'].Resource,
  1339. nsc['ldp'].Container,
  1340. nsc['ldp'].Resource,
  1341. }:
  1342. assert incl_gr[ cont_subject : RDF.type : type ]
  1343. assert not omit_gr[ cont_subject : RDF.type : type ]
  1344. def test_delete_no_tstone(self):
  1345. """
  1346. Test the `no-tombstone` Prefer option.
  1347. """
  1348. self.client.put('/ldp/test_delete_no_tstone01')
  1349. self.client.put('/ldp/test_delete_no_tstone01/a')
  1350. self.client.delete('/ldp/test_delete_no_tstone01', headers={
  1351. 'prefer' : 'no-tombstone'})
  1352. resp = self.client.get('/ldp/test_delete_no_tstone01')
  1353. assert resp.status_code == 404
  1354. child_resp = self.client.get('/ldp/test_delete_no_tstone01/a')
  1355. assert child_resp.status_code == 404
  1356. #@pytest.mark.usefixtures('client_class')
  1357. #@pytest.mark.usefixtures('db')
  1358. #class TestDigest:
  1359. # """
  1360. # Test digest and ETag handling.
  1361. # """
  1362. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  1363. # def test_digest_post(self):
  1364. # """
  1365. # Test ``Digest`` and ``ETag`` headers on resource POST.
  1366. # """
  1367. # resp = self.client.post('/ldp/')
  1368. # assert 'Digest' in resp.headers
  1369. # assert 'ETag' in resp.headers
  1370. # assert (
  1371. # b64encode(bytes.fromhex(
  1372. # resp.headers['ETag'].replace('W/', '')
  1373. # )).decode('ascii') ==
  1374. # resp.headers['Digest'].replace('SHA256=', ''))
  1375. #
  1376. #
  1377. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  1378. # def test_digest_put(self):
  1379. # """
  1380. # Test ``Digest`` and ``ETag`` headers on resource PUT.
  1381. # """
  1382. # resp_put = self.client.put('/ldp/test_digest_put')
  1383. # assert 'Digest' in resp_put.headers
  1384. # assert 'ETag' in resp_put.headers
  1385. # assert (
  1386. # b64encode(bytes.fromhex(
  1387. # resp_put.headers['ETag'].replace('W/', '')
  1388. # )).decode('ascii') ==
  1389. # resp_put.headers['Digest'].replace('SHA256=', ''))
  1390. #
  1391. # resp_get = self.client.get('/ldp/test_digest_put')
  1392. # assert 'Digest' in resp_get.headers
  1393. # assert 'ETag' in resp_get.headers
  1394. # assert (
  1395. # b64encode(bytes.fromhex(
  1396. # resp_get.headers['ETag'].replace('W/', '')
  1397. # )).decode('ascii') ==
  1398. # resp_get.headers['Digest'].replace('SHA256=', ''))
  1399. #
  1400. #
  1401. # @pytest.mark.skip(reason='TODO Need to implement async digest queue')
  1402. # def test_digest_patch(self):
  1403. # """
  1404. # Verify that the digest and ETag change on resource change.
  1405. # """
  1406. # path = '/ldp/test_digest_patch'
  1407. # self.client.put(path)
  1408. # rsp1 = self.client.get(path)
  1409. #
  1410. # self.client.patch(
  1411. # path, data=b'DELETE {} INSERT {<> a <http://ex.org/Test> .} '
  1412. # b'WHERE {}',
  1413. # headers={'Content-Type': 'application/sparql-update'})
  1414. # rsp2 = self.client.get(path)
  1415. #
  1416. # assert rsp1.headers['ETag'] != rsp2.headers['ETag']
  1417. # assert rsp1.headers['Digest'] != rsp2.headers['Digest']
  1418. @pytest.mark.usefixtures('client_class')
  1419. @pytest.mark.usefixtures('db')
  1420. class TestVersion:
  1421. """
  1422. Test version creation, retrieval and deletion.
  1423. """
  1424. def test_create_versions(self):
  1425. """
  1426. Test that POSTing multiple times to fcr:versions creates the
  1427. 'hasVersions' triple and yields multiple version snapshots.
  1428. """
  1429. self.client.put('/ldp/test_version')
  1430. create_rsp = self.client.post('/ldp/test_version/fcr:versions')
  1431. assert create_rsp.status_code == 201
  1432. rsrc_rsp = self.client.get('/ldp/test_version')
  1433. rsrc_gr = Graph().parse(data=rsrc_rsp.data, format='turtle')
  1434. assert len(set(rsrc_gr[: nsc['fcrepo'].hasVersions :])) == 1
  1435. info_rsp = self.client.get('/ldp/test_version/fcr:versions')
  1436. assert info_rsp.status_code == 200
  1437. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  1438. assert len(set(info_gr[: nsc['fcrepo'].hasVersion :])) == 1
  1439. self.client.post('/ldp/test_version/fcr:versions')
  1440. info2_rsp = self.client.get('/ldp/test_version/fcr:versions')
  1441. info2_gr = Graph().parse(data=info2_rsp.data, format='turtle')
  1442. assert len(set(info2_gr[: nsc['fcrepo'].hasVersion :])) == 2
  1443. def test_version_with_slug(self):
  1444. """
  1445. Test a version with a slug.
  1446. """
  1447. self.client.put('/ldp/test_version_slug')
  1448. create_rsp = self.client.post('/ldp/test_version_slug/fcr:versions',
  1449. headers={'slug' : 'v1'})
  1450. new_ver_uri = create_rsp.headers['Location']
  1451. assert new_ver_uri == g.webroot + '/test_version_slug/fcr:versions/v1'
  1452. info_rsp = self.client.get('/ldp/test_version_slug/fcr:versions')
  1453. info_gr = Graph().parse(data=info_rsp.data, format='turtle')
  1454. assert info_gr[
  1455. URIRef(new_ver_uri) :
  1456. nsc['fcrepo'].hasVersionLabel :
  1457. Literal('v1')]
  1458. def test_dupl_version(self):
  1459. """
  1460. Make sure that two POSTs with the same slug result in two different
  1461. versions.
  1462. """
  1463. path = '/ldp/test_duplicate_slug'
  1464. self.client.put(path)
  1465. v1_rsp = self.client.post(path + '/fcr:versions',
  1466. headers={'slug' : 'v1'})
  1467. v1_uri = v1_rsp.headers['Location']
  1468. dup_rsp = self.client.post(path + '/fcr:versions',
  1469. headers={'slug' : 'v1'})
  1470. dup_uri = dup_rsp.headers['Location']
  1471. assert v1_uri != dup_uri
  1472. def test_revert_version(self):
  1473. """
  1474. Take a version snapshot, update a resource, and then revert to the
  1475. previous vresion.
  1476. """
  1477. rsrc_path = '/ldp/test_revert_version'
  1478. payload1 = '<> <urn:demo:p1> <urn:demo:o1> .'
  1479. payload2 = '<> <urn:demo:p1> <urn:demo:o2> .'
  1480. self.client.put(rsrc_path, headers={
  1481. 'content-type': 'text/turtle'}, data=payload1)
  1482. self.client.post(
  1483. rsrc_path + '/fcr:versions', headers={'slug': 'v1'})
  1484. v1_rsp = self.client.get(rsrc_path)
  1485. v1_gr = Graph().parse(data=v1_rsp.data, format='turtle')
  1486. assert v1_gr[
  1487. URIRef(g.webroot + '/test_revert_version')
  1488. : URIRef('urn:demo:p1')
  1489. : URIRef('urn:demo:o1')
  1490. ]
  1491. self.client.put(rsrc_path, headers={
  1492. 'content-type': 'text/turtle'}, data=payload2)
  1493. v2_rsp = self.client.get(rsrc_path)
  1494. v2_gr = Graph().parse(data=v2_rsp.data, format='turtle')
  1495. assert v2_gr[
  1496. URIRef(g.webroot + '/test_revert_version')
  1497. : URIRef('urn:demo:p1')
  1498. : URIRef('urn:demo:o2')
  1499. ]
  1500. self.client.patch(rsrc_path + '/fcr:versions/v1')
  1501. revert_rsp = self.client.get(rsrc_path)
  1502. revert_gr = Graph().parse(data=revert_rsp.data, format='turtle')
  1503. assert revert_gr[
  1504. URIRef(g.webroot + '/test_revert_version')
  1505. : URIRef('urn:demo:p1')
  1506. : URIRef('urn:demo:o1')
  1507. ]
  1508. def test_resurrection(self):
  1509. """
  1510. Delete and then resurrect a resource.
  1511. Make sure that the resource is resurrected to the latest version.
  1512. """
  1513. path = '/ldp/test_lazarus'
  1514. self.client.put(path)
  1515. self.client.post(path + '/fcr:versions', headers={'slug': 'v1'})
  1516. self.client.put(
  1517. path, headers={'content-type': 'text/turtle'},
  1518. data=b'<> <urn:demo:p1> <urn:demo:o1> .')
  1519. self.client.post(path + '/fcr:versions', headers={'slug': 'v2'})
  1520. self.client.put(
  1521. path, headers={'content-type': 'text/turtle'},
  1522. data=b'<> <urn:demo:p1> <urn:demo:o2> .')
  1523. self.client.delete(path)
  1524. assert self.client.get(path).status_code == 410
  1525. self.client.post(path + '/fcr:tombstone')
  1526. laz_data = self.client.get(path).data
  1527. laz_gr = Graph().parse(data=laz_data, format='turtle')
  1528. assert laz_gr[
  1529. URIRef(g.webroot + '/test_lazarus')
  1530. : URIRef('urn:demo:p1')
  1531. : URIRef('urn:demo:o2')
  1532. ]