Skip to content

Commit 4b6fd1f

Browse files
committed
add test suite
1 parent 9dcc05f commit 4b6fd1f

9 files changed

Lines changed: 1039 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout Janeway
15+
uses: actions/checkout@v4
16+
with:
17+
repository: BirkbeckCTP/janeway
18+
ref: master
19+
20+
- name: Checkout geometadata plugin
21+
uses: actions/checkout@v4
22+
with:
23+
path: src/plugins/geometadata/
24+
25+
- name: Set up Python 3.11
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: "3.11"
29+
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install -r requirements.txt
34+
pip install -r dev-requirements.txt
35+
pip install -r src/plugins/geometadata/requirements.txt
36+
37+
- name: Set environment variables
38+
run: |
39+
echo "DB_VENDOR=sqlite" >> "$GITHUB_ENV"
40+
echo "JANEWAY_SETTINGS_MODULE=core.janeway_global_settings" >> "$GITHUB_ENV"
41+
42+
- name: Check code formatting
43+
run: ruff format --check src/plugins/geometadata/
44+
45+
- name: Lint code
46+
run: ruff check src/plugins/geometadata/
47+
continue-on-error: true
48+
49+
- name: Install Janeway
50+
working-directory: src
51+
run: python manage.py install_janeway --use-defaults
52+
53+
- name: Install plugin
54+
working-directory: src
55+
run: python manage.py install_plugins geometadata
56+
57+
- name: Run tests
58+
working-directory: src
59+
run: python manage.py test geometadata

tests/__init__.py

Whitespace-only changes.

tests/base.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Base test case for geometadata plugin tests.
3+
4+
Provides common fixtures and setup for all test modules.
5+
"""
6+
7+
from django.test import TestCase
8+
9+
from utils.testing import helpers
10+
11+
from plugins.geometadata import plugin_settings
12+
13+
14+
class GeometadataTestCase(TestCase):
15+
"""
16+
Base test case with common fixtures for geometadata tests.
17+
18+
Provides:
19+
- press: Press instance
20+
- journal: Journal instance
21+
- article: Published article with owner
22+
- editor: User with editor role on journal
23+
- repository: Preprint repository (lazy, create via create_repository())
24+
- preprint: Preprint instance (lazy, create via create_preprint())
25+
"""
26+
27+
@classmethod
28+
def setUpTestData(cls):
29+
"""Set up test data once for the entire test class."""
30+
# Install plugin settings
31+
plugin_settings.install()
32+
33+
# Create core fixtures
34+
cls.press = helpers.create_press()
35+
cls.journal, _ = helpers.create_journals()
36+
cls.editor = helpers.create_user(
37+
"editor@test.com", ["editor"], cls.journal, is_staff=True, is_active=True
38+
)
39+
cls.article = helpers.create_article(cls.journal, with_author=True)
40+
41+
@classmethod
42+
def create_repository(cls):
43+
"""Create repository and preprint fixtures (call when needed)."""
44+
if hasattr(cls, "repository"):
45+
return cls.repository, cls.preprint
46+
47+
cls.repository, cls.subject = helpers.create_repository(
48+
cls.press, [cls.editor], [cls.editor]
49+
)
50+
cls.preprint = helpers.create_preprint(
51+
cls.repository, cls.article.owner, cls.subject
52+
)
53+
return cls.repository, cls.preprint

tests/test_forms.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Tests for geometadata forms.
3+
4+
Tests WKT validation and temporal period format validation.
5+
"""
6+
7+
from django.test import TestCase
8+
9+
from plugins.geometadata.forms import ArticleGeometadataForm
10+
11+
12+
class ArticleGeometadataFormTests(TestCase):
13+
"""Tests for ArticleGeometadataForm validation."""
14+
15+
def test_valid_wkt_accepted(self):
16+
"""Valid WKT geometry passes validation."""
17+
form = ArticleGeometadataForm(data={
18+
"geometry_wkt": "POLYGON((-10 35, 40 35, 40 70, -10 70, -10 35))",
19+
"place_name": "",
20+
"admin_units": "",
21+
"temporal_periods_json": "[]",
22+
})
23+
self.assertTrue(form.is_valid(), form.errors)
24+
25+
def test_invalid_wkt_rejected(self):
26+
"""Malformed WKT raises validation error."""
27+
form = ArticleGeometadataForm(data={
28+
"geometry_wkt": "NOT_VALID_WKT(abc)",
29+
"place_name": "",
30+
"admin_units": "",
31+
"temporal_periods_json": "[]",
32+
})
33+
self.assertFalse(form.is_valid())
34+
self.assertIn("geometry_wkt", form.errors)
35+
36+
def test_temporal_periods_valid_json(self):
37+
"""Valid JSON array for temporal periods accepted."""
38+
form = ArticleGeometadataForm(data={
39+
"geometry_wkt": "",
40+
"place_name": "",
41+
"admin_units": "",
42+
"temporal_periods_json": '[["2020-01", "2021-06"]]',
43+
})
44+
self.assertTrue(form.is_valid(), form.errors)
45+
46+
def test_temporal_periods_invalid_format(self):
47+
"""Non-array JSON for temporal periods rejected."""
48+
form = ArticleGeometadataForm(data={
49+
"geometry_wkt": "",
50+
"place_name": "",
51+
"admin_units": "",
52+
"temporal_periods_json": '{"not": "array"}',
53+
})
54+
self.assertFalse(form.is_valid())
55+
self.assertIn("temporal_periods_json", form.errors)
56+
57+
def test_empty_form_valid(self):
58+
"""Empty form is valid (all fields optional)."""
59+
form = ArticleGeometadataForm(data={
60+
"geometry_wkt": "",
61+
"place_name": "",
62+
"admin_units": "",
63+
"temporal_periods_json": "[]",
64+
})
65+
self.assertTrue(form.is_valid(), form.errors)

tests/test_geocoding.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Tests for geocoding functionality.
3+
4+
Tests reverse geocoding API with mocked external services.
5+
"""
6+
7+
import json
8+
from unittest.mock import patch, MagicMock
9+
10+
from django.test import override_settings
11+
12+
from plugins.geometadata.tests.base import GeometadataTestCase
13+
14+
15+
@override_settings(
16+
URL_CONFIG="domain",
17+
ROOT_URLCONF="plugins.geometadata.tests.urls",
18+
)
19+
class ReverseGeocodeAPITests(GeometadataTestCase):
20+
"""Tests for reverse geocode API endpoint."""
21+
22+
def setUp(self):
23+
"""Log in as editor for all tests."""
24+
self.client.force_login(self.editor)
25+
26+
def _post_geocode(self, data):
27+
"""Helper to POST to reverse geocode endpoint."""
28+
return self.client.post(
29+
"/plugins/geometadata/api/reverse-geocode/",
30+
data=json.dumps(data),
31+
content_type="application/json",
32+
SERVER_NAME=self.journal.domain,
33+
)
34+
35+
@patch("plugins.geometadata.geocoding.reverse_geocode_wkt")
36+
@patch("plugins.geometadata.views._get_plugin_setting")
37+
def test_reverse_geocode_returns_place_name(self, mock_setting, mock_geocode):
38+
"""Successful geocode returns place_name and admin_units."""
39+
# Enable geocoding
40+
setting_mock = MagicMock()
41+
setting_mock.value = "on"
42+
mock_setting.return_value = setting_mock
43+
mock_geocode.return_value = {
44+
"place_name": "Berlin",
45+
"admin_units": "Berlin, Germany",
46+
}
47+
48+
response = self._post_geocode({"wkt": "POINT(13.4 52.5)"})
49+
50+
self.assertEqual(response.status_code, 200)
51+
data = response.json()
52+
self.assertTrue(data["success"])
53+
self.assertEqual(data["place_name"], "Berlin")
54+
self.assertEqual(data["admin_units"], "Berlin, Germany")
55+
56+
@patch("plugins.geometadata.geocoding.reverse_geocode_wkt")
57+
@patch("plugins.geometadata.views._get_plugin_setting")
58+
def test_reverse_geocode_handles_api_error(self, mock_setting, mock_geocode):
59+
"""API failure returns 500 with error message."""
60+
setting_mock = MagicMock()
61+
setting_mock.value = "on"
62+
mock_setting.return_value = setting_mock
63+
mock_geocode.side_effect = Exception("Network error")
64+
65+
response = self._post_geocode({"wkt": "POINT(13.4 52.5)"})
66+
67+
self.assertEqual(response.status_code, 500)
68+
data = response.json()
69+
self.assertIn("error", data)
70+
71+
@patch("plugins.geometadata.views._get_plugin_setting")
72+
def test_reverse_geocode_requires_geometry(self, mock_setting):
73+
"""Request without WKT returns 400 error."""
74+
setting_mock = MagicMock()
75+
setting_mock.value = "on"
76+
mock_setting.return_value = setting_mock
77+
78+
response = self._post_geocode({"wkt": ""})
79+
80+
self.assertEqual(response.status_code, 400)
81+
data = response.json()
82+
self.assertIn("error", data)

tests/test_hooks.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
Tests for geometadata template hooks.
3+
4+
Tests HTML metadata embedding (Dublin Core, Schema.org, geo.* meta tags).
5+
"""
6+
7+
from django.template import Context, Template
8+
from django.test import RequestFactory
9+
10+
from plugins.geometadata.models import ArticleGeometadata
11+
from plugins.geometadata.tests.base import GeometadataTestCase
12+
13+
14+
class MetaTagsTemplateTests(GeometadataTestCase):
15+
"""Tests for meta_tags.html template rendering."""
16+
17+
@classmethod
18+
def setUpTestData(cls):
19+
super().setUpTestData()
20+
cls.geometadata = ArticleGeometadata.objects.create(
21+
article=cls.article,
22+
geometry_wkt="POLYGON((-10 35, 40 35, 40 70, -10 70, -10 35))",
23+
place_name="Europe",
24+
temporal_periods=[["2020-01-01", "2021-12-31"]],
25+
)
26+
cls.factory = RequestFactory()
27+
28+
def _render_meta_tags(self, **context_overrides):
29+
"""Render meta_tags.html with test context."""
30+
geojson = self.geometadata.to_geojson()
31+
context = {
32+
"geometadata": self.geometadata,
33+
"geojson_str": '{"type":"Point","coordinates":[10,50]}',
34+
"geojson_geometry_str": '{"type":"Point","coordinates":[10,50]}',
35+
"temporal_intervals": ["2020-01-01/2021-12-31"],
36+
"temporal_interval": "2020-01-01/2021-12-31",
37+
"spatial_enabled": True,
38+
"temporal_enabled": True,
39+
"embed_dc": True,
40+
"embed_geo": True,
41+
"embed_schema": True,
42+
"embed_geojson": True,
43+
"geojson_download_url": "/download/article/1/geojson/",
44+
}
45+
context.update(context_overrides)
46+
47+
template = Template(
48+
"{% include 'geometadata/meta_tags.html' %}"
49+
)
50+
return template.render(Context(context))
51+
52+
def test_dc_spatial_coverage_contains_geojson(self):
53+
"""DC.SpatialCoverage meta tag contains GeoJSON geometry."""
54+
html = self._render_meta_tags()
55+
56+
self.assertIn('name="DC.SpatialCoverage"', html)
57+
self.assertIn('scheme="GeoJSON"', html)
58+
self.assertIn("Point", html)
59+
60+
def test_dc_box_contains_bbox(self):
61+
"""DC.box meta tag contains bounding box coordinates."""
62+
html = self._render_meta_tags()
63+
64+
self.assertIn('name="DC.box"', html)
65+
self.assertIn("northlimit=70", html)
66+
self.assertIn("southlimit=35", html)
67+
self.assertIn("westlimit=-10", html)
68+
self.assertIn("eastlimit=40", html)
69+
70+
def test_dc_temporal_rendered(self):
71+
"""DC.temporal meta tags rendered for temporal periods."""
72+
html = self._render_meta_tags()
73+
74+
self.assertIn('name="DC.temporal"', html)
75+
self.assertIn('scheme="ISO8601"', html)
76+
self.assertIn("2020-01-01/2021-12-31", html)
77+
78+
def test_geo_placename_rendered(self):
79+
"""geo.placename meta tag contains place name."""
80+
html = self._render_meta_tags()
81+
82+
self.assertIn('name="geo.placename"', html)
83+
self.assertIn('content="Europe"', html)
84+
85+
def test_schema_org_jsonld_rendered(self):
86+
"""Schema.org JSON-LD script tag rendered with spatial coverage."""
87+
html = self._render_meta_tags()
88+
89+
self.assertIn('type="application/ld+json"', html)
90+
self.assertIn('"@context": "https://schema.org"', html)
91+
self.assertIn('"spatialCoverage"', html)
92+
93+
def test_geojson_link_rendered(self):
94+
"""GeoJSON alternate link element rendered."""
95+
html = self._render_meta_tags()
96+
97+
self.assertIn('rel="alternate"', html)
98+
self.assertIn('type="application/geo+json"', html)
99+
self.assertIn("/download/article/1/geojson/", html)
100+
101+
def test_meta_tags_respect_disabled_settings(self):
102+
"""Disabled embed settings suppress corresponding output."""
103+
html = self._render_meta_tags(
104+
embed_dc=False,
105+
embed_geo=False,
106+
embed_schema=False,
107+
embed_geojson=False,
108+
)
109+
110+
self.assertNotIn("DC.SpatialCoverage", html)
111+
self.assertNotIn("geo.placename", html)
112+
self.assertNotIn("application/ld+json", html)
113+
self.assertNotIn("application/geo+json", html)

0 commit comments

Comments
 (0)