Index: trunk/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java	(revision 18650)
+++ trunk/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java	(revision 18650)
@@ -0,0 +1,210 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.common.FileSource;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.extension.Parameters;
+import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
+import com.github.tomakehurst.wiremock.http.HttpHeader;
+import com.github.tomakehurst.wiremock.http.HttpHeaders;
+import com.github.tomakehurst.wiremock.http.QueryParameter;
+import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.http.Response;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.matching.AnythingPattern;
+import com.github.tomakehurst.wiremock.matching.EqualToPattern;
+import com.github.tomakehurst.wiremock.matching.StringValuePattern;
+import mockit.Mock;
+import mockit.MockUp;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
+import org.openstreetmap.josm.data.preferences.JosmUrls;
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
+import org.openstreetmap.josm.testutils.annotations.HTTP;
+import org.openstreetmap.josm.testutils.mockers.OpenBrowserMocker;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.Logging;
+
+@BasicPreferences
+@HTTP
+class OAuth20AuthorizationTest {
+    private static final String RESPONSE_TYPE = "response_type";
+    private static final String RESPONSE_TYPE_VALUE = "code";
+    private static final String CLIENT_ID = "client_id";
+    private static final String CLIENT_ID_VALUE = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
+    private static final String REDIRECT_URI = "redirect_uri";
+    private static final String REDIRECT_URI_VALUE = "http://127.0.0.1:8111/oauth_authorization";
+    private static final String SCOPE = "scope";
+    private static final String STATE = "state";
+    private static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
+    private static final String CODE_CHALLENGE_METHOD_VALUE = "S256";
+    private static final String CODE_CHALLENGE = "code_challenge";
+
+    private static class OAuthServerWireMock extends ResponseTransformer {
+        String stateToReturn;
+        @Override
+        public Response transform(Request request, Response response, FileSource files, Parameters parameters) {
+            try {
+                if (request.getUrl().startsWith("/oauth2/authorize")) {
+                    return authorizationRequest(request, response);
+                } else if (request.getUrl().startsWith("/oauth2/token")) {
+                    return tokenRequest(request, response);
+                }
+                return response;
+            } catch (Exception e) {
+                // Make certain we actually see the exception in logs -- WireMock returns the error, but then our code needs to print it
+                Logging.error(e);
+                throw e;
+            }
+        }
+
+        private Response tokenRequest(Request request, Response response) {
+            Map<String, String> queryParameters = Stream.of(request.getBodyAsString().split("&", -1))
+                    .map(string -> string.split("=", -1))
+                    .collect(Collectors.toMap(strings -> strings[0], strings -> strings[1]));
+            if (!queryParameters.containsKey("grant_type")
+                    || !queryParameters.containsKey(REDIRECT_URI) || !queryParameters.containsKey(CLIENT_ID)
+                    || !queryParameters.containsKey("code") || !queryParameters.containsKey("code_verifier")) {
+                return Response.Builder.like(response).but().status(500).build();
+            }
+            return Response.Builder.like(response).but().body("{\"token_type\": \"bearer\", \"access_token\": \"test_access_token\"}").build();
+        }
+
+        private Response authorizationRequest(Request request, Response response) {
+            final QueryParameter state = request.queryParameter(STATE);
+            final QueryParameter codeChallenge = request.queryParameter(CODE_CHALLENGE);
+            final QueryParameter redirectUri = request.queryParameter(REDIRECT_URI);
+            final QueryParameter responseType = request.queryParameter(RESPONSE_TYPE);
+            final QueryParameter scope = request.queryParameter(SCOPE);
+            final QueryParameter clientId = request.queryParameter(CLIENT_ID);
+            final QueryParameter codeChallengeMethod = request.queryParameter(CODE_CHALLENGE_METHOD);
+            final boolean badRequest = !(state.isPresent() && state.isSingleValued());
+            if (badRequest || checkQueryParameter(redirectUri, REDIRECT_URI_VALUE) || checkQueryParameter(responseType, RESPONSE_TYPE_VALUE)
+                    || checkQueryParameter(clientId, CLIENT_ID_VALUE) || checkQueryParameter(codeChallengeMethod, CODE_CHALLENGE_METHOD_VALUE)
+                    || checkQueryParameter(scope, "read_gpx")
+                    || !codeChallenge.isPresent()) {
+                return Response.Builder.like(response).but().status(500).build();
+            }
+            return Response.Builder.like(response).but().status(307)
+                    .headers(new HttpHeaders(new HttpHeader("Location",
+                            redirectUri.values().get(0)
+                                    + "?state=" + (this.stateToReturn != null ? stateToReturn : state.firstValue())
+                                    + "&code=test_code"))).build();
+        }
+
+        private static boolean checkQueryParameter(QueryParameter parameter, String expected) {
+            return !parameter.isPresent() || !parameter.isSingleValued() || !parameter.containsValue(expected);
+        }
+
+        @Override
+        public String getName() {
+            return "OAuthServerWireMock";
+        }
+    }
+
+    private static final OAuthServerWireMock oauthServer = new OAuthServerWireMock();
+    @RegisterExtension
+    static WireMockExtension wml = WireMockExtension.newInstance()
+            .options(WireMockConfiguration.wireMockConfig().dynamicPort().dynamicHttpsPort().extensions(oauthServer))
+            .build();
+    @BeforeEach
+    @AfterEach
+    void setup() {
+        // Reset the mocker
+        OpenBrowserMocker.getCalledURIs().clear();
+        RemoteControl.stop(); // Ensure remote control is stopped
+        oauthServer.stateToReturn = null;
+    }
+
+    /**
+     * Set up the default wiremock information
+     * @param wireMockRuntimeInfo The info to set up
+     */
+    @BeforeEach
+    void setupWireMock(WireMockRuntimeInfo wireMockRuntimeInfo) {
+        Config.getPref().put("osm-server.url", wireMockRuntimeInfo.getHttpBaseUrl() + "/api/");
+        new MockUp<JosmUrls>() {
+            @Mock
+            public String getDefaultOsmApiUrl() {
+                return wireMockRuntimeInfo.getHttpBaseUrl() + "/api/";
+            }
+        };
+        new OpenBrowserMocker();
+        final Map<String, StringValuePattern> queryParams = new HashMap<>();
+        queryParams.put(RESPONSE_TYPE, new EqualToPattern(RESPONSE_TYPE_VALUE));
+        queryParams.put(CLIENT_ID, new EqualToPattern(CLIENT_ID_VALUE));
+        queryParams.put(REDIRECT_URI, new EqualToPattern(REDIRECT_URI_VALUE));
+        queryParams.put(SCOPE, new EqualToPattern("read_gpx"));
+        queryParams.put(STATE, new AnythingPattern()); // This is generated via a random UUID, and we have to return this in the redirect
+        queryParams.put(CODE_CHALLENGE_METHOD, new EqualToPattern(CODE_CHALLENGE_METHOD_VALUE));
+        queryParams.put(CODE_CHALLENGE, new AnythingPattern()); // This is generated via a random UUID
+        wireMockRuntimeInfo.getWireMock().register(WireMock.get(WireMock.urlPathEqualTo("/oauth2/authorize")).withQueryParams(queryParams));
+        wireMockRuntimeInfo.getWireMock().register(WireMock.post(WireMock.urlPathEqualTo("/oauth2/token")));
+    }
+
+    @Test
+    void testAuthorize(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
+        final OAuth20Authorization authorization = new OAuth20Authorization();
+        final AtomicReference<IOAuthToken> consumer = new AtomicReference<>();
+        OAuth20Parameters parameters = (OAuth20Parameters) OAuthParameters.createDefault(OsmApi.getOsmApi().getBaseUrl(), OAuthVersion.OAuth20);
+        RemoteControl.start();
+        authorization.authorize(new OAuth20Parameters(parameters.getClientId(), parameters.getClientSecret(),
+                wireMockRuntimeInfo.getHttpBaseUrl() + "/oauth2", wireMockRuntimeInfo.getHttpBaseUrl() + "/api",
+                parameters.getRedirectUri()), consumer::set, OsmScopes.read_gpx);
+        assertEquals(1, OpenBrowserMocker.getCalledURIs().size());
+        HttpClient client = HttpClient.create(OpenBrowserMocker.getCalledURIs().get(0).toURL());
+        try {
+            HttpClient.Response response = client.connect();
+            assertEquals(200, response.getResponseCode());
+        } finally {
+            client.disconnect();
+        }
+        assertNotNull(consumer.get());
+        assertEquals(OAuthVersion.OAuth20, consumer.get().getOAuthType());
+        OAuth20Token token = (OAuth20Token) consumer.get();
+        assertEquals("test_access_token", token.getBearerToken());
+    }
+
+    @Test
+    void testAuthorizeBadState(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
+        oauthServer.stateToReturn = "Bad_State";
+        final OAuth20Authorization authorization = new OAuth20Authorization();
+        final AtomicReference<IOAuthToken> consumer = new AtomicReference<>();
+        OAuth20Parameters parameters = (OAuth20Parameters) OAuthParameters.createDefault(OsmApi.getOsmApi().getBaseUrl(), OAuthVersion.OAuth20);
+        RemoteControl.start();
+        authorization.authorize(new OAuth20Parameters(parameters.getClientId(), parameters.getClientSecret(),
+                wireMockRuntimeInfo.getHttpBaseUrl() + "/oauth2", wireMockRuntimeInfo.getHttpBaseUrl() + "/api",
+                parameters.getRedirectUri()), consumer::set, OsmScopes.read_gpx);
+        assertEquals(1, OpenBrowserMocker.getCalledURIs().size());
+        HttpClient client = HttpClient.create(OpenBrowserMocker.getCalledURIs().get(0).toURL());
+        try {
+            HttpClient.Response response = client.connect();
+            assertEquals(400, response.getResponseCode());
+            String content = response.fetchContent();
+            assertTrue(content.contains("Unknown state for authorization"));
+        } finally {
+            client.disconnect();
+        }
+        assertNull(consumer.get());
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/gui/io/DownloadOpenChangesetsTaskTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/io/DownloadOpenChangesetsTaskTest.java	(revision 18649)
+++ trunk/test/unit/org/openstreetmap/josm/gui/io/DownloadOpenChangesetsTaskTest.java	(revision 18650)
@@ -14,4 +14,5 @@
 import javax.swing.JPanel;
 
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.Test;
@@ -19,4 +20,5 @@
 import org.openstreetmap.josm.data.UserIdentityManager;
 import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
+import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
@@ -66,4 +68,12 @@
             invocation.proceed(serverUrl);
         }
+    }
+
+    /**
+     * These tests were written with {@link org.openstreetmap.josm.data.oauth.OAuthVersion#OAuth10a} as the default auth method.
+     */
+    @BeforeEach
+    void setup() {
+        Config.getPref().put("osm-server.auth-method", "oauth");
     }
 
Index: trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java	(revision 18650)
+++ trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java	(revision 18650)
@@ -0,0 +1,104 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.auth;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openstreetmap.josm.data.oauth.OAuth20Exception;
+import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
+import org.openstreetmap.josm.data.oauth.OAuth20Token;
+import org.openstreetmap.josm.data.oauth.OAuthToken;
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
+
+/**
+ * Test interface for {@link CredentialsAgent} implementations.
+ */
+@BasicPreferences
+public interface CredentialsAgentTest<T extends CredentialsAgent> {
+    /**
+     * Create the agent to test
+     * @return The agent to test
+     */
+    T createAgent();
+
+    static List<String> getHosts() {
+        return Arrays.asList("https://somewhere.random", OsmApi.getOsmApi().getHost());
+    }
+
+    @ParameterizedTest
+    @MethodSource("getHosts")
+    @BasicPreferences // We need to reset preferences between runs
+    default void testLookUpAndStorePasswordAuthentication(final String host) throws CredentialsAgentException {
+        final T agent = createAgent();
+        for (Authenticator.RequestorType type : Authenticator.RequestorType.values()) {
+            PasswordAuthentication passwordAuthentication = agent.lookup(type, host);
+            assertNull(passwordAuthentication, "Password authentication should not be set up yet");
+            PasswordAuthentication toStore = new PasswordAuthentication("hunter", "password".toCharArray());
+            agent.store(type, host, toStore);
+            passwordAuthentication = agent.lookup(type, host);
+            assertNotNull(passwordAuthentication);
+            // We can't just use equals, since PasswordAuthentication does not override the default equals method
+            assertEquals(toStore.getUserName(), passwordAuthentication.getUserName());
+            assertArrayEquals(toStore.getPassword(), passwordAuthentication.getPassword());
+            // This is what sets the Config values. Note that PasswordAuthentication cannot take a null password.
+            agent.store(type, host, new PasswordAuthentication("hunter", new char[0]));
+            // Now we need to purge the cache
+            agent.purgeCredentialsCache(type);
+            passwordAuthentication = agent.lookup(type, host);
+            assertEquals(toStore.getUserName(), passwordAuthentication.getUserName());
+            assertArrayEquals(new char[0], passwordAuthentication.getPassword());
+            // We don't currently have a way to fully remove credentials, but that ought to be tested here.
+        }
+    }
+
+    @Test
+    default void testLookUpAndStorePasswordAuthenticationNull() throws CredentialsAgentException {
+        final T agent = createAgent();
+        assertDoesNotThrow(() -> agent.store(null, "https://somewhere.random", new PasswordAuthentication("random", new char[0])));
+        assertNull(agent.lookup(null, "https://somewhere.random"));
+        assertDoesNotThrow(() -> agent.store(Authenticator.RequestorType.SERVER, null, new PasswordAuthentication("random", new char[0])));
+        for (Authenticator.RequestorType type : Authenticator.RequestorType.values()) {
+            assertNull(agent.lookup(type, null));
+        }
+        assertNull(agent.lookup(null, null));
+    }
+
+    @Test
+    default void testLookUpAndStoreOAuth10() throws CredentialsAgentException {
+        final T agent = createAgent();
+        assertNull(agent.lookupOAuthAccessToken());
+        final OAuthToken token = new OAuthToken("foo", "bar");
+        agent.storeOAuthAccessToken(token);
+        final OAuthToken actual = agent.lookupOAuthAccessToken();
+        assertEquals(token, actual);
+        agent.storeOAuthAccessToken(null);
+        assertNull(agent.lookupOAuthAccessToken());
+    }
+
+    @ParameterizedTest
+    @MethodSource("getHosts")
+    default void testLookupAndStoreOAuthTokens(final String host) throws CredentialsAgentException, OAuth20Exception {
+        final T agent = createAgent();
+        assertNull(agent.lookupOAuthAccessToken(host));
+        agent.storeOAuthAccessToken(host, new OAuth20Token(new OAuth20Parameters("clientId", "clientSecret",
+                "tokenUrl", "authorizeUrl", "apiUrl", "redirectUrl"),
+                "{\"access_token\": \"test_token\", \"token_type\": \"bearer\"}"));
+        OAuth20Token token = (OAuth20Token) agent.lookupOAuthAccessToken(host);
+        assertNotNull(token);
+        assertEquals("test_token", token.getBearerToken());
+        agent.storeOAuthAccessToken(host, null);
+        assertNull(agent.lookupOAuthAccessToken(host));
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java	(revision 18650)
+++ trunk/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java	(revision 18650)
@@ -0,0 +1,15 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.auth;
+
+import org.openstreetmap.josm.testutils.annotations.HTTP;
+
+/**
+ * Test class for {@link CredentialsManager}
+ */
+@HTTP
+class CredentialsManagerTest implements CredentialsAgentTest<CredentialsManager> {
+    @Override
+    public CredentialsManager createAgent() {
+        return new CredentialsManager(new JosmPreferencesCredentialAgent());
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java	(revision 18650)
+++ trunk/test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java	(revision 18650)
@@ -0,0 +1,13 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.auth;
+
+/**
+ * Test {@link JosmPreferencesCredentialAgent}
+ */
+class JosmPreferencesCredentialAgentTest implements CredentialsAgentTest<JosmPreferencesCredentialAgent> {
+
+    @Override
+    public JosmPreferencesCredentialAgent createAgent() {
+        return new JosmPreferencesCredentialAgent();
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java	(revision 18650)
+++ trunk/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java	(revision 18650)
@@ -0,0 +1,93 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.remotecontrol.handler;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+
+/**
+ * Test class for {@link AuthorizationHandler}
+ */
+class AuthorizationHandlerTest {
+    private static class TestAuthorizationConsumer implements AuthorizationHandler.AuthorizationConsumer {
+        boolean validated;
+        boolean handled;
+        @Override
+        public void validateRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandler.RequestHandlerBadRequestException {
+            this.validated = true;
+        }
+
+        @Override
+        public AuthorizationHandler.ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandler.RequestHandlerErrorException, RequestHandler.RequestHandlerBadRequestException {
+            this.handled = true;
+            return null;
+        }
+    }
+
+    @Test
+    void testValidateAndHandleRequest() {
+        final AuthorizationHandler handler = new AuthorizationHandler();
+        TestAuthorizationConsumer consumer = new TestAuthorizationConsumer();
+        AuthorizationHandler.addAuthorizationConsumer("test_state", consumer);
+        assertDoesNotThrow(() -> handler.setUrl("http://localhost:8111/oauth_authorization?code=code&state=test_state"));
+        assertAll(() -> assertDoesNotThrow(handler::validateRequest),
+                () -> assertDoesNotThrow(handler::handleRequest),
+                () -> assertTrue(consumer.validated),
+                () -> assertTrue(consumer.handled));
+        // The consumer should only ever be called once
+        consumer.validated = false;
+        consumer.handled = false;
+        assertAll(() -> assertThrows(RequestHandler.RequestHandlerBadRequestException.class, handler::validateRequest),
+                () -> assertThrows(NullPointerException.class, handler::handleRequest),
+                () -> assertFalse(consumer.validated),
+                () -> assertFalse(consumer.handled));
+        // Check to make certain that a bad state doesn't work
+        AuthorizationHandler.addAuthorizationConsumer("testState", consumer);
+        AuthorizationHandler.addAuthorizationConsumer("test_state", consumer);
+        assertThrows(IllegalArgumentException.class, () -> AuthorizationHandler.addAuthorizationConsumer("test_state", consumer));
+        assertDoesNotThrow(() -> handler.setUrl("http://localhost:8111/oauth_authorization?code=code&testState=test_state"));
+        assertAll(() -> assertThrows(RequestHandler.RequestHandlerBadRequestException.class, handler::validateRequest),
+                () -> assertThrows(NullPointerException.class, handler::handleRequest),
+                () -> assertFalse(consumer.validated),
+                () -> assertFalse(consumer.handled));
+        assertDoesNotThrow(() -> handler.setUrl("http://localhost:8111/oauth_authorization?code=code&state=no_state_handler"));
+        assertAll(() -> assertThrows(RequestHandler.RequestHandlerBadRequestException.class, handler::validateRequest),
+                () -> assertThrows(NullPointerException.class, handler::handleRequest),
+                () -> assertFalse(consumer.validated),
+                () -> assertFalse(consumer.handled));
+    }
+
+    @Test
+    void testGetPermissionMessage() {
+        assertEquals("Allow OAuth remote control to set credentials", new AuthorizationHandler().getPermissionMessage());
+    }
+
+    @Test
+    void testGetPermissionPref() {
+        assertNull(new AuthorizationHandler().getPermissionPref());
+    }
+
+    @Test
+    void testGetPermissionPreference() {
+        final BooleanProperty property = new AuthorizationHandler().getPermissionPreference();
+        assertEquals("remotecontrol.permission.authorization", property.getKey());
+        assertFalse(property.getDefaultValue());
+    }
+
+    @Test
+    void testGetMandatoryParams() {
+        assertArrayEquals(new String[] {"code", "state"}, new AuthorizationHandler().getMandatoryParams());
+    }
+}
