contentgrid-application-client 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {contentgrid_application_client-0.2.4/src/contentgrid_application_client.egg-info → contentgrid_application_client-0.2.6}/PKG-INFO +2 -2
  2. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client/application.py +87 -12
  3. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6/src/contentgrid_application_client.egg-info}/PKG-INFO +2 -2
  4. contentgrid_application_client-0.2.6/src/contentgrid_application_client.egg-info/requires.txt +1 -0
  5. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/fixtures.py +2 -1
  6. contentgrid_application_client-0.2.6/tests/test_profile.py +285 -0
  7. contentgrid_application_client-0.2.6/tests/test_relations.py +315 -0
  8. contentgrid_application_client-0.2.4/src/contentgrid_application_client.egg-info/requires.txt +0 -1
  9. contentgrid_application_client-0.2.4/tests/test_profile.py +0 -102
  10. contentgrid_application_client-0.2.4/tests/test_relations.py +0 -222
  11. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/LICENSE +0 -0
  12. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/README.md +0 -0
  13. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/pyproject.toml +0 -0
  14. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/pytest.ini +0 -0
  15. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/requirements.txt +0 -0
  16. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/setup.cfg +0 -0
  17. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/setup.py +0 -0
  18. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client/__init__.py +0 -0
  19. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client/py.typed +0 -0
  20. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client.egg-info/SOURCES.txt +0 -0
  21. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client.egg-info/dependency_links.txt +0 -0
  22. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/src/contentgrid_application_client.egg-info/top_level.txt +0 -0
  23. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/example-docs/document.jpg +0 -0
  24. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/example-docs/resume.pdf +0 -0
  25. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_collection.py +0 -0
  26. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_content.py +0 -0
  27. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_curies.py +0 -0
  28. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_entity_creation.py +0 -0
  29. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_hal_form_type_check.py +0 -0
  30. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_openapi.py +0 -0
  31. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_prompting.py +0 -0
  32. {contentgrid_application_client-0.2.4 → contentgrid_application_client-0.2.6}/tests/test_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python Client for interacting with ContentGrid Applications
5
5
  Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
6
6
  License: Copyright 2024 Xenit Solutions
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: contentgrid-hal-client==0.2.4
25
+ Requires-Dist: contentgrid-hal-client==0.2.6
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -5,6 +5,7 @@ import requests
5
5
  import os
6
6
  import logging
7
7
  import mimetypes
8
+ import json
8
9
 
9
10
  from contentgrid_hal_client.exceptions import BadRequest, IncorrectAttributeType, MissingRequiredAttribute, NotFound, MissingHALTemplate
10
11
  from contentgrid_hal_client.hal import CurieRegistry, HALFormsClient, HALLink, HALResponse, InteractiveHALResponse
@@ -131,12 +132,12 @@ class EntityCollection(InteractiveApplicationResponse):
131
132
  return cast(HALLink, self.get_link("profile"))
132
133
 
133
134
  def get_entity_profile(self) -> "EntityProfile":
134
- return self.client.follow_link(self.get_entity_profile_link(), infer_type=EntityProfile)
135
+ return self.client.load_or_fetch_entity_profile(self.get_entity_profile_link())
135
136
 
136
137
  def create_entity(self, attributes: dict, attribute_validation=True) -> "EntityObject":
137
138
  self.client._transform_hal_links_to_uris(attributes=attributes)
138
139
  if attribute_validation:
139
- entity_profile = self.client.follow_link(self.get_entity_profile_link(), infer_type=EntityProfile)
140
+ entity_profile = self.client.load_or_fetch_entity_profile(self.get_entity_profile_link())
140
141
  self.client._validate_form_payload(template=entity_profile.get_template("create-form"), payload=attributes)
141
142
  response = self.client.post(self.get_self_link().uri, json=attributes, headers={"Content-Type": "application/json"})
142
143
  data = self.client._validate_json_response(response)
@@ -251,10 +252,17 @@ class EntityObject(InteractiveApplicationResponse):
251
252
  return entity_profile
252
253
  raise NotFound(f"No entity profile describes {self_link_uri}")
253
254
 
254
- def get_relation_collection(self, relation_name: str, size: int = 20, params: dict = {}):
255
+ def get_relation_collection(self, relation_name: str, size: int = 20, params: dict = {}) -> EntityCollection:
255
256
  params = self.client._add_page_and_size_to_params(size=size, params=params)
256
257
  relation_link = self.get_relation_link(relation_name=relation_name)
257
258
  return self.client.follow_link(relation_link, infer_type=EntityCollection, params=params)
259
+
260
+ def get_related_entity(self, relation_name: str) -> "EntityObject":
261
+ relation_link = self.get_relation_link(relation_name=relation_name)
262
+ return self.client.follow_link(relation_link, infer_type=EntityObject)
263
+
264
+ def set_related_entity(self, relation_name: str, related_entity : "EntityObject") -> None:
265
+ self.put_relation(relation_name=relation_name, related_entity_links=[related_entity], prefix_client_endpoint=True)
258
266
 
259
267
  def put_relation(self, relation_name: str, related_entity_links: List[str | HALLink | Self], prefix_client_endpoint : bool = True) -> None:
260
268
  set_template = self.get_template(f"set-{relation_name}")
@@ -376,8 +384,8 @@ class EntityObject(InteractiveApplicationResponse):
376
384
  lines.append(f" - {content_link.name} (content attribute name): {content_link.uri}")
377
385
  return "\n".join(lines)
378
386
 
379
- class Profile(InteractiveHALResponse):
380
- def __init__(self, data: dict, client: HALFormsClient, curie_registry: CurieRegistry = None) -> None:
387
+ class Profile(InteractiveApplicationResponse):
388
+ def __init__(self, data: dict, client: "ContentGridApplicationClient", curie_registry: CurieRegistry = None) -> None:
381
389
  super().__init__(data, client, curie_registry)
382
390
 
383
391
  def get_entity_links(self) -> List[HALLink]:
@@ -385,12 +393,12 @@ class Profile(InteractiveHALResponse):
385
393
 
386
394
  def get_entity_profiles_generator(self) -> Generator["EntityProfile", None, None]:
387
395
  for entity_profile_link in self.get_entity_links():
388
- yield self.client.follow_link(entity_profile_link, infer_type=EntityProfile)
396
+ yield self.client.load_or_fetch_entity_profile(entity_profile_link)
389
397
 
390
398
  def get_entity_profile(self, singular_entity_name: str) -> "EntityProfile": # type: ignore
391
399
  for entity_profile_link in self.get_entity_links():
392
400
  if entity_profile_link.name == singular_entity_name:
393
- return self.client.follow_link(entity_profile_link, infer_type=EntityProfile)
401
+ return self.client.load_or_fetch_entity_profile(entity_profile_link)
394
402
  raise NotFound(f"Entity Profile {singular_entity_name} does not exist.")
395
403
 
396
404
  def prompt(self) -> str:
@@ -423,7 +431,7 @@ class RelationProfile(InteractiveApplicationResponse):
423
431
  def get_related_entity_profile(self) -> "EntityProfile":
424
432
  related_profile_link = self.get_related_entity_profile_link()
425
433
  if related_profile_link:
426
- return self.client.follow_link(related_profile_link, infer_type=EntityProfile)
434
+ return self.client.load_or_fetch_entity_profile(related_profile_link)
427
435
  else:
428
436
  raise NotFound(f"Related entity profile link not found on relation profile {self.name}")
429
437
 
@@ -495,7 +503,32 @@ class AttributeProfile(InteractiveApplicationResponse):
495
503
 
496
504
  def has_constrained_values(self) -> bool:
497
505
  return self.get_constraint("allowed-values") is not None
498
-
506
+
507
+ def has_allowed_values_constraint(self) -> bool:
508
+ return self.has_constrained_values()
509
+
510
+ def has_unique_constraint(self) -> bool:
511
+ return self.get_constraint("unique") is not None
512
+
513
+ def has_created_by_constraint(self) -> bool:
514
+ return self.get_constraint("created-by") is not None
515
+
516
+ def has_created_date_constraint(self) -> bool:
517
+ return self.get_constraint("created-date") is not None
518
+
519
+ def has_modified_by_constraint(self) -> bool:
520
+ return self.get_constraint("modified-by") is not None
521
+
522
+ def has_modified_date_constraint(self) -> bool:
523
+ return self.get_constraint("modified-date") is not None
524
+
525
+ @property
526
+ def is_audit_attribute(self) -> bool:
527
+ return self.has_created_by_constraint() or \
528
+ self.has_created_date_constraint() or \
529
+ self.has_modified_by_constraint() or \
530
+ self.has_modified_date_constraint()
531
+
499
532
  def get_allowed_values(self) -> Optional[List[str]]:
500
533
  if self.has_constrained_values():
501
534
  return self.get_constraint("allowed-values").values # type: ignore
@@ -821,9 +854,10 @@ class ContentGridApplicationClient(HALFormsClient):
821
854
  client_id: str = None,
822
855
  client_secret: str = None,
823
856
  token: str = None,
824
- attribute_validation: bool = True,
857
+ attribute_validation: bool = False,
825
858
  session_cookie: str = None,
826
859
  pool_maxsize: int = 10,
860
+ use_profile_cache: bool = True
827
861
  ) -> None:
828
862
  logging.info("Initializing ContentGridApplicationClient...")
829
863
  super().__init__(
@@ -837,8 +871,38 @@ class ContentGridApplicationClient(HALFormsClient):
837
871
  pool_maxsize=pool_maxsize,
838
872
  )
839
873
  self.attribute_validation = attribute_validation
840
-
874
+ self.use_profile_cache = use_profile_cache
875
+ self.profile_cache: dict[str, dict[str, Any]] = {}
876
+
877
+ # PROFILE CACHING
878
+ def load_profile_cache(self, url: str, json_data: dict | str) -> None:
879
+ """
880
+ Load JSON data into the profile cache.
881
+
882
+ Args:
883
+ url: The URL to cache the response for (relative to client_endpoint)
884
+ json_data: The JSON data to cache (as dict or JSON string)
885
+ """
886
+ if isinstance(json_data, str):
887
+ json_data = json.loads(json_data)
888
+
889
+ self.profile_cache[url] = cast(dict, json_data)
890
+ logging.info(f"Loaded JSON data into profile cache for {url}")
891
+
892
+ def clear_profile_cache(self):
893
+ logging.info(f"Clearing profile cache... clearing {len(self.profile_cache.keys())} entries")
894
+ self.profile_cache = {}
895
+
841
896
  def get_profile(self) -> Profile:
897
+ if self.use_profile_cache:
898
+ profile_cache_key = self.client_endpoint.click("/profile").normalize().to_text()
899
+ if profile_cache_key in self.profile_cache:
900
+ data = self.profile_cache[profile_cache_key]
901
+ return Profile(data, client=self)
902
+ response = self.get("/profile", headers={"Accept": "application/json"})
903
+ data = self._validate_json_response(response)
904
+ self.profile_cache[profile_cache_key] = data
905
+ return Profile(data, client=self)
842
906
  response = self.get("/profile", headers={"Accept": "application/json"})
843
907
  data = self._validate_json_response(response)
844
908
  return Profile(data, client=self)
@@ -848,6 +912,17 @@ class ContentGridApplicationClient(HALFormsClient):
848
912
 
849
913
  def get_entity_profiles(self) -> List[EntityProfile]:
850
914
  return [entity_profile for entity_profile in self.get_profile().get_entity_profiles_generator()]
915
+
916
+ def load_or_fetch_entity_profile(self, profile_link : HALLink, params: dict={}) -> EntityProfile:
917
+ if self.use_profile_cache:
918
+ if profile_link.uri in self.profile_cache:
919
+ data = self.profile_cache[profile_link.uri]
920
+ return EntityProfile(data=data, client=self)
921
+ response = self.get(profile_link.uri, params=params)
922
+ data = self._validate_json_response(response)
923
+ self.profile_cache[profile_link.uri] = data
924
+ return EntityProfile(data=data, client=self)
925
+ return self.follow_link(profile_link, infer_type=EntityProfile, params=params)
851
926
 
852
927
  def fetch_openapi_yaml(self) -> tuple[str, bytes]:
853
928
  res = self.get("/openapi.yml")
@@ -1009,7 +1084,7 @@ class ContentGridApplicationClient(HALFormsClient):
1009
1084
  for hal_property in template.properties:
1010
1085
  if hal_property.required:
1011
1086
  if hal_property.name not in payload.keys():
1012
- raise MissingRequiredAttribute(f"Required attribute {property.name} not present in payload.")
1087
+ raise MissingRequiredAttribute(f"Required attribute {hal_property.name} not present in payload.")
1013
1088
 
1014
1089
  def _prefix_url_in_form_payload(self, template: HALFormsTemplate, payload: dict) -> None:
1015
1090
  """Prefix URL properties in the payload with the client_endpoint if needed.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python Client for interacting with ContentGrid Applications
5
5
  Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
6
6
  License: Copyright 2024 Xenit Solutions
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: contentgrid-hal-client==0.2.4
25
+ Requires-Dist: contentgrid-hal-client==0.2.6
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -30,7 +30,8 @@ def auth_manager() -> ApplicationAuthenticationManager:
30
30
  def cg_client(auth_manager: ApplicationAuthenticationManager) -> ContentGridApplicationClient:
31
31
  return ContentGridApplicationClient(
32
32
  client_endpoint=CONTENTGRID_CLIENT_ENDPOINT,
33
- auth_manager=auth_manager
33
+ auth_manager=auth_manager,
34
+ attribute_validation=True
34
35
  )
35
36
 
36
37
  @fixture
@@ -0,0 +1,285 @@
1
+ from contentgrid_application_client import ContentGridApplicationClient, Profile, EntityProfile, AttributeProfile
2
+ import pytest
3
+ from contentgrid_hal_client.exceptions import NotFound
4
+ from fixtures import auth_manager, cg_client # noqa: F401
5
+
6
+ def test_get_profile_attributes(cg_client: ContentGridApplicationClient):
7
+ profile = cg_client.get_profile()
8
+ entity_links = profile.get_entity_links()
9
+ assert len(entity_links) > 0
10
+ assert all(link.name for link in entity_links)
11
+ assert all(link.uri for link in entity_links)
12
+
13
+ def test_entity_profile_details(cg_client: ContentGridApplicationClient):
14
+ profile = cg_client.get_profile()
15
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
16
+
17
+ # Test basic entity profile properties
18
+ assert isinstance(entity_profile, EntityProfile)
19
+ assert entity_profile.name is not None
20
+ assert entity_profile.title is not None
21
+ assert entity_profile.description is not None
22
+
23
+ def test_attribute_profiles(cg_client: ContentGridApplicationClient):
24
+ profile = cg_client.get_profile()
25
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
26
+
27
+ attribute_profiles = entity_profile.get_attribute_profiles()
28
+ if not attribute_profiles:
29
+ pytest.skip(f"Entity {entity_profile.name} has no attribute profiles to test.")
30
+
31
+ # Test first attribute profile properties
32
+ first_attribute = attribute_profiles[0]
33
+ assert isinstance(first_attribute, AttributeProfile)
34
+ assert first_attribute.name is not None
35
+ assert first_attribute.type is not None
36
+ assert isinstance(first_attribute.required, bool)
37
+ assert isinstance(first_attribute.read_only, bool)
38
+
39
+ def test_invalid_entity_profile(cg_client: ContentGridApplicationClient):
40
+ with pytest.raises(NotFound):
41
+ cg_client.get_entity_profile("non_existent_entity")
42
+
43
+ def test_nested_attributes(cg_client: ContentGridApplicationClient):
44
+ profile = cg_client.get_profile()
45
+ # Use a known entity type if available, otherwise the first one
46
+ entity_name = "anonymous-candidate" if "anonymous-candidate" in [link.name for link in profile.get_entity_links()] else profile.get_entity_links()[0].name
47
+ entity_profile = cg_client.get_entity_profile(entity_name)
48
+
49
+ found_nested = False
50
+ for attribute in entity_profile.get_attribute_profiles():
51
+ nested_attributes = attribute.get_nested_attributes()
52
+ if nested_attributes:
53
+ found_nested = True
54
+ assert all(isinstance(nested, AttributeProfile) for nested in nested_attributes)
55
+ assert all(hasattr(nested, 'name') for nested in nested_attributes)
56
+ if not found_nested:
57
+ print(f"INFO: No nested attributes found for entity {entity_name} to test.")
58
+
59
+ def test_content_attribute_detection(cg_client: ContentGridApplicationClient):
60
+ profile = cg_client.get_profile()
61
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
62
+
63
+ for attribute in entity_profile.get_attribute_profiles():
64
+ if attribute.is_content_attribute():
65
+ nested_attrs = attribute.get_nested_attributes()
66
+ assert any(attr.name in ["length", "mimetype", "filename"] for attr in nested_attrs)
67
+
68
+ def test_relation_profiles(cg_client: ContentGridApplicationClient):
69
+ profile = cg_client.get_profile()
70
+ entity_profile = cg_client.get_entity_profile("anonymous-candidate")
71
+
72
+ relation_profiles = entity_profile.get_relation_profiles()
73
+ assert len(relation_profiles) >= 0
74
+
75
+ if relation_profiles:
76
+ first_relation = relation_profiles[0]
77
+ assert first_relation.name is not None
78
+ assert first_relation.title is not None
79
+ assert isinstance(first_relation.required, bool)
80
+ assert isinstance(first_relation.many_source_per_target, bool)
81
+ assert isinstance(first_relation.many_target_per_source, bool)
82
+
83
+ def test_get_specific_relation_profile(cg_client: ContentGridApplicationClient):
84
+ profile = cg_client.get_profile()
85
+ entity_profile = cg_client.get_entity_profile("anonymous-candidate")
86
+
87
+ if entity_profile.get_relation_profiles():
88
+ relation_name = entity_profile.get_relation_profiles()[0].name
89
+ relation_profile = entity_profile.get_relation_profile(relation_name)
90
+ assert relation_profile.name == relation_name
91
+
92
+ # Test related entity profile retrieval
93
+ related_entity_profile = relation_profile.get_related_entity_profile()
94
+ assert isinstance(related_entity_profile, EntityProfile)
95
+ assert related_entity_profile.name is not None
96
+
97
+ def test_invalid_relation_profile(cg_client: ContentGridApplicationClient):
98
+ profile = cg_client.get_profile()
99
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
100
+
101
+ with pytest.raises(ValueError):
102
+ entity_profile.get_relation_profile("non_existent_relation")
103
+
104
+
105
+ def test_load_profile_cache_with_dict(cg_client: ContentGridApplicationClient):
106
+ """Test loading profile cache with dict data"""
107
+ test_data = {
108
+ "_links": {
109
+ "self": {"href": "/profile/test-entity"}
110
+ },
111
+ "name": "test-entity",
112
+ "description": "Test entity"
113
+ }
114
+
115
+ cg_client.load_profile_cache("/profile/test-entity", test_data)
116
+
117
+ # Verify it's in the cache
118
+ assert "/profile/test-entity" in cg_client.profile_cache
119
+ cached_data = cg_client.profile_cache["/profile/test-entity"]
120
+ assert cached_data == test_data
121
+
122
+ def test_load_profile_cache_with_json_string(cg_client: ContentGridApplicationClient):
123
+ """Test loading profile cache with JSON string"""
124
+ import json
125
+ test_data = {
126
+ "_links": {
127
+ "self": {"href": "/profile/test-entity2"}
128
+ },
129
+ "name": "test-entity2"
130
+ }
131
+ json_string = json.dumps(test_data)
132
+
133
+ cg_client.load_profile_cache("/profile/test-entity2", json_string)
134
+
135
+ # Verify it's in the cache
136
+ assert "/profile/test-entity2" in cg_client.profile_cache
137
+ cached_data = cg_client.profile_cache["/profile/test-entity2"]
138
+ assert cached_data == test_data
139
+
140
+ def test_profile_cache_hit_returns_cached_response(cg_client: ContentGridApplicationClient):
141
+ """Test that cached profile responses are returned on subsequent requests"""
142
+ # First request to populate cache
143
+ profile1 = cg_client.get_profile()
144
+ initial_cache_size = len(cg_client.profile_cache)
145
+
146
+ # Second request should use cache
147
+ profile2 = cg_client.get_profile()
148
+
149
+ # Cache size should not have increased
150
+ assert len(cg_client.profile_cache) == initial_cache_size
151
+
152
+ # Both profiles should have the same data
153
+ assert profile1.data == profile2.data
154
+
155
+ def test_profile_cache_only_affects_profile_endpoints(cg_client: ContentGridApplicationClient):
156
+ """Test that cache only affects profile endpoints"""
157
+ initial_cache_size = len(cg_client.profile_cache)
158
+
159
+ # Get profile - should be cached
160
+ cg_client.get_profile()
161
+
162
+ # Cache should have grown
163
+ assert len(cg_client.profile_cache) > initial_cache_size
164
+
165
+ # Getting entity collection should not add to profile cache
166
+ profile = cg_client.get_profile()
167
+ if profile.get_entity_links():
168
+ entity_name = profile.get_entity_links()[0].name
169
+ entity_profile = cg_client.get_entity_profile(entity_name)
170
+ cache_size_after_entity_profile = len(cg_client.profile_cache)
171
+
172
+ # Getting the entity collection should not be cached
173
+ try:
174
+ cg_client.get_entity_collection(entity_name, size=1)
175
+ except Exception:
176
+ pass # We don't care if this fails, just testing cache behavior
177
+
178
+ # Profile cache size should not change from non-profile requests
179
+ assert len(cg_client.profile_cache) >= cache_size_after_entity_profile
180
+
181
+ def test_profile_cache_caches_entity_profiles(cg_client: ContentGridApplicationClient):
182
+ """Test that entity profile requests are also cached"""
183
+ profile = cg_client.get_profile()
184
+
185
+ if not profile.get_entity_links():
186
+ pytest.skip("No entity profiles available to test caching")
187
+
188
+ entity_name = profile.get_entity_links()[0].name
189
+ initial_cache_size = len(cg_client.profile_cache)
190
+
191
+ # First request
192
+ entity_profile1 = cg_client.get_entity_profile(entity_name)
193
+ cache_size_after_first = len(cg_client.profile_cache)
194
+
195
+ # Cache should have grown (at least entity profile URL)
196
+ assert cache_size_after_first > initial_cache_size
197
+
198
+ # Second request - should hit cache
199
+ entity_profile2 = cg_client.get_entity_profile(entity_name)
200
+
201
+ # Cache size should not change
202
+ assert len(cg_client.profile_cache) == cache_size_after_first
203
+
204
+ # Data should be the same
205
+ assert entity_profile1.name == entity_profile2.name
206
+
207
+ def test_clear_empty_profile_cache(cg_client: ContentGridApplicationClient):
208
+ """Test clearing an empty cache doesn't cause issues"""
209
+ # Clear empty cache
210
+ cg_client.clear_profile_cache()
211
+
212
+ # Verify cache is still empty
213
+ assert len(cg_client.profile_cache) == 0
214
+ assert cg_client.profile_cache == {}
215
+
216
+ def test_clear_profile_cache_removes_all_entries(cg_client: ContentGridApplicationClient):
217
+ """Test that clearing cache removes all cached entries"""
218
+ # Populate cache with some data
219
+ test_data1 = {"_links": {"self": {"href": "/profile/entity1"}}, "name": "entity1"}
220
+ test_data2 = {"_links": {"self": {"href": "/profile/entity2"}}, "name": "entity2"}
221
+
222
+ cg_client.load_profile_cache("/profile/entity1", test_data1)
223
+ cg_client.load_profile_cache("/profile/entity2", test_data2)
224
+
225
+ # Verify entries are in cache
226
+ assert len(cg_client.profile_cache) >= 2
227
+
228
+ # Clear cache
229
+ cg_client.clear_profile_cache()
230
+
231
+ # Verify cache is empty
232
+ assert len(cg_client.profile_cache) == 0
233
+ assert "/profile/entity1" not in cg_client.profile_cache
234
+ assert "/profile/entity2" not in cg_client.profile_cache
235
+
236
+ def test_clear_profile_cache_allows_repopulation(cg_client: ContentGridApplicationClient):
237
+ """Test that cache works normally after being cleared"""
238
+ # Populate cache
239
+ profile1 = cg_client.get_profile()
240
+ assert len(cg_client.profile_cache) > 0
241
+
242
+ # Clear cache
243
+ cg_client.clear_profile_cache()
244
+ assert len(cg_client.profile_cache) == 0
245
+
246
+ # Repopulate cache - should work normally
247
+ profile2 = cg_client.get_profile()
248
+ assert len(cg_client.profile_cache) > 0
249
+
250
+ # Data should be consistent
251
+ assert profile1.data == profile2.data
252
+
253
+ def test_clear_profile_cache_doesnt_affect_caching_flag(cg_client: ContentGridApplicationClient):
254
+ """Test that clearing cache doesn't disable caching"""
255
+ # Check caching is enabled
256
+ assert cg_client.use_profile_cache is True
257
+
258
+ # Load and clear cache
259
+ cg_client.load_profile_cache("/profile/test", {"name": "test"})
260
+ cg_client.clear_profile_cache()
261
+
262
+ # Caching should still be enabled
263
+ assert cg_client.use_profile_cache is True
264
+
265
+ # Cache should still work
266
+ cg_client.get_profile()
267
+ assert len(cg_client.profile_cache) > 0
268
+
269
+ def test_clear_profile_cache_forces_fresh_request(cg_client: ContentGridApplicationClient):
270
+ """Test that clearing cache forces a fresh request on next call"""
271
+ # First request - populates cache
272
+ cg_client.get_profile()
273
+ cache_size_after_first = len(cg_client.profile_cache)
274
+ assert cache_size_after_first > 0
275
+
276
+ # Clear cache
277
+ cg_client.clear_profile_cache()
278
+ assert len(cg_client.profile_cache) == 0
279
+
280
+ # Next request should repopulate cache (new request made)
281
+ cg_client.get_profile()
282
+ cache_size_after_clear = len(cg_client.profile_cache)
283
+
284
+ # Cache should be populated again
285
+ assert cache_size_after_clear > 0
@@ -0,0 +1,315 @@
1
+ from contentgrid_application_client import ContentGridApplicationClient, EntityCollection
2
+ from contentgrid_application_client.application import EntityObject
3
+ from contentgrid_hal_client.exceptions import Forbidden, MissingHALTemplate, NotFound
4
+ from contentgrid_hal_client.hal import HALLink
5
+ from contentgrid_hal_client.hal_forms import HALFormsTemplate, HALFormsProperty, HALFormsPropertyType, HALFormsMethod
6
+ from fixtures import auth_manager, cg_client # noqa: F401
7
+ from unittest.mock import MagicMock
8
+ import pytest
9
+ import hyperlink
10
+ import uuid
11
+
12
+
13
+ class TestCreateTextUriListPayload:
14
+ """Unit tests for _create_text_uri_list_payload method"""
15
+
16
+ @pytest.fixture
17
+ def entity(self):
18
+ """Create an EntityObject with a mock client for testing"""
19
+ mock_client = MagicMock()
20
+ mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
21
+ entity = EntityObject.__new__(EntityObject)
22
+ entity.client = mock_client
23
+ return entity
24
+
25
+ def test_uri_list_with_relative_paths(self, entity):
26
+ """Test that relative paths are properly prefixed with client endpoint"""
27
+ result = entity._create_text_uri_list_payload(["/entities/123", "/entities/456"])
28
+
29
+ assert result == "https://api.example.com/entities/123\nhttps://api.example.com/entities/456"
30
+
31
+ def test_uri_list_with_full_urls(self, entity):
32
+ """Test that full URLs matching endpoint are not modified"""
33
+ full_url = "https://api.example.com/entities/123"
34
+ result = entity._create_text_uri_list_payload([full_url])
35
+
36
+ assert result == full_url
37
+
38
+ def test_uri_list_with_hal_link(self, entity):
39
+ """Test that HALLink objects are properly converted"""
40
+ hal_link = HALLink(uri="/entities/123")
41
+ result = entity._create_text_uri_list_payload([hal_link])
42
+
43
+ assert result == "https://api.example.com/entities/123"
44
+
45
+ def test_uri_list_with_hal_link_full_url(self, entity):
46
+ """Test that HALLink with full URL is not double-prefixed"""
47
+ hal_link = HALLink(uri="https://api.example.com/entities/123")
48
+ result = entity._create_text_uri_list_payload([hal_link])
49
+
50
+ assert result == "https://api.example.com/entities/123"
51
+
52
+ def test_uri_list_without_prefix(self, entity):
53
+ """Test that prefix_client_endpoint=False returns URIs unchanged"""
54
+ result = entity._create_text_uri_list_payload(["/entities/123"], prefix_client_endpoint=False)
55
+
56
+ assert result == "/entities/123"
57
+
58
+
59
+ class TestPrefixUrlInFormPayload:
60
+ """Unit tests for _prefix_url_in_form_payload method"""
61
+
62
+ @pytest.fixture
63
+ def client(self):
64
+ """Create a mock ContentGridApplicationClient for testing"""
65
+ mock_client = MagicMock(spec=ContentGridApplicationClient)
66
+ mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
67
+ mock_client._prefix_url_in_form_payload = ContentGridApplicationClient._prefix_url_in_form_payload.__get__(mock_client)
68
+ return mock_client
69
+
70
+ def test_prefix_url_property_with_relative_path(self, client):
71
+ """Test that URL properties with relative paths are prefixed"""
72
+ template = HALFormsTemplate(
73
+ method=HALFormsMethod.POST,
74
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
75
+ )
76
+ payload = {"link": "/entities/123"}
77
+
78
+ client._prefix_url_in_form_payload(template=template, payload=payload)
79
+
80
+ assert payload["link"] == "https://api.example.com/entities/123"
81
+
82
+ def test_prefix_url_property_avoids_double_slashes(self, client):
83
+ """Test that double slashes are avoided"""
84
+ template = HALFormsTemplate(
85
+ method=HALFormsMethod.POST,
86
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
87
+ )
88
+ payload = {"link": "/entities/123"}
89
+
90
+ client._prefix_url_in_form_payload(template=template, payload=payload)
91
+
92
+ assert "//" not in payload["link"].replace("https://", "")
93
+
94
+ def test_prefix_url_property_with_full_url(self, client):
95
+ """Test that full URLs are not double-prefixed"""
96
+ template = HALFormsTemplate(
97
+ method=HALFormsMethod.POST,
98
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
99
+ )
100
+ payload = {"link": "https://api.example.com/entities/123"}
101
+
102
+ client._prefix_url_in_form_payload(template=template, payload=payload)
103
+
104
+ assert payload["link"] == "https://api.example.com/entities/123"
105
+
106
+ def test_prefix_url_property_list(self, client):
107
+ """Test that URL properties in lists are prefixed"""
108
+ template = HALFormsTemplate(
109
+ method=HALFormsMethod.POST,
110
+ properties=[HALFormsProperty(name="links", type=HALFormsPropertyType.url)]
111
+ )
112
+ payload = {"links": ["/entities/123", "/entities/456"]}
113
+
114
+ client._prefix_url_in_form_payload(template=template, payload=payload)
115
+
116
+ assert payload["links"] == [
117
+ "https://api.example.com/entities/123",
118
+ "https://api.example.com/entities/456"
119
+ ]
120
+
121
+ def test_non_url_properties_unchanged(self, client):
122
+ """Test that non-URL properties are not modified"""
123
+ template = HALFormsTemplate(
124
+ method=HALFormsMethod.POST,
125
+ properties=[
126
+ HALFormsProperty(name="name", type=HALFormsPropertyType.text),
127
+ HALFormsProperty(name="link", type=HALFormsPropertyType.url)
128
+ ]
129
+ )
130
+ payload = {"name": "/some/path", "link": "/entities/123"}
131
+
132
+ client._prefix_url_in_form_payload(template=template, payload=payload)
133
+
134
+ assert payload["name"] == "/some/path"
135
+ assert payload["link"] == "https://api.example.com/entities/123"
136
+
137
+
138
+ class TestToManyRelations:
139
+ entity_name = "candidate"
140
+ relation_name = "skills"
141
+
142
+ def test_relations(self, cg_client: ContentGridApplicationClient):
143
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
144
+ try:
145
+ # fetch related skills
146
+ related_skills = cg_client.get_entity_relation_collection(
147
+ entity.get_self_link(), self.relation_name
148
+ )
149
+
150
+ assert isinstance(related_skills, EntityCollection)
151
+
152
+ # entity just instantiated so no skills
153
+ assert len(related_skills.get_entities()) == 0
154
+
155
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
156
+
157
+ amt_skills_to_sample = 4
158
+ skills_selection = skills.get_entities()[:amt_skills_to_sample]
159
+ assert len(skills_selection) == amt_skills_to_sample
160
+
161
+ skills_selection[1] = skills_selection[1].get_self_link().uri
162
+ skills_selection[2] = skills_selection[2].get_self_link()
163
+
164
+ cg_client.post_entity_relation(
165
+ entity.get_self_link(),
166
+ relation_name=self.relation_name,
167
+ related_entity_links=skills_selection[:-1],
168
+ )
169
+
170
+ related_skills.refetch()
171
+
172
+ assert len(related_skills.get_entities()) == amt_skills_to_sample - 1
173
+
174
+ # post last skill
175
+ cg_client.post_entity_relation(
176
+ entity_link=entity.get_self_link(),
177
+ relation_name=self.relation_name,
178
+ related_entity_links=[skills_selection[-1]],
179
+ )
180
+
181
+ related_skills = cg_client.get_entity_relation_collection(
182
+ entity_link=entity.get_self_link(), relation_name=self.relation_name
183
+ )
184
+ assert len(related_skills.get_entities()) == amt_skills_to_sample
185
+ finally:
186
+ entity.delete()
187
+
188
+ def test_put_on_to_many_not_supported(self, cg_client: ContentGridApplicationClient):
189
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
190
+ try:
191
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
192
+ skills_selection = skills.get_entities()[:2]
193
+
194
+ # PUT on to-many relations should raise Forbidden
195
+ with pytest.raises(MissingHALTemplate):
196
+ cg_client.put_entity_relation(
197
+ entity.get_self_link(),
198
+ relation_name=self.relation_name,
199
+ related_entity_links=skills_selection,
200
+ )
201
+ finally:
202
+ entity.delete()
203
+
204
+ def test_clear_relations(self, cg_client: ContentGridApplicationClient):
205
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
206
+ try:
207
+ # no related exist yet
208
+ entity.clear_relation(relation_name=self.relation_name)
209
+
210
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
211
+ skills_selection = skills.get_entities()[:1]
212
+ cg_client.post_entity_relation(
213
+ entity.get_self_link(),
214
+ relation_name=self.relation_name,
215
+ related_entity_links=skills_selection,
216
+ )
217
+
218
+ assert len(entity.get_relation_collection(relation_name=self.relation_name).get_entities()) == 1
219
+
220
+ # clear relations again
221
+ entity.clear_relation(relation_name=self.relation_name)
222
+ finally:
223
+ entity.delete()
224
+
225
+
226
+
227
+ class TestToOneRelations:
228
+ candidate_entity_name = "candidate"
229
+ redacted_candidate_entity_name = "anonymous-candidate"
230
+ customer_company_entity_name = "customer-company"
231
+ relation_name = "redacted"
232
+ relation_name2 = "proposal"
233
+
234
+ @pytest.fixture
235
+ def entity(self):
236
+ """Create an EntityObject with a mock client for testing"""
237
+ mock_client = MagicMock()
238
+ entity = EntityObject.__new__(EntityObject)
239
+ entity.client = mock_client
240
+ return entity
241
+
242
+ def test_get_related_entity_follows_relation_link(self, entity):
243
+ """Test that get_related_entity follows the expected relation link"""
244
+ relation_link = HALLink(uri="/entities/456", name="redacted")
245
+ expected_related_entity = MagicMock(spec=EntityObject)
246
+
247
+ entity.get_relation_link = MagicMock(return_value=relation_link)
248
+ entity.client.follow_link.return_value = expected_related_entity
249
+
250
+ result = entity.get_related_entity("redacted")
251
+
252
+ assert result == expected_related_entity
253
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
254
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
255
+
256
+ def test_get_related_entity_raises_when_relation_not_found(self, entity):
257
+ """Test that relation lookup errors are propagated"""
258
+ entity.get_relation_link = MagicMock(side_effect=NotFound("missing relation"))
259
+
260
+ with pytest.raises(NotFound):
261
+ entity.get_related_entity("missing-relation")
262
+
263
+ entity.client.follow_link.assert_not_called()
264
+
265
+ def test_get_related_entity_raises_when_related_entity_not_found(self, entity):
266
+ """Test that follow_link NotFound errors are propagated"""
267
+ relation_link = HALLink(uri="/entities/456", name="redacted")
268
+ entity.get_relation_link = MagicMock(return_value=relation_link)
269
+ entity.client.follow_link = MagicMock(side_effect=NotFound("related entity not found"))
270
+
271
+ with pytest.raises(NotFound, match="related entity not found"):
272
+ entity.get_related_entity("redacted")
273
+
274
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
275
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
276
+
277
+ def test_get_related_entity_raises_when_access_is_forbidden(self, entity):
278
+ """Test that follow_link Forbidden errors are propagated"""
279
+ relation_link = HALLink(uri="/entities/456", name="redacted")
280
+ entity.get_relation_link = MagicMock(return_value=relation_link)
281
+ entity.client.follow_link = MagicMock(side_effect=Forbidden("access denied"))
282
+
283
+ with pytest.raises(Forbidden, match="access denied"):
284
+ entity.get_related_entity("redacted")
285
+
286
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
287
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
288
+
289
+ def test_get_related_entity(self, cg_client: ContentGridApplicationClient):
290
+ entity = cg_client.create_entity(self.candidate_entity_name, attributes={"name": "test"})
291
+ related_entity = None
292
+ new_company = None
293
+ try:
294
+ related_entity = cg_client.create_entity(self.redacted_candidate_entity_name, attributes={"original": entity.get_self_link().uri})
295
+ fetched_related_entity = entity.get_related_entity(self.relation_name)
296
+
297
+ assert isinstance(fetched_related_entity, EntityObject)
298
+ assert fetched_related_entity.get_self_link().uri == related_entity.get_self_link().uri
299
+
300
+ company_name = f"company-{uuid.uuid4().hex[:8]}"
301
+ new_company = cg_client.create_entity(self.customer_company_entity_name, attributes={"name": company_name})
302
+
303
+ related_entity.set_related_entity(relation_name=self.relation_name2, related_entity=new_company)
304
+ fetched_company = related_entity.get_related_entity(self.relation_name2)
305
+
306
+ assert isinstance(fetched_company, EntityObject)
307
+ assert fetched_company.get_self_link().uri == new_company.get_self_link().uri
308
+ finally:
309
+ if new_company is not None:
310
+ new_company.delete()
311
+ if related_entity is not None:
312
+ related_entity.delete()
313
+ entity.delete()
314
+
315
+
@@ -1,102 +0,0 @@
1
- from contentgrid_application_client import ContentGridApplicationClient, Profile, EntityProfile, AttributeProfile
2
- import pytest
3
- from contentgrid_hal_client.exceptions import NotFound
4
- from fixtures import auth_manager, cg_client # noqa: F401
5
-
6
- def test_get_profile_attributes(cg_client: ContentGridApplicationClient):
7
- profile = cg_client.get_profile()
8
- entity_links = profile.get_entity_links()
9
- assert len(entity_links) > 0
10
- assert all(link.name for link in entity_links)
11
- assert all(link.uri for link in entity_links)
12
-
13
- def test_entity_profile_details(cg_client: ContentGridApplicationClient):
14
- profile = cg_client.get_profile()
15
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
16
-
17
- # Test basic entity profile properties
18
- assert isinstance(entity_profile, EntityProfile)
19
- assert entity_profile.name is not None
20
- assert entity_profile.title is not None
21
- assert entity_profile.description is not None
22
-
23
- def test_attribute_profiles(cg_client: ContentGridApplicationClient):
24
- profile = cg_client.get_profile()
25
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
26
-
27
- attribute_profiles = entity_profile.get_attribute_profiles()
28
- if not attribute_profiles:
29
- pytest.skip(f"Entity {entity_profile.name} has no attribute profiles to test.")
30
-
31
- # Test first attribute profile properties
32
- first_attribute = attribute_profiles[0]
33
- assert isinstance(first_attribute, AttributeProfile)
34
- assert first_attribute.name is not None
35
- assert first_attribute.type is not None
36
- assert isinstance(first_attribute.required, bool)
37
- assert isinstance(first_attribute.read_only, bool)
38
-
39
- def test_invalid_entity_profile(cg_client: ContentGridApplicationClient):
40
- with pytest.raises(NotFound):
41
- cg_client.get_entity_profile("non_existent_entity")
42
-
43
- def test_nested_attributes(cg_client: ContentGridApplicationClient):
44
- profile = cg_client.get_profile()
45
- # Use a known entity type if available, otherwise the first one
46
- entity_name = "anonymous-candidate" if "anonymous-candidate" in [link.name for link in profile.get_entity_links()] else profile.get_entity_links()[0].name
47
- entity_profile = cg_client.get_entity_profile(entity_name)
48
-
49
- found_nested = False
50
- for attribute in entity_profile.get_attribute_profiles():
51
- nested_attributes = attribute.get_nested_attributes()
52
- if nested_attributes:
53
- found_nested = True
54
- assert all(isinstance(nested, AttributeProfile) for nested in nested_attributes)
55
- assert all(hasattr(nested, 'name') for nested in nested_attributes)
56
- if not found_nested:
57
- print(f"INFO: No nested attributes found for entity {entity_name} to test.")
58
-
59
- def test_content_attribute_detection(cg_client: ContentGridApplicationClient):
60
- profile = cg_client.get_profile()
61
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
62
-
63
- for attribute in entity_profile.get_attribute_profiles():
64
- if attribute.is_content_attribute():
65
- nested_attrs = attribute.get_nested_attributes()
66
- assert any(attr.name in ["length", "mimetype", "filename"] for attr in nested_attrs)
67
-
68
- def test_relation_profiles(cg_client: ContentGridApplicationClient):
69
- profile = cg_client.get_profile()
70
- entity_profile = cg_client.get_entity_profile("anonymous-candidate")
71
-
72
- relation_profiles = entity_profile.get_relation_profiles()
73
- assert len(relation_profiles) >= 0
74
-
75
- if relation_profiles:
76
- first_relation = relation_profiles[0]
77
- assert first_relation.name is not None
78
- assert first_relation.title is not None
79
- assert isinstance(first_relation.required, bool)
80
- assert isinstance(first_relation.many_source_per_target, bool)
81
- assert isinstance(first_relation.many_target_per_source, bool)
82
-
83
- def test_get_specific_relation_profile(cg_client: ContentGridApplicationClient):
84
- profile = cg_client.get_profile()
85
- entity_profile = cg_client.get_entity_profile("anonymous-candidate")
86
-
87
- if entity_profile.get_relation_profiles():
88
- relation_name = entity_profile.get_relation_profiles()[0].name
89
- relation_profile = entity_profile.get_relation_profile(relation_name)
90
- assert relation_profile.name == relation_name
91
-
92
- # Test related entity profile retrieval
93
- related_entity_profile = relation_profile.get_related_entity_profile()
94
- assert isinstance(related_entity_profile, EntityProfile)
95
- assert related_entity_profile.name is not None
96
-
97
- def test_invalid_relation_profile(cg_client: ContentGridApplicationClient):
98
- profile = cg_client.get_profile()
99
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
100
-
101
- with pytest.raises(ValueError):
102
- entity_profile.get_relation_profile("non_existent_relation")
@@ -1,222 +0,0 @@
1
- from contentgrid_application_client import ContentGridApplicationClient, EntityCollection
2
- from contentgrid_application_client.application import EntityObject
3
- from contentgrid_hal_client.exceptions import Forbidden, MissingHALTemplate
4
- from contentgrid_hal_client.hal import HALLink
5
- from contentgrid_hal_client.hal_forms import HALFormsTemplate, HALFormsProperty, HALFormsPropertyType, HALFormsMethod
6
- from fixtures import auth_manager, cg_client # noqa: F401
7
- from unittest.mock import MagicMock
8
- import pytest
9
- import hyperlink
10
-
11
-
12
- class TestCreateTextUriListPayload:
13
- """Unit tests for _create_text_uri_list_payload method"""
14
-
15
- @pytest.fixture
16
- def entity(self):
17
- """Create an EntityObject with a mock client for testing"""
18
- mock_client = MagicMock()
19
- mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
20
- entity = EntityObject.__new__(EntityObject)
21
- entity.client = mock_client
22
- return entity
23
-
24
- def test_uri_list_with_relative_paths(self, entity):
25
- """Test that relative paths are properly prefixed with client endpoint"""
26
- result = entity._create_text_uri_list_payload(["/entities/123", "/entities/456"])
27
-
28
- assert result == "https://api.example.com/entities/123\nhttps://api.example.com/entities/456"
29
-
30
- def test_uri_list_with_full_urls(self, entity):
31
- """Test that full URLs matching endpoint are not modified"""
32
- full_url = "https://api.example.com/entities/123"
33
- result = entity._create_text_uri_list_payload([full_url])
34
-
35
- assert result == full_url
36
-
37
- def test_uri_list_with_hal_link(self, entity):
38
- """Test that HALLink objects are properly converted"""
39
- hal_link = HALLink(uri="/entities/123")
40
- result = entity._create_text_uri_list_payload([hal_link])
41
-
42
- assert result == "https://api.example.com/entities/123"
43
-
44
- def test_uri_list_with_hal_link_full_url(self, entity):
45
- """Test that HALLink with full URL is not double-prefixed"""
46
- hal_link = HALLink(uri="https://api.example.com/entities/123")
47
- result = entity._create_text_uri_list_payload([hal_link])
48
-
49
- assert result == "https://api.example.com/entities/123"
50
-
51
- def test_uri_list_without_prefix(self, entity):
52
- """Test that prefix_client_endpoint=False returns URIs unchanged"""
53
- result = entity._create_text_uri_list_payload(["/entities/123"], prefix_client_endpoint=False)
54
-
55
- assert result == "/entities/123"
56
-
57
-
58
- class TestPrefixUrlInFormPayload:
59
- """Unit tests for _prefix_url_in_form_payload method"""
60
-
61
- @pytest.fixture
62
- def client(self):
63
- """Create a mock ContentGridApplicationClient for testing"""
64
- mock_client = MagicMock(spec=ContentGridApplicationClient)
65
- mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
66
- mock_client._prefix_url_in_form_payload = ContentGridApplicationClient._prefix_url_in_form_payload.__get__(mock_client)
67
- return mock_client
68
-
69
- def test_prefix_url_property_with_relative_path(self, client):
70
- """Test that URL properties with relative paths are prefixed"""
71
- template = HALFormsTemplate(
72
- method=HALFormsMethod.POST,
73
- properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
74
- )
75
- payload = {"link": "/entities/123"}
76
-
77
- client._prefix_url_in_form_payload(template=template, payload=payload)
78
-
79
- assert payload["link"] == "https://api.example.com/entities/123"
80
-
81
- def test_prefix_url_property_avoids_double_slashes(self, client):
82
- """Test that double slashes are avoided"""
83
- template = HALFormsTemplate(
84
- method=HALFormsMethod.POST,
85
- properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
86
- )
87
- payload = {"link": "/entities/123"}
88
-
89
- client._prefix_url_in_form_payload(template=template, payload=payload)
90
-
91
- assert "//" not in payload["link"].replace("https://", "")
92
-
93
- def test_prefix_url_property_with_full_url(self, client):
94
- """Test that full URLs are not double-prefixed"""
95
- template = HALFormsTemplate(
96
- method=HALFormsMethod.POST,
97
- properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
98
- )
99
- payload = {"link": "https://api.example.com/entities/123"}
100
-
101
- client._prefix_url_in_form_payload(template=template, payload=payload)
102
-
103
- assert payload["link"] == "https://api.example.com/entities/123"
104
-
105
- def test_prefix_url_property_list(self, client):
106
- """Test that URL properties in lists are prefixed"""
107
- template = HALFormsTemplate(
108
- method=HALFormsMethod.POST,
109
- properties=[HALFormsProperty(name="links", type=HALFormsPropertyType.url)]
110
- )
111
- payload = {"links": ["/entities/123", "/entities/456"]}
112
-
113
- client._prefix_url_in_form_payload(template=template, payload=payload)
114
-
115
- assert payload["links"] == [
116
- "https://api.example.com/entities/123",
117
- "https://api.example.com/entities/456"
118
- ]
119
-
120
- def test_non_url_properties_unchanged(self, client):
121
- """Test that non-URL properties are not modified"""
122
- template = HALFormsTemplate(
123
- method=HALFormsMethod.POST,
124
- properties=[
125
- HALFormsProperty(name="name", type=HALFormsPropertyType.text),
126
- HALFormsProperty(name="link", type=HALFormsPropertyType.url)
127
- ]
128
- )
129
- payload = {"name": "/some/path", "link": "/entities/123"}
130
-
131
- client._prefix_url_in_form_payload(template=template, payload=payload)
132
-
133
- assert payload["name"] == "/some/path"
134
- assert payload["link"] == "https://api.example.com/entities/123"
135
-
136
-
137
- class TestRelations:
138
- entity_name = "candidate"
139
- relation_name = "skills"
140
-
141
- def test_relations(self, cg_client: ContentGridApplicationClient):
142
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
143
-
144
- # fetch related skills
145
- related_skills = cg_client.get_entity_relation_collection(
146
- entity.get_self_link(), self.relation_name
147
- )
148
-
149
- assert isinstance(related_skills, EntityCollection)
150
-
151
- # entity just instantiated so no skills
152
- assert len(related_skills.get_entities()) == 0
153
-
154
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
155
-
156
- amt_skills_to_sample = 4
157
- skills_selection = skills.get_entities()[:amt_skills_to_sample]
158
- assert len(skills_selection) == amt_skills_to_sample
159
-
160
- skills_selection[1] = skills_selection[1].get_self_link().uri
161
- skills_selection[2] = skills_selection[2].get_self_link()
162
-
163
- cg_client.post_entity_relation(
164
- entity.get_self_link(),
165
- relation_name=self.relation_name,
166
- related_entity_links=skills_selection[:-1],
167
- )
168
-
169
- related_skills.refetch()
170
-
171
- assert len(related_skills.get_entities()) == amt_skills_to_sample - 1
172
-
173
- # post last skill
174
- cg_client.post_entity_relation(
175
- entity_link=entity.get_self_link(),
176
- relation_name=self.relation_name,
177
- related_entity_links=[skills_selection[-1]],
178
- )
179
-
180
- related_skills = cg_client.get_entity_relation_collection(
181
- entity_link=entity.get_self_link(), relation_name=self.relation_name
182
- )
183
- assert len(related_skills.get_entities()) == amt_skills_to_sample
184
-
185
- entity.delete()
186
-
187
- def test_put_on_to_many_not_supported(self, cg_client: ContentGridApplicationClient):
188
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
189
-
190
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
191
- skills_selection = skills.get_entities()[:2]
192
-
193
- # PUT on to-many relations should raise Forbidden
194
- with pytest.raises(MissingHALTemplate):
195
- cg_client.put_entity_relation(
196
- entity.get_self_link(),
197
- relation_name=self.relation_name,
198
- related_entity_links=skills_selection,
199
- )
200
-
201
- entity.delete()
202
-
203
- def test_clear_relations(self, cg_client: ContentGridApplicationClient):
204
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
205
- # no related exist yet
206
- entity.clear_relation(relation_name=self.relation_name)
207
-
208
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
209
- skills_selection = skills.get_entities()[:1]
210
- cg_client.post_entity_relation(
211
- entity.get_self_link(),
212
- relation_name=self.relation_name,
213
- related_entity_links=skills_selection,
214
- )
215
-
216
- assert len(entity.get_relation_collection(relation_name=self.relation_name).get_entities()) == 1
217
-
218
- #clear relations again
219
- entity.clear_relation(relation_name=self.relation_name)
220
-
221
-
222
-