Repository: Wikidata/Wikidata-Toolkit
Pull request #707 (2022-07-06 12:16:49)

Add clientLogin method to BasicApiConnection

Lhaaits: By the way, do you know where all possible login error values can be found? I wasn't able to reproduce all possible values to complete the enum. For example LOGIN_THROTTLED or LOGIN_BLOCKED. I tried looking for a repo for the api.php, but no luck yet. And the docs are also not that extensive.
Lhaaits: @guenthermi I see you added the login error values originally. Do you by any change remember what you based those values on?

Patch:
diff --git a/wdtk-distribution/pom.xml b/wdtk-distribution/pom.xml
index a47694ce..1d39ac33 100644
--- a/wdtk-distribution/pom.xml
+++ b/wdtk-distribution/pom.xml
@@ -134,7 +134,7 @@
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-assembly-plugin</artifactId>
-						<version>3.4.0</version>
+						<version>3.4.1</version>
 						<executions>
 							<execution>
 								<id>create-source-jar</id>
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnection.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnection.java
index 44539507..c455b399 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnection.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnection.java
@@ -34,6 +34,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.JsonNode;
 
+import static org.wikidata.wdtk.wikibaseapi.LoginValue.*;
+
 /**
  * A connection to the MediaWiki API established via
  * standard login with username and password.
@@ -42,81 +44,6 @@ import com.fasterxml.jackson.databind.JsonNode;
  *
  */
 public class BasicApiConnection extends ApiConnection {
-
-	/**
-	 * Name of the HTTP parameter to submit a password to the API.
-	 */
-	protected final static String PARAM_LOGIN_USERNAME = "lgname";
-	/**
-	 * Name of the HTTP parameter to submit a password to the API.
-	 */
-	protected final static String PARAM_LOGIN_PASSWORD = "lgpassword";
-	/**
-	 * Name of the HTTP parameter to submit a login token to the API.
-	 */
-	protected final static String PARAM_LOGIN_TOKEN = "lgtoken";
-
-	/**
-	 * String value in the result field of the JSON response if the login was
-	 * successful.
-	 */
-	protected final static String LOGIN_RESULT_SUCCESS = "Success";
-	/**
-	 * String value in the result field of the JSON response if the password was
-	 * wrong.
-	 */
-	protected final static String LOGIN_WRONG_PASS = "WrongPass";
-	/**
-	 * String value in the result field of the JSON response if the password was
-	 * rejected by an authentication plugin.
-	 */
-	protected final static String LOGIN_WRONG_PLUGIN_PASS = "WrongPluginPass";
-	/**
-	 * String value in the result field of the JSON response if no username was
-	 * given.
-	 */
-	protected final static String LOGIN_NO_NAME = "NoName";
-	/**
-	 * String value in the result field of the JSON response if given username
-	 * does not exist.
-	 */
-	protected final static String LOGIN_NOT_EXISTS = "NotExists";
-	/**
-	 * String value in the result field of the JSON response if the user name is
-	 * illegal.
-	 */
-	protected final static String LOGIN_ILLEGAL = "Illegal";
-	/**
-	 * String value in the result field of the JSON response if there were too
-	 * many logins in a short time.
-	 */
-	protected final static String LOGIN_THROTTLED = "Throttled";
-	/**
-	 * String value in the result field of the JSON response if password is
-	 * empty.
-	 */
-	protected final static String LOGIN_EMPTY_PASS = "EmptyPass";
-	/**
-	 * String value in the result field of the JSON response if the wiki tried
-	 * to automatically create a new account for you, but your IP address has
-	 * been blocked from account creation.
-	 */
-	protected final static String LOGIN_CREATE_BLOCKED = "CreateBlocked";
-	/**
-	 * String value in the result field of the JSON response if the user is
-	 * blocked.
-	 */
-	protected final static String LOGIN_BLOCKED = "Blocked";
-	/**
-	 * String value in the result field of the JSON response if token or session
-	 * ID is missing.
-	 */
-	protected final static String LOGIN_NEEDTOKEN = "NeedToken";
-	/**
-	 * String value in the result field of the JSON response if token is wrong.
-	 */
-	protected final static String LOGIN_WRONG_TOKEN = "WrongToken";
-
 	/**
 	 * Password used to log in.
 	 */
@@ -224,14 +151,40 @@ public class BasicApiConnection extends ApiConnection {
 	 */
 	public void login(String username, String password)
 			throws LoginFailedException {
+		login(username, password, this::confirmLogin);
+	}
+	
+	/**
+	 * Logs in using the main user credentials. After successful login, the
+	 * API connection remains in a logged in state, and future actions will be
+	 * run as a logged in user.
+	 *
+	 * @param username the name of the main user to log in
+	 * @param password the password of the main user
+	 * @throws LoginFailedException if the login failed for some reason
+	 */
+	public void clientLogin(String username, String password)
+			throws LoginFailedException {
+		login(username, password, this::confirmClientLogin);
+	}
+
+	/***
+	 * Login function that contains token logic and a function as parameter
+	 * 
+	 * @param username the name of the user to log in
+	 * @param password the password of the user
+	 * @param loginFunction the functional interface to log in with
+	 * @throws LoginFailedException if the login failed for some reason
+	 */
+	protected void login(String username, String password, ILogin loginFunction) throws LoginFailedException {
 		try {
 			String token = getOrFetchToken("login");
 			try {
-				this.confirmLogin(token, username, password);
+				loginFunction.login(token, username, password);
 			} catch (NeedLoginTokenException e) { // try once more
 				clearToken("login");
 				token = getOrFetchToken("login");
-				this.confirmLogin(token, username, password);
+				loginFunction.login(token, username, password);
 			}
 		} catch (IOException | MediaWikiApiErrorException e1) {
 			throw new LoginFailedException(e1.getMessage(), e1);
@@ -256,21 +209,68 @@ public class BasicApiConnection extends ApiConnection {
 			throws IOException, LoginFailedException, MediaWikiApiErrorException {
 		Map<String, String> params = new HashMap<>();
 		params.put(PARAM_ACTION, "login");
-		params.put(PARAM_LOGIN_USERNAME, username);
-		params.put(PARAM_LOGIN_PASSWORD, password);
-		params.put(PARAM_LOGIN_TOKEN, token);
+		params.put(PARAM_LOGIN_USERNAME.getLoginText(), username);
+		params.put(PARAM_LOGIN_PASSWORD.getLoginText(), password);
+		params.put(PARAM_LOGIN_TOKEN.getLoginText(), token);
 
 		JsonNode root = sendJsonRequest("POST", params);
 
 		String result = root.path("login").path("result").textValue();
-		if (LOGIN_RESULT_SUCCESS.equals(result)) {
+		if (LOGIN_RESULT_SUCCESS.getLoginText().equals(result)) {
+			this.loggedIn = true;
+			this.username = username;
+			this.password = password;
+		} else {
+			String message = LoginValue.of(result).getMessage(result);
+			logger.warn(message);
+			if (LOGIN_WRONG_TOKEN.getLoginText().equals(result)) {
+				throw new NeedLoginTokenException(message);
+			} else {
+				throw new LoginFailedException(message);
+			}
+		}
+	}
+
+	/**
+	 * Issues a Web API query to confirm that the previous client login attempt was
+	 * successful, and sets the internal state of the API connection accordingly
+	 * in this case.
+	 *
+	 * @param token
+	 *            the login token string
+	 * @param username
+	 *            the name of the main user that was logged in
+	 * @param password
+	 *            the password used to log in
+	 * @throws IOException
+	 * @throws LoginFailedException
+	 */
+	protected void confirmClientLogin(String token, String username, String password)
+			throws IOException, LoginFailedException, MediaWikiApiErrorException {
+		Map<String, String> params = new HashMap<>();
+		params.put(PARAM_ACTION, "clientlogin");
+		params.put(PARAM_LOGIN_USERNAME.getClientLoginText(), username);
+		params.put(PARAM_LOGIN_PASSWORD.getClientLoginText(), password);
+		params.put(PARAM_LOGIN_TOKEN.getClientLoginText(), token);
+		params.put("loginreturnurl", apiBaseUrl); // isn't really used in this case, but the api requires either this or logincontinue
+
+		JsonNode root = sendJsonRequest("POST", params);
+
+		String result = root.path("clientlogin").path("status").textValue();
+		if ("PASS".equals(result)) {
 			this.loggedIn = true;
 			this.username = username;
 			this.password = password;
 		} else {
-			String message = getLoginErrorMessage(result);
+			String messagecode;
+			if ("FAIL".equals(result)) {
+				messagecode = root.path("clientlogin").path("messagecode").textValue();
+			} else {
+				messagecode = root.path("error").path("code").textValue();
+			}
+			String message = LoginValue.of(messagecode).getMessage(messagecode);
 			logger.warn(message);
-			if (LOGIN_WRONG_TOKEN.equals(result)) {
+			if (LOGIN_WRONG_TOKEN.getClientLoginText().equals(messagecode)) {
 				throw new NeedLoginTokenException(message);
 			} else {
 				throw new LoginFailedException(message);
@@ -296,46 +296,6 @@ public class BasicApiConnection extends ApiConnection {
 		cookieManager.getCookieStore().removeAll();
 	}
 
-	/**
-	 * Returns a user-readable message for a given API response.
-	 *
-	 * @param loginResult
-	 *            a API login request result string other than
-	 *            {@link #LOGIN_RESULT_SUCCESS}
-	 * @return error message
-	 */
-	protected String getLoginErrorMessage(String loginResult) {
-		switch (loginResult) {
-		case LOGIN_WRONG_PASS:
-			return loginResult + ": Wrong Password.";
-		case LOGIN_WRONG_PLUGIN_PASS:
-			return loginResult
-					+ ": Wrong Password. An authentication plugin rejected the password.";
-		case LOGIN_NOT_EXISTS:
-			return loginResult + ": Username does not exist.";
-		case LOGIN_BLOCKED:
-			return loginResult + ": User is blocked.";
-		case LOGIN_EMPTY_PASS:
-			return loginResult + ": Password is empty.";
-		case LOGIN_NO_NAME:
-			return loginResult + ": No user name given.";
-		case LOGIN_CREATE_BLOCKED:
-			return loginResult
-					+ ": The wiki tried to automatically create a new account for you, "
-					+ "but your IP address has been blocked from account creation.";
-		case LOGIN_ILLEGAL:
-			return loginResult + ": Username is illegal.";
-		case LOGIN_THROTTLED:
-			return loginResult + ": Too many login attempts in a short time.";
-		case LOGIN_WRONG_TOKEN:
-			return loginResult + ": Token is wrong.";
-		case LOGIN_NEEDTOKEN:
-			return loginResult + ": Token or session ID is missing.";
-		default:
-			return "Login Error: " + loginResult;
-		}
-	}
-
 	/**
 	 * Logs the current user out.
 	 *
@@ -396,4 +356,11 @@ public class BasicApiConnection extends ApiConnection {
 			return httpCookie;
 		}
 	}
+
+	/***
+	 * Functional interface for logging in
+	 */
+	private interface ILogin {
+		void login(String token, String username, String password) throws IOException, LoginFailedException, MediaWikiApiErrorException;
+	}
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/LoginValue.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/LoginValue.java
new file mode 100644
index 00000000..580eb23b
--- /dev/null
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/LoginValue.java
@@ -0,0 +1,134 @@
+package org.wikidata.wdtk.wikibaseapi;
+
+/*-
+ * #%L
+ * Wikidata Toolkit Wikibase API
+ * %%
+ * Copyright (C) 2014 - 2022 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public enum LoginValue {
+    /**
+     * Name of the HTTP parameter to submit a password to the API.
+     */
+    PARAM_LOGIN_USERNAME("lgname", "username", ""),
+    /**
+     * Name of the HTTP parameter to submit a password to the API.
+     */
+    PARAM_LOGIN_PASSWORD("lgpassword", "password", ""),
+    /**
+     * Name of the HTTP parameter to submit a login token to the API.
+     */
+    PARAM_LOGIN_TOKEN("lgtoken", "logintoken", ""),
+
+    /**
+     * String value in the result field of the JSON response if the login was
+     * successful.
+     */
+    LOGIN_RESULT_SUCCESS("Success", "PASS", ""),
+    /**
+     * String value in the result field of the JSON response if the password was
+     * wrong.
+     */
+    LOGIN_WRONG_PASS("WrongPass", "wrongpassword", "Wrong Password."),
+    /**
+     * String value in the result field of the JSON response if the password was
+     * rejected by an authentication plugin.
+     */
+    LOGIN_WRONG_PLUGIN_PASS("WrongPluginPass", "wrongpluginpass", "Wrong Password. An authentication plugin rejected the password."), // not sure about this one
+    /**
+     * String value in the result field of the JSON response if no username was
+     * given.
+     */
+    LOGIN_NO_NAME("NoName", "authmanager-authn-no-primary", "No user name given."),
+    /**
+     * String value in the result field of the JSON response if given username
+     * does not exist.
+     */
+    LOGIN_NOT_EXISTS("NotExists", "wrongpassword", "Username does not exist."), // no distinction for clientLogin
+    /**
+     * String value in the result field of the JSON response if the username is
+     * illegal.
+     */
+    LOGIN_ILLEGAL("Illegal", "wrongpassword", "Username is illegal."), // no distinction for clientLogin
+    /**
+     * String value in the result field of the JSON response if there were too
+     * many logins in a short time.
+     */
+    LOGIN_THROTTLED("Throttled", "throttled", "Too many login attempts in a short time."), // not sure about this one
+    /**
+     * String value in the result field of the JSON response if password is
+     * empty.
+     */
+    LOGIN_EMPTY_PASS("EmptyPass", "authmanager-authn-no-primary", "Password is empty."),
+    /**
+     * String value in the result field of the JSON response if the wiki tried
+     * to automatically create a new account for you, but your IP address has
+     * been blocked from account creation.
+     */
+    LOGIN_CREATE_BLOCKED("CreateBlocked", "createblocked", "The wiki tried to automatically create a new account for you, "
+            + "but your IP address has been blocked from account creation."), // not sure about this one
+    /**
+     * String value in the result field of the JSON response if the user is
+     * blocked.
+     */
+    LOGIN_BLOCKED("Blocked", "blocked", "User is blocked."), // not sure about this one
+    /**
+     * String value in the result field of the JSON response if token or session
+     * ID is missing.
+     */
+    LOGIN_NEEDTOKEN("NeedToken", "missingparam", "Token or session ID is missing."),
+    /**
+     * String value in the result field of the JSON response if token is wrong.
+     */
+    LOGIN_WRONG_TOKEN("WrongToken", "badtoken", "Token is wrong."),
+    /**
+     * Value for unknown response text
+     */
+    UNKNOWN("unknown", "unknown", "Error text not recognized");
+    
+    private final String loginText;
+    private final String clientLoginText;
+    private final String message;
+    
+    LoginValue(String loginText, String clientLoginText, String message) {
+        this.loginText = loginText;
+        this.clientLoginText = clientLoginText;
+        this.message = message;
+    }
+    
+    public static LoginValue of(String text) {
+        Optional<LoginValue> optionalLoginValue = Arrays.stream(LoginValue.values())
+                .filter(loginValue -> text != null && (text.equals(loginValue.loginText) || text.equals(loginValue.clientLoginText))).findFirst();
+        return optionalLoginValue.orElse(UNKNOWN);
+    }
+
+    public String getLoginText() {
+        return loginText;
+    }
+
+    public String getClientLoginText() {
+        return clientLoginText;
+    }
+    
+    public String getMessage(String loginType) {
+        return loginType + ": " + message;
+    }
+    
+}


Test patch:
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
index 2f7a764d..f49acb11 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
@@ -29,7 +29,7 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.net.HttpCookie;
 import java.net.URL;
-import java.util.ArrayList;
+import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -122,8 +122,18 @@ public class BasicApiConnectionTest {
 						case "assert=user&format=json&action=query":
 							return makeJsonResponseFrom("/assert-user-failed.json");
 					}
-					
-				}catch (Exception e) {
+					// finally check clientLogin. This uses server.url, so cannot be used in switch statement because it is not constant.
+					String url = server.url("/w/api.php").toString();
+					String encodedUrl = URLEncoder.encode(url, "UTF-8");
+					final String clientLoginRequest = String.format("password=password&format=json&action=clientlogin&logintoken=b5780b6e2f27e20b450921d9461010b4&loginreturnurl=%s&username=Admin" , encodedUrl);
+					final String clientLoginErrorRequest = String.format("password=password1&format=json&action=clientlogin&logintoken=b5780b6e2f27e20b450921d9461010b4&loginreturnurl=%s&username=Admin" , encodedUrl);
+					if (requestBody.equals(clientLoginRequest)) {
+						return makeJsonResponseFrom("/clientLoginSuccess.json");
+					} else if (requestBody.equals(clientLoginErrorRequest)) {
+						return makeJsonResponseFrom("/clientLoginError.json");
+					}
+
+				} catch (Exception e) {
 					return new MockResponse().setResponseCode(404);
 				}
 				return new MockResponse().setResponseCode(404);
@@ -162,6 +172,12 @@ public class BasicApiConnectionTest {
 		connection.confirmLogin(token, "username", "password");
 	}
 
+	@Test
+	public void testConfirmClientLogin() throws LoginFailedException, IOException, MediaWikiApiErrorException {
+		String token = connection.getOrFetchToken("login");
+		connection.confirmClientLogin(token, "Admin", "password");
+	}
+
 	@Test
 	public void testLogin() throws LoginFailedException {
 		assertFalse(connection.loggedIn);
@@ -171,6 +187,15 @@ public class BasicApiConnectionTest {
 		assertTrue(connection.isLoggedIn());
 	}
 
+	@Test
+	public void testClientLogin() throws LoginFailedException {
+		assertFalse(connection.loggedIn);
+		connection.clientLogin("Admin", "password");
+		assertEquals("Admin", connection.getCurrentUser());
+		assertEquals("password", connection.password);
+		assertTrue(connection.isLoggedIn());
+	}
+
 
 	@Test
 	public void testSerialize() throws LoginFailedException, IOException {
@@ -314,33 +339,6 @@ public class BasicApiConnectionTest {
 				connection.apiBaseUrl);
 	}
 
-	@Test
-	public void testErrorMessages() {
-		BasicApiConnection connection = BasicApiConnection.getTestWikidataApiConnection();
-		String[] knownErrors = { BasicApiConnection.LOGIN_WRONG_PASS,
-				BasicApiConnection.LOGIN_WRONG_PLUGIN_PASS,
-				BasicApiConnection.LOGIN_NOT_EXISTS, BasicApiConnection.LOGIN_BLOCKED,
-				BasicApiConnection.LOGIN_EMPTY_PASS, BasicApiConnection.LOGIN_NO_NAME,
-				BasicApiConnection.LOGIN_CREATE_BLOCKED,
-				BasicApiConnection.LOGIN_ILLEGAL, BasicApiConnection.LOGIN_THROTTLED,
-				BasicApiConnection.LOGIN_WRONG_TOKEN, BasicApiConnection.LOGIN_NEEDTOKEN };
-
-		ArrayList<String> messages = new ArrayList<>();
-		for (String error : knownErrors) {
-			messages.add(connection.getLoginErrorMessage(error));
-		}
-
-		String unknownMessage = connection
-				.getLoginErrorMessage("unkonwn error code");
-
-		int i = 0;
-		for (String message : messages) {
-			assertNotEquals(unknownMessage, message);
-			assertTrue(message.contains(knownErrors[i]));
-			i++;
-		}
-	}
-
 	@Test(expected = AssertUserFailedException.class)
 	public void testCheckCredentials() throws IOException, MediaWikiApiErrorException, LoginFailedException {
 		// we first login successfully
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/LoginValueTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/LoginValueTest.java
new file mode 100644
index 00000000..2846a746
--- /dev/null
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/LoginValueTest.java
@@ -0,0 +1,59 @@
+package org.wikidata.wdtk.wikibaseapi;
+
+/*-
+ * #%L
+ * Wikidata Toolkit Wikibase API
+ * %%
+ * Copyright (C) 2014 - 2022 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+public class LoginValueTest {
+
+    @Test
+    public void testOf() {
+        LoginValue needToken = LoginValue.of("NeedToken");
+        assertEquals(LoginValue.LOGIN_NEEDTOKEN, needToken);
+    }
+
+    @Test
+    public void testOfReturnsTopMatch() {
+        LoginValue wrongpassword = LoginValue.of("wrongpassword");
+        assertEquals(LoginValue.LOGIN_WRONG_PASS, wrongpassword);
+        assertNotEquals(LoginValue.LOGIN_NOT_EXISTS, wrongpassword);
+    }
+
+    @Test
+    public void testOfReturnsUnknownIfTextUnknown() {
+        LoginValue unknown = LoginValue.of("dunno");
+        assertEquals(LoginValue.UNKNOWN, unknown);
+    }
+
+    @Test
+    public void testOfReturnsUnknownIfTextNull() {
+        LoginValue unknown = LoginValue.of(null);
+        assertEquals(LoginValue.UNKNOWN, unknown);
+    }
+
+    @Test
+    public void testGetMessage() {
+        assertEquals("NeedToken: Token or session ID is missing.", LoginValue.LOGIN_NEEDTOKEN.getMessage("NeedToken"));
+    }
+}
diff --git a/wdtk-wikibaseapi/src/test/resources/clientLoginError.json b/wdtk-wikibaseapi/src/test/resources/clientLoginError.json
new file mode 100644
index 00000000..d6986903
--- /dev/null
+++ b/wdtk-wikibaseapi/src/test/resources/clientLoginError.json
@@ -0,0 +1,7 @@
+{
+  "error": {
+    "code": "badtoken",
+    "info": "Invalid CSRF token.",
+    "*": "See http://localhost/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."
+  }
+}
\ No newline at end of file
diff --git a/wdtk-wikibaseapi/src/test/resources/clientLoginSuccess.json b/wdtk-wikibaseapi/src/test/resources/clientLoginSuccess.json
new file mode 100644
index 00000000..177cc8c0
--- /dev/null
+++ b/wdtk-wikibaseapi/src/test/resources/clientLoginSuccess.json
@@ -0,0 +1,6 @@
+{
+    "clientlogin": {
+        "status": "PASS",
+        "username": "Admin"
+    }
+}
\ No newline at end of file