package social.grain import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import social.grain.feature.settings.ModerationMode import social.grain.feature.settings.ModerationViewModel import social.grain.support.Harness import social.grain.support.MainDispatcherRule import social.grain.support.awaitCall import social.grain.support.awaitValue import social.grain.support.settle /** * The blocked and muted lists in Settings. * * A row here leaves only once the write lands — unlike a favourite, this is a * list of deliberate decisions, and a row that vanishes and then comes back * reads as the app undoing something the reader chose. */ @RunWith(AndroidJUnit4::class) class ModerationViewModelTest { @get:Rule val mainDispatcher = MainDispatcherRule() private lateinit var harness: Harness @Before fun setUp() { harness = Harness() } @After fun tearDown() = harness.tearDown() private fun viewModel(mode: ModerationMode) = ModerationViewModel( client = harness.client, auth = harness.auth, savedStateHandle = SavedStateHandle(mapOf("mode" to mode)), ) @Test fun theBlockedListCarriesTheRecordUriThatUndoesEachBlock() { harness.server.on( "social.grain.unspecced.getBlocks", """{"items":[{"did":"did:plc:bob","handle":"bob.test","blockUri":"at://did:plc:me/social.grain.graph.block/b1"}]}""", ) val model = viewModel(ModerationMode.BLOCKED) val state = model.state.awaitValue("the list") { !it.isLoading } assertEquals("Blocked users", state.title) assertEquals("Unblock", state.actionLabel) assertEquals("You haven't blocked anyone.", state.emptyText) assertEquals("at://did:plc:me/social.grain.graph.block/b1", state.actors.single().blockUri) } @Test fun theMutedListHasNoRecordToDeleteAndItsOwnWording() { harness.server.on( "social.grain.unspecced.getMutes", """{"items":[{"did":"did:plc:carol","handle":"carol.test"}]}""", ) val state = viewModel(ModerationMode.MUTED).state.awaitValue("the list") { !it.isLoading } assertEquals("Muted users", state.title) assertEquals("Unmute", state.actionLabel) assertEquals("You haven't muted anyone.", state.emptyText) assertNull(state.actors.single().blockUri) } @Test fun unblockingDeletesTheRecordAndThenRemovesTheRow() { harness.server.on( "social.grain.unspecced.getBlocks", """{"items":[{"did":"did:plc:bob","blockUri":"at://did:plc:me/social.grain.graph.block/b1"}]}""", ) val model = viewModel(ModerationMode.BLOCKED) val actor = model.state.awaitValue("the list") { it.actors.isNotEmpty() }.actors.single() model.remove(actor) assertTrue(harness.server.awaitCall("dev.hatk.deleteRecord").body.contains("\"rkey\":\"b1\"")) val state = model.state.awaitValue("the row to go") { it.actors.isEmpty() } assertTrue(state.pending.isEmpty()) } @Test fun unmutingCallsTheGraphEndpoint() { harness.server.on( "social.grain.unspecced.getMutes", """{"items":[{"did":"did:plc:carol"}]}""", ) val model = viewModel(ModerationMode.MUTED) val actor = model.state.awaitValue("the list") { it.actors.isNotEmpty() }.actors.single() model.remove(actor) assertTrue( harness.server.awaitCall("social.grain.graph.unmuteActor").body.contains("did:plc:carol"), ) model.state.awaitValue("the row to go") { it.actors.isEmpty() } } @Test fun aRemovalThatFailsKeepsTheRowAndSaysSo() { harness.server .on( "social.grain.unspecced.getBlocks", """{"items":[{"did":"did:plc:bob","blockUri":"at://x/social.grain.graph.block/b1"}]}""", ) .on("dev.hatk.deleteRecord", """{"error":"no"}""", code = 500) val model = viewModel(ModerationMode.BLOCKED) val actor = model.state.awaitValue("the list") { it.actors.isNotEmpty() }.actors.single() model.remove(actor) val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals(1, state.actors.size) assertEquals("Couldn't do that. Try again.", state.errorMessage) assertTrue(state.pending.isEmpty()) model.dismissError() assertNull(model.state.value.errorMessage) } @Test fun signedOutThereIsNothingToListAndNothingToUndo() { harness.tearDown() harness = Harness(signedIn = false) val model = viewModel(ModerationMode.BLOCKED) val state = model.state.awaitValue("the load to finish") { !it.isLoading } assertTrue(state.actors.isEmpty()) assertEquals( "A signed-out viewer has no lists to read", 0, harness.server.countOf("social.grain.unspecced.getBlocks"), ) model.remove( social.grain.feature.settings.ModeratedActor( did = "did:plc:bob", handle = null, displayName = null, avatar = null, blockUri = "at://x/social.grain.graph.block/b1", ), ) settle() assertEquals(0, harness.server.countOf("dev.hatk.deleteRecord")) assertTrue(model.state.value.pending.isEmpty()) } @Test fun aFailedListLoadSaysSo() { harness.server.on("social.grain.unspecced.getBlocks", """{"error":"no"}""", code = 500) val state = viewModel(ModerationMode.BLOCKED) .state.awaitValue("the error") { it.errorMessage != null } assertEquals("Couldn't load that list.", state.errorMessage) assertFalse(state.isLoading) } }