test_3_0_ldp.py 79 KB

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