Skip to content

Commit 5454bfa

Browse files
committed
Upgrade the plugin and test
1 parent 1fec3ab commit 5454bfa

2 files changed

Lines changed: 113 additions & 3 deletions

File tree

ckanext/datastore/tests/sample_dump_plugin.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
ALWAYS_INVALID_REASON = "Always invalid format"
99

1010

11-
def invalid_validate(resource_id):
11+
def invalid_validate(context):
1212
"""Validator that always rejects the format.
1313
1414
Used by tests to confirm that a ``validate`` callable on a
1515
registered format flows through to the helper, the blueprint, and
16-
the resource-read template.
16+
the resource-read template. Receives the dump context dict (see
17+
:class:`~ckanext.datastore.interfaces.IDatastoreDump`).
1718
"""
1819
return ALWAYS_INVALID_REASON
1920

2021

21-
def valid_validate(resource_id):
22+
def valid_validate(context):
2223
"""Validator that always approves.
2324
2425
Used by tests to cover the branch where a format declares a
@@ -38,6 +39,10 @@ class SampleDumpPlugin(p.SingletonPlugin):
3839
(covers the "validator returns reason" path).
3940
* ``passes`` — a format whose ``validate`` callable always
4041
approves (covers the "validator returns None" path).
42+
* ``capped`` — a format with declarative ``max_rows``/``max_columns``
43+
and no ``validate`` callable (covers the framework-evaluated path).
44+
``max_rows`` is 1 so any resource with more than one (post-filter)
45+
row trips it.
4146
* Removes the built-in ``xml`` format via the ``None`` sentinel.
4247
"""
4348

@@ -71,5 +76,17 @@ def register_dump_formats(self):
7176
"file_extension": "passes",
7277
"validate": valid_validate,
7378
},
79+
# Declarative limits evaluated by core (no validate callable).
80+
# max_rows=1 lets tests trip the row limit with a 2-row
81+
# resource and confirm a filter can bring it back under.
82+
"capped": {
83+
"label": "Capped",
84+
"writer_factory": csv_writer,
85+
"records_format": "csv",
86+
"content_type": "application/x-capped; charset=utf-8",
87+
"file_extension": "capped",
88+
"max_rows": 1,
89+
"max_columns": 5,
90+
},
7491
"xml": None,
7592
}

ckanext/datastore/tests/test_dump.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import unittest.mock as mock
44
import json
5+
import urllib.parse
56
import pytest
67
import ckan.plugins.toolkit as toolkit
78
import ckan.tests.helpers as helpers
@@ -1114,3 +1115,95 @@ def test_resource_read_page_renders_disabled_dropdown_item(self, app):
11141115
# And the built-in CSV format must still render as an active
11151116
# dropdown-item link (not disabled).
11161117
assert 'class="dropdown-item"' in body
1118+
1119+
1120+
@pytest.mark.ckan_config("ckan.plugins", "datastore sample_dump_plugin")
1121+
@pytest.mark.usefixtures("clean_datastore", "with_plugins")
1122+
class TestDatastoreDumpDeclarativeLimits(object):
1123+
"""Tests for declarative ``max_rows``/``max_columns`` controls.
1124+
1125+
Uses the ``capped`` format from ``sample_dump_plugin`` (``max_rows``
1126+
is 1, ``max_columns`` is 5, no ``validate`` callable). These exercise
1127+
the framework-evaluated availability path and, crucially, that the
1128+
decision is made against the *filtered* export scope rather than the
1129+
resource totals.
1130+
"""
1131+
1132+
def _make_resource(self, records):
1133+
resource = factories.Resource(url_type="datastore")
1134+
helpers.call_action(
1135+
"datastore_create",
1136+
resource_id=resource["id"],
1137+
records=records,
1138+
)
1139+
return resource
1140+
1141+
def test_helper_marks_over_row_limit_unavailable(self):
1142+
# 2 rows > max_rows=1 -> capped is unavailable with a
1143+
# framework-generated reason mentioning rows.
1144+
resource = self._make_resource(
1145+
[{"book": "annakarenina"}, {"book": "warandpeace"}])
1146+
1147+
formats = toolkit.h.datastore_dump_formats(
1148+
resource_id=resource["id"])
1149+
by_name = {f["name"]: f for f in formats}
1150+
1151+
assert by_name["capped"]["available"] is False
1152+
assert "rows" in by_name["capped"]["reason"].lower()
1153+
# Formats without controls stay available.
1154+
assert by_name["csv"]["available"] is True
1155+
1156+
def test_helper_within_limit_available(self):
1157+
# 1 row == max_rows=1 (rejected only when strictly greater).
1158+
resource = self._make_resource([{"book": "annakarenina"}])
1159+
1160+
formats = toolkit.h.datastore_dump_formats(
1161+
resource_id=resource["id"])
1162+
by_name = {f["name"]: f for f in formats}
1163+
1164+
assert by_name["capped"]["available"] is True
1165+
assert by_name["capped"]["reason"] is None
1166+
1167+
def test_dump_endpoint_rejects_over_limit_with_400(self, app):
1168+
resource = self._make_resource(
1169+
[{"book": "annakarenina"}, {"book": "warandpeace"}])
1170+
1171+
response = app.get(
1172+
f"/datastore/dump/{resource['id']}?format=capped",
1173+
status=400,
1174+
)
1175+
assert "rows" in response.get_data(as_text=True).lower()
1176+
1177+
def test_dump_endpoint_filtered_under_limit_succeeds(self, app):
1178+
# The resource has 2 rows (over the cap), but a filter reduces
1179+
# the export to a single row, so the capped format becomes
1180+
# available and the download succeeds. This is the case a
1181+
# resource-id-only validator would get wrong.
1182+
resource = self._make_resource(
1183+
[{"book": "annakarenina"}, {"book": "warandpeace"}])
1184+
1185+
qs = urllib.parse.urlencode({
1186+
"format": "capped",
1187+
"filters": json.dumps({"book": "annakarenina"}),
1188+
})
1189+
response = app.get(f"/datastore/dump/{resource['id']}?{qs}")
1190+
1191+
assert response.status_code == 200
1192+
assert "annakarenina" in response.get_data(as_text=True)
1193+
assert "warandpeace" not in response.get_data(as_text=True)
1194+
1195+
def test_helper_filtered_scope_marks_available(self):
1196+
# Same as above but through the helper: passing search_params
1197+
# that filter to one row flips capped from unavailable to
1198+
# available.
1199+
resource = self._make_resource(
1200+
[{"book": "annakarenina"}, {"book": "warandpeace"}])
1201+
1202+
formats = toolkit.h.datastore_dump_formats(
1203+
resource_id=resource["id"],
1204+
search_params={"filters": {"book": "annakarenina"}},
1205+
)
1206+
by_name = {f["name"]: f for f in formats}
1207+
1208+
assert by_name["capped"]["available"] is True
1209+
assert by_name["capped"]["reason"] is None

0 commit comments

Comments
 (0)