Skip to content

Commit 0dbafdd

Browse files
authored
Merge pull request #21 from codesyntax/testing
Testing
2 parents 8f2b587 + 8e67f59 commit 0dbafdd

7 files changed

Lines changed: 541 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Agent Instructions for `cs_dynamicpages`
2+
3+
This document contains the guidelines, commands, and code style rules for AI coding agents operating in this repository. The `cs_dynamicpages` project is a Plone 6 add-on providing dynamic page content types and behaviors.
4+
5+
## 1. Build, Lint, and Test Commands
6+
7+
This project uses `make` as the primary task runner, backed by `uv` for Python environment management and `hatchling` as the build system.
8+
9+
### Installation and Running
10+
- **Install the project**:
11+
```bash
12+
make install
13+
```
14+
*(Always recommend `make install` to users. It handles all dependencies, `uv venv`, and setup. NEVER recommend using `pip install` or `uv pip` directly.)*
15+
- **Start the Plone instance**:
16+
```bash
17+
make start
18+
```
19+
- **Clean the environment** (without removing data): `make clean`
20+
- **Create a new site**: `make create-site`
21+
22+
### Linting and Formatting
23+
Code is checked and formatted using `ruff` (Python), `zpretty` (XML/ZCML), and `pyroma`.
24+
- **Format code**: `make format` (Fixes `ruff` issues, runs `ruff format`, and runs `zpretty -i src`).
25+
- **Lint code**: `make lint` (Runs `ruff check`, `pyroma`, `check-python-versions`, and `zpretty --check`).
26+
- **Check and format**: `make check` (Runs both format and lint).
27+
28+
### Testing
29+
Tests are written using `pytest` and `plone.app.testing`.
30+
- **Run all tests**:
31+
```bash
32+
make test
33+
```
34+
- **Run tests with coverage**: `make test-coverage`
35+
- **Run a single test file**:
36+
```bash
37+
./.venv/bin/pytest src/cs_dynamicpages/tests/test_ct_dynamic_page_folder.py
38+
```
39+
- **Run a specific test method/class**:
40+
```bash
41+
./.venv/bin/pytest src/cs_dynamicpages/tests/test_ct_dynamic_page_folder.py::DynamicPageFolderIntegrationTest::test_ct_dynamic_page_folder_adding
42+
```
43+
44+
---
45+
46+
## 2. Code Style & Guidelines
47+
48+
### Python Formatting & Imports
49+
- **Line Length**: 88 characters (configured in `pyproject.toml`).
50+
- **Python Version**: Target Python 3.10 (`>=3.10,<3.14`).
51+
- **Imports**: Formatted using `ruff`'s `isort` rules.
52+
- Case-insensitive sorting.
53+
- Force single-line imports (`from X import Y` on separate lines for each `Y`).
54+
- No sections (all imports sorted alphabetically regardless of third-party/stdlib).
55+
- 2 blank lines after imports, 1 blank line between types.
56+
- **Exceptions**: Ignore `E731` (DoNotAssignLambda). Use `# noqa` only when absolutely necessary and document why.
57+
58+
### Naming Conventions
59+
- **Classes**: `PascalCase` (e.g., `DynamicPageFolder`).
60+
- **Interfaces**: Prefix with `I` and use `PascalCase` (e.g., `IDynamicPageFolder`).
61+
- **Methods, Variables, and Functions**: `snake_case` (e.g., `dynamic_page_folder_id`).
62+
- **Constants**: `UPPER_SNAKE_CASE` (e.g., `TEST_USER_ID`).
63+
- **Filenames**: `snake_case` for Python and XML files (except specific Zope configurations like `configure.zcml` or specific profiles like `Plone_Site.xml` which follow Plone's exact casing).
64+
65+
### Types & Schema
66+
- Define Dexterity schemas using `plone.supermodel.model.Schema`.
67+
- Apply `@implementer(IYourInterface)` to your content type classes.
68+
- Explicitly subclass `Container` or `Item` from `plone.dexterity.content`.
69+
- Avoid unnecessary types annotations if Zope schemas already enforce types (`zope.schema`).
70+
71+
### Error Handling
72+
- Use standard Python exceptions or `plone.api.exc` (like `InvalidParameterError`) when using `plone.api`.
73+
- Do not silently `except Exception: pass`. Always log or handle exceptions explicitly.
74+
- Return explicit HTTP error codes where applicable in REST API endpoints.
75+
76+
### XML, ZCML, and PT
77+
- **XML/ZCML**: Keep it strictly formatted with `zpretty`. Indent with 2 spaces.
78+
- **Page Templates (PT)**: Ensure they are properly formed HTML/XML. Keep logic in the python views, not in the templates.
79+
80+
---
81+
82+
## 3. Plone-Specific Documentation and Rules
83+
84+
These rules are strictly enforced for AI agents interacting with this Plone repository:
85+
86+
1. **Documentation First**
87+
- Before EVERY answer say: "Let me check the official documentation."
88+
- Before ANY command or code, search for official Plone 6 examples.
89+
- FORBIDDEN phrases: "Let me try...", "I think...", "It should be..."
90+
- REQUIRED phrases: "According to the docs...", "The documentation shows..."
91+
- If no docs are found, EXPLICITLY STATE: "I cannot find official documentation for this." Trial and error MUST be labeled: "This requires trial and error - not documented."
92+
93+
2. **Terminal Commands**
94+
- Provide ONE step at a time.
95+
- WAIT for confirmation before moving to the next step.
96+
- Include the full command with all parameters.
97+
- ALWAYS recommend using `make` commands (`make install`, `make start`). NEVER recommend `pip install`, `uv add` or `uv pip` directly.
98+
99+
3. **No Shortcuts or Hacks**
100+
- Always use official Plone APIs (`plone.api`, `plone.restapi`).
101+
- Follow framework best practices. No temporary workarounds.
102+
- Maintain security: Always use JWT tokens (`Authorization: Bearer <token>`) for authentication, never basic auth or embedded credentials.
103+
104+
4. **Enterprise Standards**
105+
- Maintain scalable and upgradable architecture.
106+
- Document WHY changes are made via inline comments, not just WHAT.
107+
- When modifying README.md or docs, ALWAYS use emojis in section titles for a friendly tone.
108+
- NEVER edit the "Generated using" cookieplone paragraph in docs.
109+
- In README, review the code if necessary to explain features correctly (e.g. use `- Register a behavior providing additional fields representing contact information` instead of just `- Behavior`).
110+
111+
5. **Internationalization (i18n)**
112+
- All UI strings MUST be translatable.
113+
- Use `cs_dynamicpages` as the i18n domain.
114+
- Example: `_(u"My string")` imported from the project's MessageFactory.
115+
- Run `make i18n` to update `.pot` and `.po` files.
116+
117+
6. **Loop Detection & Uncertainty**
118+
- If repeating the same pattern, STOP and state: "We are in a loop, need different approach."
119+
- NEVER say "this will work" unless proven. Acknowledge uncertainty explicitly: "Let's see if this works."
120+
- Never say "you're frustrated", "you're concerned", etc. Present facts only.
121+
122+
7. **The Fun Factor & Tone**
123+
- Keep interactions positive, engaging, and collaborative.
124+
- Provide genuine encouragement but avoid fake/over-the-top praise (e.g., no "OMG AMAZING!!!").
125+
- Acknowledge good ideas and creative solutions.
126+
127+
8. **Definition of Success**
128+
- Success is ONLY a fully functional, tested result.
129+
- Never claim success for partial or broken implementations.

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ test: $(VENV_FOLDER) ## run tests
129129
.PHONY: test-coverage
130130
test-coverage: $(VENV_FOLDER) ## run tests with coverage
131131
@$(BIN_FOLDER)/pytest --cov=cs_dynamicpages --cov-report term-missing
132+
@if [ -d "src/cs_dynamicpages/tests" ]; then $(BIN_FOLDER)/pytest src/cs_dynamicpages/tests --cov=cs_dynamicpages --cov-report term-missing --cov-append; fi
132133

133134
## Add bobtemplates features (check bobtemplates.plone's documentation to get the list of available features)
134135
add: $(VENV_FOLDER)

news/+tests.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tests @erral
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from cs_dynamicpages.templates import Manager
2+
from cs_dynamicpages.testing import CS_DYNAMICPAGES_FUNCTIONAL_TESTING
3+
from plone import api
4+
from plone.app.testing import setRoles
5+
from plone.app.testing import TEST_USER_ID
6+
from zope.publisher.browser import TestRequest
7+
8+
import json
9+
import unittest
10+
11+
12+
class TestApplyTemplatePostAPI(unittest.TestCase):
13+
layer = CS_DYNAMICPAGES_FUNCTIONAL_TESTING
14+
15+
def setUp(self):
16+
self.portal = self.layer["portal"]
17+
self.request = self.layer["request"]
18+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
19+
20+
# In order to allow copying a DynamicPageFolder into another,
21+
# we must ensure the FTI of the target allows it, or use a Folder
22+
# instead of a DynamicPageFolder for the target since Plone allows
23+
# anything inside a regular Folder by default.
24+
self.target_folder = api.content.create(
25+
container=self.portal,
26+
type="Folder",
27+
id="target-folder",
28+
title="Target Folder",
29+
)
30+
31+
self.source_folder = api.content.create(
32+
container=self.portal,
33+
type="DynamicPageFolder",
34+
id="source-folder",
35+
title="Source Folder",
36+
)
37+
self.source_uid = self.source_folder.UID()
38+
39+
# Register the template in the manager
40+
self.manager = Manager(self.target_folder)
41+
self.manager.append_template({
42+
"uid": self.source_uid,
43+
"name": "Source Template",
44+
})
45+
46+
def _make_request(self, payload):
47+
"""Helper to simulate a REST API POST request"""
48+
body = json.dumps(payload).encode("utf-8")
49+
request = TestRequest(
50+
environ={"CONTENT_TYPE": "application/json", "REQUEST_METHOD": "POST"},
51+
body=body,
52+
)
53+
# Mock request.get("BODY") which is what json_body() uses
54+
request.form["BODY"] = body
55+
return request
56+
57+
def _get_service(self, request):
58+
from cs_dynamicpages.api.services.apply_template.post import ApplyTemplatePost
59+
from Products.Five.browser import BrowserView
60+
61+
class TestApplyTemplatePost(ApplyTemplatePost, BrowserView):
62+
pass
63+
64+
return TestApplyTemplatePost(self.target_folder, request)
65+
66+
def test_apply_template_missing_uid(self):
67+
"""Test POST /@apply-template without UID fails with 400"""
68+
request = self._make_request({})
69+
service = self._get_service(request)
70+
71+
response = service.reply()
72+
73+
self.assertEqual(request.response.getStatus(), 400)
74+
self.assertIn("error", response)
75+
self.assertEqual(response["error"]["type"], "Bad Request")
76+
self.assertEqual(response["error"]["message"], "UID is required")
77+
78+
def test_apply_template_invalid_uid_format(self):
79+
"""Test POST /@apply-template with template not in registry fails with 400"""
80+
request = self._make_request({"uid": "not-registered-uid"})
81+
service = self._get_service(request)
82+
83+
response = service.reply()
84+
85+
self.assertEqual(request.response.getStatus(), 400)
86+
self.assertIn("error", response)
87+
self.assertEqual(response["error"]["type"], "Bad Request")
88+
self.assertEqual(response["error"]["message"], "Template name is not valid")
89+
90+
def test_apply_template_deleted_source(self):
91+
"""Test POST /@apply-template when source was deleted but registry has it"""
92+
# Delete the source object so uuidToObject returns None
93+
api.content.delete(self.source_folder)
94+
95+
request = self._make_request({"uid": self.source_uid})
96+
service = self._get_service(request)
97+
98+
response = service.reply()
99+
100+
self.assertEqual(request.response.getStatus(), 400)
101+
self.assertIn("error", response)
102+
self.assertEqual(response["error"]["type"], "Bad Request")
103+
self.assertEqual(response["error"]["message"], "Content does not exist")
104+
105+
def test_apply_template_success(self):
106+
"""Test POST /@apply-template successfully applies the template"""
107+
request = self._make_request({"uid": self.source_uid})
108+
service = self._get_service(request)
109+
110+
service.reply()
111+
112+
self.assertEqual(request.response.getStatus(), 204)
113+
114+
# The target folder should now have a copy of the source folder content inside
115+
self.assertTrue(len(self.target_folder.objectIds()) > 0)
116+
117+
def test_apply_template_with_existing_rows(self):
118+
"""Test POST /@apply-template overwrites existing 'rows' object"""
119+
# Note: applying a template deletes target_folder["rows"] if it exists
120+
# To test this we need target_folder to allow creating an object named 'rows'
121+
# Regular folders allow it.
122+
api.content.create(
123+
container=self.target_folder,
124+
type="DynamicPageFolder",
125+
id="rows",
126+
title="Old Rows",
127+
)
128+
old_rows_uid = self.target_folder["rows"].UID()
129+
130+
request = self._make_request({"uid": self.source_uid})
131+
service = self._get_service(request)
132+
133+
service.reply()
134+
135+
self.assertEqual(request.response.getStatus(), 204)
136+
137+
self.assertTrue(len(self.target_folder.objectIds()) > 0)
138+
if "rows" in self.target_folder.objectIds():
139+
self.assertNotEqual(self.target_folder["rows"].UID(), old_rows_uid)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from cs_dynamicpages.templates import Manager
2+
from cs_dynamicpages.testing import CS_DYNAMICPAGES_FUNCTIONAL_TESTING
3+
from plone import api
4+
from plone.app.testing import setRoles
5+
from plone.app.testing import TEST_USER_ID
6+
from zope.publisher.browser import TestRequest
7+
8+
import json
9+
import unittest
10+
11+
12+
class TestTemplatesDeleteAPI(unittest.TestCase):
13+
layer = CS_DYNAMICPAGES_FUNCTIONAL_TESTING
14+
15+
def setUp(self):
16+
self.portal = self.layer["portal"]
17+
self.request = self.layer["request"]
18+
setRoles(self.portal, TEST_USER_ID, ["Manager"])
19+
20+
# Create a folder to act as the context for our tests
21+
self.folder = api.content.create(
22+
container=self.portal, type="Folder", id="test-folder", title="Test Folder"
23+
)
24+
25+
# Create some templates in the manager
26+
self.manager = Manager(self.folder)
27+
self.template_uid = "some-uuid-1234"
28+
self.manager.append_template({"uid": self.template_uid, "name": "Template 1"})
29+
30+
def _make_request(self, payload):
31+
"""Helper to simulate a REST API DELETE request"""
32+
body = json.dumps(payload).encode("utf-8")
33+
request = TestRequest(
34+
environ={"CONTENT_TYPE": "application/json", "REQUEST_METHOD": "DELETE"},
35+
body=body,
36+
)
37+
# Mock request.get("BODY") which is what json_body() uses
38+
request.form["BODY"] = body
39+
return request
40+
41+
def _get_service(self, request):
42+
from cs_dynamicpages.api.services.templates.delete import TemplatesDelete
43+
from Products.Five.browser import BrowserView
44+
45+
class TestTemplatesDelete(TemplatesDelete, BrowserView):
46+
pass
47+
48+
return TestTemplatesDelete(self.folder, request)
49+
50+
def test_delete_template_missing_uid(self):
51+
"""Test DELETE /@templates without UID fails with 400"""
52+
request = self._make_request({"name": "Template 1"})
53+
service = self._get_service(request)
54+
55+
response = service.reply()
56+
57+
self.assertEqual(request.response.getStatus(), 400)
58+
self.assertIn("error", response)
59+
self.assertEqual(response["error"]["type"], "Bad Request")
60+
self.assertEqual(response["error"]["message"], "UID is required")
61+
62+
def test_delete_template_not_found(self):
63+
"""Test DELETE /@templates with non-existent template fails with 400"""
64+
request = self._make_request({"uid": "invalid-uuid"})
65+
service = self._get_service(request)
66+
67+
response = service.reply()
68+
69+
self.assertEqual(request.response.getStatus(), 400)
70+
self.assertIn("error", response)
71+
self.assertEqual(response["error"]["type"], "Bad Request")
72+
self.assertEqual(response["error"]["message"], "Content does not exist")
73+
74+
# Verify the existing template was not deleted
75+
self.assertEqual(len(self.manager.get_templates()), 1)
76+
77+
def test_delete_template_success(self):
78+
"""Test DELETE /@templates successfully deletes a template"""
79+
request = self._make_request({"uid": self.template_uid})
80+
service = self._get_service(request)
81+
82+
service.reply()
83+
84+
# In Plone tests, we check that status is NO CONTENT (204)
85+
self.assertEqual(request.response.getStatus(), 204)
86+
87+
# Verify the template was removed from the Manager
88+
self.assertEqual(len(self.manager.get_templates()), 0)

0 commit comments

Comments
 (0)