diff --git a/modules/admin/lib/Controller/Config.php b/modules/admin/lib/Controller/Config.php
index b9ef5145c5e1cd78000ab4c90a583734bffcec0d..3c4062e59cde2de6b7925e0adce1e343c7917bb9 100644
--- a/modules/admin/lib/Controller/Config.php
+++ b/modules/admin/lib/Controller/Config.php
@@ -29,6 +29,12 @@ class Config
     /** @var \SimpleSAML\Configuration */
     protected $config;
 
+    /**
+     * @var \SimpleSAML\Utils\Auth|string
+     * @psalm-var \SimpleSAML\Utils\Auth|class-string
+     */
+    protected $authUtils = Utils\Auth::class;
+
     /** @var Menu */
     protected $menu;
 
@@ -50,16 +56,27 @@ class Config
     }
 
 
+    /**
+     * Inject the \SimpleSAML\Utils\Auth dependency.
+     *
+     * @param \SimpleSAML\Utils\Auth $authUtils
+     */
+    public function setAuthUtils(Utils\Auth $authUtils): void
+    {
+        $this->authUtils = $authUtils;
+    }
+
+
     /**
      * Display basic diagnostic information on hostname, port and protocol.
      *
-     * @param Request $request The current request.
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
      *
      * @return \SimpleSAML\XHTML\Template
      */
     public function diagnostics(Request $request): Template
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         $t = new Template($this->config, 'admin:diagnostics.twig');
         $t->data = [
@@ -88,11 +105,13 @@ class Config
     /**
      * Display the main admin page.
      *
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
+     *
      * @return \SimpleSAML\XHTML\Template
      */
-    public function main(): Template
+    public function main(/** @scrutinizer ignore-unused */ Request $request): Template
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         $t = new Template($this->config, 'admin:config.twig');
         $t->data = [
@@ -125,11 +144,13 @@ class Config
     /**
      * Display the output of phpinfo().
      *
-     * @return RunnableResponse
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
+     *
+     * @return \SimpleSAML\HTTP\RunnableResponse
      */
-    public function phpinfo(): RunnableResponse
+    public function phpinfo(/** @scrutinizer ignore-unused */ Request $request): RunnableResponse
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         return new RunnableResponse('phpinfo');
     }
@@ -397,7 +418,6 @@ class Config
                     curl_close($ch);
                 }
 
-
                 if ($latest && version_compare($this->config->getVersion(), ltrim($latest['tag_name'], 'v'), 'lt')) {
                     $warnings[] = [
                         Translate::noop(
diff --git a/modules/admin/lib/Controller/Federation.php b/modules/admin/lib/Controller/Federation.php
index d0e1ead676cb4bfce07f8bced91b651c9708b2b9..b0cdef7bd96d54ec553e8a4d72db48cccc5b9e99 100644
--- a/modules/admin/lib/Controller/Federation.php
+++ b/modules/admin/lib/Controller/Federation.php
@@ -38,7 +38,19 @@ class Federation
     /** @var \SimpleSAML\Configuration */
     protected $config;
 
-    /** @var MetaDataStorageHandler */
+    /**
+     * @var \SimpleSAML\Auth\Source|string
+     * @psalm-var \SimpleSAML\Auth\Source|class-string
+     */
+    protected $authSource = Auth\Source::class;
+
+    /**
+     * @var \SimpleSAML\Utils\Auth|string
+     * @psalm-var \SimpleSAML\Utils\Auth|class-string
+     */
+    protected $authUtils = Utils\Auth::class;
+
+    /** @var \SimpleSAML\Metadata\MetaDataStorageHandler */
     protected $mdHandler;
 
     /** @var Menu */
@@ -58,16 +70,50 @@ class Federation
     }
 
 
+    /**
+     * Inject the \SimpleSAML\Auth\Source dependency.
+     *
+     * @param \SimpleSAML\Auth\Source $authSource
+     */
+    public function setAuthSource(Auth\Source $authSource): void
+    {
+        $this->authSource = $authSource;
+    }
+
+
+    /**
+     * Inject the \SimpleSAML\Utils\Auth dependency.
+     *
+     * @param \SimpleSAML\Utils\Auth $authUtils
+     */
+    public function setAuthUtils(Utils\Auth $authUtils): void
+    {
+        $this->authUtils = $authUtils;
+    }
+
+
+    /**
+     * Inject the \SimpleSAML\Metadata\MetadataStorageHandler dependency.
+     *
+     * @param \SimpleSAML\Metadata\MetaDataStorageHandler $mdHandler
+     */
+    public function setMetadataStorageHandler(MetadataStorageHandler $mdHandler): void
+    {
+        $this->mdHandler = $mdHandler;
+    }
+
+
     /**
      * Display the federation page.
      *
+     * @param \Symfony\Component\HttpFoundation\Request $request
      * @return \SimpleSAML\XHTML\Template
      * @throws \SimpleSAML\Error\Exception
      * @throws \SimpleSAML\Error\Exception
      */
-    public function main(): Template
+    public function main(/** @scrutinizer ignore-unused */ Request $request): Template
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         // initialize basic metadata array
         $hostedSPs = $this->getHostedSP();
@@ -289,7 +335,7 @@ class Federation
         $entities = [];
 
         /** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */
-        foreach (Auth\Source::getSourcesOfType('saml:SP') as $source) {
+        foreach ($this->authSource::getSourcesOfType('saml:SP') as $source) {
             $metadata = $source->getHostedMetadata();
             if (isset($metadata['keys'])) {
                 $certificates = $metadata['keys'];
@@ -345,13 +391,13 @@ class Federation
     /**
      * Metadata converter
      *
-     * @param Request $request The current request.
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
      *
      * @return \SimpleSAML\XHTML\Template
      */
     public function metadataConverter(Request $request): Template
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
         if ($xmlfile = $request->files->get('xmlfile')) {
             $xmldata = trim(file_get_contents($xmlfile->getPathname()));
         } elseif ($xmldata = $request->request->get('xmldata')) {
@@ -422,16 +468,16 @@ class Federation
     /**
      * Download a certificate for a given entity.
      *
-     * @param Request $request The current request.
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
      *
-     * @return Response PEM-encoded certificate.
+     * @return \Symfony\Component\HttpFoundation\Response PEM-encoded certificate.
      */
     public function downloadCert(Request $request): Response
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         $set = $request->get('set');
-        $prefix = $request->get('prefix');
+        $prefix = $request->get('prefix', '');
 
         if ($set === 'saml20-sp-hosted') {
             $sourceID = $request->get('source');
@@ -439,7 +485,7 @@ class Federation
              * The second argument ensures non-nullable return-value
              * @var \SimpleSAML\Module\saml\Auth\Source\SP $source
              */
-            $source = \SimpleSAML\Auth\Source::getById($sourceID, Module\saml\Auth\Source\SP::class);
+            $source = $this->authSource::getById($sourceID, Module\saml\Auth\Source\SP::class);
             $mdconfig = $source->getMetadata();
         } else {
             $entityID = $request->get('entity');
@@ -465,13 +511,13 @@ class Federation
     /**
      * Show remote entity metadata
      *
-     * @param Request $request The current request.
+     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
      *
-     * @return Response
+     * @return \SimpleSAML\XHTML\Template
      */
-    public function showRemoteEntity(Request $request): Response
+    public function showRemoteEntity(Request $request): Template
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
 
         $entityId = $request->get('entityid');
         $set = $request->get('set');
diff --git a/modules/admin/lib/Controller/Test.php b/modules/admin/lib/Controller/Test.php
index 16ac227195e8af82c9bea6933698a57871ad747e..5d23f41eb197e488e6ac10d9f982df811e1b7f12 100644
--- a/modules/admin/lib/Controller/Test.php
+++ b/modules/admin/lib/Controller/Test.php
@@ -30,6 +30,24 @@ class Test
     /** @var \SimpleSAML\Configuration */
     protected $config;
 
+    /**
+     * @var \SimpleSAML\Utils\Auth|string
+     * @psalm-var \SimpleSAML\Utils\Auth|class-string
+     */
+    protected $authUtils = Utils\Auth::class;
+
+    /**
+     * @var \SimpleSAML\Auth\Simple|string
+     * @psalm-var \SimpleSAML\Auth\Simple|class-string
+     */
+    protected $authSimple = Auth\Simple::class;
+
+    /**
+     * @var \SimpleSAML\Auth\State|string
+     * @psalm-var \SimpleSAML\Auth\State|class-string
+     */
+    protected $authState = Auth\State::class;
+
     /** @var Menu */
     protected $menu;
 
@@ -51,29 +69,63 @@ class Test
     }
 
 
+    /**
+     * Inject the \SimpleSAML\Utils\Auth dependency.
+     *
+     * @param \SimpleSAML\Utils\Auth $authUtils
+     */
+    public function setAuthUtils(Utils\Auth $authUtils): void
+    {
+        $this->authUtils = $authUtils;
+    }
+
+
+    /**
+     * Inject the \SimpleSAML\Auth\Simple dependency.
+     *
+     * @param \SimpleSAML\Auth\Simple $authSimple
+     */
+    public function setAuthSimple(Auth\Simple $authSimple): void
+    {
+        $this->authSimple = $authSimple;
+    }
+
+
+    /**
+     * Inject the \SimpleSAML\Auth\State dependency.
+     *
+     * @param \SimpleSAML\Auth\State $authState
+     */
+    public function setAuthState(Auth\State $authState): void
+    {
+        $this->authState = $authState;
+    }
+
+
     /**
      * Display the list of available authsources.
      *
      * @param \Symfony\Component\HttpFoundation\Request $request
      * @param string|null $as
-     * @return \SimpleSAML\XHTML\Template
+     * @return \SimpleSAML\XHTML\Template|\SimpleSAML\HTTP\RunnableResponse
      */
     public function main(Request $request, string $as = null)
     {
-        Utils\Auth::requireAdmin();
+        $this->authUtils::requireAdmin();
         if (is_null($as)) {
             $t = new Template($this->config, 'admin:authsource_list.twig');
             $t->data = [
                 'sources' => Auth\Source::getSources(),
             ];
         } else {
-            $authsource = new Auth\Simple($as);
+            $simple = $this->authSimple;
+            $authsource = new $simple($as);
             if (!is_null($request->query->get('logout'))) {
-                $authsource->logout($this->config->getBasePath() . 'logout.php');
+                return new RunnableResponse([$authsource, 'logout'], [$this->config->getBasePath() . 'logout.php']);
             } elseif (!is_null($request->query->get(Auth\State::EXCEPTION_PARAM))) {
                 // This is just a simple example of an error
                 /** @var array $state */
-                $state = Auth\State::loadExceptionState();
+                $state = $this->authState::loadExceptionState();
                 Assert::keyExists($state, Auth\State::EXCEPTION_DATA);
                 throw $state[Auth\State::EXCEPTION_DATA];
             }
@@ -84,7 +136,7 @@ class Test
                     'ErrorURL' => $url,
                     'ReturnTo' => $url,
                 ];
-                $authsource->login($params);
+                return new RunnableResponse([$authsource, 'login'], [$params]);
             }
 
             $attributes = $authsource->getAttributes();
diff --git a/phpunit.xml b/phpunit.xml
index 28fd6ba44330c06a80dfed2bc23678f712a014e1..d00aef1cc1b3b2d6cb1a62b197047db0b8e4c808 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -17,6 +17,7 @@
     <filter>
         <whitelist processUncoveredFilesFromWhitelist="true">
             <directory suffix=".php">./lib/</directory>
+            <directory suffix=".php">./modules/admin/lib/</directory>
             <directory suffix=".php">./modules/core/lib/</directory>
             <directory suffix=".php">./modules/saml/lib/</directory>
             <exclude>
diff --git a/tests/modules/admin/lib/Controller/ConfigTest.php b/tests/modules/admin/lib/Controller/ConfigTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8685e4c9cfabeee927dd55aff585648809ce660f
--- /dev/null
+++ b/tests/modules/admin/lib/Controller/ConfigTest.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Test\Module\admin\Controller;
+
+use PHPUnit\Framework\TestCase;
+use SimpleSAML\Configuration;
+use SimpleSAML\HTTP\RunnableResponse;
+use SimpleSAML\Module\admin\Controller;
+use SimpleSAML\Session;
+use SimpleSAML\Utils;
+use SimpleSAML\XHTML\Template;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Set of tests for the controllers in the "admin" module.
+ *
+ * @package SimpleSAML\Test
+ */
+class ConfigTest extends TestCase
+{
+    /** @var \SimpleSAML\Configuration */
+    protected $config;
+
+    /** @var \SimpleSAML\Utils\Auth */
+    protected $authUtils;
+
+    /** @var \SimpleSAML\Session */
+    protected $session;
+
+
+    /**
+     * Set up for each test.
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->config = new class (
+            [
+                'module.enable' => ['admin' => true],
+                'secretsalt' => 'defaultsecretsalt',
+                'admin.checkforupdates' => true
+            ],
+            '[ARRAY]'
+        ) extends Configuration
+        {
+            public function getVersion(): string
+            {
+                return '1.14.7';
+            }
+        };
+
+        // Dirty hack, but Session relies on config being actually loaded
+        $this->config::setPreloadedConfig(
+            Configuration::loadFromArray([], '[ARRAY]', 'simplesaml'),
+            'config.php',
+            'simplesaml'
+        );
+
+        $this->authUtils = new class () extends Utils\Auth {
+            public static function requireAdmin(): void
+            {
+                // stub
+            }
+        };
+
+        $session = $this->createMock(Session::class);
+        $session->method('getData')->willReturn(['tag_name' => 'v1.18.7', 'html_url' => 'https://example.org']);
+
+        /** @var \SimpleSAML\Session $session */
+        $this->session = $session;
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testDiagnostics(): void
+    {
+        $request = Request::create(
+            '/diagnostics',
+            'GET'
+        );
+
+        $c = new Controller\Config($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->diagnostics($request);
+
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMain(): void
+    {
+        $request = Request::create(
+            '/',
+            'GET'
+        );
+
+        $c = new Controller\Config($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->main($request);
+
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testPhpinfo(): void
+    {
+        $request = Request::create(
+            '/phpinfo',
+            'GET'
+        );
+
+        $c = new Controller\Config($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->phpinfo($request);
+
+        $this->assertTrue($response->isSuccessful());
+    }
+}
diff --git a/tests/modules/admin/lib/Controller/FederationTest.php b/tests/modules/admin/lib/Controller/FederationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..efb007116b97681a1b809c1ada54e26d64703f60
--- /dev/null
+++ b/tests/modules/admin/lib/Controller/FederationTest.php
@@ -0,0 +1,402 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Test\Module\admin\Controller;
+
+use PHPUnit\Framework\TestCase;
+use SimpleSAML\Auth;
+use SimpleSAML\Configuration;
+use SimpleSAML\Metadata\MetaDataStorageHandler;
+use SimpleSAML\Module;
+use SimpleSAML\Module\admin\Controller;
+use SimpleSAML\Session;
+use SimpleSAML\Utils;
+use SimpleSAML\XHTML\Template;
+use Symfony\Component\HttpFoundation\Cookie;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * Set of tests for the controllers in the "admin" module.
+ *
+ * @package SimpleSAML\Test
+ */
+class FederationTest extends TestCase
+{
+    /** @var string */
+    private const FRAMEWORK = 'vendor/simplesamlphp/simplesamlphp-test-framework';
+
+    /** @var string */
+    public const CERT_KEY = '../' . self::FRAMEWORK . '/certificates/pem/selfsigned.example.org.key';
+
+    /** @var string */
+    public const CERT_PUBLIC = '../' . self::FRAMEWORK . '/certificates/pem/selfsigned.example.org.crt';
+
+    /** @var \SimpleSAML\Configuration */
+    protected $config;
+
+    /** @var \SimpleSAML\Utils\Auth */
+    protected $authUtils;
+
+    /** @var string */
+    private $metadata_xml = self::FRAMEWORK . '/metadata/xml/valid-metadata-selfsigned.xml';
+
+    /** @var string */
+    private $broken_metadata_xml = self::FRAMEWORK . '/metadata/xml/corrupted-metadata-selfsigned.xml';
+
+    /** @var string */
+    private $ssp_metadata = self::FRAMEWORK . '/metadata/simplesamlphp/saml20-idp-remote_cert_selfsigned.php';
+
+    /**
+     * Set up for each test.
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->config = Configuration::loadFromArray(
+            [
+                'module.enable' => ['adfs' => true, 'admin' => true],
+                'enable.saml20-idp' => true,
+                'enable.adfs-idp' => true,
+                'language.default' => 'fr',
+                'language.get_language_function' => [$this, 'getLanguage']
+            ],
+            '[ARRAY]',
+            'simplesaml'
+        );
+
+        $this->authUtils = new class () extends Utils\Auth {
+            public static function requireAdmin(): void
+            {
+                // stub
+            }
+        };
+
+        Configuration::setPreLoadedConfig(
+            Configuration::loadFromArray(
+                [
+                    'admin' => ['core:AdminPassword'],
+                ],
+                '[ARRAY]',
+                'simplesaml'
+            ),
+            'authsources.php',
+            'simplesaml'
+        );
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMain(): void
+    {
+        $request = Request::create(
+            '/federation',
+            'GET'
+        );
+
+        $mdh = new class () extends MetaDataStorageHandler {
+            public function __construct()
+            {
+            }
+
+            public function getList(string $set = 'saml20-idp-remote', bool $showExpired = false): array
+            {
+                if ($set === 'saml20-idp-hosted') {
+                    return [
+                        0 => [
+                            'name' => 'SimpleSAMLphp Hosted IDP',
+                            'descr' => 'The local IDP',
+                            'OrganizationDisplayName' => ['en' => 'My IDP', 'nl' => 'Mijn IDP']
+                        ]
+                    ];
+                } elseif ($set === 'saml20-sp-remote') {
+                    return [
+                        0 => [
+                            'name' => ['nl' => 'SimpleSAMLphp Remote SP'],
+                            'descr' => 'The remote SP',
+                            'OrganizationDisplayName' => ['en' => 'His SP', 'nl' => 'Zijn SP', 'fr' => 'Son SP']
+                        ],
+                        1 => [
+                            'name' => ['fr' => 'SimpleSAMLphp Remote SP'],
+                            'descr' => 'The remote SP',
+                            'OrganizationDisplayName' => ['en' => 'Her SP']
+                        ]
+                    ];
+                }
+                return [];
+            }
+        };
+
+        $authSource = new class () extends Auth\Source {
+            public function __construct()
+            {
+                // stub
+            }
+
+            public function authenticate(array &$state): void
+            {
+                // stub
+            }
+
+            public static function getSourcesOfType(string $type): array
+            {
+                return [
+                    new \SimpleSAML\Module\saml\Auth\Source\SP(
+                        ['AuthId' => 'AuthId'],
+                        [
+                            'saml:SP',
+
+                            'name' => [
+                                'en' => 'A service',
+                            ],
+                            'entityID' => null,
+                            'privatekey' => FederationTest::CERT_KEY,
+                            'certificate' => FederationTest::CERT_PUBLIC,
+                            'attributes' => ['uid', 'mail']
+                        ]
+                    )
+                ];
+            }
+        };
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $c->setAuthSource($authSource);
+        $c->setMetadataStorageHandler($mdh);
+        $response = $c->main($request);
+
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMetadataConverterFileUpload(): void
+    {
+        $request = Request::create(
+            '/federation/metadata-converter',
+            'POST'
+        );
+        $request->files->add(
+            [
+                'xmlfile' => new UploadedFile(
+                    $this->metadata_xml,
+                    'valid-metadata-selfsigned.xml',
+                    'application/xml',
+                    null,
+                    true
+                )
+            ]
+        );
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->metadataConverter($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertNull($response->data['error']);
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMetadataConverterData(): void
+    {
+        $request = Request::create(
+            '/federation/metadata-converter',
+            'POST',
+            ['xmldata' => file_get_contents($this->metadata_xml)]
+        );
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->metadataConverter($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertNull($response->data['error']);
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMetadataConverterInvalidMetadataShowsError(): void
+    {
+        $request = Request::create(
+            '/federation/metadata-converter',
+            'POST',
+            ['xmldata' => file_get_contents($this->broken_metadata_xml)]
+        );
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+
+        $response = $c->metadataConverter($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertNotNull($response->data['error']);
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMetadataConverterEmptyInput(): void
+    {
+        $request = Request::create(
+            '/federation/metadata-converter',
+            'POST',
+            ['xmldata' => '']
+        );
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+
+        $response = $c->metadataConverter($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertEquals([], $response->data['output']);
+        $this->assertEquals('', $response->data['xmldata']);
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testDownloadCertSP(): void
+    {
+        $request = Request::create(
+            '/federation/cert',
+            'GET',
+            [
+                'set' => 'saml20-sp-hosted',
+                'source' => 'default-sp'
+            ]
+        );
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $authSource = new class () extends Auth\Source {
+            public function __construct()
+            {
+                // stub
+            }
+
+            public function authenticate(array &$state): void
+            {
+                // stub
+            }
+
+            public function getMetadata(): Configuration
+            {
+                return Configuration::loadFromArray(
+                    ['certData' => 'abc123'],
+                    '[ARRAY]',
+                    'simplesaml'
+                );
+            }
+
+            public static function getById(string $authId, ?string $type = null): ?Auth\Source
+            {
+                return new static();
+            }
+        };
+
+        $c->setAuthSource($authSource);
+
+        $response = $c->downloadCert($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertNotNull($response->headers->get('Content-Disposition'));
+        $this->assertEquals('application/x-pem-file', $response->headers->get('Content-Type'));
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testDownloadCertFile(): void
+    {
+        $request = Request::create(
+            '/federation/cert',
+            'GET',
+            [
+                'set' => 'saml20-idp-hosted',
+                'entity' => 'some entity'
+            ]
+        );
+
+        $mdh = new class () extends MetaDataStorageHandler {
+            public function __construct()
+            {
+            }
+
+            public function getMetaDataConfig(string $entityId, string $set): Configuration
+            {
+                return Configuration::loadFromArray([
+                    'AssertionConsumerService' => 'https://example.org/acs/or/something',
+                    'certData' => 'some cert',
+                ]);
+            }
+        };
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $c->setMetaDataStorageHandler($mdh);
+
+        $response = $c->downloadCert($request);
+
+        $this->assertTrue($response->isSuccessful());
+        $this->assertNotNull($response->headers->get('Content-Disposition'));
+        $this->assertEquals('application/x-pem-file', $response->headers->get('Content-Type'));
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testShowRemoteEntity(): void
+    {
+        $request = Request::create(
+            '/federation/show',
+            'GET',
+            ['set' => 'saml20-sp-hosted', 'entityid' => 'some entity']
+        );
+
+        $mdh = new class () extends MetaDataStorageHandler {
+            public function __construct()
+            {
+            }
+
+            public function getMetaData(?string $entityId, string $set): array
+            {
+                return [];
+            }
+        };
+
+        $c = new Controller\Federation($this->config);
+        $c->setAuthUtils($this->authUtils);
+        $c->setMetaDataStorageHandler($mdh);
+        $response = $c->showRemoteEntity($request);
+
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * Helper method for the main-controller
+     * @return string
+     */
+    public function getLanguage(): string
+    {
+        return 'nl';
+    }
+}
diff --git a/tests/modules/admin/lib/Controller/TestTest.php b/tests/modules/admin/lib/Controller/TestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a74fa83199efec54bfd837a01e502fe7b405ca21
--- /dev/null
+++ b/tests/modules/admin/lib/Controller/TestTest.php
@@ -0,0 +1,260 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Test\Module\admin\Controller;
+
+use PHPUnit\Framework\TestCase;
+use SAML2\XML\saml\NameID;
+use SimpleSAML\Auth;
+use SimpleSAML\Configuration;
+use SimpleSAML\Error;
+use SimpleSAML\HTTP\RunnableResponse;
+use SimpleSAML\Module\admin\Controller;
+use SimpleSAML\Session;
+use SimpleSAML\Utils;
+use SimpleSAML\XHTML\Template;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Set of tests for the controllers in the "admin" module.
+ *
+ * @package SimpleSAML\Test
+ */
+class TestTest extends TestCase
+{
+    /** @var \SimpleSAML\Configuration */
+    protected $config;
+
+    /** @var \SimpleSAML\Utils\Auth */
+    protected $authUtils;
+
+    /** @var \SimpleSAML\Session */
+    protected $session;
+
+
+    /**
+     * Set up for each test.
+     * @return void
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->config = Configuration::loadFromArray(
+            [
+                'module.enable' => ['admin' => true],
+            ],
+            '[ARRAY]',
+            'simplesaml'
+        );
+
+        $this->authUtils = new class () extends Utils\Auth {
+            public static function requireAdmin(): void
+            {
+                // stub
+            }
+        };
+
+        $this->session = Session::getSessionFromRequest();
+
+        Configuration::setPreLoadedConfig(
+            Configuration::loadFromArray(
+                [
+                    'admin' => ['core:AdminPassword'],
+                ],
+                '[ARRAY]',
+                'simplesaml'
+            ),
+            'authsources.php',
+            'simplesaml'
+        );
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMainWithoutAuthSource(): void
+    {
+        $request = Request::create(
+            '/test',
+            'GET'
+        );
+
+        $c = new Controller\Test($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $response = $c->main($request);
+
+        $this->assertInstanceOf(Template::class, $response);
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMainWithAuthSourceAndLogout(): void
+    {
+        $request = Request::create(
+            '/test',
+            'GET',
+            ['logout' => 'notnull']
+        );
+
+        $c = new Controller\Test($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $c->setAuthSimple(new class ('admin') extends Auth\Simple {
+            public function logout($params = null): void
+            {
+                // stub
+            }
+        });
+
+        $response = $c->main($request, 'admin');
+
+        $this->assertInstanceOf(RunnableResponse::class, $response);
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMainWithAuthSourceAndException(): void
+    {
+        $request = Request::create(
+            '/test',
+            'GET',
+            [Auth\State::EXCEPTION_PARAM => 'someException']
+        );
+
+        $c = new Controller\Test($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $c->setAuthState(new class () extends Auth\State {
+            public static function loadExceptionState(?string $id = null): ?array
+            {
+                return [Auth\State::EXCEPTION_DATA => new Error\NoState()];
+            }
+        });
+
+        $this->expectException(Error\NoState::class);
+        $this->expectExceptionMessage('NOSTATE');
+        $c->main($request, 'admin');
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMainWithAuthSourceNotAuthenticated(): void
+    {
+        $request = Request::create(
+            '/test',
+            'GET',
+            ['as' => 'admin']
+        );
+
+        $c = new Controller\Test($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $c->setAuthSimple(new class ('admin') extends Auth\Simple {
+            public function isAuthenticated(): bool
+            {
+                return false;
+            }
+
+            public function login(array $params = []): void
+            {
+                // stub
+            }
+        });
+
+        $response = $c->main($request, 'admin');
+
+        $this->assertInstanceOf(RunnableResponse::class, $response);
+        $this->assertTrue($response->isSuccessful());
+    }
+
+
+    /**
+     * @return void
+     */
+    public function testMainWithAuthSourceAuthenticated(): void
+    {
+        $request = Request::create(
+            '/test',
+            'GET'
+        );
+
+        $c = new Controller\Test($this->config, $this->session);
+        $c->setAuthUtils($this->authUtils);
+        $c->setAuthSimple(new class ('admin') extends Auth\Simple {
+            public function isAuthenticated(): bool
+            {
+                return true;
+            }
+
+            public function getAttributes(): array
+            {
+                $nameId = new NameID();
+                $nameId->setValue('_b806c4f98188b42e48d3eb5444db613dbde463e2e8');
+                $nameId->setSPProvidedID('some:entity');
+                $nameId->setNameQualifier('some name qualifier');
+                $nameId->setSPNameQualifier('some SP name qualifier');
+                $nameId->setFormat('urn:oasis:names:tc:SAML:2.0:nameid-format:transient');
+
+                /** @psalm-suppress PossiblyNullPropertyFetch */
+                return [
+                    'urn:mace:dir:attribute-def:cn' => [
+                        'Tim van Dijen'
+                    ],
+                    'urn:mace:dir:attribute-def:givenName' => [
+                        'Tim'
+                    ],
+                    'urn:mace:dir:attribute-def:sn' => [
+                        'van Dijen'
+                    ],
+                    'urn:mace:dir:attribute-def:displayName' => [
+                        'Mr. T. van Dijen BSc'
+                    ],
+                    'urn:mace:dir:attribute-def:mail' => [
+                        'tvdijen@hotmail.com',
+                        'tvdijen@gmail.com'
+                    ],
+                    'urn:mace:dir:attribute-def:eduPersonTargetedID' => [
+                        $nameId->toXML()->ownerDocument->childNodes
+                    ],
+                    'jpegPhoto' => [
+                        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
+                    ],
+                    'nameId' => [
+                        $nameId
+                    ]
+                ];
+            }
+
+            public function getAuthDataArray(): ?array
+            {
+                return [];
+            }
+
+            public function getAuthData(string $name)
+            {
+                $nameId = new NameID();
+                $nameId->setValue('_b806c4f98188b42e48d3eb5444db613dbde463e2e8');
+                $nameId->setSPProvidedID('some:entity');
+                $nameId->setNameQualifier('some name qualifier');
+                $nameId->setSPNameQualifier('some SP name qualifier');
+                $nameId->setFormat('urn:oasis:names:tc:SAML:2.0:nameid-format:transient');
+
+                return $nameId;
+            }
+        });
+
+        $response = $c->main($request, 'admin');
+
+        $this->assertInstanceOf(Template::class, $response);
+        $this->assertTrue($response->isSuccessful());
+    }
+}