public final class RxJavaRules implements TestRule {

    @Override
    public Statement apply(final Statement base, Description description) {
        return new SchedulerStatement(base);
    }

    private static final class SchedulerStatement extends Statement {

        private final Statement mStatement;

        SchedulerStatement(Statement statement) {
            mStatement = statement;
        }

        @Override
        public void evaluate() throws Throwable {
            try {
                setSchedulers();
                mStatement.evaluate();
            } finally {
                resetSchedulers();
            }
        }

        private void resetSchedulers() {
            RxAndroidPlugins.reset();
            RxJavaPlugins.reset();
        }

        private void setSchedulers() {
            RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
            RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
        }
    }

} public class AuthPresenter extends MvpPresenter<AuthView> {

    private final TravisRestClient mTravisRestClient;
    private final GitHubRestClient mGitHubRestClient;
    private final AppSettings mAppSettings;

    private final CompositeDisposable mSubscriptions;

    private String mBasicAuth;
    private String mSecurityCode;
    private Authorization mAuthorization;

    private boolean mSecurityCodeInput;

    @Inject
    public AuthPresenter(TravisRestClient travisRestClient, GitHubRestClient gitHubRestClient, AppSettings appSettings) {
        mTravisRestClient = travisRestClient;
        mGitHubRestClient = gitHubRestClient;
        mAppSettings = appSettings;

        mSubscriptions = new CompositeDisposable();
    }

    @Override
    public void onAttach() {
        getView().setInputView(mSecurityCodeInput);
    }

    @Override
    public void onDetach() {
        mSubscriptions.clear();
    }

    /**
     * Updates server endpoint
     *
     * @param newServer New server endpoint
     */
    public void updateServer(String newServer) {
        mAppSettings.putServerUrl(newServer);
        mTravisRestClient.updateTravisEndpoint(newServer);
    }

    /**
     * Executes login action
     *
     * @param userName Username
     * @param password Password
     */
    public void login(String userName, String password) {
        mBasicAuth = EncryptionUtils.generateBasicAuthorization(userName, password);

        doLogin(getAuthorizationJob(false));
    }

    /**
     * Executes two-factor auth call
     *
     * @param securityCode Security code
     */
    public void twoFactorAuth(String securityCode) {
        mSecurityCode = securityCode;
        doLogin(getAuthorizationJob(true));
    }

    /**
     * Returns server URL
     *
     * @return Server URL
     */
    public String getServerUrl() {
        return mAppSettings.getServerUrl();
    }

    private Single<Authorization> getAuthorizationJob(boolean twoFactorAuth) {
        if (twoFactorAuth) {
            return mGitHubRestClient.getApiService()
                    .createNewAuthorization(mBasicAuth, mSecurityCode, prepareAuthorizationRequest());
        } else {
            return mGitHubRestClient.getApiService()
                    .createNewAuthorization(mBasicAuth, prepareAuthorizationRequest());
        }
    }

    private void doLogin(Single<Authorization> authorizationJob) {
        Disposable subscription = authorizationJob
                .flatMap(this::doAuthorization)
                .doOnSuccess(this::saveAccessToken)
                .doAfterSuccess(accessToken -> cleanUpAfterAuthorization())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe((authorization, throwable) -> {
                    getView().hideProgress();

                    if (throwable == null) {
                        getView().finishView();
                    } else {
                        if (throwable instanceof HttpException && isTwoFactorAuthRequired((HttpException) throwable)) {
                            mSecurityCodeInput = true;
                            getView().showTwoFactorAuth();
                        } else {
                            getView().showErrorMessage(throwable.getMessage());
                        }
                    }
                });

        mSubscriptions.add(subscription);
    }

    private void cleanUpAfterAuthorization() {
        // start deletion authorization on Github, because we don't need it anymore
        Single<Object> cleanUpJob;
        if (!TextUtils.isEmpty(mSecurityCode)) {
            cleanUpJob = mGitHubRestClient.getApiService()
                    .deleteAuthorization(mBasicAuth, mSecurityCode, String.valueOf(mAuthorization.getId()));
        } else {
            cleanUpJob = mGitHubRestClient.getApiService()
                    .deleteAuthorization(mBasicAuth, String.valueOf(mAuthorization.getId()));
        }
        Disposable task = cleanUpJob
                .onErrorReturn(throwable -> new Object())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe();
        mSubscriptions.add(task);
    }

    private void saveAccessToken(AccessToken accessToken) {
        // save access token to settings
        String token = accessToken.getAccessToken();
        mAppSettings.putAccessToken(token);
    }

    private Single<AccessToken> doAuthorization(Authorization authorization) {
        mAuthorization = authorization;
        AccessTokenRequest request = new AccessTokenRequest();
        request.setGithubToken(authorization.getToken());
        return mTravisRestClient.getApiService().auth(request);
    }

    private boolean isTwoFactorAuthRequired(HttpException exception) {
        Response response = exception.response();

        boolean twoFactorAuthRequired = false;
        for (String header : response.headers().names()) {
            if (GithubApiService.TWO_FACTOR_HEADER.equals(header)) {
                twoFactorAuthRequired = true;
                break;
            }
        }

        return response.code() == HttpURLConnection.HTTP_UNAUTHORIZED && twoFactorAuthRequired;
    }

    /**
     * Prepares authorization request
     *
     * @return Authorization request
     */
    private AuthorizationRequest prepareAuthorizationRequest() {
        List<String> scopes = Arrays.asList("read:org", "user:email", "repo_deployment",
                "repo:status", "write:repo_hook", "repo");
        String note = String.format("varis_client_%1$s", StringUtils.getRandomString());
        return new AuthorizationRequest(scopes, note);
    }
} public final class AccessTokenRequest {

    @SerializedName("github_token")
    private String mGithubToken;

    public String getGithubToken() {
        return mGithubToken;
    }

    public void setGithubToken(String githubToken) {
        mGithubToken = githubToken;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof AccessTokenRequest))
            return false;
        if (o == this)
            return true;

        AccessTokenRequest that = (AccessTokenRequest) o;
        return mGithubToken.equals(that.mGithubToken);
    }

    @Override
    public int hashCode() {
        return mGithubToken.hashCode();
    }
} public class AccessToken {

    @SerializedName("access_token")
    private String mAccessToken;

    public String getAccessToken() {
        return mAccessToken;
    }

    public void setAccessToken(String accessToken) {
        mAccessToken = accessToken;
    }
} public final class Authorization {

    @SerializedName("id")
    private long mId;

    @SerializedName("url")
    private String mUrl;

    @SerializedName("scopes")
    private List<String> mScopes;

    @SerializedName("token")
    private String mToken;

    @SerializedName("token_last_eight")
    private String mTokenLastEight;

    @SerializedName("hashed_token")
    private String mHashedToken;

    @SerializedName("app")
    private App mApp;

    @SerializedName("note")
    private String mNote;

    @SerializedName("note_url")
    private String mNoteUrl;

    @SerializedName("updated_at")
    private String mUpdatedAt;

    @SerializedName("created_at")
    private String mCreatedAt;

    @SerializedName("fingerprint")
    private String mFingerprint;


    public long getId() {
        return mId;
    }

    public void setId(long id) {
        mId = id;
    }

    public String getUrl() {
        return mUrl;
    }

    public void setUrl(String url) {
        mUrl = url;
    }

    public List<String> getScopes() {
        return mScopes;
    }

    public void setScopes(List<String> scopes) {
        mScopes = scopes;
    }

    public String getToken() {
        return mToken;
    }

    public void setToken(String token) {
        mToken = token;
    }

    public String getTokenLastEight() {
        return mTokenLastEight;
    }

    public void setTokenLastEight(String tokenLastEight) {
        mTokenLastEight = tokenLastEight;
    }

    public String getHashedToken() {
        return mHashedToken;
    }

    public void setHashedToken(String hashedToken) {
        mHashedToken = hashedToken;
    }

    public App getApp() {
        return mApp;
    }

    public void setApp(App app) {
        mApp = app;
    }

    public String getNote() {
        return mNote;
    }

    public void setNote(String note) {
        mNote = note;
    }

    public String getNoteUrl() {
        return mNoteUrl;
    }

    public void setNoteUrl(String noteUrl) {
        mNoteUrl = noteUrl;
    }

    public String getUpdatedAt() {
        return mUpdatedAt;
    }

    public void setUpdatedAt(String updatedAt) {
        mUpdatedAt = updatedAt;
    }

    public String getCreatedAt() {
        return mCreatedAt;
    }

    public void setCreatedAt(String createdAt) {
        mCreatedAt = createdAt;
    }

    public String getFingerprint() {
        return mFingerprint;
    }

    public void setFingerprint(String fingerprint) {
        mFingerprint = fingerprint;
    }

    public class App {

        @SerializedName("url")
        private String mUrl;

        @SerializedName("name")
        private String mName;

        @SerializedName("client_id")
        private String mClientId;

        public String getUrl() {
            return mUrl;
        }

        public void setUrl(String url) {
            mUrl = url;
        }

        public String getName() {
            return mName;
        }

        public void setName(String name) {
            mName = name;
        }

        public String getClientId() {
            return mClientId;
        }

        public void setClientId(String clientId) {
            mClientId = clientId;
        }
    }
} public class GitHubRestClient {

    private static final String GITHUB_URL = Constants.GITHUB_URL;

    private final Retrofit mRetrofit;

    private GithubApiService mGithubApiService;

    public GitHubRestClient(Retrofit retrofit) {
        mRetrofit = retrofit;

        // rest adapter for github API service
        Retrofit newRetrofit = mRetrofit.newBuilder()
                .baseUrl(GITHUB_URL)
                .build();
        mGithubApiService = newRetrofit.create(GithubApiService.class);
    }

    /**
     * Gets Github API service
     *
     * @return Github API service
     */
    public GithubApiService getApiService() {
        return mGithubApiService;
    }

} public interface GithubApiService {

    String TWO_FACTOR_HEADER = "X-GitHub-OTP";

    @POST("/authorizations")
    Single<Authorization> createNewAuthorization(@Header("Authorization") String basicAuth,
                                                @Body AuthorizationRequest authorizationRequest);

    @POST("/authorizations")
    Single<Authorization> createNewAuthorization(@Header("Authorization") String basicAuth,
                                         @Header(TWO_FACTOR_HEADER) String twoFactorCode,
                                         @Body AuthorizationRequest authorizationRequest);

    @DELETE("/authorizations/{authorizationId}")
    Single<Object> deleteAuthorization(@Header("Authorization") String basicAuth,
                               @Path("authorizationId") String authorizationId);

    @DELETE("/authorizations/{authorizationId}")
    Single<Object> deleteAuthorization(@Header("Authorization") String basicAuth,
                               @Header(TWO_FACTOR_HEADER) String twoFactorCode,
                               @Path("authorizationId") String authorizationId);
} public class TravisRestClient {

    private final Retrofit mRetrofit;

    private final OkHttpClient mOkHttpClient;

    private final AppSettings mAppSettings;

    private TravisApiService mApiService;

    public TravisRestClient(Retrofit retrofit, OkHttpClient okHttpClient, AppSettings appSettings) {
        mRetrofit = retrofit;
        mOkHttpClient = okHttpClient;
        mAppSettings = appSettings;
        final String travisUrl = appSettings.getServerUrl();
        updateTravisEndpoint(travisUrl);
    }

    /**
     * Updates Travis endpoint
     *
     * @param newEndpoint New endpoint
     */
    public void updateTravisEndpoint(String newEndpoint) {
        Retrofit retrofit = mRetrofit.newBuilder()
                .baseUrl(newEndpoint)
                .client(getHttpClient())
                .build();

        mApiService = retrofit.create(TravisApiService.class);
    }

    private OkHttpClient getHttpClient() {
        final String userAgent = String.format("TravisClient/%1$s", PackageUtils.getAppVersion());

        return mOkHttpClient.newBuilder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request original = chain.request();

                        Request.Builder request = original.newBuilder()
                                .header("User-Agent", userAgent)
                                .header("Accept", "application/vnd.travis-ci.2+json");

                        String accessToken = mAppSettings.getAccessToken();
                        if (!TextUtils.isEmpty(accessToken)) {
                            String headerValue = String.format("token %1$s", accessToken);
                            request.addHeader("Authorization", headerValue);
                        }

                        return chain.proceed(request.build());
                    }
                })
                .build();
    }

    /**
     * Gets travis API service
     *
     * @return Travis API service
     */
    public TravisApiService getApiService() {
        return mApiService;
    }

} public class AppSettings {

    private static final String ACCESS_TOKEN_KEY = "TravisAccessToken";
    private static final String SERVER_TYPE_KEY = "ServerTypeKey";
    private static final String SERVER_URL_KEY = "ServerUrlKey";

    private SharedPreferences mSharedPreferences;

    public AppSettings(Context context) {
        mSharedPreferences = getPreferences(context);
    }

    /**
     * Gets shared preferences
     *
     * @return Shared preferences
     */
    private SharedPreferences getPreferences(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    /**
     * Gets access token
     *
     * @return Access token
     */
    public String getAccessToken() {
        return mSharedPreferences.getString(ACCESS_TOKEN_KEY, "");
    }

    /**
     * Puts access token to settings
     *
     * @param accessToken Access token
     */
    public void putAccessToken(String accessToken) {
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(ACCESS_TOKEN_KEY, accessToken);
        editor.apply();
    }

    /**
     * Gets server type from Settings
     *
     * @return Server type
     */
    public int getServerType() {
        return mSharedPreferences.getInt(SERVER_TYPE_KEY, 1);
    }

    /**
     * Puts server type to the settings
     *
     * @param serverType Server type
     */
    public void putServerType(int serverType) {
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putInt(SERVER_TYPE_KEY, serverType);
        editor.apply();
    }

    /**
     * Gets server url from Settings
     *
     * @return Server type
     */
    public String getServerUrl() {
        return mSharedPreferences.getString(SERVER_URL_KEY, "");
    }

    /**
     * Puts server url to the settings
     *
     * @param serverUrl Server type
     */
    public void putServerUrl(String serverUrl) {
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(SERVER_URL_KEY, serverUrl);
        editor.apply();
    }

} public final class EncryptionUtils {

    // denied constructor
    private EncryptionUtils() {

    }

    /**
     * Encodes input string to Base64
     *
     * @param input Input string
     * @return Base64 string
     */
    public static String toBase64(String input) {
        String base64Str = Base64.encodeToString(input.getBytes(), Base64.NO_WRAP);
        return base64Str;
    }

    /**
     * Decodes base64 string to original
     *
     * @param base64 Base64 string
     * @return Original string
     */
    public static String fromBase64(String base64) {
        byte[] data = Base64.decode(base64, Base64.DEFAULT);
        String origin = new String(data);
        return origin;
    }

    /**
     * Generates string for Basic authorization
     *
     * @param username Username
     * @param password Password
     * @return Basic authorization string
     */
    public static String generateBasicAuthorization(String username, String password) {
        String credentials = String.format("%1$s:%2$s", username, password);
        String basic = String.format("Basic %1$s", toBase64(credentials)).trim();
        return basic;
    }
}
import com.khmelenko.lab.varis.BuildConfig;
import com.khmelenko.lab.varis.RxJavaRules;
import com.khmelenko.lab.varis.auth.AuthPresenter;
import com.khmelenko.lab.varis.dagger.DaggerTestComponent;
import com.khmelenko.lab.varis.dagger.TestComponent;
import com.khmelenko.lab.varis.network.request.AccessTokenRequest;
import com.khmelenko.lab.varis.network.request.AuthorizationRequest;
import com.khmelenko.lab.varis.network.response.AccessToken;
import com.khmelenko.lab.varis.network.response.Authorization;
import com.khmelenko.lab.varis.network.retrofit.github.GitHubRestClient;
import com.khmelenko.lab.varis.network.retrofit.github.GithubApiService;
import com.khmelenko.lab.varis.network.retrofit.travis.TravisRestClient;
import com.khmelenko.lab.varis.storage.AppSettings;
import com.khmelenko.lab.varis.util.EncryptionUtils;
import com.khmelenko.lab.varis.auth.AuthView;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import javax.inject.Inject;
import io.reactivex.Single;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.ResponseBody;
import retrofit2.HttpException;
import retrofit2.Response;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
package com.khmelenko.lab.varis.viewmodel;






/**
 * Testing {@link AuthPresenter}
 *
 * @author Dmytro Khmelenko (d.khmelenko@gmail.com)
 */
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class TestAuthPresenter {

    @Rule
    public RxJavaRules mRxJavaRules = new RxJavaRules();

    @Inject
    TravisRestClient mTravisRestClient;

    @Inject
    GitHubRestClient mGitHubRestClient;

    @Inject
    AppSettings mAppSettings;

    private AuthPresenter mAuthPresenter;
    private AuthView mAuthView;

    @Before
    public void setup() {
        TestComponent component = DaggerTestComponent.builder().build();
        component.inject(this);

        mAuthPresenter = spy(new AuthPresenter(mTravisRestClient, mGitHubRestClient, mAppSettings));
        mAuthView = mock(AuthView.class);
        mAuthPresenter.attach(mAuthView);
    }

    @Test
    public void testUpdateServer() {
        final String newEndpoint = "newEndpoint";
        mAuthPresenter.updateServer(newEndpoint);

        verify(mTravisRestClient).updateTravisEndpoint(newEndpoint);
    }

    @Test
    public void testLoginSuccess() {
        final String login = "login";
        final String password = "password";
        String auth = EncryptionUtils.generateBasicAuthorization(login, password);

        final String gitHubToken = "gitHubToken";
        Authorization authorization = new Authorization();
        authorization.setToken(gitHubToken);
        authorization.setId(1L);
        when(mGitHubRestClient.getApiService().createNewAuthorization(eq(auth), any(AuthorizationRequest.class)))
                .thenReturn(Single.just(authorization));

        final String accessToken = "token";
        AccessTokenRequest request = new AccessTokenRequest();
        request.setGithubToken(gitHubToken);
        AccessToken token = new AccessToken();