Repository: andregasser/bigbone (kotlin)

Issue:
Implement Admin Domain Blocks API
Mastodon's Admin Domain Blocks API allows an admin to disallow certain domains to federate. The goal of this task is to implement the following endpoints of this API in BigBone:

- List all blocked domains
- Get a single blocked domain
- Block a domain from federating
- Update a domain block
- Remove a domain block

The Admin Domain Blocks API as it is documented by Mastodon can be found here:
https://docs.joinmastodon.org/methods/admin/domain_blocks/

Tip: A lot of other endpoints similar to this have already been implemented. In order to get an idea how to implement this, I suggest that you study the code upfront. Start with one of the `*Methods.kt` files.

Please consult the following documents to make sure your contribution meets our projects criteria:

- [Code of Conduct](https://github.com/andregasser/bigbone/blob/master/CODE_OF_CONDUCT.md)
- [Code Contribution Guidelines](https://github.com/andregasser/bigbone/blob/master/CONTRIBUTING.md)

Pull request:
# Description

This PR adds support for the Admin::DomainBlocks entity and methods.

> [!NOTE]
> The `Severity` class is already implemented in the non-Admin `DomainBlocks` entity but does not contain the `NOOP` variant.
> I decided to implement another one inside of `AdminDomainBlocks`. That’ll leave us with some duplication but will also ensure expected behaviour.

Closes #322.

# Type of Change

- New feature
- Documentation

# Breaking Changes

- None

# How Has This Been Tested?

I’ve added new unit tests for all methods

# Mandatory Checklist

- [x] I ran `gradle check` and there were no errors reported
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] All tests pass locally with my changes
- [x] I have added KDoc documentation to all public methods

# Optional Things To Check

The items below are some more things to check before asking other people to review your code.

- [x] In case you worked on a new feature: Did you also implement the reactive endpoint (bigbone-rx)?
- [x] In case you added new `*Methods` classes: Did you also reference it in the `MastodonClient` main class?
- [x] Did you also update the documentation in the `/docs` folder (e.g. API Coverage page)?

Patch:
diff --git a/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminDomainBlockMethods.kt b/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminDomainBlockMethods.kt
new file mode 100644
index 000000000..6e361a0dc
--- /dev/null
+++ b/bigbone-rx/src/main/kotlin/social/bigbone/rx/admin/RxAdminDomainBlockMethods.kt
@@ -0,0 +1,124 @@
+package social.bigbone.rx.admin
+
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.core.Single
+import social.bigbone.MastodonClient
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.admin.AdminDomainBlock
+import social.bigbone.api.entity.admin.AdminDomainBlock.Severity
+import social.bigbone.api.method.admin.AdminDomainBlockMethods
+
+/**
+ * Reactive implementation of [AdminDomainBlockMethods].
+ *
+ * Disallow certain domains to federate.
+ *
+ * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/">Mastodon admin/domain_blocks API methods</a>
+ */
+class RxAdminDomainBlockMethods(client: MastodonClient) {
+
+    private val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+
+    /**
+     * Show information about all blocked domains.
+
+     * @param range optional Range for the pageable return value
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/domain_blocks/#get">Mastodon API documentation: methods/domain_blocks/#get</a>
+     */
+    @JvmOverloads
+    fun getAllBlockedDomains(range: Range = Range()): Single<Pageable<AdminDomainBlock>> = Single.fromCallable {
+        adminDomainBlockMethods.getAllBlockedDomains(range = range).execute()
+    }
+
+    /**
+     * Show information about a single blocked domain.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/domain_blocks/#get-one">Mastodon API documentation: methods/domain_blocks/#get-one</a>
+     */
+    fun getBlockedDomain(id: String): Single<AdminDomainBlock> = Single.fromCallable {
+        adminDomainBlockMethods.getBlockedDomain(id = id).execute()
+    }
+
+    /**
+     * Add a domain to the list of domains blocked from federating.
+     *
+     * @param domain The domain to block federation with.
+     * @param severity Whether to apply a [Severity.SILENCE], [Severity.SUSPEND], or [Severity.NOOP] to the domain. Defaults to [Severity.SILENCE].
+     * @param rejectMedia Whether media attachments should be rejected. Defaults to false.
+     * @param rejectReports Whether reports from this domain should be rejected. Defaults to false.
+     * @param obfuscate Whether to partially censor the domain when shown in public. Defaults to false.
+     * @param privateComment A private note about this domain block, visible only to admins.
+     * @param publicComment A public note about this domain block, optionally shown on the About page.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#create">Mastodon API documentation: admin/domain_blocks/#create</a>
+     */
+    @JvmOverloads
+    fun blockDomain(
+        domain: String,
+        severity: Severity? = null,
+        rejectMedia: Boolean? = null,
+        rejectReports: Boolean? = null,
+        obfuscate: Boolean? = null,
+        privateComment: String? = null,
+        publicComment: String? = null
+    ): Single<AdminDomainBlock> = Single.fromCallable {
+        adminDomainBlockMethods.blockDomain(
+            domain = domain,
+            severity = severity,
+            rejectMedia = rejectMedia,
+            rejectReports = rejectReports,
+            obfuscate = obfuscate,
+            privateComment = privateComment,
+            publicComment = publicComment
+        ).execute()
+    }
+
+    /**
+     * Change parameters for an existing domain block.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     * @param severity Whether to apply a [Severity.SILENCE], [Severity.SUSPEND], or [Severity.NOOP] to the domain. Defaults to [Severity.SILENCE].
+     * @param rejectMedia Whether media attachments should be rejected. Defaults to false.
+     * @param rejectReports Whether reports from this domain should be rejected. Defaults to false.
+     * @param obfuscate Whether to partially censor the domain when shown in public. Defaults to false.
+     * @param privateComment A private note about this domain block, visible only to admins.
+     * @param publicComment A public note about this domain block, optionally shown on the About page.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#update">Mastodon API documentation: admin/domain_blocks/#update</a>
+     */
+    @JvmOverloads
+    fun updateBlockedDomain(
+        id: String,
+        severity: Severity? = null,
+        rejectMedia: Boolean? = null,
+        rejectReports: Boolean? = null,
+        obfuscate: Boolean? = null,
+        privateComment: String? = null,
+        publicComment: String? = null
+    ): Single<AdminDomainBlock> = Single.fromCallable {
+        adminDomainBlockMethods.updateBlockedDomain(
+            id = id,
+            severity = severity,
+            rejectMedia = rejectMedia,
+            rejectReports = rejectReports,
+            obfuscate = obfuscate,
+            privateComment = privateComment,
+            publicComment = publicComment
+        ).execute()
+    }
+
+    /**
+     * Lift a block against a domain.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete">Mastodon API documentation: admin/domain_blocks/#delete</a>
+     */
+    fun removeBlockedDomain(id: String): Completable = Completable.fromAction {
+        adminDomainBlockMethods.removeBlockedDomain(id = id)
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
index a7e1e1c43..f3218b035 100644
--- a/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
@@ -66,6 +66,7 @@ import social.bigbone.api.method.TagMethods
 import social.bigbone.api.method.TimelineMethods
 import social.bigbone.api.method.TrendMethods
 import social.bigbone.api.method.admin.AdminDimensionMethods
+import social.bigbone.api.method.admin.AdminDomainBlockMethods
 import social.bigbone.api.method.admin.AdminIpBlockMethods
 import social.bigbone.api.method.admin.AdminMeasureMethods
 import social.bigbone.api.method.admin.AdminRetentionMethods
@@ -112,6 +113,13 @@ private constructor(
     @get:JvmName("adminDimensions")
     val adminDimensions: AdminDimensionMethods by lazy { AdminDimensionMethods(this) }
 
+    /**
+     * Access API methods under the "admin/domain_blocks" endpoint.
+     */
+    @Suppress("unused") // public API
+    @get:JvmName("adminDomainBlock")
+    val adminDomainBlock: AdminDomainBlockMethods by lazy { AdminDomainBlockMethods(this) }
+
     /**
      * Access API methods under the "admin/ip_blocks" endpoint.
      */
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminDomainBlock.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminDomainBlock.kt
new file mode 100644
index 000000000..46a64cda0
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/admin/AdminDomainBlock.kt
@@ -0,0 +1,98 @@
+package social.bigbone.api.entity.admin
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import social.bigbone.DateTimeSerializer
+import social.bigbone.PrecisionDateTime
+
+/**
+ * Represents a domain limited from federating.
+ *
+ * @see <a href="https://docs.joinmastodon.org/entities/Admin_DomainBlock/">Mastodon documentation Admin::DomainBlock</a>
+ */
+@Serializable
+data class AdminDomainBlock(
+    /**
+     * The ID of the DomainBlock in the database.
+     * String, cast from an integer, but not guaranteed to be a number.
+     */
+    @SerialName("id")
+    val id: String,
+
+    /**
+     * The domain that is not allowed to federate.
+     */
+    @SerialName("domain")
+    val domain: String,
+
+    /**
+     * When the domain was blocked from federating.
+     */
+    @SerialName("created_at")
+    @Serializable(with = DateTimeSerializer::class)
+    val createdAt: PrecisionDateTime = PrecisionDateTime.InvalidPrecisionDateTime.Unavailable,
+
+    /**
+     * The policy to be applied by this domain block.
+     */
+    @SerialName("severity")
+    val severity: Severity,
+
+    /**
+     * Whether to reject media attachments from this domain.
+     */
+    @SerialName("reject_media")
+    val rejectMedia: Boolean,
+
+    /**
+     * Whether to reject reports from this domain.
+     */
+    @SerialName("reject_reports")
+    val rejectReports: Boolean,
+
+    /**
+     * Private note about this domain block, visible only to admins.
+     */
+    @SerialName("private_comment")
+    val privateComment: String?,
+
+    /**
+     * Public note about this domain block, optionally shown on the About page.
+     */
+    @SerialName("public_comment")
+    val publicComment: String?,
+
+    /**
+     * Whether to obfuscate public displays of this domain block.
+     */
+    @SerialName("obfuscate")
+    val obfuscate: Boolean
+) {
+    /**
+     * The policy to be applied by this domain block.
+     */
+    @Serializable
+    enum class Severity {
+        /**
+         * Account statuses from this domain will be hidden by default.
+         */
+        @SerialName("silence")
+        SILENCE,
+
+        /**
+         * All incoming data from this domain will be rejected.
+         */
+        @SerialName("suspend")
+        SUSPEND,
+
+        /**
+         * Do nothing. Allows for rejecting media or reports.
+         */
+        @SerialName("noop")
+        NOOP;
+
+        @OptIn(ExperimentalSerializationApi::class)
+        val apiName: String get() = serializer().descriptor.getElementName(ordinal)
+    }
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethods.kt b/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethods.kt
new file mode 100644
index 000000000..1dc8f10ac
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethods.kt
@@ -0,0 +1,145 @@
+package social.bigbone.api.method.admin
+
+import social.bigbone.MastodonClient
+import social.bigbone.MastodonRequest
+import social.bigbone.Parameters
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.admin.AdminDomainBlock
+import social.bigbone.api.entity.admin.AdminDomainBlock.Severity
+
+/**
+ * Disallow certain domains to federate.
+ *
+ * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/">Mastodon admin/domain_blocks API methods</a>
+ */
+class AdminDomainBlockMethods(private val client: MastodonClient) {
+
+    private val adminDomainBlocksEndpoint = "api/v1/admin/domain_blocks"
+
+    /**
+     * Show information about all blocked domains.
+     *
+     * @param range optional Range for the pageable return value
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/domain_blocks/#get">Mastodon API documentation: methods/domain_blocks/#get</a>
+     */
+    @JvmOverloads
+    fun getAllBlockedDomains(range: Range = Range()): MastodonRequest<Pageable<AdminDomainBlock>> {
+        return client.getPageableMastodonRequest(
+            endpoint = adminDomainBlocksEndpoint,
+            method = MastodonClient.Method.GET,
+            parameters = range.toParameters()
+        )
+    }
+
+    /**
+     * Show information about a single blocked domain.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/domain_blocks/#get-one">Mastodon API documentation: methods/domain_blocks/#get-one</a>
+     */
+    fun getBlockedDomain(id: String): MastodonRequest<AdminDomainBlock> {
+        return client.getMastodonRequest(
+            endpoint = "$adminDomainBlocksEndpoint/$id",
+            method = MastodonClient.Method.GET
+        )
+    }
+
+    /**
+     * Add a domain to the list of domains blocked from federating.
+     *
+     * @param domain The domain to block federation with.
+     * @param severity Whether to apply a [Severity.SILENCE], [Severity.SUSPEND], or [Severity.NOOP] to the domain. Defaults to [Severity.SILENCE].
+     * @param rejectMedia Whether media attachments should be rejected. Defaults to false.
+     * @param rejectReports Whether reports from this domain should be rejected. Defaults to false.
+     * @param obfuscate Whether to partially censor the domain when shown in public. Defaults to false.
+     * @param privateComment A private note about this domain block, visible only to admins.
+     * @param publicComment A public note about this domain block, optionally shown on the About page.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#create">Mastodon API documentation: admin/domain_blocks/#create</a>
+     */
+    @JvmOverloads
+    fun blockDomain(
+        domain: String,
+        severity: Severity? = null,
+        rejectMedia: Boolean? = null,
+        rejectReports: Boolean? = null,
+        obfuscate: Boolean? = null,
+        privateComment: String? = null,
+        publicComment: String? = null
+    ): MastodonRequest<AdminDomainBlock> {
+        return client.getMastodonRequest(
+            endpoint = adminDomainBlocksEndpoint,
+            method = MastodonClient.Method.POST,
+            parameters = Parameters().apply {
+                append("domain", domain)
+
+                severity?.let { append("severity", severity.apiName) }
+
+                rejectMedia?.let { append("reject_media", rejectMedia) }
+                rejectReports?.let { append("reject_reports", rejectReports) }
+
+                obfuscate?.let { append("obfuscate", obfuscate) }
+
+                privateComment?.let { append("private_comment", privateComment) }
+                publicComment?.let { append("public_comment", publicComment) }
+            }
+        )
+    }
+
+    /**
+     * Change parameters for an existing domain block.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     * @param severity Whether to apply a [Severity.SILENCE], [Severity.SUSPEND], or [Severity.NOOP] to the domain. Defaults to [Severity.SILENCE].
+     * @param rejectMedia Whether media attachments should be rejected. Defaults to false.
+     * @param rejectReports Whether reports from this domain should be rejected. Defaults to false.
+     * @param obfuscate Whether to partially censor the domain when shown in public. Defaults to false.
+     * @param privateComment A private note about this domain block, visible only to admins.
+     * @param publicComment A public note about this domain block, optionally shown on the About page.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#update">Mastodon API documentation: admin/domain_blocks/#update</a>
+     */
+    @JvmOverloads
+    fun updateBlockedDomain(
+        id: String,
+        severity: Severity? = null,
+        rejectMedia: Boolean? = null,
+        rejectReports: Boolean? = null,
+        obfuscate: Boolean? = null,
+        privateComment: String? = null,
+        publicComment: String? = null
+    ): MastodonRequest<AdminDomainBlock> {
+        return client.getMastodonRequest(
+            endpoint = "$adminDomainBlocksEndpoint/$id",
+            method = MastodonClient.Method.PUT,
+            parameters = Parameters().apply {
+                severity?.let { append("severity", severity.apiName) }
+
+                rejectMedia?.let { append("reject_media", rejectMedia) }
+                rejectReports?.let { append("reject_reports", rejectReports) }
+
+                obfuscate?.let { append("obfuscate", obfuscate) }
+
+                privateComment?.let { append("private_comment", privateComment) }
+                publicComment?.let { append("public_comment", publicComment) }
+            }
+        )
+    }
+
+    /**
+     * Lift a block against a domain.
+     *
+     * @param id The ID of the DomainBlock in the database.
+     *
+     * @see <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete">Mastodon API documentation: admin/domain_blocks/#delete</a>
+     */
+    fun removeBlockedDomain(id: String) {
+        client.performAction(
+            endpoint = "$adminDomainBlocksEndpoint/$id",
+            method = MastodonClient.Method.DELETE
+        )
+    }
+}
diff --git a/docs/api-coverage/admin/domain-blocks.md b/docs/api-coverage/admin/domain-blocks.md
index 259cba92c..f7d4d1e94 100644
--- a/docs/api-coverage/admin/domain-blocks.md
+++ b/docs/api-coverage/admin/domain-blocks.md
@@ -10,10 +10,10 @@ nav_order: 2
 
 <a href="https://docs.joinmastodon.org/methods/admin/domain_blocks/" target="_blank">https://docs.joinmastodon.org/methods/admin/domain_blocks/</a>
 
-| Method                                   | Description                    | Status | Comments | 
-|------------------------------------------|--------------------------------|--------|----------|
-| `GET /api/v1/admin/domain_blocks`        | List all blocked domains       | 🔴     |          |
-| `GET /api/v1/admin/domain_blocks/:id`    | Get a single blocked domain    | 🔴     |          |
-| `POST /api/v1/admin/domain_blocks`       | Block a domain from federating | 🔴     |          |
-| `PUT /api/v1/admin/domain_blocks/:id`    | Update a domain block          | 🔴     |          |
-| `DELETE /api/v1/admin/domain_blocks/:id` | Remove a domain block          | 🔴     |          |
+| Method                                   | Description                    | Status                          | Comments         | 
+|------------------------------------------|--------------------------------|---------------------------------|------------------|
+| `GET /api/v1/admin/domain_blocks`        | List all blocked domains       | <img src="/assets/green16.png"> | Fully supported. |
+| `GET /api/v1/admin/domain_blocks/:id`    | Get a single blocked domain    | <img src="/assets/green16.png"> | Fully supported. |
+| `POST /api/v1/admin/domain_blocks`       | Block a domain from federating | <img src="/assets/green16.png"> | Fully supported. |
+| `PUT /api/v1/admin/domain_blocks/:id`    | Update a domain block          | <img src="/assets/green16.png"> | Fully supported. |
+| `DELETE /api/v1/admin/domain_blocks/:id` | Remove a domain block          | <img src="/assets/green16.png"> | Fully supported. |


Test patch:
diff --git a/bigbone/src/test/assets/admin_domain_blocks_block_success.json b/bigbone/src/test/assets/admin_domain_blocks_block_success.json
new file mode 100644
index 000000000..660de4b3c
--- /dev/null
+++ b/bigbone/src/test/assets/admin_domain_blocks_block_success.json
@@ -0,0 +1,11 @@
+{
+  "id": "1",
+  "domain": "example.com",
+  "created_at": "2022-11-16T08:15:34.238Z",
+  "severity": "noop",
+  "reject_media": true,
+  "reject_reports": false,
+  "private_comment": "private comment",
+  "public_comment": "public comment",
+  "obfuscate": true
+}
diff --git a/bigbone/src/test/assets/admin_domain_blocks_delete_success.json b/bigbone/src/test/assets/admin_domain_blocks_delete_success.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/bigbone/src/test/assets/admin_domain_blocks_delete_success.json
@@ -0,0 +1,1 @@
+{}
diff --git a/bigbone/src/test/assets/admin_domain_blocks_get_all_success.json b/bigbone/src/test/assets/admin_domain_blocks_get_all_success.json
new file mode 100644
index 000000000..cee409a2c
--- /dev/null
+++ b/bigbone/src/test/assets/admin_domain_blocks_get_all_success.json
@@ -0,0 +1,13 @@
+[
+  {
+    "id": "1",
+    "domain": "example.com",
+    "created_at": "2022-11-16T08:15:34.238Z",
+    "severity": "noop",
+    "reject_media": false,
+    "reject_reports": false,
+    "private_comment": null,
+    "public_comment": null,
+    "obfuscate": false
+  }
+]
diff --git a/bigbone/src/test/assets/admin_domain_blocks_get_single_success.json b/bigbone/src/test/assets/admin_domain_blocks_get_single_success.json
new file mode 100644
index 000000000..d7a22d725
--- /dev/null
+++ b/bigbone/src/test/assets/admin_domain_blocks_get_single_success.json
@@ -0,0 +1,11 @@
+{
+  "id": "1",
+  "domain": "example.com",
+  "created_at": "2022-11-16T08:15:34.238Z",
+  "severity": "noop",
+  "reject_media": false,
+  "reject_reports": false,
+  "private_comment": null,
+  "public_comment": null,
+  "obfuscate": false
+}
diff --git a/bigbone/src/test/assets/admin_domain_blocks_update_success.json b/bigbone/src/test/assets/admin_domain_blocks_update_success.json
new file mode 100644
index 000000000..d7a22d725
--- /dev/null
+++ b/bigbone/src/test/assets/admin_domain_blocks_update_success.json
@@ -0,0 +1,11 @@
+{
+  "id": "1",
+  "domain": "example.com",
+  "created_at": "2022-11-16T08:15:34.238Z",
+  "severity": "noop",
+  "reject_media": false,
+  "reject_reports": false,
+  "private_comment": null,
+  "public_comment": null,
+  "obfuscate": false
+}
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethodsTest.kt
new file mode 100644
index 000000000..8ff79fc88
--- /dev/null
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/admin/AdminDomainBlockMethodsTest.kt
@@ -0,0 +1,225 @@
+@file:Suppress("MaxLineLength")
+
+package social.bigbone.api.method.admin
+
+import io.mockk.slot
+import io.mockk.verify
+import org.amshove.kluent.AnyException
+import org.amshove.kluent.invoking
+import org.amshove.kluent.shouldBeEmpty
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldHaveSize
+import org.amshove.kluent.shouldNotThrow
+import org.junit.jupiter.api.Test
+import social.bigbone.MastodonClient
+import social.bigbone.Parameters
+import social.bigbone.PrecisionDateTime
+import social.bigbone.api.Pageable
+import social.bigbone.api.Range
+import social.bigbone.api.entity.admin.AdminDomainBlock
+import social.bigbone.testtool.MockClient
+import social.bigbone.testtool.TestUtil.urlEncode
+import java.time.Instant
+
+class AdminDomainBlockMethodsTest {
+
+    @Test
+    fun `Given client returning success, when getting all domain blocks without Range, then ensure proper deserialisation and correct endpoint and parameter usage`() {
+        val client = MockClient.mock("admin_domain_blocks_get_all_success.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+
+        val domainBlocks: Pageable<AdminDomainBlock> = adminDomainBlockMethods.getAllBlockedDomains().execute()
+
+        with(domainBlocks.part) {
+            shouldHaveSize(1)
+
+            with(get(0)) {
+                id shouldBeEqualTo "1"
+                domain shouldBeEqualTo "example.com"
+                createdAt shouldBeEqualTo PrecisionDateTime.ValidPrecisionDateTime.ExactTime(Instant.parse("2022-11-16T08:15:34.238Z"))
+                severity shouldBeEqualTo AdminDomainBlock.Severity.NOOP
+                rejectMedia.shouldBeFalse()
+                rejectReports.shouldBeFalse()
+                obfuscate.shouldBeFalse()
+                privateComment.shouldBeNull()
+                publicComment.shouldBeNull()
+            }
+        }
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.get(
+                path = "api/v1/admin/domain_blocks",
+                query = capture(parametersCapturingSlot)
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            toQuery().shouldBeEmpty()
+        }
+    }
+
+    @Test
+    fun `Given client returning success, when getting all domain blocks with Range params, then ensure correct parameter usage`() {
+        val client = MockClient.mock("admin_domain_blocks_get_all_success.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+        val range = Range(
+            minId = "1234",
+            maxId = "5678"
+        )
+
+        val domainBlocks: Pageable<AdminDomainBlock> = adminDomainBlockMethods.getAllBlockedDomains(
+            range = range
+        ).execute()
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.get(
+                path = "api/v1/admin/domain_blocks",
+                query = capture(parametersCapturingSlot)
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            toQuery() shouldBeEqualTo "max_id=${range.maxId}&min_id=${range.minId}"
+        }
+    }
+
+    @Test
+    fun `Given client returning success, when getting single domain block, then ensure proper deserialisation and correct endpoint and parameter usage`() {
+        val client = MockClient.mock("admin_domain_blocks_get_single_success.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+        val blockedDomainId = "1"
+
+        val domainBlock: AdminDomainBlock = adminDomainBlockMethods.getBlockedDomain(
+            id = blockedDomainId
+        ).execute()
+
+        with(domainBlock) {
+            id shouldBeEqualTo "1"
+            domain shouldBeEqualTo "example.com"
+            createdAt shouldBeEqualTo PrecisionDateTime.ValidPrecisionDateTime.ExactTime(Instant.parse("2022-11-16T08:15:34.238Z"))
+            severity shouldBeEqualTo AdminDomainBlock.Severity.NOOP
+            rejectMedia.shouldBeFalse()
+            rejectReports.shouldBeFalse()
+            obfuscate.shouldBeFalse()
+            privateComment.shouldBeNull()
+            publicComment.shouldBeNull()
+        }
+
+        verify {
+            client.get(
+                path = "api/v1/admin/domain_blocks/$blockedDomainId",
+                query = null
+            )
+        }
+    }
+
+    @Test
+    fun `Given client returning success, when blocking a domain, then ensure proper deserialisation and correct endpoint and parameter usage`() {
+        val client = MockClient.mock("admin_domain_blocks_block_success.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+        val domain = "example.com"
+        val severity = AdminDomainBlock.Severity.NOOP
+        val rejectMedia = true
+        val rejectReports = false
+        val obfuscate = true
+        val privateComment = "private comment"
+        val publicComment = "public comment"
+
+        val domainBlock: AdminDomainBlock = adminDomainBlockMethods.blockDomain(
+            domain = domain,
+            severity = severity,
+            rejectMedia = rejectMedia,
+            rejectReports = rejectReports,
+            obfuscate = obfuscate,
+            privateComment = privateComment,
+            publicComment = publicComment
+        ).execute()
+
+        with(domainBlock) {
+            this.id shouldBeEqualTo "1"
+            this.domain shouldBeEqualTo "example.com"
+            this.createdAt shouldBeEqualTo PrecisionDateTime.ValidPrecisionDateTime.ExactTime(Instant.parse("2022-11-16T08:15:34.238Z"))
+            this.severity shouldBeEqualTo AdminDomainBlock.Severity.NOOP
+            this.rejectMedia.shouldBeTrue()
+            this.rejectReports.shouldBeFalse()
+            this.obfuscate.shouldBeTrue()
+            this.privateComment shouldBeEqualTo privateComment
+            this.publicComment shouldBeEqualTo publicComment
+        }
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.post(
+                path = "api/v1/admin/domain_blocks",
+                body = capture(parametersCapturingSlot),
+                addIdempotencyKey = false
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            toQuery() shouldBeEqualTo "domain=example.com" +
+                "&severity=noop" +
+                "&reject_media=true" +
+                "&reject_reports=false" +
+                "&obfuscate=true" +
+                "&private_comment=${privateComment.urlEncode()}" +
+                "&public_comment=${publicComment.urlEncode()}"
+        }
+    }
+
+    @Test
+    fun `Given client returning success, when updating a blocked domain, then ensure proper deserialisation and correct endpoint and parameter usage`() {
+        val client = MockClient.mock("admin_domain_blocks_update_success.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+        val blockedDomainId = "1"
+        val severity = AdminDomainBlock.Severity.SUSPEND
+
+        val domainBlock: AdminDomainBlock = adminDomainBlockMethods.updateBlockedDomain(
+            id = blockedDomainId,
+            severity = severity
+        ).execute()
+
+        with(domainBlock) {
+            this.id shouldBeEqualTo "1"
+            this.domain shouldBeEqualTo "example.com"
+            this.createdAt shouldBeEqualTo PrecisionDateTime.ValidPrecisionDateTime.ExactTime(Instant.parse("2022-11-16T08:15:34.238Z"))
+            this.severity shouldBeEqualTo AdminDomainBlock.Severity.NOOP
+            this.rejectMedia.shouldBeFalse()
+            this.rejectReports.shouldBeFalse()
+            this.obfuscate.shouldBeFalse()
+            this.privateComment.shouldBeNull()
+            this.publicComment.shouldBeNull()
+        }
+
+        val parametersCapturingSlot = slot<Parameters>()
+        verify {
+            client.put(
+                path = "api/v1/admin/domain_blocks/$blockedDomainId",
+                body = capture(parametersCapturingSlot)
+            )
+        }
+        with(parametersCapturingSlot.captured) {
+            toQuery() shouldBeEqualTo "severity=suspend"
+        }
+    }
+
+    @Test
+    fun `Given client returning success, when removing blocked domain, then do not throw any exception and expect correct endpoint`() {
+        val client = MockClient.mock("admin_ip_blocks_single.json")
+        val adminDomainBlockMethods = AdminDomainBlockMethods(client)
+        val blockedDomainId = "1"
+
+        invoking {
+            adminDomainBlockMethods.removeBlockedDomain(id = blockedDomainId)
+        } shouldNotThrow AnyException
+
+        verify {
+            client.performAction(
+                endpoint = "api/v1/admin/domain_blocks/$blockedDomainId",
+                method = MastodonClient.Method.DELETE
+            )
+        }
+    }
+}