package social.grain import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd 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 kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import social.grain.data.api.ReportSubject import social.grain.data.model.GrainLabels import social.grain.feature.moderation.ReportViewModel 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 report sheet. * * The categories are the labels grain gives out, and a report names the * `reasonType` grain-serve files under the label picked. Reopening the sheet * has to start blank, or the second report on the same gallery opens onto the * first one's receipt. */ @RunWith(AndroidJUnit4::class) class ReportViewModelTest { @get:Rule val mainDispatcher = MainDispatcherRule() private lateinit var harness: Harness private val subject = ReportSubject.Record("at://did:plc:alice/social.grain.gallery/g1", "cid1") @Before fun setUp() { harness = Harness() } @After fun tearDown() = harness.tearDown() private fun viewModel() = ReportViewModel(harness.client, harness.auth, harness.labelDefinitions) @Test fun openingTheSheetListsGrainsLabelsAndPreSelectsTheFirst() { val model = viewModel() model.start(subject) val state = model.state.value assertEquals(GrainLabels.definitions.map { it.identifier }, state.categories.map { it.identifier }) assertEquals("copyright", state.selected) assertTrue(state.canSubmit) } @Test fun theSheetListsTheDefinitionsTheServerDescribes() { harness.server.on( "social.grain.moderation.describeLabels", """{"policies":{"labelValues":["!takedown","spam","gore"],"labelValueDefinitions":[ {"identifier":"spam","severity":"inform","blurs":"none","defaultSetting":"warn", "locales":[{"lang":"en","name":"Spam","description":"Unwanted."}]}, {"identifier":"gore","severity":"alert","blurs":"media","defaultSetting":"warn", "locales":[{"lang":"en","name":"Gore","description":"Blood."}]}]}}""", ) val model = viewModel() model.start(subject) val state = model.state.awaitValue("the described categories") { it.categories.size == 2 } assertEquals(listOf("spam", "gore"), state.categories.map { it.identifier }) assertEquals("spam", state.selected) } @Test fun aFailedDescribeLeavesTheBundledCategories() { harness.server.on("social.grain.moderation.describeLabels", """{"error":"InternalServerError"}""", code = 500) val model = viewModel() model.start(subject) harness.server.awaitCall("social.grain.moderation.describeLabels") Thread.sleep(200) assertEquals(GrainLabels.definitions, model.state.value.categories) assertEquals("copyright", model.state.value.selected) } @Test fun reopeningTheSheetClearsTheReceiptFromLastTime() { val model = viewModel() model.start(subject) model.submit() model.state.awaitValue("the receipt") { it.isSubmitted } model.start(subject) assertFalse("A fresh report doesn't open onto the last one", model.state.value.isSubmitted) } @Test fun submittingSendsTheChosenLabelAndTheTypedReason() { harness.server.on("com.atproto.moderation.createReport", """{"id":1}""") val model = viewModel() model.start(subject) model.select("gore") model.reasonState.setTextAndPlaceCursorAtEnd(" Not okay ") model.submit() model.state.awaitValue("the receipt") { it.isSubmitted } val call = harness.server.awaitCall("com.atproto.moderation.createReport") val body = call.body assertEquals( "tools.ozone.report.defs#reasonViolenceGraphicContent", call.json["reasonType"]!!.jsonPrimitive.content, ) assertEquals( "The PDS forwards the report to grain-serve", "did:web:stub.grain.test#grain_appview", call.headers["atproto-proxy"], ) assertTrue("Whitespace is trimmed off the reason", body.contains("\"reason\":\"Not okay\"")) assertTrue(body.contains("com.atproto.repo.strongRef")) } @Test fun anEmptyReasonIsLeftOutEntirely() { harness.server.on("com.atproto.moderation.createReport", """{"id":1}""") val model = viewModel() model.start(subject) model.submit() model.state.awaitValue("the receipt") { it.isSubmitted } assertNull(harness.server.awaitCall("com.atproto.moderation.createReport").json["reason"]) } @Test fun reportingAnAccountSendsARepoRef() { harness.server.on("com.atproto.moderation.createReport", """{"id":1}""") val model = viewModel() model.start(ReportSubject.Account("did:plc:bob")) model.submit() model.state.awaitValue("the receipt") { it.isSubmitted } val subject = harness.server.awaitCall("com.atproto.moderation.createReport").json["subject"]!!.jsonObject assertEquals("com.atproto.admin.defs#repoRef", subject["\$type"]!!.jsonPrimitive.content) assertEquals("did:plc:bob", subject["did"]!!.jsonPrimitive.content) } @Test fun aSubmitThatFailsSaysSoAndLeavesTheFormUsable() { harness.server.on("com.atproto.moderation.createReport", """{"error":"no"}""", code = 500) val model = viewModel() model.start(subject) model.submit() val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals("Couldn't submit that report. Try again.", state.errorMessage) assertFalse(state.isSubmitted) assertTrue(state.canSubmit) } @Test fun submittingBeforeTheSheetHasASubjectDoesNothing() { val model = viewModel() model.submit() settle() assertEquals(0, harness.server.countOf("com.atproto.moderation.createReport")) } }