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
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
Comment #3
benstallings commentedComment #4
benstallings commentedNote that the failures in the pipeline are in files other than the ones added by this MR.
Comment #5
benstallings commentedadding tag for contrib day!
Comment #6
jproctorThank 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?
Comment #7
benstallings commentedThank 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.
Comment #8
benstallings commented@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!