diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 9dd5706d..81fa8a8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -33,6 +33,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; @@ -52,6 +53,7 @@ import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Filter; +import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; @@ -445,7 +447,8 @@ public abstract class SFragment extends BaseFragment implements Injectable { }); } - void reloadFilters(boolean forceRefresh) { + @VisibleForTesting + public void reloadFilters(boolean forceRefresh) { if (filters != null && !forceRefresh) { applyFilters(forceRefresh); return; @@ -469,7 +472,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { }); } - protected boolean filterIsRelevant(Filter filter) { + protected boolean filterIsRelevant(@NonNull Filter filter) { // Called when building local filter expression // Override to select relevant filters for your fragment return false; @@ -480,7 +483,17 @@ public abstract class SFragment extends BaseFragment implements Injectable { // Override to refresh your fragment } - boolean shouldFilterStatus(Status status) { + @VisibleForTesting + public boolean shouldFilterStatus(Status status) { + + if(filterRemoveRegex && status.getPoll() != null) { + for(PollOption option: status.getPoll().getOptions()) { + if(filterRemoveRegexMatcher.reset(option.getTitle()).find()) { + return true; + } + } + } + return (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find() || (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find()))); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index a4dc23b8..44937d15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -376,7 +376,7 @@ public class TimelineFragment extends SFragment implements } @Override - protected boolean filterIsRelevant(Filter filter) { + protected boolean filterIsRelevant(@NonNull Filter filter) { return filterContextMatchesKind(kind, filter.getContext()); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 00464311..825cd23c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -686,7 +686,7 @@ public final class ViewThreadFragment extends SFragment implements } @Override - protected boolean filterIsRelevant(Filter filter) { + protected boolean filterIsRelevant(@NonNull Filter filter) { return filter.getContext().contains(Filter.THREAD); } diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt new file mode 100644 index 00000000..70458644 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -0,0 +1,238 @@ +package com.keylesspalace.tusky + +import android.os.Bundle +import android.text.SpannedString +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.PollOption +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.network.MastodonApi +import com.nhaarman.mockitokotlin2.mock +import okhttp3.Request +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.Robolectric +import org.robolectric.annotation.Config +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* + +@Config(application = FakeTuskyApplication::class) +@RunWith(AndroidJUnit4::class) +class FilterTest { + + private val fragment = FakeFragment() + + @Before + fun setup() { + + val controller = Robolectric.buildActivity(FakeActivity::class.java) + val activity = controller.get() + + activity.accountManager = mock() + activity.themeUtils = mock() + val apiMock = Mockito.mock(MastodonApi::class.java) + Mockito.`when`(apiMock.getFilters()).thenReturn(object: Call> { + override fun isExecuted(): Boolean { + return false + } + override fun clone(): Call> { + throw Error("not implemented") + } + override fun isCanceled(): Boolean { + throw Error("not implemented") + } + override fun cancel() { + throw Error("not implemented") + } + override fun execute(): Response> { + throw Error("not implemented") + } + override fun request(): Request { + throw Error("not implemented") + } + + override fun enqueue(callback: Callback>) { + callback.onResponse( + this, + Response.success( + listOf( + Filter( + id = "123", + phrase = "badWord", + context = listOf(Filter.HOME), + expiresAt = null, + irreversible = false, + wholeWord = false + ), + Filter( + id = "123", + phrase = "badWholeWord", + context = listOf(Filter.HOME, Filter.PUBLIC), + expiresAt = null, + irreversible = false, + wholeWord = true + ), + Filter( + id = "123", + phrase = "wrongContext", + context = listOf(Filter.PUBLIC), + expiresAt = null, + irreversible = false, + wholeWord = true + ) + ) + ) + ) + } + }) + + activity.mastodonApi = apiMock + + + controller.create().start() + + fragment.mastodonApi = apiMock + + + activity.supportFragmentManager.beginTransaction() + .replace(R.id.activity_main, fragment, "fragment") + .commit() + + fragment.reloadFilters(false) + + } + + @Test + fun shouldNotFilter() { + assertFalse(fragment.shouldFilterStatus( + mockStatus(content = "should not be filtered") + )) + } + + @Test + fun shouldNotFilter_whenContextDoesNotMatch() { + assertFalse(fragment.shouldFilterStatus( + mockStatus(content = "one two wrongContext three") + )) + } + + @Test + fun shouldFilter_whenContentMatchesBadWord() { + assertTrue(fragment.shouldFilterStatus( + mockStatus(content = "one two badWord three") + )) + } + + @Test + fun shouldFilter_whenContentMatchesBadWordPart() { + assertTrue(fragment.shouldFilterStatus( + mockStatus(content = "one two badWordPart three") + )) + } + + @Test + fun shouldFilter_whenContentMatchesBadWholeWord() { + assertTrue(fragment.shouldFilterStatus( + mockStatus(content = "one two badWholeWord three") + )) + } + + @Test + fun shouldNotFilter_whenContentDoesNotMAtchWholeWord() { + assertFalse(fragment.shouldFilterStatus( + mockStatus(content = "one two badWholeWordTest three") + )) + } + + @Test + fun shouldFilter_whenSpoilerTextDoesMatch() { + assertTrue(fragment.shouldFilterStatus( + mockStatus( + content = "should not be filtered", + spoilerText = "badWord should be filtered" + ) + )) + } + + @Test + fun shouldFilter_whenPollTextDoesMatch() { + assertTrue(fragment.shouldFilterStatus( + mockStatus( + content = "should not be filtered", + spoilerText = "should not be filtered", + pollOptions = listOf("should not be filtered", "badWord") + ) + )) + } + + private fun mockStatus( + content: String = "", + spoilerText: String = "", + pollOptions: List? = null + ): Status { + return Status( + id = "123", + url = "https://mastodon.social/@Tusky/100571663297225812", + account = mock(), + inReplyToId = null, + inReplyToAccountId = null, + reblog = null, + content = SpannedString(content), + createdAt = Date(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + sensitive = false, + spoilerText = spoilerText, + visibility = Status.Visibility.PUBLIC, + attachments = arrayListOf(), + mentions = emptyArray(), + application = null, + pinned = false, + poll = if (pollOptions != null) { + Poll( + id = "1234", + expiresAt = null, + expired = false, + multiple = false, + votesCount = 0, + options = pollOptions.map { + PollOption(it, 0) + }, + voted = false + ) + } else null, + card = null + ) + } + +} + +class FakeActivity: BottomSheetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} + +class FakeFragment: SFragment() { + override fun removeItem(position: Int) { + } + + override fun onReblog(reblog: Boolean, position: Int) { + } + + override fun filterIsRelevant(filter: Filter): Boolean { + return filter.context.contains(Filter.HOME) + } +} \ No newline at end of file