|
7 | 7 |
|
8 | 8 | import pytest |
9 | 9 | import requests |
| 10 | +from lxml import etree |
10 | 11 | from mako.lookup import TemplateLookup |
11 | 12 | from wsgi_intercept.interceptor import RequestsInterceptor, UrllibInterceptor |
12 | 13 |
|
13 | 14 | from pyff.api import mkapp |
14 | 15 | from pyff.constants import config |
| 16 | +from pyff.samlmd import iter_entities |
15 | 17 | from pyff.test import SignerTestCase |
16 | 18 | from pyff.test.test_pipeline import PipeLineTest |
17 | 19 |
|
@@ -226,3 +228,83 @@ def test_api_resources(self): |
226 | 228 | assert (last_seen - now).total_seconds() < 60 |
227 | 229 |
|
228 | 230 | assert os.path.exists(os.path.join(config.local_copy_dir, urlescape(f'file://{self.test01}'))) |
| 231 | + |
| 232 | + |
| 233 | +class PyFFAPITestTrailingSlash(PipeLineTest): |
| 234 | + """ |
| 235 | + A trailing slash is part of an entityID, except when there is no entityID at all - |
| 236 | + a bare /entities/ selects everything, just like /entities does. |
| 237 | + """ |
| 238 | + |
| 239 | + mdx = None |
| 240 | + app = None |
| 241 | + idp = 'https://idp.example.com/saml2/idp/metadata.php' |
| 242 | + sp = 'https://sp.example.com/saml2/metadata/' |
| 243 | + |
| 244 | + @classmethod |
| 245 | + def setUpClass(cls): |
| 246 | + SignerTestCase.setUpClass() |
| 247 | + config.local_copy_dir = tempfile.mkdtemp() |
| 248 | + cls.test01 = os.path.join(cls.datadir, 'metadata', 'test01.xml') |
| 249 | + cls.test04 = os.path.join(cls.datadir, 'metadata', 'test04-trailing-slash-sp.xml') |
| 250 | + cls.mdx = tempfile.NamedTemporaryFile('w').name |
| 251 | + with open(cls.mdx, "w") as fd: |
| 252 | + fd.write( |
| 253 | + f""" |
| 254 | +- when update: |
| 255 | + - load: |
| 256 | + - {cls.test01} |
| 257 | + - {cls.test04} |
| 258 | +- when request: |
| 259 | + - select |
| 260 | + - pipe: |
| 261 | + - when accept application/xml: |
| 262 | + - finalize: |
| 263 | + cacheDuration: PT5H |
| 264 | + validUntil: P10D |
| 265 | + - emit application/xml |
| 266 | + - break |
| 267 | +""" |
| 268 | + ) |
| 269 | + cls._app = mkapp(cls.mdx) |
| 270 | + cls.app = lambda *args, **kwargs: cls._app |
| 271 | + |
| 272 | + @classmethod |
| 273 | + def tearDownClass(cls): |
| 274 | + SignerTestCase.tearDownClass() |
| 275 | + if os.path.exists(cls.mdx): |
| 276 | + os.unlink(cls.mdx) |
| 277 | + if os.path.exists(config.local_copy_dir): |
| 278 | + shutil.rmtree(config.local_copy_dir) |
| 279 | + |
| 280 | + def _entity_ids(self, url, path): |
| 281 | + """Return the set of entityIDs the API serves for path""" |
| 282 | + r = requests.get(f'{url}{path}', headers={'Accept': 'application/xml'}) |
| 283 | + assert r.status_code == 200, f'{path} -> {r.status_code}' |
| 284 | + t = etree.fromstring(r.content) |
| 285 | + return {e.get('entityID') for e in iter_entities(t)} |
| 286 | + |
| 287 | + def test_entities_without_trailing_slash(self): |
| 288 | + with RequestsInterceptor(self.app, host='127.0.0.1', port=80) as url: |
| 289 | + assert requests.post(f'{url}/api/call/update').status_code == 200 |
| 290 | + assert self._entity_ids(url, '/entities') == {self.idp, self.sp} |
| 291 | + |
| 292 | + def test_entities_with_trailing_slash(self): |
| 293 | + with RequestsInterceptor(self.app, host='127.0.0.1', port=80) as url: |
| 294 | + assert requests.post(f'{url}/api/call/update').status_code == 200 |
| 295 | + assert self._entity_ids(url, '/entities/') == {self.idp, self.sp} |
| 296 | + |
| 297 | + def test_entity_id_keeps_its_trailing_slash(self): |
| 298 | + """An entityID ending in a slash must not be truncated - cf. issue #298""" |
| 299 | + with RequestsInterceptor(self.app, host='127.0.0.1', port=80) as url: |
| 300 | + assert requests.post(f'{url}/api/call/update').status_code == 200 |
| 301 | + # the unescaped form - WSGI hands us a single slash after the scheme |
| 302 | + assert self._entity_ids(url, '/entities/https:/sp.example.com/saml2/metadata/') == {self.sp} |
| 303 | + # ... and the escaped form |
| 304 | + assert self._entity_ids(url, f'/entities/{urlescape(self.sp, safe="")}') == {self.sp} |
| 305 | + |
| 306 | + def test_entity_id_without_trailing_slash_is_not_found(self): |
| 307 | + """Dropping the slash from an entityID that has one must not match it""" |
| 308 | + with RequestsInterceptor(self.app, host='127.0.0.1', port=80) as url: |
| 309 | + assert requests.post(f'{url}/api/call/update').status_code == 200 |
| 310 | + assert self._entity_ids(url, '/entities/https:/sp.example.com/saml2/metadata') == set() |
0 commit comments