Repository: andregasser/bigbone (kotlin)

Issue:
Update Instance entity with 4.4.0 attributes
According to [Trunk & Tidbits March 2025](https://blog.joinmastodon.org/2025/04/trunk-tidbits-march-2025/), the `Instance` entity has been updated with at least the following:

- new fields in the instance endpoint to get the instance’s about, terms of service and privacy policy pages
- an attribute to the instance endpoint to understand whether a reason is required for sign up

These need to be added to our Instance entity.

Pull request:
# Description

Adds new Instance properties that were added in Mastodon server versions 4.4.0 and 4.3.0 respectively.

> [!IMPORTANT]
> I have added a custom `Dimension` type so that we don’t expose e.g. `36x36` as a `String` for the `InstanceIcon` but instead split it by `width` and `height` in the `Dimension` object. I wasn’t entirely sure whether we should throw exceptions in error cases or fail gracefully. As the defined format is pretty straightforward, I went for exceptions as, if any of our low requirements fail, that would _really_ be an exception.
> If you disagree with that approach, I can remove either the `Dimension` class and return a String again or at least fail gracefully instead of throwing exceptions.

Closes #553.

# Type of Change

- New entity properties
- Documentation

# Breaking Changes

- None

# How Has This Been Tested?

New unit tests

# 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

Patch:
diff --git a/bigbone/src/main/kotlin/social/bigbone/Dimension.kt b/bigbone/src/main/kotlin/social/bigbone/Dimension.kt
new file mode 100644
index 000000000..0f77cce20
--- /dev/null
+++ b/bigbone/src/main/kotlin/social/bigbone/Dimension.kt
@@ -0,0 +1,14 @@
+package social.bigbone
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the dimensions of an image or video.
+ */
+@Serializable(with = DimensionSerializer::class)
+data class Dimension(
+    val width: Int,
+    val height: Int
+) {
+    override fun toString(): String = "${width}x$height"
+}
diff --git a/bigbone/src/main/kotlin/social/bigbone/JsonSerializer.kt b/bigbone/src/main/kotlin/social/bigbone/JsonSerializer.kt
index f9d201cce..484e98054 100644
--- a/bigbone/src/main/kotlin/social/bigbone/JsonSerializer.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/JsonSerializer.kt
@@ -22,13 +22,52 @@ internal val JSON_SERIALIZER: Json = Json {
     coerceInputValues = true
 }
 
+/**
+ * Custom [KSerializer] to serialize and deserialize a string in the format "widthxheight"
+ * into a [Dimension] object and vice versa.
+ */
+object DimensionSerializer : KSerializer<Dimension> {
+
+    override val descriptor: SerialDescriptor =
+        PrimitiveSerialDescriptor("social.bigbone.DimensionSerializer", PrimitiveKind.STRING)
+
+    override fun serialize(encoder: Encoder, value: Dimension) {
+        encoder.encodeString(value.toString())
+    }
+
+    override fun deserialize(decoder: Decoder): Dimension {
+        val dimensionString = decoder.decodeString()
+
+        val parts = dimensionString.split('x')
+        require(parts.size == 2) {
+            "Invalid dimension format: $dimensionString. Expected format: widthxheight"
+        }
+
+        try {
+            val width = parts[0].toInt()
+            val height = parts[1].toInt()
+
+            require(width > 0) { "Width must be greater than 0, but was $width" }
+            require(height > 0) { "Height must be greater than 0, but was $height" }
+
+            return Dimension(width, height)
+        } catch (e: NumberFormatException) {
+            throw IllegalArgumentException(
+                "Invalid dimension values in: $dimensionString. Width and height must be integers.",
+                e
+            )
+        }
+    }
+}
+
 /**
  * Custom [KSerializer] to serialize and deserialize ISO 8601 datetime
  * or date strings into a [PrecisionDateTime] and vice versa.
  *
  * This will attempt to retrieve a [ValidPrecisionDateTime.ExactTime] first.
  * If that fails, it will try with [ValidPrecisionDateTime.StartOfDay].
- * If that _also_ fails, it will return [Invalid], swallowing any [DateTimeParseException]s in the process.
+ * If that _also_ fails, it will return [InvalidPrecisionDateTime.Invalid],
+ * swallowing any [DateTimeParseException]s in the process.
  */
 object DateTimeSerializer : KSerializer<PrecisionDateTime> {
 
@@ -50,7 +89,7 @@ object DateTimeSerializer : KSerializer<PrecisionDateTime> {
         return try {
             try {
                 parseExactDateTime(decodedString)
-            } catch (dateTimeParseException: DateTimeParseException) {
+            } catch (_: DateTimeParseException) {
                 parseStartOfDay(decodedString)
             }
         } catch (dateTimeParseException: DateTimeParseException) {
diff --git a/bigbone/src/main/kotlin/social/bigbone/api/entity/Instance.kt b/bigbone/src/main/kotlin/social/bigbone/api/entity/Instance.kt
index 8fd3be642..5558d04b8 100644
--- a/bigbone/src/main/kotlin/social/bigbone/api/entity/Instance.kt
+++ b/bigbone/src/main/kotlin/social/bigbone/api/entity/Instance.kt
@@ -3,11 +3,12 @@ package social.bigbone.api.entity
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import social.bigbone.DateTimeSerializer
+import social.bigbone.Dimension
 import social.bigbone.PrecisionDateTime
 
 /**
  * Represents the software instance of Mastodon running on this domain.
- * @see <a href="https://docs.joinmastodon.org/entities/V1_Instance/">Mastodon API V1::Instance</a>
+ * @see <a href="https://docs.joinmastodon.org/entities/Instance/">Mastodon API Instance</a>
  */
 @Serializable
 data class Instance(
@@ -50,6 +51,13 @@ data class Instance(
     @SerialName("thumbnail")
     val thumbnail: Thumbnail = Thumbnail(),
 
+    /**
+     * The list of available size variants for this instance configured icon.
+     * @since Mastodon 4.3.0
+     */
+    @SerialName("icon")
+    val icon: List<InstanceIcon> = emptyList(),
+
     /**
      * Primary languages of the website and its staff.
      * [List] of [String] (ISO 639-1 two-letter code)
@@ -89,6 +97,7 @@ data class Instance(
 ) {
     /**
      * Usage data for this instance.
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#usage">Mastodon API Instance/#usage</a>
      */
     @Serializable
     data class Usage(
@@ -113,6 +122,7 @@ data class Instance(
 
     /**
      * An image used to represent this instance.
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#thumbnail">Mastodon API Instance/#thumbnail</a>
      */
     @Serializable
     data class Thumbnail(
@@ -131,6 +141,7 @@ data class Instance(
 
         /**
          * Links to scaled resolution images, for high DPI screens.
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#thumbnail-versions">Mastodon API Instance/#thumbnail-versions</a>
          */
         @SerialName("versions")
         val versions: Versions? = null
@@ -154,6 +165,28 @@ data class Instance(
         )
     }
 
+    /**
+     * An available size variant for this instance configured icon.
+     * @since Mastodon 4.3.0
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#InstanceIcon">Mastodon API Instance/#InstanceIcon</a>
+     */
+    @Serializable
+    data class InstanceIcon(
+        /**
+         * The URL of this icon.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("src")
+        val src: String,
+
+        /**
+         * The size of this icon.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("size")
+        val size: Dimension
+    )
+
     /**
      * Configured values and limits for this website.
      * @see <a href="https://docs.joinmastodon.org/entities/Instance/#configuration">Mastodon API Instance/#configuration</a>
@@ -194,7 +227,21 @@ data class Instance(
          * Hints related to translation.
          */
         @SerialName("translation")
-        val translation: Translation = Translation()
+        val translation: Translation = Translation(),
+
+        /**
+         * Whether federation is limited to explicitly allowed domains.
+         * @since Mastodon 4.4.0
+         */
+        @SerialName("limited_federation")
+        val limitedFederation: Boolean = false,
+
+        /**
+         * The instances VAPID data, used for push notifications.
+         * @since Mastodon 4.3.0
+         */
+        @SerialName("vapid")
+        val vapid: Vapid = Vapid(),
     ) {
         /**
          * URLs of interest for clients apps.
@@ -206,12 +253,33 @@ data class Instance(
              * The Websockets URL for connecting to the streaming API.
              */
             @SerialName("streaming")
-            val streaming: String = ""
+            val streaming: String = "",
+
+            /**
+             * The URL of the server’s about page.
+             * @since Mastodon 4.4.0
+             */
+            @SerialName("about")
+            val about: String = "",
+
+            /**
+             * The URL of the server’s privacy policy.
+             * @since Mastodon 4.4.0
+             */
+            @SerialName("privacy_policy")
+            val privacyPolicy: String? = null,
+
+            /**
+             * The URL of the server’s current terms of service, if any.
+             * @since Mastodon 4.4.0
+             */
+            @SerialName("terms_of_service")
+            val termsOfService: String? = null,
         )
 
         /**
          * Limits related to accounts.
-         * @see <a href="https://docs.joinmastodon.org/entities/V1_Instance/">Mastodon API V1::Instance</a>
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#accounts">Mastodon API Instance/#accounts</a>
          */
         @Serializable
         data class Accounts(
@@ -219,12 +287,19 @@ data class Instance(
              * The maximum number of featured tags allowed for each account.
              */
             @SerialName("max_featured_tags")
-            val maxFeaturedTags: Int = 0
+            val maxFeaturedTags: Int = 0,
+
+            /**
+             * The maximum number of pinned statuses for each account.
+             * @since Mastodon 4.3.0
+             */
+            @SerialName("max_pinned_statuses")
+            val maxPinnedStatuses: Int = 0,
         )
 
         /**
          * Limits related to authoring statuses.
-         * @see <a href="https://docs.joinmastodon.org/entities/V1_Instance/">Mastodon API V1::Instance</a>
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#statuses">Mastodon API Instance/#statuses</a>
          */
         @Serializable
         data class Statuses(
@@ -249,7 +324,7 @@ data class Instance(
 
         /**
          * Hints for which attachments will be accepted.
-         * @see <a href="https://docs.joinmastodon.org/entities/V1_Instance/">Mastodon API V1::Instance</a>
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#media_attachments">Mastodon API Instance/#media_attachments</a>
          */
         @Serializable
         data class MediaAttachments(
@@ -259,6 +334,13 @@ data class Instance(
             @SerialName("supported_mime_types")
             val supportedMimeTypes: List<String> = emptyList(),
 
+            /**
+             * The maximum size of a description, in characters.
+             * @since Mastodon 4.4.0
+             */
+            @SerialName("description_limit")
+            val descriptionLimit: Int = 0,
+
             /**
              * The maximum size of any uploaded image, in bytes.
              */
@@ -292,7 +374,7 @@ data class Instance(
 
         /**
          * Limits related to polls.
-         * @see <a href="https://docs.joinmastodon.org/entities/V1_Instance/">Mastodon API V1::Instance</a>
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#polls">Mastodon API Instance/#polls</a>
          */
         @Serializable
         data class Polls(
@@ -323,6 +405,7 @@ data class Instance(
 
         /**
          * Hints related to translation.
+         * @see <a href="https://docs.joinmastodon.org/entities/Instance/#translation">Mastodon API Instance/#translation</a>
          */
         @Serializable
         data class Translation(
@@ -332,10 +415,25 @@ data class Instance(
             @SerialName("enabled")
             val enabled: Boolean = false
         )
+
+        /**
+         * The instances VAPID data, used for push notifications.
+         */
+        @Serializable
+        data class Vapid(
+            /**
+             * The instances VAPID public key, used for push notifications.
+             * The same as [WebPushSubscription.serverKey].
+             * @since Mastodon 4.3.0
+             */
+            @SerialName("public_key")
+            val publicKey: String = ""
+        )
     }
 
     /**
      * Information about registering for this website.
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#registrations">Mastodon API Instance/#registrations</a>
      */
     @Serializable
     data class Registrations(
@@ -351,6 +449,14 @@ data class Instance(
         @SerialName("approval_required")
         val approvalRequired: Boolean = false,
 
+        /**
+         * Whether registrations require the user to provide a reason for joining.
+         * Only applicable when [approvalRequired] is true.
+         * @since Mastodon 4.4.0
+         */
+        @SerialName("reason_required")
+        val reasonRequired: Boolean? = false,
+
         /**
          * The minimum user age in years, if set.
          * @since Mastodon 4.4.0
@@ -363,11 +469,12 @@ data class Instance(
          * Nullable String (HTML) or null
          */
         @SerialName("message")
-        val message: String? = null
+        val message: String? = null,
     )
 
     /**
      * Information about which version of the API is implemented by this server.
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#api-versions">Mastodon API Instance/#api-versions</a>
      */
     @Serializable
     data class ApiVersions(
@@ -381,6 +488,7 @@ data class Instance(
 
     /**
      * Hints related to contacting a representative of the website.
+     * @see <a href="https://docs.joinmastodon.org/entities/Instance/#contact">Mastodon API Instance/#contact</a>
      */
     @Serializable
     data class Contact(


Test patch:
diff --git a/bigbone/src/test/assets/instance_extended.json b/bigbone/src/test/assets/instance_extended.json
index 829c64a17..39a0c3452 100644
--- a/bigbone/src/test/assets/instance_extended.json
+++ b/bigbone/src/test/assets/instance_extended.json
@@ -1,7 +1,7 @@
 {
   "domain": "mastodon.social",
   "title": "Mastodon",
-  "version": "4.0.0rc1",
+  "version": "4.4.0-alpha.3",
   "source_url": "https://github.com/mastodon/mastodon",
   "description": "The original server operated by the Mastodon gGmbH non-profit",
   "usage": {
@@ -17,15 +17,40 @@
       "@2x": "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png"
     }
   },
+  "icon": [
+    {
+      "src": "https://files.mastodon.social/site_uploads/files/000/000/003/36/accf17b0104f18e5.png",
+      "size": "36x36"
+    },
+    {
+      "src": "https://files.mastodon.social/site_uploads/files/000/000/003/72/accf17b0104f18e5.png",
+      "size": "72x72"
+    },
+    {
+      "src": "https://files.mastodon.social/site_uploads/files/000/000/003/192/accf17b0104f18e5.png",
+      "size": "192x192"
+    },
+    {
+      "src": "https://files.mastodon.social/site_uploads/files/000/000/003/512/accf17b0104f18e5.png",
+      "size": "512x512"
+    }
+  ],
   "languages": [
     "en"
   ],
   "configuration": {
     "urls": {
-      "streaming": "wss://mastodon.social"
+      "streaming": "wss://mastodon.social",
+      "about": "https://mastodon.social/about",
+      "privacy_policy": "https://mastodon.social/privacy-policy",
+      "terms_of_service": "https://mastodon.social/terms-of-service"
+    },
+    "vapid": {
+      "public_key": "BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc="
     },
     "accounts": {
-      "max_featured_tags": 10
+      "max_featured_tags": 10,
+      "max_pinned_statuses": 4
     },
     "statuses": {
       "max_characters": 500,
@@ -62,6 +87,7 @@
         "audio/3gpp",
         "video/x-ms-asf"
       ],
+      "description_limit": 1500,
       "image_size_limit": 10485760,
       "image_matrix_limit": 16777216,
       "video_size_limit": 41943040,
@@ -81,8 +107,12 @@
   "registrations": {
     "enabled": false,
     "approval_required": false,
-    "min_age": 42,
-    "message": null
+    "reason_required": false,
+    "message": null,
+    "min_age": 16
+  },
+  "api_versions": {
+    "mastodon": 1
   },
   "contact": {
     "email": "staff@mastodon.social",
diff --git a/bigbone/src/test/java/social/bigbone/DimensionSerializerJavaTest.java b/bigbone/src/test/java/social/bigbone/DimensionSerializerJavaTest.java
new file mode 100644
index 000000000..f65be164e
--- /dev/null
+++ b/bigbone/src/test/java/social/bigbone/DimensionSerializerJavaTest.java
@@ -0,0 +1,50 @@
+package social.bigbone;
+
+import org.intellij.lang.annotations.Language;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static social.bigbone.JsonSerializerKt.getJSON_SERIALIZER;
+
+public class DimensionSerializerJavaTest {
+
+    @Test
+    void validDimensionStringToDimension() {
+        @Language(value = "json", prefix = "", suffix = "")
+        String jsonString = "\"800x600\"";
+
+        Dimension decodedDimension = getJSON_SERIALIZER().decodeFromString(DimensionSerializer.INSTANCE, jsonString);
+
+        Assertions.assertEquals(800, decodedDimension.getWidth());
+        Assertions.assertEquals(600, decodedDimension.getHeight());
+    }
+
+    @Test
+    void dimensionToString() {
+        Dimension dimension = new Dimension(1920, 1080);
+
+        String encodedDimension = getJSON_SERIALIZER().encodeToString(DimensionSerializer.INSTANCE, dimension);
+
+        Assertions.assertEquals("\"1920x1080\"", encodedDimension);
+    }
+
+    @Test
+    void dimensionRoundTrip() {
+        Dimension dimension = new Dimension(640, 480);
+
+        @Language(value = "json", prefix = "", suffix = "")
+        String encodedDimension = getJSON_SERIALIZER().encodeToString(DimensionSerializer.INSTANCE, dimension);
+        Dimension decodedDimension = getJSON_SERIALIZER().decodeFromString(DimensionSerializer.INSTANCE, encodedDimension);
+
+        Assertions.assertEquals(dimension, decodedDimension);
+    }
+
+    @Test
+    void invalidDimensionStringThrowsException() {
+        @Language(value = "json", prefix = "", suffix = "") String jsonString = "\"800600\"";
+
+        Assertions.assertThrows(IllegalArgumentException.class, () -> {
+            getJSON_SERIALIZER().decodeFromString(DimensionSerializer.INSTANCE, jsonString);
+        });
+    }
+}
diff --git a/bigbone/src/test/kotlin/social/bigbone/DimensionSerializerTest.kt b/bigbone/src/test/kotlin/social/bigbone/DimensionSerializerTest.kt
new file mode 100644
index 000000000..93d4daa1f
--- /dev/null
+++ b/bigbone/src/test/kotlin/social/bigbone/DimensionSerializerTest.kt
@@ -0,0 +1,92 @@
+package social.bigbone
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class DimensionSerializerTest {
+
+    @Test
+    fun `Given a valid dimension string, when decoding from string, then return Dimension with correct values`() {
+        val jsonString = "\"800x600\""
+
+        val decodedDimension: Dimension = JSON_SERIALIZER.decodeFromString(jsonString)
+
+        decodedDimension.width shouldBeEqualTo 800
+        decodedDimension.height shouldBeEqualTo 600
+    }
+
+    @Test
+    fun `Given a Dimension object, when encoding into JSON string, then return expected dimension string`() {
+        val dimension = Dimension(1920, 1080)
+
+        val encodedDimension = JSON_SERIALIZER.encodeToString(dimension)
+
+        encodedDimension shouldBeEqualTo "\"1920x1080\""
+    }
+
+    @Test
+    fun `Given a Dimension object, when encoding and then decoding, then return the original Dimension object`() {
+        val dimension = Dimension(640, 480)
+
+        val encodedDimension: String = JSON_SERIALIZER.encodeToString(dimension)
+        val decodedDimension: Dimension = JSON_SERIALIZER.decodeFromString(encodedDimension)
+
+        decodedDimension shouldBeEqualTo dimension
+    }
+
+    @Test
+    fun `Given a dimension string without x separator, when decoding from string, then throw IllegalArgumentException`() {
+        val jsonString = "\"800600\""
+
+        val exception = assertThrows<IllegalArgumentException> {
+            JSON_SERIALIZER.decodeFromString<Dimension>(jsonString)
+        }
+        exception.message shouldBeEqualTo "Invalid dimension format: 800600. Expected format: widthxheight"
+        exception.cause shouldBe null
+    }
+
+    @Test
+    fun `Given a dimension string with non-integer width, when decoding from string, then throw IllegalArgumentException`() {
+        val jsonString = "\"abcx600\""
+
+        val exception = assertThrows<IllegalArgumentException> {
+            JSON_SERIALIZER.decodeFromString<Dimension>(jsonString)
+        }
+        exception.message shouldBeEqualTo "Invalid dimension values in: abcx600. Width and height must be integers."
+        exception.cause shouldBeInstanceOf NumberFormatException::class
+    }
+
+    @Test
+    fun `Given a dimension string with non-integer height, when decoding from string, then throw IllegalArgumentException`() {
+        val jsonString = "\"800xabc\""
+
+        val exception = assertThrows<IllegalArgumentException> {
+            JSON_SERIALIZER.decodeFromString<Dimension>(jsonString)
+        }
+        exception.message shouldBeEqualTo "Invalid dimension values in: 800xabc. Width and height must be integers."
+        exception.cause shouldBeInstanceOf NumberFormatException::class
+    }
+
+    @Test
+    fun `Given a dimension string with too many x separators, when decoding from string, then throw IllegalArgumentException`() {
+        val jsonString = "\"800x600x400\""
+
+        val exception = assertThrows<IllegalArgumentException> {
+            JSON_SERIALIZER.decodeFromString<Dimension>(jsonString)
+        }
+        exception.message shouldBeEqualTo "Invalid dimension format: 800x600x400. Expected format: widthxheight"
+    }
+
+    @Test
+    fun `Given a dimension string with negative width, when decoding from string, then throw IllegalArgumentException`() {
+        val jsonString = "\"-800x600\""
+
+        val exception = assertThrows<IllegalArgumentException> {
+            JSON_SERIALIZER.decodeFromString<Dimension>(jsonString)
+        }
+        exception.message shouldBeEqualTo "Width must be greater than 0, but was -800"
+    }
+}
diff --git a/bigbone/src/test/kotlin/social/bigbone/api/method/InstanceMethodsTest.kt b/bigbone/src/test/kotlin/social/bigbone/api/method/InstanceMethodsTest.kt
index da447ad2d..a3f2c3dea 100644
--- a/bigbone/src/test/kotlin/social/bigbone/api/method/InstanceMethodsTest.kt
+++ b/bigbone/src/test/kotlin/social/bigbone/api/method/InstanceMethodsTest.kt
@@ -5,11 +5,13 @@ import org.amshove.kluent.invoking
 import org.amshove.kluent.shouldBeEqualTo
 import org.amshove.kluent.shouldBeNull
 import org.amshove.kluent.shouldHaveSize
+import org.amshove.kluent.shouldNotBe
 import org.amshove.kluent.shouldNotBeEmpty
 import org.amshove.kluent.shouldNotBeNull
 import org.amshove.kluent.shouldThrow
 import org.amshove.kluent.withMessage
 import org.junit.jupiter.api.Test
+import social.bigbone.Dimension
 import social.bigbone.PrecisionDateTime.ValidPrecisionDateTime.ExactTime
 import social.bigbone.api.entity.DomainBlock
 import social.bigbone.api.entity.ExtendedDescription
@@ -49,7 +51,7 @@ class InstanceMethodsTest {
         val instance = instanceMethods.getInstance().execute()
         instance.domain shouldBeEqualTo "mastodon.social"
         instance.title shouldBeEqualTo "Mastodon"
-        instance.version shouldBeEqualTo "4.0.0rc1"
+        instance.version shouldBeEqualTo "4.4.0-alpha.3"
         instance.sourceUrl shouldBeEqualTo "https://github.com/mastodon/mastodon"
         instance.description shouldBeEqualTo "The original server operated by the Mastodon gGmbH non-profit"
         instance.usage.users.activeMonth shouldBeEqualTo 123_122
@@ -64,14 +66,32 @@ class InstanceMethodsTest {
             }
         }
 
+        with(instance.icon) {
+            size shouldBeEqualTo 4
+            get(0).src shouldBeEqualTo "https://files.mastodon.social/site_uploads/files/000/000/003/36/accf17b0104f18e5.png"
+            get(0).size shouldBeEqualTo Dimension(width = 36, height = 36)
+            get(1).size shouldBeEqualTo Dimension(width = 72, height = 72)
+            get(2).size shouldBeEqualTo Dimension(width = 192, height = 192)
+            get(3).size shouldBeEqualTo Dimension(width = 512, height = 512)
+        }
+
         with(instance.languages) {
             size shouldBeEqualTo 1
             get(0) shouldBeEqualTo "en"
         }
 
         val config = instance.configuration
-        config.urls.streaming shouldBeEqualTo "wss://mastodon.social"
-        config.accounts.maxFeaturedTags shouldBeEqualTo 10
+        with(config.urls) {
+            streaming shouldBeEqualTo "wss://mastodon.social"
+            about shouldBeEqualTo "https://mastodon.social/about"
+            privacyPolicy shouldBeEqualTo "https://mastodon.social/privacy-policy"
+            termsOfService shouldBeEqualTo "https://mastodon.social/terms-of-service"
+        }
+        config.vapid.publicKey shouldNotBe null
+        with(config.accounts) {
+            maxFeaturedTags shouldBeEqualTo 10
+            maxPinnedStatuses shouldBeEqualTo 4
+        }
         with(config.statuses) {
             maxCharacters shouldBeEqualTo 500
             maxMediaAttachments shouldBeEqualTo 4
@@ -79,6 +99,7 @@ class InstanceMethodsTest {
         }
         with(config.mediaAttachments) {
             supportedMimeTypes shouldHaveSize 27
+            descriptionLimit shouldBeEqualTo 1500
             imageSizeLimit shouldBeEqualTo 10_485_760
             imageMatrixLimit shouldBeEqualTo 16_777_216
             videoSizeLimit shouldBeEqualTo 41_943_040
@@ -96,9 +117,11 @@ class InstanceMethodsTest {
         with(instance.registrations) {
             enabled shouldBeEqualTo false
             approvalRequired shouldBeEqualTo false
-            minAge shouldBeEqualTo 42
+            reasonRequired shouldBeEqualTo false
+            minAge shouldBeEqualTo 16
             message.shouldBeNull()
         }
+        instance.apiVersions.mastodon shouldBeEqualTo 1
 
         with(instance.contact) {
             email shouldBeEqualTo "staff@mastodon.social"