Add espresso test for usernames.

fork-5.53.8
Alex Hart 2022-09-13 10:15:07 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 4882a4d11c
commit a340ebf74a
10 zmienionych plików z 333 dodań i 91 usunięć

Wyświetl plik

@ -561,6 +561,10 @@ dependencies {
androidTestImplementation testLibs.mockito.kotlin
androidTestImplementation testLibs.square.okhttp.mockserver
instrumentationImplementation (testLibs.androidx.fragment.testing) {
exclude group: 'androidx.test', module: 'core'
}
testImplementation testLibs.espresso.core
implementation libs.kotlin.stdlib.jdk8

Wyświetl plik

@ -0,0 +1,135 @@
package org.thoughtcrime.securesms.profiles.manage
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.reactivex.rxjava3.schedulers.TestScheduler
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class UsernameEditFragmentTest {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
private val ioScheduler = TestScheduler()
private val computationScheduler = TestScheduler()
@get:Rule
val testSchedulerRule = RxTestSchedulerRule(
ioTestScheduler = ioScheduler,
computationTestScheduler = computationScheduler
)
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testUsernameCreationInRegistration() {
val scenario = createScenario(true)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNull()
}
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Test
fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario()
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNotNull()
}
onView(withText(R.string.UsernameEditFragment_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Test
fun testNicknameUpdateHappyPath() {
val nickname = "Spiderman"
val discriminator = "4578"
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/username/reserved") {
MockResponse().success(ReserveUsernameResponse("$nickname#$discriminator", "reservationToken"))
},
Put("/v1/accounts/username/confirm") {
MockResponse().success()
}
)
val scenario = createScenario(isInRegistration = true)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname))
computationScheduler.advanceTimeBy(501, TimeUnit.MILLISECONDS)
computationScheduler.triggerActions()
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
ioScheduler.triggerActions()
computationScheduler.triggerActions()
onView(withId(R.id.username_text)).perform(closeSoftKeyboard())
onView(withId(R.id.username_done_button)).check(matches(isDisplayed()))
onView(withId(R.id.username_done_button)).check(matches(isEnabled()))
onView(withId(R.id.username_done_button)).perform(click())
computationScheduler.triggerActions()
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
}
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
return launchFragmentInContainer(
fragmentArgs = fragmentArgs,
themeResId = R.style.Signal_DayNight_NoActionBar
)
}
}

Wyświetl plik

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.testing
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.TestScheduler
import org.junit.rules.ExternalResource
/**
* JUnit Rule which initialises Rx thread schedulers. If a specific
* scheduler is not specified, it defaults to the `defaultTestScheduler`
*/
class RxTestSchedulerRule(
val defaultTestScheduler: TestScheduler = TestScheduler(),
val ioTestScheduler: TestScheduler = defaultTestScheduler,
val computationTestScheduler: TestScheduler = defaultTestScheduler,
val singleTestScheduler: TestScheduler = defaultTestScheduler,
val newThreadTestScheduler: TestScheduler = defaultTestScheduler,
) : ExternalResource() {
override fun before() {
RxJavaPlugins.setInitIoSchedulerHandler { ioTestScheduler }
RxJavaPlugins.setIoSchedulerHandler { ioTestScheduler }
RxJavaPlugins.setInitComputationSchedulerHandler { computationTestScheduler }
RxJavaPlugins.setComputationSchedulerHandler { computationTestScheduler }
RxJavaPlugins.setInitSingleSchedulerHandler { singleTestScheduler }
RxJavaPlugins.setSingleSchedulerHandler { singleTestScheduler }
RxJavaPlugins.setInitNewThreadSchedulerHandler { newThreadTestScheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { newThreadTestScheduler }
}
override fun after() {
RxJavaPlugins.reset()
}
}

Wyświetl plik

@ -35,7 +35,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.FragmentResultContract;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.UsernameUtil;
@ -91,7 +90,7 @@ public class UsernameEditFragment extends LoggingFragment {
viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory(args.getIsInRegistration())).get(UsernameEditViewModel.class);
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
lifecycleDisposable.add(viewModel.getEvents().subscribe(this::onEvent));
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
@ -142,6 +141,8 @@ public class UsernameEditFragment extends LoggingFragment {
suffixProgress = new ImageView(requireContext());
suffixProgress.setImageDrawable(getInProgressDrawable());
suffixProgress.setContentDescription(getString(R.string.load_more_header__loading));
suffixProgress.setVisibility(View.GONE);
suffixParent.addView(suffixProgress, 0, layoutParams);
suffixTextView.setOnClickListener(this::onLearnMore);

Wyświetl plik

@ -18,28 +18,29 @@ import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse;
import java.io.IOException;
import java.util.concurrent.Executor;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
class UsernameEditRepository {
private static final String TAG = Log.tag(UsernameEditRepository.class);
private final SignalServiceAccountManager accountManager;
private final Executor executor;
UsernameEditRepository() {
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.executor = SignalExecutors.UNBOUNDED;
}
void reserveUsername(@NonNull String nickname, @NonNull Callback<Result<ReserveUsernameResponse, UsernameSetResult>> callback) {
executor.execute(() -> callback.onComplete(reserveUsernameInternal(nickname)));
@NonNull Single<Result<ReserveUsernameResponse, UsernameSetResult>> reserveUsername(@NonNull String nickname) {
return Single.fromCallable(() -> reserveUsernameInternal(nickname)).subscribeOn(Schedulers.io());
}
void confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse, @NonNull Callback<UsernameSetResult> callback) {
executor.execute(() -> callback.onComplete(confirmUsernameInternal(reserveUsernameResponse)));
@NonNull Single<UsernameSetResult> confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
return Single.fromCallable(() -> confirmUsernameInternal(reserveUsernameResponse)).subscribeOn(Schedulers.io());
}
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
executor.execute(() -> callback.onComplete(deleteUsernameInternal()));
@NonNull Single<UsernameDeleteResult> deleteUsername() {
return Single.fromCallable(this::deleteUsernameInternal).subscribeOn(Schedulers.io());
}
@WorkerThread

Wyświetl plik

@ -3,14 +3,11 @@ package org.thoughtcrime.securesms.profiles.manage;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
import org.thoughtcrime.securesms.util.rx.RxStore;
@ -21,27 +18,29 @@ import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.processors.PublishProcessor;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
/**
* Manages the state around username updates.
*
* <p>
* A note on naming conventions:
*
* <p>
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
*
* <p>
* [nickname]#[discriminator]
*
* <p>
* The nickname is user-controlled, whereas the discriminator is controlled by the server.
*/
class UsernameEditViewModel extends ViewModel {
private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500;
private final SingleLiveEvent<Event> events;
private final PublishSubject<Event> events;
private final UsernameEditRepository repo;
private final RxStore<State> uiState;
private final PublishProcessor<String> nicknamePublisher;
@ -50,8 +49,9 @@ class UsernameEditViewModel extends ViewModel {
private UsernameEditViewModel(boolean isInRegistration) {
this.repo = new UsernameEditRepository();
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation());
this.events = new SingleLiveEvent<>();
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new)
.orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation());
this.events = PublishSubject.create();
this.nicknamePublisher = PublishProcessor.create();
this.disposables = new CompositeDisposable();
this.isInRegistration = isInRegistration;
@ -84,7 +84,7 @@ class UsernameEditViewModel extends ViewModel {
void onUsernameSkipped() {
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
events.setValue(Event.SKIPPED);
events.onNext(Event.SKIPPED);
}
void onUsernameSubmitted() {
@ -109,66 +109,67 @@ class UsernameEditViewModel extends ViewModel {
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState));
repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse(), (result) -> {
ThreadUtil.runOnMain(() -> {
String nickname = usernameState.getNickname();
Disposable confirmUsernameDisposable = repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse())
.subscribe(result -> {
String nickname = usernameState.getNickname();
switch (result) {
case SUCCESS:
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.SUBMIT_SUCCESS);
break;
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState));
events.postValue(Event.SUBMIT_FAIL_INVALID);
switch (result) {
case SUCCESS:
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation();
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
events.onNext(Event.SUBMIT_SUCCESS);
break;
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState));
events.onNext(Event.SUBMIT_FAIL_INVALID);
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState));
events.postValue(Event.SUBMIT_FAIL_TAKEN);
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState));
events.onNext(Event.SUBMIT_FAIL_TAKEN);
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.NETWORK_FAILURE);
break;
}
});
});
if (nickname != null) {
onNicknameUpdated(nickname);
}
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState));
events.onNext(Event.NETWORK_FAILURE);
break;
}
});
disposables.add(confirmUsernameDisposable);
}
void onUsernameDeleted() {
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState));
repo.deleteUsername((result) -> {
ThreadUtil.runOnMain(() -> {
switch (result) {
case SUCCESS:
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.DELETE_SUCCESS);
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState));
events.postValue(Event.NETWORK_FAILURE);
break;
}
});
Disposable deletionDisposable = repo.deleteUsername().subscribe(result -> {
switch (result) {
case SUCCESS:
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState));
events.onNext(Event.DELETE_SUCCESS);
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState));
events.onNext(Event.NETWORK_FAILURE);
break;
}
});
disposables.add(deletionDisposable);
}
@NonNull Flowable<State> getUiState() {
return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Event> getEvents() {
return events;
@NonNull Observable<Event> getEvents() {
return events.observeOn(AndroidSchedulers.mainThread());
}
private void onNicknameChanged(@NonNull String nickname) {
@ -177,33 +178,33 @@ class UsernameEditViewModel extends ViewModel {
}
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE));
repo.reserveUsername(nickname, result -> {
ThreadUtil.runOnMain(() -> {
result.either(
reserveUsernameJsonResponse -> {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse)));
return null;
},
failure -> {
switch (failure) {
case SUCCESS:
throw new AssertionError();
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE));
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
events.postValue(Event.NETWORK_FAILURE);
break;
}
Disposable reserveDisposable = repo.reserveUsername(nickname).subscribe(result -> {
result.either(
reserveUsernameJsonResponse -> {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse)));
return null;
},
failure -> {
switch (failure) {
case SUCCESS:
throw new AssertionError();
case USERNAME_INVALID:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE));
break;
case USERNAME_UNAVAILABLE:
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
break;
case NETWORK_ERROR:
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
events.onNext(Event.NETWORK_FAILURE);
break;
}
return null;
});
});
return null;
});
});
disposables.add(reserveDisposable);
}
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {

Wyświetl plik

@ -9,6 +9,7 @@ import android.util.AttributeSet
import android.view.ViewAnimationUtils
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.core.animation.doOnEnd
import androidx.core.content.withStyledAttributes
import com.google.android.material.button.MaterialButton
@ -88,6 +89,11 @@ class CircularProgressMaterialButton @JvmOverloads constructor(
materialButton.setOnClickListener(onClickListener)
}
@VisibleForTesting
fun getRequestedState(): State {
return requestedState
}
fun setSpinning() {
transformTo(State.PROGRESS, true)
}

Wyświetl plik

@ -132,6 +132,7 @@ dependencyResolutionManagement {
alias('androidx-test-core-ktx').to('androidx.test', 'core-ktx').versionRef('androidx-test')
alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1')
alias('androidx-test-ext-junit-ktx').to('androidx.test.ext:junit-ktx:1.1.1')
alias('androidx-fragment-testing').to('androidx.fragment:fragment-testing:1.3.2')
alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0')
alias('mockito-core').to('org.mockito:mockito-inline:4.6.1')
alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0')

Wyświetl plik

@ -229,6 +229,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.1.0">
<artifact name="core-1.1.0.aar">
<sha256 value="76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.2.0">
<artifact name="core-1.2.0.aar">
<sha256 value="524b8b88ceb6a74a7e44e6b567a135660f211799904cb218bfee5be1166820b2" origin="Generated by Gradle"/>
@ -374,6 +379,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f7b07632a0e21994a66a6dfb291a4f5ae55c2699827ff45df071168560509c9b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment" version="1.3.2">
<artifact name="fragment-1.3.2.aar">
<sha256 value="299879e2ce39d35214391db4ef2d7257a801f9b7fbe76488cc64897d16a98b25" origin="Generated by Gradle"/>
</artifact>
<artifact name="fragment-1.3.2.module">
<sha256 value="769e3922eb5500d2e10786f99abd3cf75a24d754f1dfcdad47d8f297b973bcc1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment" version="1.3.5">
<artifact name="fragment-1.3.5.aar">
<sha256 value="a2826109ce34d6bc3792c7791e911e526f3d9c5f8b41f7ff1650638897a5d445" origin="Generated by Gradle"/>
@ -382,6 +395,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="cfeb7db9039743b44fc585590bf8af571bd04309d3d1aa86251cf6e07779ea97" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment-ktx" version="1.3.2">
<artifact name="fragment-ktx-1.3.2.aar">
<sha256 value="29af1e9ee0e93b5fc638600c230705584aecc49205c363f0923ba1e5be675533" origin="Generated by Gradle"/>
</artifact>
<artifact name="fragment-ktx-1.3.2.module">
<sha256 value="b3955b619e8a16c38af39c19126867c72d1954db05551709e58c082b946078c4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment-ktx" version="1.3.5">
<artifact name="fragment-ktx-1.3.5.aar">
<sha256 value="549965cc33b69270b7b3ba5d9fcb2cd746ae9ceca17c5e12219888ea281edc0f" origin="Generated by Gradle"/>
@ -390,6 +411,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8198cba5ec555ae47332297219f984e8e686504e03a0c627a26892687dc2fb8a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.fragment" name="fragment-testing" version="1.3.2">
<artifact name="fragment-testing-1.3.2.aar">
<sha256 value="c9e4b3bfd105fecf3aff8a9105fdccf2e86ff6d5e53dc342a3aa65c087c1591e" origin="Generated by Gradle"/>
</artifact>
<artifact name="fragment-testing-1.3.2.module">
<sha256 value="75714382c8f9e292b01762852fd0672d33973254c2bd2b9918b50f861792b171" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.gridlayout" name="gridlayout" version="1.0.0">
<artifact name="gridlayout-1.0.0.aar">
<sha256 value="a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755" origin="Generated by Gradle"/>
@ -804,6 +833,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="core" version="1.3.0">
<artifact name="core-1.3.0.aar">
<sha256 value="86549cae8c5b848f817e2c716e174c7dab61caf0b4df9848680eeb753089a337" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="core" version="1.4.0">
<artifact name="core-1.4.0.aar">
<sha256 value="671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" origin="Generated by Gradle"/>
@ -814,6 +848,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="e4f9ca2b8f700cc278d878ed3925730e1ad5d60135bbfd05ab6708a528ebfa58" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="monitor" version="1.3.0">
<artifact name="monitor-1.3.0.aar">
<sha256 value="f73a31306a783e63150c60c49e140dc38da39a1b7947690f4b73387b5ebad77e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.test" name="monitor" version="1.4.0">
<artifact name="monitor-1.4.0.aar">
<sha256 value="46a912a1e175f27a97521af3f50e5af87c22c49275dd2c57c043740012806325" origin="Generated by Gradle"/>
@ -3138,6 +3177,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5ace22b102a96425e4ac44e0558b927f3857b56a33cbc289cf1b70aee645e6a7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.4.20">
<artifact name="kotlin-stdlib-1.4.20.jar">
<sha256 value="b8ab1da5cdc89cb084d41e1f28f20a42bd431538642a5741c52bbfae3fa3e656" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.4.21">
<artifact name="kotlin-stdlib-1.4.21.jar">
<sha256 value="f78c5d8c09db985912ab83a1de3c3b53ddf208d7b151f06a72358ea3e137d01b" origin="Generated by Gradle"/>
@ -3173,6 +3217,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="4681f2d436a68c7523595d84ed5758e1382f9da0f67c91e6a848690d711274fe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.4.20">
<artifact name="kotlin-stdlib-common-1.4.20.jar">
<sha256 value="a7112c9b3cefee418286c9c9372f7af992bd1e6e030691d52f60cb36dbec8320" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.4.21">
<artifact name="kotlin-stdlib-common-1.4.21.jar">
<sha256 value="812cf197d9c4c67e1f47f95e2d72a9b600f0d1124560617bfe9850773eccbcff" origin="Generated by Gradle"/>

Wyświetl plik

@ -11,6 +11,14 @@ public class ReserveUsernameResponse {
ReserveUsernameResponse() {}
/**
* Visible for testing.
*/
public ReserveUsernameResponse(String username, String reservationToken) {
this.username = username;
this.reservationToken = reservationToken;
}
public String getUsername() {
return username;
}