• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grafana / django-saml2-auth / 10813098328

11 Sep 2024 01:52PM UTC coverage: 90.667% (+0.2%) from 90.439%
10813098328

push

github

web-flow
feat(trigger): add custom get metadata hook (#342)

* feat(trigger): add custom get metadata hook
* use custom trigger to override metadata retrieval
* update README
* update tests with new functions
* Remove prints
* Ignore types
* Remove unused import
* Update Django patch versions

---------

Co-authored-by: Mostafa Moradian <mstfmoradian@gmail.com>
Co-authored-by: Mostafa Moradian <mostafa@grafana.com>

26 of 27 new or added lines in 2 files covered. (96.3%)

11 existing lines in 2 files now uncovered.

952 of 1050 relevant lines covered (90.67%)

5.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.55
/django_saml2_auth/tests/test_saml.py
1
"""
6✔
2
Tests for saml.py
3
"""
4

5
from typing import Dict, Optional, List, Mapping, Union
6✔
6

7
import pytest
6✔
8
import responses
6✔
9
from django.contrib.sessions.middleware import SessionMiddleware
6✔
10
from unittest.mock import MagicMock
6✔
11
from django.http import HttpRequest
6✔
12
from django.test.client import RequestFactory
6✔
13
from django.urls import NoReverseMatch
6✔
14
from django_saml2_auth.exceptions import SAMLAuthError
6✔
15
from django_saml2_auth.saml import (
6✔
16
    decode_saml_response,
17
    extract_user_identity,
18
    get_assertion_url,
19
    get_default_next_url,
20
    get_metadata,
21
    get_saml_client,
22
    validate_metadata_url,
23
)
24
from django_saml2_auth.views import acs
6✔
25
from pytest_django.fixtures import SettingsWrapper
6✔
26
from saml2.client import Saml2Client
6✔
27
from saml2.response import AuthnResponse
6✔
28
from django_saml2_auth import user
6✔
29

30

31
GET_METADATA_AUTO_CONF_URLS = "django_saml2_auth.tests.test_saml.get_metadata_auto_conf_urls"
6✔
32
METADATA_URL1 = "https://testserver1.com/saml/sso/metadata"
6✔
33
METADATA_URL2 = "https://testserver2.com/saml/sso/metadata"
6✔
34
# Ref: https://en.wikipedia.org/wiki/SAML_metadata#Entity_metadata
35
METADATA1 = b"""
6✔
36
<md:EntityDescriptor entityID="https://testserver1.com/entity" validUntil="2025-08-30T19:10:29Z"
37
    xmlns:md="urn:oasis:names:tc:SAML:2.0:METADATA1"
38
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
39
    xmlns:mdrpi="urn:oasis:names:tc:SAML:METADATA1:rpi"
40
    xmlns:mdattr="urn:oasis:names:tc:SAML:METADATA1:attribute"
41
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
42
    <!-- insert ds:Signature element (omitted) -->
43
    <md:Extensions>
44
    <mdrpi:RegistrationInfo registrationAuthority="https://testserver1.com/"/>
45
    <mdrpi:PublicationInfo creationInstant="2025-08-16T19:10:29Z" publisher="https://testserver1.com/"/>
46
    <mdattr:EntityAttributes>
47
        <saml:Attribute Name="https://testserver1.com/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
48
        <saml:AttributeValue>https://testserver1.com/category/self-certified</saml:AttributeValue>
49
        </saml:Attribute>
50
    </mdattr:EntityAttributes>
51
    </md:Extensions>
52
    <!-- insert one or more concrete instances of the md:RoleDescriptor abstract type (see below) -->
53
    <md:Organization>
54
    <md:OrganizationName xml:lang="en">...</md:OrganizationName>
55
    <md:OrganizationDisplayName xml:lang="en">...</md:OrganizationDisplayName>
56
    <md:OrganizationURL xml:lang="en">https://testserver1.com/</md:OrganizationURL>
57
    </md:Organization>
58
    <md:ContactPerson contactType="technical">
59
    <md:SurName>SAML Technical Support</md:SurName>
60
    <md:EmailAddress>mailto:technical-support@example.info</md:EmailAddress>
61
    </md:ContactPerson>
62
</md:EntityDescriptor>"""
63
METADATA2 = b"""
6✔
64
<md:EntityDescriptor entityID="https://testserver2.com/entity" validUntil="2025-08-30T19:10:29Z"
65
    xmlns:md="urn:oasis:names:tc:SAML:2.0:METADATA1"
66
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
67
    xmlns:mdrpi="urn:oasis:names:tc:SAML:METADATA1:rpi"
68
    xmlns:mdattr="urn:oasis:names:tc:SAML:METADATA1:attribute"
69
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
70
    <!-- insert ds:Signature element (omitted) -->
71
    <md:Extensions>
72
    <mdrpi:RegistrationInfo registrationAuthority="https://testserver2.com/"/>
73
    <mdrpi:PublicationInfo creationInstant="2025-08-16T19:10:29Z" publisher="https://testserver2.com/"/>
74
    <mdattr:EntityAttributes>
75
        <saml:Attribute Name="https://testserver2.com/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
76
        <saml:AttributeValue>https://testserver2.com/category/self-certified</saml:AttributeValue>
77
        </saml:Attribute>
78
    </mdattr:EntityAttributes>
79
    </md:Extensions>
80
    <!-- insert one or more concrete instances of the md:RoleDescriptor abstract type (see below) -->
81
    <md:Organization>
82
    <md:OrganizationName xml:lang="en">...</md:OrganizationName>
83
    <md:OrganizationDisplayName xml:lang="en">...</md:OrganizationDisplayName>
84
    <md:OrganizationURL xml:lang="en">https://testserver2.com/</md:OrganizationURL>
85
    </md:Organization>
86
    <md:ContactPerson contactType="technical">
87
    <md:SurName>SAML Technical Support</md:SurName>
88
    <md:EmailAddress>mailto:technical-support@example.info</md:EmailAddress>
89
    </md:ContactPerson>
90
</md:EntityDescriptor>"""
91
DOMAIN_PATH_MAP = {
6✔
92
    "example.org": "django_saml2_auth/tests/metadata.xml",
93
    "example.com": "django_saml2_auth/tests/metadata2.xml",
94
    "api.example.com": "django_saml2_auth/tests/metadata.xml",
95
}
96

97

98
def get_metadata_auto_conf_urls(
6✔
99
    user_id: Optional[str] = None,
100
) -> List[Optional[Mapping[str, str]]]:
101
    """Fixture for returning metadata autoconf URL(s) based on the user_id.
102

103
    Args:
104
        user_id (str, optional): User identifier: username or email. Defaults to None.
105

106
    Returns:
107
        list: Either an empty list or a list of valid metadata URL(s)
108
    """
109
    if user_id == "nonexistent_user@example.com":
6✔
110
        return []
6✔
111
    if user_id == "test@example.com":
6✔
112
        return [{"url": METADATA_URL1}]
6✔
113
    return [{"url": METADATA_URL1}, {"url": METADATA_URL2}]
6✔
114

115

116
def get_user_identity() -> Mapping[str, List[str]]:
6✔
117
    """Fixture for returning user identity produced by pysaml2.
118

119
    Returns:
120
        dict: keys are SAML attributes and values are lists of attribute values
121
    """
122
    return {
6✔
123
        "user.username": ["test@example.com"],
124
        "user.email": ["test@example.com"],
125
        "user.first_name": ["John"],
126
        "user.last_name": ["Doe"],
127
        "token": ["TOKEN"],
128
    }
129

130

131
def get_user_identify_with_slashed_keys() -> Mapping[str, List[str]]:
6✔
132
    """Fixture for returning user identity produced by pysaml2 with slashed, claim-like keys.
133

134
    Returns:
135
        dict: keys are SAML attributes and values are lists of attribute values
136
    """
137
    return {
6✔
138
        "http://schemas.org/user/username": ["test@example.com"],
139
        "http://schemas.org/user/claim2.0/email": ["test@example.com"],
140
        "http://schemas.org/user/claim2.0/first_name": ["John"],
141
        "http://schemas.org/user/claim2.0/last_name": ["Doe"],
142
        "http://schemas.org/auth/server/token": ["TOKEN"],
143
    }
144

145

146
def mock_parse_authn_request_response(
6✔
147
    self: Saml2Client, response: AuthnResponse, binding: str
148
) -> "MockAuthnResponse":  # type: ignore # noqa: F821
149
    """Mock function to return an mocked instance of AuthnResponse.
150

151
    Returns:
152
        MockAuthnResponse: A mocked instance of AuthnResponse
153
    """
154

155
    class MockAuthnRequest:
6✔
156
        """Mock class for AuthnRequest."""
6✔
157

158
        name_id = "Username"
6✔
159

160
        @staticmethod
6✔
161
        def issuer():
6✔
162
            """Mock function for AuthnRequest.issuer()."""
163
            return METADATA_URL1
6✔
164

165
        @staticmethod
6✔
166
        def get_identity():
6✔
167
            """Mock function for AuthnRequest.get_identity()."""
168
            return get_user_identity()
6✔
169

170
    return MockAuthnRequest()
6✔
171

172

173
def test_get_assertion_url_success():
6✔
174
    """Test get_assertion_url function to verify if it correctly returns the default assertion URL."""
175
    assertion_url = get_assertion_url(HttpRequest())
6✔
176
    assert assertion_url == "https://api.example.com"
6✔
177

178

179
def test_get_assertion_url_no_assertion_url(settings: SettingsWrapper):
6✔
180
    """Test get_assertion_url function to verify if it correctly returns the server's assertion URL
181
    based on the incoming request.
182

183
    Args:
184
        settings (SettingsWrapper): Fixture for django settings
185
    """
186
    settings.SAML2_AUTH["ASSERTION_URL"] = None
6✔
187
    get_request = RequestFactory().get("/acs/")
6✔
188
    assertion_url = get_assertion_url(get_request)
6✔
189
    assert assertion_url == "http://testserver"
6✔
190

191

192
def test_get_default_next_url_success():
6✔
193
    """Test get_default_next_url to verify if it returns the correct default next URL."""
194
    default_next_url = get_default_next_url()
6✔
195
    assert default_next_url == "http://app.example.com/account/login"
6✔
196

197

198
def test_get_default_next_url_no_default_next_url(settings: SettingsWrapper):
6✔
199
    """Test get_default_next_url function with no default next url for redirection to see if it
200
    returns the admin:index route.
201

202
    Args:
203
        settings (SettingsWrapper): Fixture for django settings
204
    """
205
    settings.SAML2_AUTH["DEFAULT_NEXT_URL"] = None
6✔
206
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
207
        get_default_next_url()
6✔
208

209
    # This doesn't happen on a real instance, unless you don't have "admin:index" route
210
    assert str(exc_info.value) == "We got a URL reverse issue: ['admin:index']"
6✔
211
    assert exc_info.value.extra is not None
6✔
212
    assert issubclass(exc_info.value.extra["exc_type"], NoReverseMatch)
6✔
213

214

215
@responses.activate
6✔
216
def test_validate_metadata_url_success():
6✔
217
    """Test validate_metadata_url function to verify a valid metadata URL."""
218
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
219
    result = validate_metadata_url(METADATA_URL1)
6✔
220
    assert result
6✔
221

222

223
@responses.activate
6✔
224
def test_validate_metadata_url_failure():
6✔
225
    """Test validate_metadata_url function to verify if it correctly identifies an invalid metadata
226
    URL."""
227
    responses.add(responses.GET, METADATA_URL1)
6✔
228
    result = validate_metadata_url(METADATA_URL1)
6✔
229
    assert result is False
6✔
230

231

232
@responses.activate
6✔
233
def test_get_metadata_success_with_single_metadata_url(settings: SettingsWrapper):
6✔
234
    """Test get_metadata function to verify if it returns a valid metadata URL with a correct
235
    format.
236

237
    Args:
238
        settings (SettingsWrapper): Fixture for django settings
239
    """
240
    settings.SAML2_AUTH["METADATA_AUTO_CONF_URL"] = METADATA_URL1
6✔
241
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = None
6✔
242
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
243

244
    result = get_metadata()
6✔
245
    assert result == {"remote": [{"url": METADATA_URL1}]}
6✔
246

247

248
def test_get_metadata_failure_with_invalid_metadata_url(settings: SettingsWrapper):
6✔
249
    """Test get_metadata function to verify if it fails with invalid metadata information.
250

251
    Args:
252
        settings (SettingsWrapper): Fixture for django settings
253
    """
254
    # HTTP Responses are not mocked, so this will fail.
255
    settings.SAML2_AUTH["METADATA_AUTO_CONF_URL"] = METADATA_URL1
6✔
256
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = None
6✔
257

258
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
259
        get_metadata()
6✔
260

261
    assert str(exc_info.value) == "Invalid metadata URL."
6✔
262

263

264
@responses.activate
6✔
265
def test_get_metadata_success_with_multiple_metadata_urls(settings: SettingsWrapper):
6✔
266
    """Test get_metadata function to verify if it returns multiple metadata URLs if the user_id is
267
    unknown.
268

269
    Args:
270
        settings (SettingsWrapper): Fixture for django settings
271
    """
272
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
273
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
274
    responses.add(responses.GET, METADATA_URL2, body=METADATA2)
6✔
275

276
    result = get_metadata()
6✔
277
    assert result == {"remote": [{"url": METADATA_URL1}, {"url": METADATA_URL2}]}
6✔
278

279

280
@responses.activate
6✔
281
def test_get_metadata_success_with_user_id(settings: SettingsWrapper):
6✔
282
    """Test get_metadata function to verify if it returns a valid metadata URLs given the user_id.
283

284
    Args:
285
        settings (SettingsWrapper): Fixture for django settings
286
    """
287
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
288
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
289

290
    result = get_metadata("test@example.com")
6✔
291
    assert result == {"remote": [{"url": METADATA_URL1}]}
6✔
292

293

294
def test_get_metadata_failure_with_nonexistent_user_id(settings: SettingsWrapper):
6✔
295
    """Test get_metadata function to verify if it raises an exception given a nonexistent user_id.
296

297
    Args:
298
        settings (SettingsWrapper): Fixture for django settings
299
    """
300
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
301

302
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
303
        get_metadata("nonexistent_user@example.com")
6✔
304
    assert str(exc_info.value) == "No metadata URL associated with the given user identifier."
6✔
305

306

307
def test_get_metadata_success_with_local_file(settings: SettingsWrapper):
6✔
308
    """Test get_metadata function to verify if correctly returns path to local metadata file.
309

310
    Args:
311
        settings (SettingsWrapper): Fixture for django settings
312
    """
313
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = None
6✔
314
    settings.SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = "/absolute/path/to/metadata.xml"
6✔
315

316
    result = get_metadata()
6✔
317
    assert result == {"local": ["/absolute/path/to/metadata.xml"]}
6✔
318

319

320
def test_get_saml_client_success(settings: SettingsWrapper):
6✔
321
    """Test get_saml_client function to verify if it is correctly instantiated with local metadata
322
    file.
323

324
    Args:
325
        settings (SettingsWrapper): Fixture for django settings
326
    """
327
    settings.SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = "django_saml2_auth/tests/metadata.xml"
6✔
328
    result = get_saml_client("example.com", acs)
6✔
329
    assert isinstance(result, Saml2Client)
6✔
330

331

332
@responses.activate
6✔
333
def test_get_saml_client_success_with_user_id(settings: SettingsWrapper):
6✔
334
    """Test get_saml_client function to verify if it is correctly instantiated with remote metadata
335
    URL and valid user_id.
336

337
    Args:
338
        settings (SettingsWrapper): Fixture for django settings
339
    """
340
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
341
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
342

343
    result = get_saml_client("example.com", acs, "test@example.com")
6✔
344
    assert isinstance(result, Saml2Client)
6✔
345

346

347
def test_get_saml_client_failure_with_missing_metadata_url(settings: SettingsWrapper):
6✔
348
    """Test get_saml_client function to verify if it raises an exception given a missing non-mocked
349
    metadata URL.
350

351
    Args:
352
        settings (SettingsWrapper): Fixture for django settings
353
    """
354
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
355

356
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
357
        get_saml_client("example.com", acs, "test@example.com")
6✔
358

359
    assert str(exc_info.value) == "Metadata URL/file is missing."
6✔
360

361

362
def test_get_saml_client_failure_with_invalid_file(settings: SettingsWrapper):
6✔
363
    """Test get_saml_client function to verify if it raises an exception given an invalid path to
364
    metadata file.
365

366
    Args:
367
        settings (SettingsWrapper): Fixture for django settings
368
    """
369
    settings.SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = "/invalid/metadata.xml"
6✔
370
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = None
6✔
371

372
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
373
        get_saml_client("example.com", acs)
6✔
374

375
    assert str(exc_info.value) == "[Errno 2] No such file or directory: '/invalid/metadata.xml'"
6✔
376
    assert exc_info.value.extra is not None
6✔
377
    assert isinstance(exc_info.value.extra["exc"], FileNotFoundError)
6✔
378

379

380
@pytest.mark.parametrize(
6✔
381
    "supplied_config_values,expected_encryption_keypairs",
382
    [
383
        (
384
            {
385
                "KEY_FILE": "django_saml2_auth/tests/dummy_key.pem",
386
            },
387
            None,
388
        ),
389
        (
390
            {
391
                "CERT_FILE": "django_saml2_auth/tests/dummy_cert.pem",
392
            },
393
            None,
394
        ),
395
        (
396
            {
397
                "KEY_FILE": "django_saml2_auth/tests/dummy_key.pem",
398
                "CERT_FILE": "django_saml2_auth/tests/dummy_cert.pem",
399
            },
400
            [
401
                {
402
                    "key_file": "django_saml2_auth/tests/dummy_key.pem",
403
                    "cert_file": "django_saml2_auth/tests/dummy_cert.pem",
404
                }
405
            ],
406
        ),
407
    ],
408
)
409
def test_get_saml_client_success_with_key_and_cert_files(
6✔
410
    settings: SettingsWrapper,
411
    supplied_config_values: Dict[str, str],
412
    expected_encryption_keypairs: Union[List, None],
413
):
414
    """Test get_saml_client function to verify that it is correctly instantiated with encryption_keypairs
415
    if both key_file and cert_file are provided (even if encryption_keypairs isn't).
416

417
    Args:
418
        settings (SettingsWrapper): Fixture for django settings
419
    """
420

421
    settings.SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = "django_saml2_auth/tests/metadata.xml"
6✔
422

423
    for key, value in supplied_config_values.items():
6✔
424
        settings.SAML2_AUTH[key] = value
6✔
425

426
    result = get_saml_client("example.com", acs)
6✔
427
    assert isinstance(result, Saml2Client)
6✔
428
    assert result.config.encryption_keypairs == expected_encryption_keypairs
6✔
429

430
    for key, value in supplied_config_values.items():
6✔
431
        # ensure that the added settings do not get carried over to other tests
432
        del settings.SAML2_AUTH[key]
6✔
433

434

435
@responses.activate
6✔
436
def test_decode_saml_response_success(
6✔
437
    settings: SettingsWrapper,
438
    monkeypatch: "MonkeyPatch",  # type: ignore # noqa: F821
439
):
440
    """Test decode_saml_response function to verify if it correctly decodes the SAML response.
441

442
    Args:
443
        settings (SettingsWrapper): Fixture for django settings
444
        monkeypatch (MonkeyPatch): PyTest monkeypatch fixture
445
    """
446
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
447
    settings.SAML2_AUTH["ASSERTION_URL"] = "https://api.example.com"
6✔
448
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = GET_METADATA_AUTO_CONF_URLS
6✔
449

450
    post_request = RequestFactory().post(METADATA_URL1, {"SAMLResponse": "SAML RESPONSE"})
6✔
451
    monkeypatch.setattr(
6✔
452
        Saml2Client, "parse_authn_request_response", mock_parse_authn_request_response
453
    )
454
    result = decode_saml_response(post_request, acs)
6✔
455
    assert len(result.get_identity()) > 0  # type: ignore
6✔
456

457

458
def test_extract_user_identity_success():
6✔
459
    """Test extract_user_identity function to verify if it correctly extracts user identity
460
    information from a (pysaml2) parsed SAML response."""
461
    result = extract_user_identity(get_user_identity())  # type: ignore
6✔
462
    assert len(result) == 6
6✔
463
    assert result["username"] == result["email"] == "test@example.com"
6✔
464
    assert result["first_name"] == "John"
6✔
465
    assert result["last_name"] == "Doe"
6✔
466
    assert result["token"] == "TOKEN"
6✔
467
    assert result["user_identity"] == get_user_identity()
6✔
468

469

470
def test_extract_user_identity_with_slashed_attribute_keys_success(settings: SettingsWrapper):
6✔
471
    """Test extract_user_identity function to verify if it correctly extracts user identity
472
    information from a (pysaml2) parsed SAML response with slashed attribute keys."""
473
    settings.SAML2_AUTH = {
6✔
474
        "ATTRIBUTES_MAP": {
475
            "email": "http://schemas.org/user/claim2.0/email",
476
            "username": "http://schemas.org/user/username",
477
            "first_name": "http://schemas.org/user/claim2.0/first_name",
478
            "last_name": "http://schemas.org/user/claim2.0/last_name",
479
            "token": "http://schemas.org/auth/server/token",
480
        }
481
    }
482

483
    result = extract_user_identity(get_user_identify_with_slashed_keys())  # type: ignore
6✔
484

485
    assert len(result) == 6
6✔
486
    assert result["username"] == result["email"] == "test@example.com"
6✔
487
    assert result["first_name"] == "John"
6✔
488
    assert result["last_name"] == "Doe"
6✔
489
    assert result["token"] == "TOKEN"
6✔
490
    assert result["user_identity"] == get_user_identify_with_slashed_keys()
6✔
491

492

493
def test_extract_user_identity_token_not_required(settings: SettingsWrapper):
6✔
494
    """Test extract_user_identity function to verify if it correctly extracts user identity
495
    information from a (pysaml2) parsed SAML response when token is not required."""
496
    settings.SAML2_AUTH["TOKEN_REQUIRED"] = False
6✔
497

498
    result = extract_user_identity(get_user_identity())  # type: ignore
6✔
499
    assert len(result) == 5
6✔
500
    assert "token" not in result
6✔
501

502

503
@pytest.mark.django_db
6✔
504
@responses.activate
6✔
505
def test_acs_view_when_next_url_is_none(
6✔
506
    settings: SettingsWrapper,
507
    monkeypatch: "MonkeyPatch",  # type: ignore # noqa: F821
508
):
509
    """Test Acs view when login_next_url is None in the session"""
510
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
511
    settings.SAML2_AUTH = {
6✔
512
        "ASSERTION_URL": "https://api.example.com",
513
        "DEFAULT_NEXT_URL": "default_next_url",
514
        "USE_JWT": False,
515
        "TRIGGER": {
516
            "BEFORE_LOGIN": None,
517
            "AFTER_LOGIN": None,
518
            "GET_METADATA_AUTO_CONF_URLS": GET_METADATA_AUTO_CONF_URLS,
519
        },
520
    }
521
    post_request = RequestFactory().post(METADATA_URL1, {"SAMLResponse": "SAML RESPONSE"})
6✔
522

523
    monkeypatch.setattr(
6✔
524
        Saml2Client, "parse_authn_request_response", mock_parse_authn_request_response
525
    )
526

527
    created, mock_user = user.get_or_create_user(
6✔
528
        {"username": "test@example.com", "first_name": "John", "last_name": "Doe"}
529
    )
530

531
    monkeypatch.setattr(
6✔
532
        user,
533
        "get_or_create_user",
534
        (
535
            created,
536
            mock_user,
537
        ),
538
    )
539

540
    middleware = SessionMiddleware(MagicMock())
6✔
541
    middleware.process_request(post_request)
6✔
542
    post_request.session["login_next_url"] = None
6✔
543
    post_request.session.save()
6✔
544

545
    result = acs(post_request)
6✔
546
    assert result["Location"] == "default_next_url"
6✔
547

548

549
@pytest.mark.django_db
6✔
550
@responses.activate
6✔
551
def test_acs_view_when_redirection_state_is_passed_in_relay_state(
6✔
552
    settings: SettingsWrapper,
553
    monkeypatch: "MonkeyPatch",  # type: ignore # noqa: F821
554
):
555
    """Test Acs view when login_next_url is None and redirection state in POST request"""
556
    responses.add(responses.GET, METADATA_URL1, body=METADATA1)
6✔
557
    settings.SAML2_AUTH = {
6✔
558
        "ASSERTION_URL": "https://api.example.com",
559
        "DEFAULT_NEXT_URL": "default_next_url",
560
        "USE_JWT": False,
561
        "TRIGGER": {
562
            "BEFORE_LOGIN": None,
563
            "AFTER_LOGIN": None,
564
            "GET_METADATA_AUTO_CONF_URLS": GET_METADATA_AUTO_CONF_URLS,
565
        },
566
    }
567
    post_request = RequestFactory().post(
6✔
568
        METADATA_URL1, {"SAMLResponse": "SAML RESPONSE", "RelayState": "/admin/logs"}
569
    )
570

571
    monkeypatch.setattr(
6✔
572
        Saml2Client, "parse_authn_request_response", mock_parse_authn_request_response
573
    )
574

575
    created, mock_user = user.get_or_create_user(
6✔
576
        {"username": "test@example.com", "first_name": "John", "last_name": "Doe"}
577
    )
578

579
    monkeypatch.setattr(
6✔
580
        user,
581
        "get_or_create_user",
582
        (
583
            created,
584
            mock_user,
585
        ),
586
    )
587

588
    middleware = SessionMiddleware(MagicMock())
6✔
589
    middleware.process_request(post_request)
6✔
590
    post_request.session["login_next_url"] = None
6✔
591
    post_request.session.save()
6✔
592

593
    result = acs(post_request)
6✔
594
    assert result["Location"] == "/admin/logs"
6✔
595

596

597
def get_custom_metadata_example(
6✔
598
    user_id: Optional[str] = None,
599
    domain:  Optional[str] = None,
600
    saml_response: Optional[str] = None,
601
):
602
    """
603
    Get metadata file locally depending on current SP domain
604
    """
605
    metadata_file_path = "/absolute/path/to/metadata.xml"
6✔
606
    if domain:
6✔
607
        protocol_idx = domain.find("https://")
6✔
608
        if protocol_idx > -1:
6✔
609
            domain = domain[protocol_idx + 8:]
6✔
610
        if domain in DOMAIN_PATH_MAP:
6✔
611
            print('metadata domain', domain)
6✔
612
            metadata_file_path = DOMAIN_PATH_MAP[domain]
6✔
613
            print('metadata path', metadata_file_path)
6✔
614
        else:
615
            raise SAMLAuthError(f"Domain {domain} not mapped!")
6✔
616
    else:
617
        # Fallback to local path
NEW
UNCOV
618
        metadata_file_path = "/absolute/path/to/metadata.xml"
×
619
    return {"local": [metadata_file_path]}
6✔
620

621

622
# WARNING: leave this test at the end or add
623
# settings.SAML2_AUTH["TRIGGER"]["GET_CUSTOM_METADATA"] = None
624
# to following tests that uses settings, otherwise the TRIGGER.GET_CUSTOM_METADATA is always set
625
# and used in the get_metadata function
626

627
def test_get_metadata_success_with_custom_trigger(settings: SettingsWrapper):
6✔
628
    """Test get_metadata function to verify if correctly returns path to local metadata file.
629

630
    Args:
631
        settings (SettingsWrapper): Fixture for django settings
632
    """
633
    settings.SAML2_AUTH["TRIGGER"]["GET_METADATA_AUTO_CONF_URLS"] = None
6✔
634
    settings.SAML2_AUTH["TRIGGER"]["GET_CUSTOM_METADATA"] = "django_saml2_auth.tests.test_saml.get_custom_metadata_example"
6✔
635
    
636
    result = get_metadata(domain="https://example.com")
6✔
637
    assert result == {"local": ["django_saml2_auth/tests/metadata2.xml"]}
6✔
638

639
    with pytest.raises(SAMLAuthError) as exc_info:
6✔
640
        get_metadata(domain="not-mapped-example.com")
6✔
641

642
    assert str(exc_info.value) == "Domain not-mapped-example.com not mapped!"
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc