Problem/Motivation

Starting point: Zero existing tests. The module has 3 layers: pure PHP logic, Drupal-integrated services, and HTTP/UI
flows.

Proposed resolution

Layer 1 — Unit Tests (tests/src/Unit/)

These require no Drupal bootstrap. Target pure logic only.

SamlSP/SamlSPResponseTest.php

- testValidateSignedElements_twoUniqueElements_passes() — valid case
- testValidateSignedElements_duplicateElement_throws() — uniqueness check
- testValidateSignedElements_moreThanTwoElements_throws() — max 2 check

SamlSP/ExtractOutboundIdTest.php (tests _saml_sp__extract_outbound_id())

- testExtractsValidId_fromEncodedUrl() — happy path with a real encoded SAML redirect
- testReturnsFalse_whenUrlIsGarbage() — invalid input
- testReturnsFalse_whenIdFormatMismatches() — ID prefix not ONELOGIN_

SamlSP/ExtractInboundIdTest.php (tests _saml_sp__extract_inbound_id())

- testExtractsInResponseTo_fromValidBase64Xml()
- testReturnsFalse_whenXmlIsInvalid()
- testReturnsFalse_whenInResponseToAttributeMissing()

SamlSP/AuthnContextClassRefsTest.php

- testReturnsForwardMapping() — URI → slug
- testReturnsReverseMapping() — slug → URI
- testContainsAllSixConstants()

Entity/IdpTest.php

- Getter/setter round-trip for all 7 properties (label, app_name, entity_id, login_url, logout_url, nameid_field,
x509_cert)
- testGetAuthnContextClassRef_returnsFalse_whenEmpty()
- testGetX509Cert_returnsArray()

---
Layer 2 — Kernel Tests (tests/src/Kernel/)

Use KernelTestBase. Boot Drupal, install schemas and config, no HTTP.

IdpEntityCrudTest.php

$modules = ['saml_sp', 'system', 'user']
- testCreateAndLoadIdp() — Idp::create()->save() then Idp::load()
- testLoadMultipleIdps() — saml_sp__load_all_idps()
- testIdpLoad_withString() and testIdpLoad_withArray() — saml_sp_idp_load() branches
- testDeleteIdp()

IdpListBuilderTest.php

- testBuildHeader_returnsExpectedColumns()
- testBuildRow_returnsIdpData()

SamlSpSettingsTest.php

$modules = ['saml_sp', 'system', 'user', 'language']
- testGetSettings_withEmptyIdp_returnsDefaults() — covers all config → array branches
- testGetSettings_withFullIdp_populatesIdpBlock()
- testGetSettings_withCertArray_usesFirstCert() — the is_array(getX509Cert()) branch
- testGetSettings_contactPersonIncluded_whenBothNameAndEmailSet()
- testGetSettings_contactPersonOmitted_whenFieldsMissing()
- testGetSettings_organizationIncluded_whenAllFieldsSet()
- testGetSettings_authnContexts_mappedCorrectly()
- testGetSettings_alterHookInvoked() — install a test module that implements hook_saml_sp_settings_alter

TemporaryRequestTrackingTest.php

- testTrackRequest_andRetrieve() — tempstore round-trip
- testGetTrackedRequest_returnsFalse_whenNotFound()

MetadataGenerationTest.php

- testGetMetadata_returnsXmlString()
- testGetMetadata_returnsNoErrors_withMinimalConfig()

SamlSpSubscriberTest.php

- testCheckForCertExpiration_noWarning_whenNoCertLocation()
- testCheckForCertExpiration_showsError_whenCertExpired() — mock cert file
- testCheckForCertExpiration_showsWarning_whenCertExpiresSoon() — within 30 days
- testCheckForCertExpiration_noMessage_forAnonymousUser() — non-admin user

ConfigureProxySubscriberTest.php

- testOnRequest_setsProxySettings_whenDefinedInSettings()
- testOnRequest_doesNothing_whenNoProxyConfigured()

DrupalLogin/GetUserTest.php

$modules = ['saml_sp', 'saml_sp_drupal_login', 'user', 'system', 'field', ...]
This is the most logic-rich function — covers all 4 lookup paths:
- testGetUser_byMail_whenNameIdFieldIsMail()
- testGetUser_byCustomStandardField_whenFieldExistsOnUsersFieldData()
- testGetUser_byCustomFieldTable_whenUser__fieldTableExists()
- testGetUser_byEmailFallback_whenNameIdNotFound()
- testGetUser_returnsFalse_whenNoMatchFound()
- testGetUser_withStringEmail_triggersDeprecation()

DrupalLogin/SessionTest.php

- testSetSession_andIsAuthenticated_returnsTrue()
- testIsAuthenticated_returnsFalse_whenNotSet()
- testSetSession_storesIdpAndSessionIndex()

DrupalLogin/SamlSpDrupalLoginSubscriberTest.php

- testOnKernelRequest_noWarning_whenIdpsNotConfigured()
- testOnKernelRequest_warnsAdmin_whenSecKitMissingIdpUrl()
- testOnKernelRequest_silent_forNonAdmin()

---
Layer 3 — Functional Tests (tests/src/Functional/)

Use BrowserTestBase (or DTT ExistingSiteBase in future). Full HTTP stack.

SamlSpConfigFormTest.php

- testFormAccessDenied_forAnonymous()
- testFormAccessDenied_withoutPermission()
- testFormLoads_withAdminPermission()
- testFormSavesValues_andPersistsToConfig()
- testMetadataTextareaRendered_whenSettingsValid()

IdpFormTest.php

- testAddIdp_withRequiredFieldsOnly()
- testAddIdp_withXmlMetadataInput_populatesFields() — the XML parse path in IdpForm
- testEditIdp_updatesExistingEntity()
- testDeleteIdp_removesEntity()
- testAddCert_dynamicAjax_addsNewCertField()
- testMachineNameConflict_showsValidationError() — exist() check

MetadataRouteTest.php

- testMetadataEndpoint_returns200_withXmlContentType()
- testMetadataEndpoint_accessible_withNoAuthentication()
- testMetadataXml_containsEntityId()

SamlSpDrupalLoginConfigFormTest.php

- testFormLoads_withPermission()
- testSaveConfig_enablesIdps()

LoginFormAlterTest.php

- testLoginForm_showsSamlLinks_whenIdpsEnabled()
- testLoginForm_hidesNativeForm_whenForceSamlOnly()
- testPasswordResetForm_disabledSubmit_whenForceSamlOnly()

SamlSpDrupalLoginControllerTest.php

- testAccess_forAnonymous_allowsRegistrationRoute()
- testAccess_forAuthenticated_deniesRegistrationRoute()

---
Layer 4 — Integration / Auth Flow Tests

The SAML authentication callback (saml_sp_drupal_login__saml_authenticate) is the most complex function in the
codebase and covers 6 distinct code paths. These are hard to test without a mock IdP but can be tested at the Kernel
level by constructing a mock OneLogin\Saml2\Response.

DrupalLogin/SamlAuthenticateTest.php (Kernel)

- testCallback_invalidResponse_redirectsWithError()
- testCallback_existingUser_logsIn() — user found by NameID
- testCallback_noAccount_registrationOpen_createsNewUser() — REGISTER_VISITORS path
- testCallback_noAccount_genericAccountConfigured_grantsAccess() — no_account_authenticated_user_account path
- testCallback_noAccount_adminOnlyRegistration_showsWarning()
- testCallback_noAccount_approvalRequired_redirectsToRegister()
- testCallback_blockedUser_showsError_doesNotLogin()
- testCallback_noEmailInResponse_logsWarning()
- testCallback_storesSessionData_onSuccess()

---
Summary by File Count

┌────────────┬────────────┬──────────────────────┐
│ Layer │ Test Files │ Approx. Test Methods │
├────────────┼────────────┼──────────────────────┤
│ Unit │ 5 │ ~20 │
├────────────┼────────────┼──────────────────────┤
│ Kernel │ 9 │ ~45 │
├────────────┼────────────┼──────────────────────┤
│ Functional │ 6 │ ~25 │
├────────────┼────────────┼──────────────────────┤
│ Auth Flow │ 1 │ ~9 │
├────────────┼────────────┼──────────────────────┤
│ Total │ 21 │ ~100 │
└────────────┴────────────┴──────────────────────┘

---
Suggested Implementation Order

1. Unit tests first — zero dependencies, fast feedback
2. IdpEntityCrudTest + GetUserTest — highest risk/most branching logic
3. SamlAuthenticateTest — most business-critical path
4. Remaining kernel tests
5. Functional tests last — slowest, need browser bootstrap

Notes on Test Infrastructure

- The saml_sp_drupal_login__saml_authenticate callback requires a OneLogin\Saml2\Response instance. You'll need to
either: (a) construct a mock via \PHPUnit\Framework\TestCase::createMock(), or (b) use a pre-signed SAML fixture.
Option (a) is safer for unit/kernel tests.
- Certificate expiry tests in SamlSpSubscriberTest need a temp cert file; use vfsStream or sys_get_temp_dir().
- Tests touching saml_sp__get_settings() should use \Drupal::configFactory()->getEditable() in setUp() to inject known
values rather than relying on config/install defaults.

Remaining tasks

Write the tests, starting with the simplest, and make an MR.

Issue fork saml_sp-3581492

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

benstallings created an issue. See original summary.

benstallings’s picture

Assigned: benstallings » Unassigned
Status: Active » Needs review
benstallings’s picture

Note that the failures in the pipeline are in files other than the ones added by this MR.

benstallings’s picture

Issue tags: +Chicago2026

adding tag for contrib day!

jproctor’s picture

Thank you!

I think most of the maintainers have said “yeah, this really needs tests” at some point, but nobody made any significant progress on it.

I am so eager to merge these, and also kinda feel like we should at least look over 3491 lines of code before dropping them in (even though they are completely isolated from the rest of the project).

Hey @jrglasgow, @benstallings has contributed a lot of good code over the last few months; do you have any objection to granting maintainership?

benstallings’s picture

Thank you, @jproctor, but I am not looking for maintainership at this time. <3

Full disclosure, as you might have guessed, these tests were written by Claude Code. I read them over, but they absolutely should be reviewed by someone with more familiarity with the project.

benstallings’s picture

@jproctor, thanks for calling out the excessive size of this branch. I made a pass through and was able to delete 4+ files of unnecessary tests!