test_resource_api.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import pdb
  2. import pytest
  3. from io import BytesIO
  4. from uuid import uuid4
  5. from rdflib import Graph, Literal, URIRef
  6. from lakesuperior.api import resource as rsrc_api
  7. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  8. from lakesuperior.exceptions import (
  9. IncompatibleLdpTypeError, InvalidResourceError, ResourceNotExistsError,
  10. TombstoneError)
  11. from lakesuperior.globals import RES_CREATED, RES_UPDATED
  12. from lakesuperior.model.ldpr import Ldpr
  13. @pytest.fixture(scope='module')
  14. def random_uuid():
  15. return str(uuid.uuid4())
  16. @pytest.fixture
  17. def dc_rdf():
  18. return b'''
  19. PREFIX dcterms: <http://purl.org/dc/terms/>
  20. PREFIX ldp: <http://www.w3.org/ns/ldp#>
  21. <> dcterms:title "Direct Container" ;
  22. ldp:membershipResource <info:fcres/member> ;
  23. ldp:hasMemberRelation dcterms:relation .
  24. '''
  25. @pytest.fixture
  26. def ic_rdf():
  27. return b'''
  28. PREFIX dcterms: <http://purl.org/dc/terms/>
  29. PREFIX ldp: <http://www.w3.org/ns/ldp#>
  30. PREFIX ore: <http://www.openarchives.org/ore/terms/>
  31. <> dcterms:title "Indirect Container" ;
  32. ldp:membershipResource <info:fcres/top_container> ;
  33. ldp:hasMemberRelation dcterms:relation ;
  34. ldp:insertedContentRelation ore:proxyFor .
  35. '''
  36. @pytest.mark.usefixtures('db')
  37. class TestResourceCRUD:
  38. '''
  39. Test interaction with the Resource API.
  40. '''
  41. def test_nodes_exist(self):
  42. """
  43. Verify whether nodes exist or not.
  44. """
  45. assert rsrc_api.exists('/') is True
  46. assert rsrc_api.exists('/{}'.format(uuid4())) is False
  47. def test_get_root_node_metadata(self):
  48. """
  49. Get the root node metadata.
  50. The ``dcterms:title`` property should NOT be included.
  51. """
  52. gr = rsrc_api.get_metadata('/')
  53. assert isinstance(gr, Graph)
  54. assert len(gr) == 9
  55. assert gr[gr.identifier : nsc['rdf'].type : nsc['ldp'].Resource ]
  56. assert not gr[gr.identifier : nsc['dcterms'].title : "Repository Root"]
  57. def test_get_root_node(self):
  58. """
  59. Get the root node.
  60. The ``dcterms:title`` property should be included.
  61. """
  62. rsrc = rsrc_api.get('/')
  63. assert isinstance(rsrc, Ldpr)
  64. gr = rsrc.imr
  65. assert len(gr) == 10
  66. assert gr[gr.identifier : nsc['rdf'].type : nsc['ldp'].Resource ]
  67. assert gr[
  68. gr.identifier : nsc['dcterms'].title : Literal('Repository Root')]
  69. def test_get_nonexisting_node(self):
  70. """
  71. Get a non-existing node.
  72. """
  73. with pytest.raises(ResourceNotExistsError):
  74. gr = rsrc_api.get('/{}'.format(uuid4()))
  75. def test_create_ldp_rs(self):
  76. """
  77. Create an RDF resource (LDP-RS) from a provided graph.
  78. """
  79. uid = '/rsrc_from_graph'
  80. uri = nsc['fcres'][uid]
  81. gr = Graph().parse(
  82. data='<> a <http://ex.org/type#A> .', format='turtle',
  83. publicID=uri)
  84. #pdb.set_trace()
  85. evt = rsrc_api.create_or_replace(uid, graph=gr)
  86. rsrc = rsrc_api.get(uid)
  87. assert rsrc.imr[
  88. rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
  89. assert rsrc.imr[
  90. rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
  91. def test_create_ldp_nr(self):
  92. """
  93. Create a non-RDF resource (LDP-NR).
  94. """
  95. uid = '/{}'.format(uuid4())
  96. data = b'Hello. This is some dummy content.'
  97. rsrc_api.create_or_replace(
  98. uid, stream=BytesIO(data), mimetype='text/plain')
  99. rsrc = rsrc_api.get(uid)
  100. assert rsrc.content.read() == data
  101. def test_replace_rsrc(self):
  102. uid = '/test_replace'
  103. uri = nsc['fcres'][uid]
  104. gr1 = Graph().parse(
  105. data='<> a <http://ex.org/type#A> .', format='turtle',
  106. publicID=uri)
  107. evt = rsrc_api.create_or_replace(uid, graph=gr1)
  108. assert evt == RES_CREATED
  109. rsrc = rsrc_api.get(uid)
  110. assert rsrc.imr[
  111. rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
  112. assert rsrc.imr[
  113. rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
  114. gr2 = Graph().parse(
  115. data='<> a <http://ex.org/type#B> .', format='turtle',
  116. publicID=uri)
  117. #pdb.set_trace()
  118. evt = rsrc_api.create_or_replace(uid, graph=gr2)
  119. assert evt == RES_UPDATED
  120. rsrc = rsrc_api.get(uid)
  121. assert not rsrc.imr[
  122. rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
  123. assert rsrc.imr[
  124. rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#B')]
  125. assert rsrc.imr[
  126. rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
  127. def test_replace_incompatible_type(self):
  128. """
  129. Verify replacing resources with incompatible type.
  130. Replacing a LDP-NR with a LDP-RS, or vice versa, should fail.
  131. """
  132. uid_rs = '/test_incomp_rs'
  133. uid_nr = '/test_incomp_nr'
  134. data = b'mock binary content'
  135. gr = Graph().parse(
  136. data='<> a <http://ex.org/type#A> .', format='turtle',
  137. publicID=nsc['fcres'][uid_rs])
  138. rsrc_api.create_or_replace(uid_rs, graph=gr)
  139. rsrc_api.create_or_replace(
  140. uid_nr, stream=BytesIO(data), mimetype='text/plain')
  141. with pytest.raises(IncompatibleLdpTypeError):
  142. rsrc_api.create_or_replace(uid_nr, graph=gr)
  143. with pytest.raises(IncompatibleLdpTypeError):
  144. rsrc_api.create_or_replace(
  145. uid_rs, stream=BytesIO(data), mimetype='text/plain')
  146. with pytest.raises(IncompatibleLdpTypeError):
  147. rsrc_api.create_or_replace(uid_nr)
  148. def test_delta_update(self):
  149. """
  150. Update a resource with two sets of add and remove triples.
  151. """
  152. uid = '/test_delta_patch'
  153. uri = nsc['fcres'][uid]
  154. init_trp = {
  155. (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
  156. (URIRef(uri), nsc['foaf'].name, Literal('Joe Bob')),
  157. }
  158. remove_trp = {
  159. (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
  160. }
  161. add_trp = {
  162. (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Organization),
  163. }
  164. gr = Graph()
  165. gr += init_trp
  166. rsrc_api.create_or_replace(uid, graph=gr)
  167. rsrc_api.update_delta(uid, remove_trp, add_trp)
  168. rsrc = rsrc_api.get(uid)
  169. assert rsrc.imr[
  170. rsrc.uri : nsc['rdf'].type : nsc['foaf'].Organization]
  171. assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
  172. assert not rsrc.imr[
  173. rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
  174. def test_delta_update_wildcard(self):
  175. """
  176. Update a resource using wildcard modifiers.
  177. """
  178. uid = '/test_delta_patch_wc'
  179. uri = nsc['fcres'][uid]
  180. init_trp = {
  181. (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
  182. (URIRef(uri), nsc['foaf'].name, Literal('Joe Bob')),
  183. (URIRef(uri), nsc['foaf'].name, Literal('Joe Average Bob')),
  184. (URIRef(uri), nsc['foaf'].name, Literal('Joe 12oz Bob')),
  185. }
  186. remove_trp = {
  187. (URIRef(uri), nsc['foaf'].name, None),
  188. }
  189. add_trp = {
  190. (URIRef(uri), nsc['foaf'].name, Literal('Joan Knob')),
  191. }
  192. gr = Graph()
  193. gr += init_trp
  194. rsrc_api.create_or_replace(uid, graph=gr)
  195. rsrc_api.update_delta(uid, remove_trp, add_trp)
  196. rsrc = rsrc_api.get(uid)
  197. assert rsrc.imr[
  198. rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
  199. assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joan Knob')]
  200. assert not rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
  201. assert not rsrc.imr[
  202. rsrc.uri : nsc['foaf'].name : Literal('Joe Average Bob')]
  203. assert not rsrc.imr[
  204. rsrc.uri : nsc['foaf'].name : Literal('Joe 12oz Bob')]
  205. def test_sparql_update(self):
  206. """
  207. Update a resource using a SPARQL Update string.
  208. Use a mix of relative and absolute URIs.
  209. """
  210. uid = '/test_sparql'
  211. rdf_data = b'<> <http://purl.org/dc/terms/title> "Original title." .'
  212. update_str = '''DELETE {
  213. <> <http://purl.org/dc/terms/title> "Original title." .
  214. } INSERT {
  215. <> <http://purl.org/dc/terms/title> "Title #2." .
  216. <info:fcres/test_sparql>
  217. <http://purl.org/dc/terms/title> "Title #3." .
  218. <#h1> <http://purl.org/dc/terms/title> "This is a hash." .
  219. } WHERE {
  220. }'''
  221. rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle')
  222. ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
  223. rsrc = rsrc_api.update(uid, update_str)
  224. assert (
  225. (rsrc.uri, nsc['dcterms'].title, Literal('Original title.'))
  226. not in set(rsrc.imr))
  227. assert (
  228. (rsrc.uri, nsc['dcterms'].title, Literal('Title #2.'))
  229. in set(rsrc.imr))
  230. assert (
  231. (rsrc.uri, nsc['dcterms'].title, Literal('Title #3.'))
  232. in set(rsrc.imr))
  233. assert ((
  234. URIRef(str(rsrc.uri) + '#h1'),
  235. nsc['dcterms'].title, Literal('This is a hash.'))
  236. in set(rsrc.imr))
  237. def test_create_ldp_dc_post(self, dc_rdf):
  238. """
  239. Create an LDP Direct Container via POST.
  240. """
  241. rsrc_api.create_or_replace('/member')
  242. dc_uid = rsrc_api.create(
  243. '/', 'test_dc_post', rdf_data=dc_rdf, rdf_fmt='turtle')
  244. dc_rsrc = rsrc_api.get(dc_uid)
  245. member_rsrc = rsrc_api.get('/member')
  246. assert nsc['ldp'].Container in dc_rsrc.ldp_types
  247. assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
  248. def test_create_ldp_dc_put(self, dc_rdf):
  249. """
  250. Create an LDP Direct Container via PUT.
  251. """
  252. dc_uid = '/test_dc_put01'
  253. rsrc_api.create_or_replace(
  254. dc_uid, rdf_data=dc_rdf, rdf_fmt='turtle')
  255. dc_rsrc = rsrc_api.get(dc_uid)
  256. member_rsrc = rsrc_api.get('/member')
  257. assert nsc['ldp'].Container in dc_rsrc.ldp_types
  258. assert nsc['ldp'].DirectContainer in dc_rsrc.ldp_types
  259. def test_add_dc_member(self, dc_rdf):
  260. """
  261. Add members to a direct container and verify special properties.
  262. """
  263. dc_uid = '/test_dc_put02'
  264. rsrc_api.create_or_replace(
  265. dc_uid, rdf_data=dc_rdf, rdf_fmt='turtle')
  266. dc_rsrc = rsrc_api.get(dc_uid)
  267. child_uid = rsrc_api.create(dc_uid, None)
  268. member_rsrc = rsrc_api.get('/member')
  269. assert member_rsrc.imr[
  270. member_rsrc.uri: nsc['dcterms'].relation: nsc['fcres'][child_uid]]
  271. def test_indirect_container(self, ic_rdf):
  272. """
  273. Create an indirect container verify special properties.
  274. """
  275. cont_uid = '/top_container'
  276. ic_uid = '{}/test_ic'.format(cont_uid)
  277. member_uid = '{}/ic_member'.format(ic_uid)
  278. target_uid = '/ic_target'
  279. ic_member_rdf = b'''
  280. PREFIX ore: <http://www.openarchives.org/ore/terms/>
  281. <> ore:proxyFor <info:fcres/ic_target> .'''
  282. rsrc_api.create_or_replace(cont_uid)
  283. rsrc_api.create_or_replace(target_uid)
  284. rsrc_api.create_or_replace(ic_uid, rdf_data=ic_rdf, rdf_fmt='turtle')
  285. rsrc_api.create_or_replace(
  286. member_uid, rdf_data=ic_member_rdf, rdf_fmt='turtle')
  287. ic_rsrc = rsrc_api.get(ic_uid)
  288. assert nsc['ldp'].Container in ic_rsrc.ldp_types
  289. assert nsc['ldp'].IndirectContainer in ic_rsrc.ldp_types
  290. assert nsc['ldp'].DirectContainer not in ic_rsrc.ldp_types
  291. member_rsrc = rsrc_api.get(member_uid)
  292. top_cont_rsrc = rsrc_api.get(cont_uid)
  293. assert top_cont_rsrc.imr[
  294. top_cont_rsrc.uri: nsc['dcterms'].relation:
  295. nsc['fcres'][target_uid]]
  296. def test_soft_delete(self):
  297. """
  298. Soft-delete (bury) a resource.
  299. """
  300. uid = '/test_soft_delete01'
  301. rsrc_api.create_or_replace(uid)
  302. rsrc_api.delete(uid)
  303. with pytest.raises(TombstoneError):
  304. rsrc_api.get(uid)
  305. def test_resurrect(self):
  306. """
  307. Restore (resurrect) a soft-deleted resource.
  308. """
  309. uid = '/test_soft_delete02'
  310. rsrc_api.create_or_replace(uid)
  311. rsrc_api.delete(uid)
  312. rsrc_api.resurrect(uid)
  313. rsrc = rsrc_api.get(uid)
  314. assert nsc['ldp'].Resource in rsrc.ldp_types
  315. def test_hard_delete(self):
  316. """
  317. Hard-delete (forget) a resource.
  318. """
  319. uid = '/test_hard_delete01'
  320. rsrc_api.create_or_replace(uid)
  321. rsrc_api.delete(uid, False)
  322. with pytest.raises(ResourceNotExistsError):
  323. rsrc_api.get(uid)
  324. with pytest.raises(ResourceNotExistsError):
  325. rsrc_api.resurrect(uid)
  326. def test_delete_children(self):
  327. """
  328. Soft-delete a resource with children.
  329. """
  330. uid = '/test_soft_delete_children01'
  331. rsrc_api.create_or_replace(uid)
  332. for i in range(3):
  333. rsrc_api.create_or_replace('{}/child{}'.format(uid, i))
  334. rsrc_api.delete(uid)
  335. with pytest.raises(TombstoneError):
  336. rsrc_api.get(uid)
  337. for i in range(3):
  338. with pytest.raises(TombstoneError):
  339. rsrc_api.get('{}/child{}'.format(uid, i))
  340. # Cannot resurrect children of a tombstone.
  341. with pytest.raises(TombstoneError):
  342. rsrc_api.resurrect('{}/child{}'.format(uid, i))
  343. def test_resurrect_children(self):
  344. """
  345. Resurrect a resource with its children.
  346. This uses fixtures from the previous test.
  347. """
  348. uid = '/test_soft_delete_children01'
  349. rsrc_api.resurrect(uid)
  350. parent_rsrc = rsrc_api.get(uid)
  351. assert nsc['ldp'].Resource in parent_rsrc.ldp_types
  352. for i in range(3):
  353. child_rsrc = rsrc_api.get('{}/child{}'.format(uid, i))
  354. assert nsc['ldp'].Resource in child_rsrc.ldp_types
  355. def test_hard_delete_children(self):
  356. """
  357. Hard-delete (forget) a resource with its children.
  358. This uses fixtures from the previous test.
  359. """
  360. uid = '/test_hard_delete_children01'
  361. rsrc_api.create_or_replace(uid)
  362. for i in range(3):
  363. rsrc_api.create_or_replace('{}/child{}'.format(uid, i))
  364. rsrc_api.delete(uid, False)
  365. with pytest.raises(ResourceNotExistsError):
  366. rsrc_api.get(uid)
  367. with pytest.raises(ResourceNotExistsError):
  368. rsrc_api.resurrect(uid)
  369. for i in range(3):
  370. with pytest.raises(ResourceNotExistsError):
  371. rsrc_api.get('{}/child{}'.format(uid, i))
  372. with pytest.raises(ResourceNotExistsError):
  373. rsrc_api.resurrect('{}/child{}'.format(uid, i))
  374. @pytest.mark.usefixtures('db')
  375. class TestResourceVersioning:
  376. '''
  377. Test resource version lifecycle.
  378. '''
  379. def test_create_version(self):
  380. """
  381. Create a version snapshot.
  382. """
  383. uid = '/test_version1'
  384. rdf_data = b'<> <http://purl.org/dc/terms/title> "Original title." .'
  385. update_str = '''DELETE {
  386. <> <http://purl.org/dc/terms/title> "Original title." .
  387. } INSERT {
  388. <> <http://purl.org/dc/terms/title> "Title #2." .
  389. } WHERE {
  390. }'''
  391. rsrc_api.create_or_replace(uid, rdf_data=rdf_data, rdf_fmt='turtle')
  392. ver_uid = rsrc_api.create_version(uid, 'v1').split('fcr:versions/')[-1]
  393. rsrc_api.update(uid, update_str)
  394. current = rsrc_api.get(uid)
  395. assert (
  396. (current.uri, nsc['dcterms'].title, Literal('Title #2.'))
  397. in current.imr)
  398. assert (
  399. (current.uri, nsc['dcterms'].title, Literal('Original title.'))
  400. not in current.imr)
  401. v1 = rsrc_api.get_version(uid, ver_uid)
  402. assert (
  403. (v1.identifier, nsc['dcterms'].title, Literal('Original title.'))
  404. in set(v1))
  405. assert (
  406. (v1.identifier, nsc['dcterms'].title, Literal('Title #2.'))
  407. not in set(v1))
  408. def test_revert_to_version(self):
  409. """
  410. Test reverting to a previous version.
  411. Uses assets from previous test.
  412. """
  413. uid = '/test_version1'
  414. ver_uid = 'v1'
  415. rsrc_api.revert_to_version(uid, ver_uid)
  416. rev = rsrc_api.get(uid)
  417. assert (
  418. (rev.uri, nsc['dcterms'].title, Literal('Original title.'))
  419. in rev.imr)
  420. def test_versioning_children(self):
  421. """
  422. Test that children are not affected by version restoring.
  423. This test does the following:
  424. 1. create parent resource
  425. 2. Create child 1
  426. 3. Version parent
  427. 4. Create child 2
  428. 5. Restore parent to previous version
  429. 6. Verify that restored version still has 2 children
  430. """
  431. uid = '/test_version_children'
  432. ver_uid = 'v1'
  433. ch1_uid = '{}/kid_a'.format(uid)
  434. ch2_uid = '{}/kid_b'.format(uid)
  435. rsrc_api.create_or_replace(uid)
  436. rsrc_api.create_or_replace(ch1_uid)
  437. ver_uid = rsrc_api.create_version(uid, ver_uid).split('fcr:versions/')[-1]
  438. rsrc = rsrc_api.get(uid)
  439. assert nsc['fcres'][ch1_uid] in rsrc.imr.objects(
  440. rsrc.uri, nsc['ldp'].contains)
  441. rsrc_api.create_or_replace(ch2_uid)
  442. rsrc = rsrc_api.get(uid)
  443. assert nsc['fcres'][ch2_uid] in rsrc.imr.objects(
  444. rsrc.uri, nsc['ldp'].contains)
  445. rsrc_api.revert_to_version(uid, ver_uid)
  446. rsrc = rsrc_api.get(uid)
  447. assert nsc['fcres'][ch1_uid] in rsrc.imr.objects(
  448. rsrc.uri, nsc['ldp'].contains)
  449. assert nsc['fcres'][ch2_uid] in rsrc.imr.objects(
  450. rsrc.uri, nsc['ldp'].contains)