Update ProfileFragmentViewModel
This commit is contained in:
parent
b1628492f5
commit
1ebf7a2e4b
@ -192,6 +192,7 @@ dependencies {
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
def room_version = "2.3.0"
|
def room_version = "2.3.0"
|
||||||
@ -244,6 +245,7 @@ dependencies {
|
|||||||
testImplementation "androidx.test:core-ktx:1.3.0"
|
testImplementation "androidx.test:core-ktx:1.3.0"
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
testImplementation "org.robolectric:robolectric:4.5.1"
|
testImplementation "org.robolectric:robolectric:4.5.1"
|
||||||
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
|
||||||
|
|
||||||
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
|
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
|
||||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||||
|
@ -6,11 +6,12 @@ import androidx.savedstate.SavedStateRegistryOwner
|
|||||||
import awais.instagrabber.db.repositories.AccountRepository
|
import awais.instagrabber.db.repositories.AccountRepository
|
||||||
import awais.instagrabber.db.repositories.FavoriteRepository
|
import awais.instagrabber.db.repositories.FavoriteRepository
|
||||||
import awais.instagrabber.managers.DirectMessagesManager
|
import awais.instagrabber.managers.DirectMessagesManager
|
||||||
import awais.instagrabber.models.enums.BroadcastItemType
|
|
||||||
import awais.instagrabber.models.Resource
|
import awais.instagrabber.models.Resource
|
||||||
import awais.instagrabber.repositories.responses.User
|
import awais.instagrabber.repositories.responses.User
|
||||||
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
|
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
|
||||||
import awais.instagrabber.webservices.*
|
import awais.instagrabber.webservices.*
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
class ProfileFragmentViewModel(
|
class ProfileFragmentViewModel(
|
||||||
state: SavedStateHandle,
|
state: SavedStateHandle,
|
||||||
@ -21,12 +22,61 @@ class ProfileFragmentViewModel(
|
|||||||
graphQLRepository: GraphQLRepository,
|
graphQLRepository: GraphQLRepository,
|
||||||
accountRepository: AccountRepository,
|
accountRepository: AccountRepository,
|
||||||
favoriteRepository: FavoriteRepository,
|
favoriteRepository: FavoriteRepository,
|
||||||
|
ioDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _profile = MutableLiveData<Resource<User?>>(Resource.loading(null))
|
private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null))
|
||||||
private val _isLoggedIn = MutableLiveData(false)
|
|
||||||
private var messageManager: DirectMessagesManager? = null
|
private var messageManager: DirectMessagesManager? = null
|
||||||
|
|
||||||
val profile: LiveData<Resource<User?>> = _profile
|
val currentUser: LiveData<Resource<User?>> = _currentUser
|
||||||
|
val isLoggedIn: LiveData<Boolean> = currentUser.map { it.data != null }
|
||||||
|
|
||||||
|
private val currentUserAndStateUsernameLiveData: LiveData<Pair<Resource<User?>, Resource<String?>>> =
|
||||||
|
object : MediatorLiveData<Pair<Resource<User?>, Resource<String?>>>() {
|
||||||
|
var user: Resource<User?> = Resource.loading(null)
|
||||||
|
var stateUsername: Resource<String?> = Resource.loading(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
addSource(currentUser) { currentUser ->
|
||||||
|
this.user = currentUser
|
||||||
|
value = currentUser to stateUsername
|
||||||
|
}
|
||||||
|
addSource(state.getLiveData<String?>("username")) { username ->
|
||||||
|
this.stateUsername = Resource.success(username)
|
||||||
|
value = user to this.stateUsername
|
||||||
|
}
|
||||||
|
// trigger currentUserAndStateUsernameLiveData switch map with a state username success resource
|
||||||
|
if (!state.contains("username")) {
|
||||||
|
this.stateUsername = Resource.success(null)
|
||||||
|
value = user to this.stateUsername
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap {
|
||||||
|
val (userResource, stateUsernameResource) = it
|
||||||
|
liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
|
||||||
|
if (userResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) {
|
||||||
|
emit(Resource.loading(null))
|
||||||
|
return@liveData
|
||||||
|
}
|
||||||
|
val user = userResource.data
|
||||||
|
val stateUsername = stateUsernameResource.data
|
||||||
|
if (stateUsername.isNullOrBlank()) {
|
||||||
|
emit(Resource.success(user))
|
||||||
|
return@liveData
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val fetchedUser = if (user != null) {
|
||||||
|
userRepository.getUsernameInfo(stateUsername) // logged in
|
||||||
|
} else {
|
||||||
|
graphQLRepository.fetchUser(stateUsername) // anonymous
|
||||||
|
}
|
||||||
|
emit(Resource.success(fetchedUser))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emit(Resource.error(e.message, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Username of profile without '`@`'
|
* Username of profile without '`@`'
|
||||||
@ -37,30 +87,12 @@ class ProfileFragmentViewModel(
|
|||||||
Resource.Status.SUCCESS -> it.data?.username ?: ""
|
Resource.Status.SUCCESS -> it.data?.username ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isLoggedIn: LiveData<Boolean> = _isLoggedIn
|
|
||||||
|
|
||||||
var currentUser: Resource<User?>? = null
|
|
||||||
set(value) {
|
|
||||||
_isLoggedIn.postValue(value?.data != null)
|
|
||||||
// if no profile, and value is valid, set it as profile
|
|
||||||
val profileValue = profile.value
|
|
||||||
if (
|
|
||||||
profileValue?.status != Resource.Status.LOADING
|
|
||||||
&& profileValue?.data == null
|
|
||||||
&& value?.status == Resource.Status.SUCCESS
|
|
||||||
&& value.data != null
|
|
||||||
) {
|
|
||||||
_profile.postValue(Resource.success(value.data))
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Log.d(TAG, "${state.keys()} $userRepository $friendshipRepository $storiesRepository $mediaRepository")
|
// Log.d(TAG, "${state.keys()} $userRepository $friendshipRepository $storiesRepository $mediaRepository")
|
||||||
val usernameFromState = state.get<String?>("username")
|
}
|
||||||
if (usernameFromState.isNullOrBlank()) {
|
|
||||||
_profile.postValue(Resource.success(null))
|
fun setCurrentUser(currentUser: Resource<User?>) {
|
||||||
}
|
_currentUser.postValue(currentUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareDm(result: RankedRecipient) {
|
fun shareDm(result: RankedRecipient) {
|
||||||
@ -104,6 +136,7 @@ class ProfileFragmentViewModelFactory(
|
|||||||
graphQLRepository,
|
graphQLRepository,
|
||||||
accountRepository,
|
accountRepository,
|
||||||
favoriteRepository,
|
favoriteRepository,
|
||||||
|
Dispatchers.IO,
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import org.json.JSONObject
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class GraphQLRepository(private val service: GraphQLService) {
|
open class GraphQLRepository(private val service: GraphQLService) {
|
||||||
|
|
||||||
// TODO convert string response to a response class
|
// TODO convert string response to a response class
|
||||||
private suspend fun fetch(
|
private suspend fun fetch(
|
||||||
@ -176,7 +176,7 @@ class GraphQLRepository(private val service: GraphQLService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO convert string response to a response class
|
// TODO convert string response to a response class
|
||||||
suspend fun fetchUser(
|
open suspend fun fetchUser(
|
||||||
username: String,
|
username: String,
|
||||||
): User {
|
): User {
|
||||||
val response = service.getUser(username)
|
val response = service.getUser(username)
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Google LLC
|
||||||
|
*
|
||||||
|
* 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 awais.instagrabber
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineScope
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MainCoroutineRule installs a TestCoroutineDispatcher for Disptachers.Main.
|
||||||
|
*
|
||||||
|
* Since it extends TestCoroutineScope, you can directly launch coroutines on the MainCoroutineRule
|
||||||
|
* as a [CoroutineScope]:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* mainCoroutineRule.launch { aTestCoroutine() }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* All coroutines started on [MainCoroutineScopeRule] must complete (including timeouts) before the test
|
||||||
|
* finishes, or it will throw an exception.
|
||||||
|
*
|
||||||
|
* When using MainCoroutineRule you should always invoke runBlockingTest on it to avoid creating two
|
||||||
|
* instances of [TestCoroutineDispatcher] or [TestCoroutineScope] in your test:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* @Test
|
||||||
|
* fun usingRunBlockingTest() = mainCoroutineRule.runBlockingTest {
|
||||||
|
* aTestCoroutine()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You may call [DelayController] methods on [MainCoroutineScopeRule] and they will control the
|
||||||
|
* virtual-clock.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* mainCoroutineRule.pauseDispatcher()
|
||||||
|
* // do some coroutines
|
||||||
|
* mainCoroutineRule.advanceUntilIdle() // run all pending coroutines until the dispatcher is idle
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* By default, [MainCoroutineScopeRule] will be in a *resumed* state.
|
||||||
|
*
|
||||||
|
* @param dispatcher if provided, this [TestCoroutineDispatcher] will be used.
|
||||||
|
*/
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class MainCoroutineScopeRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
|
||||||
|
TestWatcher(),
|
||||||
|
TestCoroutineScope by TestCoroutineScope(dispatcher) {
|
||||||
|
override fun starting(description: Description?) {
|
||||||
|
super.starting(description)
|
||||||
|
// If your codebase allows the injection of other dispatchers like
|
||||||
|
// Dispatchers.Default and Dispatchers.IO, consider injecting all of them here
|
||||||
|
// and renaming this class to `CoroutineScopeRule`
|
||||||
|
//
|
||||||
|
// All injected dispatchers in a test should point to a single instance of
|
||||||
|
// TestCoroutineDispatcher.
|
||||||
|
Dispatchers.setMain(dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finished(description: Description?) {
|
||||||
|
super.finished(description)
|
||||||
|
cleanupTestCoroutines()
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package awais.instagrabber.viewmodels
|
|||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import awais.instagrabber.MainCoroutineScopeRule
|
||||||
import awais.instagrabber.common.*
|
import awais.instagrabber.common.*
|
||||||
import awais.instagrabber.db.datasources.AccountDataSource
|
import awais.instagrabber.db.datasources.AccountDataSource
|
||||||
import awais.instagrabber.db.datasources.FavoriteDataSource
|
import awais.instagrabber.db.datasources.FavoriteDataSource
|
||||||
@ -12,6 +13,7 @@ import awais.instagrabber.getOrAwaitValue
|
|||||||
import awais.instagrabber.models.Resource
|
import awais.instagrabber.models.Resource
|
||||||
import awais.instagrabber.repositories.responses.User
|
import awais.instagrabber.repositories.responses.User
|
||||||
import awais.instagrabber.webservices.*
|
import awais.instagrabber.webservices.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@ -21,12 +23,22 @@ import org.junit.runner.RunWith
|
|||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
internal class ProfileFragmentViewModelTest {
|
internal class ProfileFragmentViewModelTest {
|
||||||
|
|
||||||
|
private val testPublicUser = User(
|
||||||
|
pk = 100,
|
||||||
|
username = "test",
|
||||||
|
fullName = "Test user"
|
||||||
|
)
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var instantExecutorRule = InstantTaskExecutorRule()
|
var instantExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@get:Rule
|
||||||
|
val coroutineScope = MainCoroutineScopeRule()
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@Test
|
@Test
|
||||||
fun testNoUsernameNoCurrentUser() {
|
fun testNoUsernameNoCurrentUser() {
|
||||||
val accountDataSource = AccountDataSource(AccountDaoAdapter())
|
|
||||||
val viewModel = ProfileFragmentViewModel(
|
val viewModel = ProfileFragmentViewModel(
|
||||||
SavedStateHandle(),
|
SavedStateHandle(),
|
||||||
UserRepository(UserServiceAdapter()),
|
UserRepository(UserServiceAdapter()),
|
||||||
@ -34,46 +46,75 @@ internal class ProfileFragmentViewModelTest {
|
|||||||
StoriesRepository(StoriesServiceAdapter()),
|
StoriesRepository(StoriesServiceAdapter()),
|
||||||
MediaRepository(MediaServiceAdapter()),
|
MediaRepository(MediaServiceAdapter()),
|
||||||
GraphQLRepository(GraphQLServiceAdapter()),
|
GraphQLRepository(GraphQLServiceAdapter()),
|
||||||
AccountRepository(accountDataSource),
|
AccountRepository(AccountDataSource(AccountDaoAdapter())),
|
||||||
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter()))
|
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
|
||||||
|
coroutineScope.dispatcher,
|
||||||
)
|
)
|
||||||
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
||||||
|
viewModel.setCurrentUser(Resource.success(null))
|
||||||
assertNull(viewModel.profile.getOrAwaitValue().data)
|
assertNull(viewModel.profile.getOrAwaitValue().data)
|
||||||
assertEquals("", viewModel.username.getOrAwaitValue())
|
assertEquals("", viewModel.username.getOrAwaitValue())
|
||||||
viewModel.currentUser = Resource.success(null)
|
viewModel.setCurrentUser(Resource.success(null))
|
||||||
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@Test
|
@Test
|
||||||
fun testNoUsernameWithCurrentUser() {
|
fun testNoUsernameWithCurrentUser() {
|
||||||
// val state = SavedStateHandle(
|
|
||||||
// mutableMapOf<String, Any?>(
|
|
||||||
// "username" to "test"
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
val userRepository = UserRepository(UserServiceAdapter())
|
|
||||||
val friendshipRepository = FriendshipRepository(FriendshipServiceAdapter())
|
|
||||||
val storiesRepository = StoriesRepository(StoriesServiceAdapter())
|
|
||||||
val mediaRepository = MediaRepository(MediaServiceAdapter())
|
|
||||||
val graphQLRepository = GraphQLRepository(GraphQLServiceAdapter())
|
|
||||||
val accountDataSource = AccountDataSource(AccountDaoAdapter())
|
|
||||||
val accountRepository = AccountRepository(accountDataSource)
|
|
||||||
val favoriteRepository = FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter()))
|
|
||||||
val viewModel = ProfileFragmentViewModel(
|
val viewModel = ProfileFragmentViewModel(
|
||||||
SavedStateHandle(),
|
SavedStateHandle(),
|
||||||
userRepository,
|
UserRepository(UserServiceAdapter()),
|
||||||
friendshipRepository,
|
FriendshipRepository(FriendshipServiceAdapter()),
|
||||||
storiesRepository,
|
StoriesRepository(StoriesServiceAdapter()),
|
||||||
mediaRepository,
|
MediaRepository(MediaServiceAdapter()),
|
||||||
graphQLRepository,
|
GraphQLRepository(GraphQLServiceAdapter()),
|
||||||
accountRepository,
|
AccountRepository(AccountDataSource(AccountDaoAdapter())),
|
||||||
favoriteRepository
|
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
|
||||||
|
coroutineScope.dispatcher,
|
||||||
)
|
)
|
||||||
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
||||||
assertNull(viewModel.profile.getOrAwaitValue().data)
|
assertNull(viewModel.profile.getOrAwaitValue().data)
|
||||||
val user = User()
|
val user = User()
|
||||||
viewModel.currentUser = Resource.success(user)
|
viewModel.setCurrentUser(Resource.success(user))
|
||||||
assertEquals(true, viewModel.isLoggedIn.getOrAwaitValue())
|
assertEquals(true, viewModel.isLoggedIn.getOrAwaitValue())
|
||||||
assertEquals(user, viewModel.profile.getOrAwaitValue().data)
|
var profile = viewModel.profile.getOrAwaitValue()
|
||||||
|
while (profile.status == Resource.Status.LOADING) {
|
||||||
|
profile = viewModel.profile.getOrAwaitValue()
|
||||||
|
}
|
||||||
|
assertEquals(user, profile.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Test
|
||||||
|
fun testPublicUsernameWithNoCurrentUser() {
|
||||||
|
// username without `@`
|
||||||
|
val state = SavedStateHandle(
|
||||||
|
mutableMapOf<String, Any?>(
|
||||||
|
"username" to testPublicUser.username
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val graphQLRepository = object : GraphQLRepository(GraphQLServiceAdapter()) {
|
||||||
|
override suspend fun fetchUser(username: String): User {
|
||||||
|
return testPublicUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val viewModel = ProfileFragmentViewModel(
|
||||||
|
state,
|
||||||
|
UserRepository(UserServiceAdapter()),
|
||||||
|
FriendshipRepository(FriendshipServiceAdapter()),
|
||||||
|
StoriesRepository(StoriesServiceAdapter()),
|
||||||
|
MediaRepository(MediaServiceAdapter()),
|
||||||
|
graphQLRepository,
|
||||||
|
AccountRepository(AccountDataSource(AccountDaoAdapter())),
|
||||||
|
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
|
||||||
|
coroutineScope.dispatcher,
|
||||||
|
)
|
||||||
|
viewModel.setCurrentUser(Resource.success(null))
|
||||||
|
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
|
||||||
|
var profile = viewModel.profile.getOrAwaitValue()
|
||||||
|
while (profile.status == Resource.Status.LOADING) {
|
||||||
|
profile = viewModel.profile.getOrAwaitValue()
|
||||||
|
}
|
||||||
|
assertEquals(testPublicUser, profile.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user