From bcd8283181b929c276d07dad2717213fa1bd3a74 Mon Sep 17 00:00:00 2001
From: Romain Courteaud <romain@nexedi.com>
Date: Mon, 19 Aug 2024 13:12:31 +0000
Subject: [PATCH] erp5_hal_json_style/erp5_web: fix http cache condition

The main query to the hateoas web section must be cache by the browser,
to reduce the number of queries by 2.

Caching policy manager conditions are checked before calculating the query result,
and so, it is not possible anymore to wait for the script to explicitely
ask for the result to be cached.
---
 .../ERP5Document_getHateoas.py                |   1 -
 .../test.erp5.testHalJsonStyle.py             | 111 ++++++++++++++++++
 .../caching_policy_manager.xml                |   2 +-
 3 files changed, 112 insertions(+), 2 deletions(-)

diff --git a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
index 64297d0adb..1d8d885d25 100644
--- a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
+++ b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
@@ -1615,7 +1615,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
     if is_site_root:
 
       result_dict['default_view'] = 'view'
-      REQUEST.set("X-HATEOAS-CACHE", 1)
 
       # Global action users for the jIO plugin
       # XXX Would be better to not hardcode them but put them as portal type
diff --git a/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py b/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py
index 24eda5620c..0b3867bf16 100644
--- a/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py
+++ b/bt5/erp5_hal_json_style/TestTemplateItem/portal_components/test.erp5.testHalJsonStyle.py
@@ -15,6 +15,7 @@ import DateTime
 import StringIO
 import json
 import re
+import io
 from six.moves.urllib.parse import quote, quote_plus
 
 import mock
@@ -3265,3 +3266,113 @@ class TestERP5ODS(ERP5HALJSONStyleSkinsMixin):
     self.assertTrue('Read-Only Quantity' in result, result)
     # Ensure it is not the list mode rendering
     self.assertTrue(len(result.split('\n')) > 50, result)
+
+
+class TestERP5Document_getHateoas_cache(ERP5HALJSONStyleSkinsMixin):
+
+  def testCache_root_authenticated(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'application/hal+json')
+    self.assertEqual(ret.getHeader('cache-control'), 'max-age=1800, private')
+
+  def testCache_root_authenticatedWrongPath(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath(),
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'application/hal+json')
+    self.assertEqual(ret.getHeader('cache-control'), 'max-age=1800, private')
+
+  def testCache_root_authenticatedAndMethodCall(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/getId',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'text/plain; charset=utf-8')
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
+
+  def testCache_root_authenticatedWrongQueryString(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/?foo=bar',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'application/hal+json')
+    self.assertEqual(ret.getHeader('cache-control'), 'max-age=0, no-cache, private')
+
+  def testCache_root_authenticatedWrongMethod(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/',
+      user='ERP5TypeTestCase',
+      request_method='POST'
+    )
+    self.assertEqual(ret.getStatus(), 405)
+    self.assertEqual(ret.getHeader('content-type'), None)
+    self.assertEqual(ret.getHeader('cache-control'), 'max-age=0, no-cache, private')
+
+  def testCache_root_anonymous(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/',
+    )
+    self.assertEqual(ret.getStatus(), 401)
+    self.assertEqual(ret.getHeader('content-type'), None)
+    self.assertEqual(ret.getHeader('cache-control'), 'max-age=0, no-cache, private')
+
+  def testCache_traverse_authenticatedAndWrongTraverse(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/ERP5Document_getHateoas?mode=traverse&relative_url=unexisting_module&view=view',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 404)
+    self.assertEqual(ret.getHeader('content-type'), None)
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
+
+  def testCache_traverse_authenticatedAndTraverse(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/ERP5Document_getHateoas?mode=traverse&relative_url=foo_module&view=view',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'application/hal+json')
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
+
+  def testCache_traverse_authenticatedAndTraverseWrongView(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/ERP5Document_getHateoas?mode=traverse&relative_url=foo_module&view=foobarview',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 404)
+    self.assertEqual(ret.getHeader('content-type'), None)
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
+
+  def testCache_traverse_authenticatedAndSearch(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/ERP5Document_getHateoas?mode=search',
+      user='ERP5TypeTestCase'
+    )
+    self.assertEqual(ret.getStatus(), 200)
+    self.assertEqual(ret.getHeader('content-type'), 'application/hal+json')
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
+
+  def testCache_traverse_authenticatedAndDoAction(self):
+    ret = self.publish(
+      self.portal.web_site_module.hateoas.getPath() + '/foo_module/Base_callDialogMethod',
+      user='ERP5TypeTestCase',
+      request_method='POST',
+      stdin=io.BytesIO(
+        'field_your_select_action=add Foo&' +
+        'form_id=FooModule_viewFooList&' +
+        'dialog_id=Base_viewNewContentDialog&' +
+        'dialog_method=Base_doAction'
+      ),
+      env={'CONTENT_TYPE': 'application/x-www-form-urlencoded'}
+    )
+    self.assertEqual(ret.getStatus(), 201)
+    self.assertEqual(ret.getHeader('content-type'), 'application/json; charset=utf-8')
+    self.assertEqual(ret.getHeader('cache-control'), 'private')
diff --git a/bt5/erp5_web/ToolTemplateItem/caching_policy_manager.xml b/bt5/erp5_web/ToolTemplateItem/caching_policy_manager.xml
index 5c4d980c25..b0876be8f1 100644
--- a/bt5/erp5_web/ToolTemplateItem/caching_policy_manager.xml
+++ b/bt5/erp5_web/ToolTemplateItem/caching_policy_manager.xml
@@ -1506,7 +1506,7 @@
       <dictionary>
         <item>
             <key> <string>text</string> </key>
-            <value> <string>python: member is not None and (lambda x: x is not None and x.getCachingPolicy() =="hateoas")(object.getWebSectionValue()) and request.get("X-HATEOAS-CACHE")</string> </value>
+            <value> <string>python: (member is not None) and (request.other[\'method\'] == "GET") and (request.environ[\'QUERY_STRING\'] == "") and (lambda x: (x is not None) and (request.other[\'PUBLISHED\'] == x) and (x.getCachingPolicy() == "hateoas"))(object.getWebSectionValue())</string> </value>
         </item>
       </dictionary>
     </pickle>
-- 
2.30.9