From: David ‘Bombe’ Roden Date: Sun, 6 Apr 2025 09:01:01 +0000 (+0200) Subject: 🚧 Add preferences helpers and tests X-Git-Url: https://git.pterodactylus.net/?a=commitdiff_plain;h=fb367e80ff11e7f2012e05fb339a500081119ef0;p=Sone.git 🚧 Add preferences helpers and tests --- diff --git a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt index dc15810..b701cc3 100644 --- a/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt +++ b/src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt @@ -18,6 +18,9 @@ package net.pterodactylus.sone.core import com.google.common.eventbus.EventBus +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KMutableProperty0 +import kotlin.reflect.KProperty import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent @@ -145,3 +148,55 @@ interface Preferences { } private val unsupported: Nothing get() = throw UnsupportedOperationException() + +fun Preferences.withDefaults() = object : Preferences by this { + override var insertionDelay: Int? by default(60, this@withDefaults::insertionDelay) + override var postsPerPage: Int? by default(10, this@withDefaults::postsPerPage) + override var imagesPerPage: Int? by default(9, this@withDefaults::imagesPerPage) + override var charactersPerPost: Int? by default(400, this@withDefaults::charactersPerPost) + override var postCutOffLength: Int? by default(200, this@withDefaults::postCutOffLength) + override var requireFullAccess: Boolean? by default(false, this@withDefaults::requireFullAccess) + override var fcpInterfaceActive: Boolean? by default(false, this@withDefaults::fcpInterfaceActive) + override var fcpFullAccessRequired: FullAccessRequired? by default(ALWAYS, this@withDefaults::fcpFullAccessRequired) + override var strictFiltering: Boolean? by default(false, this@withDefaults::strictFiltering) +} + +fun Preferences.validated() = object : Preferences by this { + override var insertionDelay: Int? by validated(this@validated::insertionDelay) { v -> (v == null) || (v >= 0) } + override var postsPerPage: Int? by validated(this@validated::postsPerPage) { v -> (v == null) || (v >= 1) } + override var imagesPerPage: Int? by validated(this@validated::imagesPerPage) { v -> (v == null) || (v >= 1) } + override var charactersPerPost: Int? by validated(this@validated::charactersPerPost) { v -> (v == null) || (v == -1) || (v >= 50) } + override var postCutOffLength: Int? by validated(this@validated::postCutOffLength) { v -> (v == null) || (v >= 50) } +} + +fun Preferences.withEvents(eventBus: EventBus) = object : Preferences by this { + override var insertionDelay: Int? by sendEvent(this@withEvents::insertionDelay) { eventBus.post(InsertionDelayChangedEvent(it!!)) } + override var fcpInterfaceActive: Boolean? by sendEvent(this@withEvents::fcpInterfaceActive) { eventBus.post(if (it!!) FcpInterfaceActivatedEvent() else FcpInterfaceDeactivatedEvent()) } + override var fcpFullAccessRequired: FullAccessRequired? by sendEvent(this@withEvents::fcpFullAccessRequired) { eventBus.post(FullAccessRequiredChanged(it!!)) } + override var strictFiltering: Boolean? by sendEvent(this@withEvents::strictFiltering) { eventBus.post(if (it!!) StrictFilteringActivatedEvent() else StrictFilteringDeactivatedEvent()) } +} + +private fun default(defaultValue: V, parent: KMutableProperty0) = object : ReadWriteProperty { + override operator fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get() ?: defaultValue + override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) { + parent.set(value) + } +} + +private fun validated(parent: KMutableProperty0, validator: (V?) -> Boolean) = object : ReadWriteProperty { + override fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get() + override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) { + if (!validator(value)) { + throw IllegalArgumentException("$value is not valid for ${property.name}") + } + parent.set(value) + } +} + +private fun sendEvent(parent: KMutableProperty0, valueConsumer: (V?) -> Unit) = object : ReadWriteProperty { + override operator fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get() + override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) { + parent.set(value) + valueConsumer(parent.get()) + } +} diff --git a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt index 37a845a..cd4d9cd 100644 --- a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt @@ -2,6 +2,7 @@ package net.pterodactylus.sone.core import com.google.common.eventbus.EventBus import com.google.common.eventbus.Subscribe +import java.util.concurrent.atomic.AtomicBoolean import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent @@ -12,10 +13,13 @@ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged +import net.pterodactylus.sone.test.allNullPreferences +import net.pterodactylus.sone.test.assertThrows import net.pterodactylus.util.config.Configuration import net.pterodactylus.util.config.MapConfigurationBackend import org.hamcrest.Matcher import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.emptyIterable import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasItem @@ -348,3 +352,282 @@ class DefaultPreferencesTest { } } + +class PreferencesTest { + + @Test + fun `preferences with defaults return 60s post insertion delay if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().insertionDelay, equalTo(60)) + } + + @Test + fun `preferences with defaults return 10 posts per page if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().postsPerPage, equalTo(10)) + } + + @Test + fun `preferences with defaults return 9 images per page if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().imagesPerPage, equalTo(9)) + } + + @Test + fun `preferences with defaults return 400 characters per post if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().charactersPerPost, equalTo(400)) + } + + @Test + fun `preferences with defaults return a post cut off length of 200 if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().postCutOffLength, equalTo(200)) + } + + @Test + fun `preferences with defaults return false for require full access if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().requireFullAccess, equalTo(false)) + } + + @Test + fun `preferences with defaults return false for fcp interface active if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().fcpInterfaceActive, equalTo(false)) + } + + @Test + fun `preferences with defaults return always for fcp full access required if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().fcpFullAccessRequired, equalTo(ALWAYS)) + } + + @Test + fun `preferences with defaults return false for strict filtering if underlying preferences return null`() { + assertThat(testPreferences.withDefaults().strictFiltering, equalTo(false)) + } + + @Test + fun `preferences with validation refuse negative insertion delay`() { + assertThrows { testPreferences.validated().insertionDelay = -1 } + assertThrows { testPreferences.validated().insertionDelay = -17 } + assertThrows { testPreferences.validated().insertionDelay = -38 } + } + + @Test + fun `preferences with validation accept positive insertion delay`() { + testPreferences.validated().insertionDelay = 0 + testPreferences.validated().insertionDelay = 1 + testPreferences.validated().insertionDelay = 300 + } + + @Test + fun `preferences with validation accept null as insertion delay`() { + testPreferences.validated().insertionDelay = null + } + + @Test + fun `preferences with validation refuse less than one post per page`() { + assertThrows { testPreferences.validated().postsPerPage = 0 } + assertThrows { testPreferences.validated().postsPerPage = -1 } + assertThrows { testPreferences.validated().postsPerPage = -23 } + } + + @Test + fun `preferences with validation accept one or more posts per page`() { + testPreferences.validated().postsPerPage = 1 + testPreferences.validated().postsPerPage = 2 + testPreferences.validated().postsPerPage = 432 + } + + @Test + fun `preferences with validation accept null as posts per page`() { + testPreferences.validated().postsPerPage = null + } + + @Test + fun `preferences with validation refuse less than one image per page`() { + assertThrows { testPreferences.validated().imagesPerPage = 0 } + assertThrows { testPreferences.validated().imagesPerPage = -1 } + assertThrows { testPreferences.validated().imagesPerPage = -71 } + } + + @Test + fun `preferences with validation accept one or more images per page`() { + testPreferences.validated().imagesPerPage = 1 + testPreferences.validated().imagesPerPage = 2 + testPreferences.validated().imagesPerPage = 123 + } + + @Test + fun `preferences with validation accept null as images per page`() { + testPreferences.validated().imagesPerPage = null + } + + @Test + fun `preferences with validation refuse less than 50 characters per post other than -1`() { + assertThrows { testPreferences.validated().charactersPerPost = 49 } + assertThrows { testPreferences.validated().charactersPerPost = 0 } + assertThrows { testPreferences.validated().charactersPerPost = -2 } + } + + @Test + fun `preferences with validation accept -1 or greater than or equal to 50 characters per post`() { + testPreferences.validated().charactersPerPost = -1 + testPreferences.validated().charactersPerPost = 50 + testPreferences.validated().charactersPerPost = 234 + } + + @Test + fun `preferences with validation accept null as characters per post`() { + testPreferences.validated().charactersPerPost = null + } + + @Test + fun `preferences with validation refuse less than 50 characters post cut off length`() { + assertThrows { testPreferences.validated().postCutOffLength = 49 } + assertThrows { testPreferences.validated().postCutOffLength = 0 } + assertThrows { testPreferences.validated().postCutOffLength = -1 } + } + + @Test + fun `preferences with validation accept greater than or equal to 50 characters post cut off length`() { + testPreferences.validated().postCutOffLength = 50 + testPreferences.validated().postCutOffLength = 135 + testPreferences.validated().postCutOffLength = 234 + } + + @Test + fun `preferences with validation accept null as characters post cut off length`() { + testPreferences.validated().postCutOffLength = null + } + + @Test + fun `preferences with validation allows all booleans for require full access`() { + testPreferences.validated().requireFullAccess = false + testPreferences.validated().requireFullAccess = true + } + + @Test + fun `preferences with validation accept null as require full access`() { + testPreferences.validated().requireFullAccess = null + } + + @Test + fun `preferences with validation allows all booleans for fcp interface active`() { + testPreferences.validated().fcpInterfaceActive = false + testPreferences.validated().fcpInterfaceActive = true + } + + @Test + fun `preferences with validation accept null as fcp interface active`() { + testPreferences.validated().fcpInterfaceActive = null + } + + @Test + fun `preferences with validation allows all enum values for fcp full access required`() { + testPreferences.validated().fcpFullAccessRequired = NO + testPreferences.validated().fcpFullAccessRequired = WRITING + testPreferences.validated().fcpFullAccessRequired = ALWAYS + } + + @Test + fun `preferences with validation accept null as fcp full access required`() { + testPreferences.validated().fcpFullAccessRequired = null + } + + @Test + fun `preferences with validation allows all booleans for strict filtering`() { + testPreferences.validated().strictFiltering = false + testPreferences.validated().strictFiltering = true + } + + @Test + fun `preferences with validation accept null as strict filtering`() { + testPreferences.validated().strictFiltering = null + } + + @Test + fun `preferences with events send insertion delay changed event when insertion delay is changed`() { + val newInsertionDelay = mutableListOf() + eventBus.register(object { + @Subscribe + fun onInsertionDelayChanged(event: InsertionDelayChangedEvent) { + newInsertionDelay += event.insertionDelay + } + }) + testPreferences.withEvents(eventBus).insertionDelay = 123 + assertThat(newInsertionDelay, contains(123)) + } + + @Test + fun `preferences with defaults and events send default insertion delay changed event when insertion delay is changed to null`() { + val newInsertionDelay = mutableListOf() + eventBus.register(object { + @Subscribe + fun onInsertionDelayChanged(event: InsertionDelayChangedEvent) { + newInsertionDelay += event.insertionDelay + } + }) + testPreferences.withDefaults().withEvents(eventBus).insertionDelay = null + assertThat(newInsertionDelay, contains(60)) + } + + @Test + fun `preferences with events send fcp interface activated event when fcp interface active is set to true`() { + val called = AtomicBoolean(false) + eventBus.register(object { + @Subscribe + fun onFcpInterfaceActivated(event: FcpInterfaceActivatedEvent) = called.set(true) + }) + testPreferences.withEvents(eventBus).fcpInterfaceActive = true + assertThat(called.get(), equalTo(true)) + } + + @Test + fun `preferences with events send fcp interface deactivated event when fcp interface active is set to false`() { + val called = AtomicBoolean(false) + eventBus.register(object { + @Subscribe + fun onFcpInterfaceDeactivated(event: FcpInterfaceDeactivatedEvent) = called.set(true) + }) + testPreferences.withEvents(eventBus).fcpInterfaceActive = false + assertThat(called.get(), equalTo(true)) + } + + @Test + fun `preferences with events send fcp full access required changed event when fcp full access required is changed`() { + val fcpFullAccessRequiredValues = mutableListOf() + eventBus.register(object { + @Subscribe + fun onFcpFullAccessRequiredChanged(event: FullAccessRequiredChanged) { + fcpFullAccessRequiredValues += event.fullAccessRequired + } + }) + testPreferences.withEvents(eventBus).apply { + fcpFullAccessRequired = WRITING + fcpFullAccessRequired = ALWAYS + fcpFullAccessRequired = NO + } + assertThat(fcpFullAccessRequiredValues, contains(WRITING, ALWAYS, NO)) + } + + @Test + fun `preferences with events send strict filtering activated event when strict filtering is set to true`() { + val called = AtomicBoolean(false) + eventBus.register(object { + @Subscribe + fun onStrictFilteringActivated(event: StrictFilteringActivatedEvent) = called.set(true) + }) + testPreferences.withEvents(eventBus).strictFiltering = true + assertThat(called.get(), equalTo(true)) + } + + @Test + fun `preferences with events send strict filtering deactivated event when strict filtering is set to false`() { + val called = AtomicBoolean(false) + eventBus.register(object { + @Subscribe + fun onStrictFilteringDeactivated(event: StrictFilteringDeactivatedEvent) = called.set(true) + }) + testPreferences.withEvents(eventBus).strictFiltering = false + assertThat(called.get(), equalTo(true)) + } + + private val eventBus = EventBus() + private val testPreferences = allNullPreferences() + +} diff --git a/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt index 54307a8..cf6532b 100644 --- a/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt +++ b/src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt @@ -35,6 +35,10 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity import net.pterodactylus.sone.utils.asFreenetBase64 import net.pterodactylus.sone.utils.asOptional import java.util.UUID +import net.pterodactylus.sone.core.Preferences +import net.pterodactylus.sone.core.validated +import net.pterodactylus.sone.core.withDefaults +import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired val remoteSone1 = createRemoteSone() val remoteSone2 = createRemoteSone() @@ -96,3 +100,17 @@ fun createPostReply(text: String = "text", post: Post? = createPost(), sone: Son fun createImage(sone: Sone): Image = ImageImpl().modify().setSone(sone).update() + +fun allNullPreferences() = object : Preferences { + override var insertionDelay: Int? = null + override var postsPerPage: Int? = null + override var imagesPerPage: Int? = null + override var charactersPerPost: Int? = null + override var postCutOffLength: Int? = null + override var requireFullAccess: Boolean? = null + override var fcpInterfaceActive: Boolean? = null + override var fcpFullAccessRequired: FullAccessRequired? = null + override var strictFiltering: Boolean? = null +} + +fun testPreferences() = allNullPreferences().withDefaults().validated()