ApiTest.java

/***************************************************************************
   Copyright 2015 Emily Estes

   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.
***************************************************************************/
package net.metanotion.authident;


import java.util.HashSet;
import net.metanotion.util.SecureString;
import net.metanotion.util.StateMachine;

/** This class exists as a test suite of the authentication/identification API's. It is included as an official part of
the package so that implementations can use this test suite as part of their test code. */
public final class ApiTest {
	private static void assertTrue(final boolean result) {
		if(!result) { throw new AssertionError("Unexpected result"); }
	}

	/** Guarantee that the realm provided conforms to the Realm interface requirements.
		@param realm The realm to test. It should be empty of users.
		@throws AssertionError if the instance does not properly implement the interface.
	*/
	public static void testRealm(final Realm realm) throws AssertionError {
		assertTrue(realm.getUser(1) == null);
		final UserToken user = realm.createUser();
		assertTrue(user != null);
		assertTrue(user.realm().equals(realm));
		final UserToken same = realm.getUser(user.getID());
		assertTrue(same.getID() == user.getID());
		assertTrue(user.equals(same));
		final long uid = user.getID();
		user.delete();
		assertTrue(realm.getUser(uid)==null);
		final UserToken next = realm.createUser();
		assertTrue(next != null);
		assertTrue(next.getID() != uid);

		final UserToken u3 = realm.createUser();
		assertTrue(u3.equals(u3));
		assertTrue(!u3.equals(user));
		assertTrue(u3.equals(new UserToken() {
			@Override public long getID() { return u3.getID(); }
			@Override public void delete() { }
			@Override public Realm realm() { return realm; }
		}));
		assertTrue(!u3.equals(new UserToken() {
			@Override public long getID() { return u3.getID(); }
			@Override public void delete() { }
			@Override public Realm realm() { return null; }
		}));
		assertTrue(!u3.equals(null));
		assertTrue(!u3.equals(new Object()));
		final HashSet<UserToken> uts = new HashSet<>();
		uts.add(u3);
		assertTrue(uts.contains(u3));
	}

	private static final String USER1 = "test";
	private static final String PASS1 = "pass";

	private static final String OOPS = "oops";
	private static final String USER2= "zxcv1234@example.com";
	private static final String PASS2 = "asdf\u1E9B\u0323awef";
	private static final String PASS2_NFKC = "asdf\u1E69awef";
	private static final String PASS2_BAD = "asdf\u1E9Bawef";
	private static final String USER2_2 = "@$#$$1...1234!";
	private static final String PASS2_2 = "2.....kllkl@$@!";

	private static final String USER3 = "u3";
	private static final String PASS3_1 = "pawef1";
	private static final String PASS3_2 = "@#$%@$@@@@!()^%&$%";

	private static final String USER4 = "uawef3";
	private static final String PASS4 = "pawef43";

	private static final String AU_USER = "wef3";
	private static final String AU_PASS = "pawef421!";

	/** Guarantee that the password authentication provider conforms to the AuthPassword interface requirements.
		@param realm The (empty) realm backing the password authentication provider.
		@param pw The password authentication provider to test. It should be empty of users.
		@throws AssertionError if the instance does not properly implement the interface.
	*/
	public static void testAuthPassword(final Realm realm, final AuthPassword<? extends AuthPassword> pw) {
		assertTrue(pw.listUsernames(null) != null);
		for(final String uname: pw.listUsernames(null)) {
			throw new AssertionError("Null user token to listUsernames should return an empty list.");
		}

		assertTrue(pw.authenticate(USER1, new SecureString(PASS1)) == null);
		assertTrue(pw.authenticate(USER1, null) == null);
		assertTrue(pw.authenticate(null, null) == null);
		assertTrue(pw.authenticate("", null) == null);
		assertTrue(pw.getIdentity(USER1) == null);
		assertTrue(pw.getIdentity(null) == null);
		final UserToken u1 = realm.createUser();
		final Iterable<String> unames = pw.listUsernames(u1);
		for(final String s: unames) { throw new AssertionError("There should be no identities associated with this token."); }

		final CredentialedUserToken cut = pw.createAuthentication(USER1, new SecureString(PASS1));
		assertTrue(cut != null);
		assertTrue(cut.getID() != u1.getID());
		for(final String s: unames) { assertTrue(USER1.equals(s)); }
		assertTrue(pw.authenticate(USER1, new SecureString(PASS1 + "oops."))==null);
		assertTrue(pw.authenticate(USER1, null)==null);
		final CredentialedUserToken cut1 = pw.authenticate(USER1, new SecureString(PASS1));
		assertTrue(cut1.getID() == cut.getID());
		assertTrue(USER1.equals(cut.getCredential()));
		assertTrue(USER1.equals(cut1.getCredential()));
		assertTrue(pw.getIdentity(USER1).getID() == cut.getID());
		assertTrue(realm.getUser(cut.getID()).equals(cut));
		assertTrue(cut.equals(realm.getUser(cut.getID())));

		final CredentialedUserToken cut2 = pw.createAuthentication(USER2, new SecureString(PASS2));
		assertTrue(cut2 != null);
		assertTrue(cut2.getID() != u1.getID());
		for(final String s: unames) { assertTrue(USER2.equals(s)); }
		assertTrue(pw.authenticate(USER2, new SecureString(PASS2 + OOPS))==null);
		final CredentialedUserToken cut3 = pw.authenticate(USER2, new SecureString(PASS2));
		assertTrue(cut2.getID() == cut3.getID());
		final CredentialedUserToken cut4 = pw.authenticate(USER2, new SecureString(PASS2_NFKC));
		assertTrue(cut2.getID() == cut4.getID());
		assertTrue(USER2.equals(cut2.getCredential()));
		assertTrue(USER2.equals(cut3.getCredential()));
		assertTrue(USER2.equals(cut4.getCredential()));
		assertTrue(pw.authenticate(USER2, new SecureString(PASS2_BAD))==null);
		assertTrue(pw.getIdentity(USER2).getID() != cut.getID());
		assertTrue(pw.getIdentity(USER1).getID() != cut2.getID());

		final long u1id = cut.getID();
		pw.deleteAuthentication(cut, USER1);
		assertTrue(pw.authenticate(USER1, new SecureString(PASS1))==null);
		assertTrue(pw.getIdentity(USER1) == null);
		final UserToken u1v2 = realm.getUser(u1id);
		assertTrue(u1v2 != null);
		try {
			pw.addAuthentication(u1v2, USER2);
			throw new AssertionError("Added a username to an identity for a username that already exists.");
		} catch (final RuntimeException ex) {
			// This is expected.
		}

		pw.addAuthentication(cut2, USER2_2);
		assertTrue(pw.authenticate(USER2, new SecureString(PASS2)).getID() == cut2.getID());
		assertTrue(pw.authenticate(USER2_2, new SecureString(PASS2_NFKC)).getID() == cut2.getID());
		final HashSet<String> un = new HashSet<>();
		int ct = 0;
		for(final String s: pw.listUsernames(cut2)) {
			ct++;
			un.add(s);
		}
		if(ct > 2) { throw new AssertionError("Too many credentials for user:" + Long.toString(cut2.getID())); }
		final String missing = "Missing username:";
		if(!un.contains(USER2)) { throw new AssertionError(missing + USER2); }
		if(!un.contains(USER2_2)) { throw new AssertionError(missing + USER2_2); }

		assertTrue(pw.authenticate(USER1, null) == null);
		assertTrue(pw.authenticate(null, null) == null);
		assertTrue(pw.authenticate("", null) == null);

		final UserToken u3 = realm.createUser();
		pw.addAuthentication(u3, USER3);
		assertTrue(pw.authenticate(USER3, null)==null);
		assertTrue(pw.authenticate(USER3, new SecureString(PASS3_1))==null);
		pw.setPassword(USER3, new SecureString(PASS3_1));
		assertTrue(pw.authenticate(USER3, new SecureString(PASS3_1)).getID() == u3.getID());

		pw.setPassword(USER2_2, new SecureString(PASS2_2));
		assertTrue(pw.authenticate(USER2, new SecureString(PASS2)) == null);
		assertTrue(pw.authenticate(USER2_2, new SecureString(PASS2)) == null);
		assertTrue(pw.authenticate(USER2, new SecureString(PASS2_2)).getID() == cut2.getID());
		assertTrue(pw.authenticate(USER2_2, new SecureString(PASS2_2)).getID() == cut2.getID());
		assertTrue(pw.authenticate(USER2, null) == null);
		assertTrue(pw.authenticate(USER2_2, null) == null);

		assertTrue(pw.authenticate("", null) == null);

		pw.setPassword(USER3, new SecureString(PASS3_2));
		assertTrue(pw.authenticate(USER3, new SecureString(PASS3_1)) == null);
		assertTrue(pw.authenticate(USER3, new SecureString(PASS3_2)).getID() == u3.getID());
		assertTrue(pw.getIdentity(USER3).getID() == u3.getID());

		final CredentialedUserToken cut5 = pw.createAuthentication(USER4, new SecureString(PASS4));
		assertTrue(cut5.equals(pw.getIdentity(USER4)));
		assertTrue(cut5.equals(cut5));
		assertTrue(!cut5.equals(cut2));
		assertTrue(!cut5.equals(new UserToken() {
			@Override public long getID() { return cut5.getID(); }
			@Override public void delete() { }
			@Override public Realm realm() { return null; }
		}));
		assertTrue(!cut5.equals(null));
		assertTrue(!cut5.equals(new Object()));
		final HashSet<UserToken> uts = new HashSet<>();
		uts.add(cut5);
		assertTrue(uts.contains(cut5));
		cut5.delete();
		assertTrue(pw.getIdentity(USER4) == null);
		try {
			pw.setPassword(USER2_2, null);
			throw new AssertionError("Attempting to call setPassword with a null password should throw an exception.");
		} catch (RuntimeException ex) {
			// This is expected.
		}

		final UserToken evilCut5 = new UserToken() {
			@Override public long getID() { return cut5.getID(); }
			@Override public void delete() { }
			@Override public Realm realm() { return realm; }
		};
		for(final String u: pw.listUsernames(evilCut5)) {
			throw new AssertionError("List usernames of a deleted user should work, but should return an empty list.");
		}
		try {
			pw.addAuthentication(evilCut5, USER4 + USER4);
			throw new AssertionError("This is a bad user token. So we shouldn't be able to add authentication.");
		} catch (final RuntimeException ex) {
			// Expected.
		}
		try {
			pw.deleteAuthentication(evilCut5, USER4);
			throw new AssertionError("This is a bad user token. So we shouldn't be able to delete authentication.");
		} catch (final RuntimeException ex) {
			// Expected.
		}
		checkAuthUtils(realm, pw);
	}

	private static void checkAuthUtils(final Realm realm, final AuthPassword<? extends AuthPassword> pw) {
		final CredentialedUserToken au = pw.createAuthentication(AU_USER, new SecureString(AU_PASS));
		final UserToken aut = realm.getUser(au.getID());

		final StateMachine<ApiTest> sm = new StateMachine<ApiTest>() {
			@Override public void nextState(final Object event) { assertTrue(au.equals(event)); }
			@Override public ApiTest state() { return null; }
		};

		AuthUtils.externalAuthenticate(pw, sm, AU_USER, new SecureString(AU_PASS + OOPS));
		AuthUtils.externalAuthenticate(pw, sm, AU_USER, new SecureString(AU_PASS));
		AuthUtils.externalAuthenticate(pw, sm, AU_USER, null);
		AuthUtils.externalAuthenticate(pw, sm, null, null);
		AuthUtils.externalAuthenticate(pw, sm, null, new SecureString(AU_PASS));
		assertTrue(AU_USER.equals(AuthUtils.lookupUsername(pw, au)));
		assertTrue(AU_USER.equals(AuthUtils.lookupUsername(pw, aut)));
		assertTrue(AuthUtils.lookupUsername(pw, realm.createUser())==null);
		assertTrue(AuthUtils.checkAuthenticatedIdentityMatches(pw, au, AU_USER, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, USER3, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, null, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, "", new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, AU_USER, new SecureString(AU_PASS + OOPS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, USER3, new SecureString(AU_PASS + OOPS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, null, new SecureString(AU_PASS + OOPS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, "", new SecureString(AU_PASS + OOPS)));
		assertTrue(pw.authenticate(AU_USER, null)==null);
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, AU_USER, null));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, USER3, null));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, null, null));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, au, "", null));
		assertTrue(AuthUtils.checkAuthenticatedIdentityMatches(pw, aut, AU_USER, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, aut, USER3, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, aut, null, new SecureString(AU_PASS)));
		assertTrue(!AuthUtils.checkAuthenticatedIdentityMatches(pw, aut, "", new SecureString(AU_PASS)));
		try {
			pw.deleteAuthentication(null, null);
			throw new AssertionError("Null user token and password should throw an exception");
		} catch (final RuntimeException ex) {
			// Expected
		}
		try {
			pw.deleteAuthentication(null, AU_USER + OOPS);
			throw new AssertionError("Null user token should throw an exception");
		} catch (final RuntimeException ex) {
			// Expected
		}
		try {
			pw.deleteAuthentication(au, AU_USER + OOPS);
			throw new AssertionError("Bad username should throw an exception");
		} catch (final RuntimeException ex) {
			// Expected
		}
		pw.deleteAuthentication(au, AU_USER);
		try {
			pw.deleteAuthentication(au, AU_USER);
			throw new AssertionError("A previously deleted username should throw an exception");
		} catch (final RuntimeException ex) {
			// Expected
		}
	}
}