|
2 | 2 |
|
3 | 3 | import unittest.mock as mock |
4 | 4 | import json |
| 5 | +import urllib.parse |
5 | 6 | import pytest |
6 | 7 | import ckan.plugins.toolkit as toolkit |
7 | 8 | import ckan.tests.helpers as helpers |
@@ -1114,3 +1115,95 @@ def test_resource_read_page_renders_disabled_dropdown_item(self, app): |
1114 | 1115 | # And the built-in CSV format must still render as an active |
1115 | 1116 | # dropdown-item link (not disabled). |
1116 | 1117 | 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