diff --git a/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt b/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt index cd41155..199b886 100644 --- a/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt +++ b/app/src/main/java/com/swoosh/microblog/data/api/GhostApiService.kt @@ -2,6 +2,8 @@ package com.swoosh.microblog.data.api import com.swoosh.microblog.data.model.PostWrapper import com.swoosh.microblog.data.model.PostsResponse +import com.swoosh.microblog.data.model.TagsResponse +import com.swoosh.microblog.data.model.TagWrapper import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Response @@ -40,6 +42,26 @@ interface GhostApiService { @GET("ghost/api/admin/users/me/") suspend fun getCurrentUser(): Response + @GET("ghost/api/admin/tags/") + suspend fun getTags( + @Query("limit") limit: String = "all", + @Query("include") include: String = "count.posts" + ): Response + + @GET("ghost/api/admin/tags/{id}/") + suspend fun getTag(@Path("id") id: String): Response + + @POST("ghost/api/admin/tags/") + @Headers("Content-Type: application/json") + suspend fun createTag(@Body body: TagWrapper): Response + + @PUT("ghost/api/admin/tags/{id}/") + @Headers("Content-Type: application/json") + suspend fun updateTag(@Path("id") id: String, @Body body: TagWrapper): Response + + @DELETE("ghost/api/admin/tags/{id}/") + suspend fun deleteTag(@Path("id") id: String): Response + @Multipart @POST("ghost/api/admin/images/upload/") suspend fun uploadImage( diff --git a/app/src/main/java/com/swoosh/microblog/data/model/TagModels.kt b/app/src/main/java/com/swoosh/microblog/data/model/TagModels.kt new file mode 100644 index 0000000..3b21270 --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/data/model/TagModels.kt @@ -0,0 +1,26 @@ +package com.swoosh.microblog.data.model + +data class TagsResponse( + val tags: List, + val meta: Meta? +) + +data class TagWrapper( + val tags: List +) + +data class GhostTagFull( + val id: String? = null, + val name: String, + val slug: String? = null, + val description: String? = null, + val feature_image: String? = null, + val visibility: String? = "public", + val accent_color: String? = null, + val count: TagCount? = null, + val created_at: String? = null, + val updated_at: String? = null, + val url: String? = null +) + +data class TagCount(val posts: Int?) diff --git a/app/src/test/java/com/swoosh/microblog/data/model/TagModelsTest.kt b/app/src/test/java/com/swoosh/microblog/data/model/TagModelsTest.kt new file mode 100644 index 0000000..83630ef --- /dev/null +++ b/app/src/test/java/com/swoosh/microblog/data/model/TagModelsTest.kt @@ -0,0 +1,196 @@ +package com.swoosh.microblog.data.model + +import com.google.gson.Gson +import org.junit.Assert.* +import org.junit.Test + +class TagModelsTest { + + private val gson = Gson() + + // --- GhostTagFull defaults --- + + @Test + fun `GhostTagFull default id is null`() { + val tag = GhostTagFull(name = "test") + assertNull(tag.id) + } + + @Test + fun `GhostTagFull default visibility is public`() { + val tag = GhostTagFull(name = "test") + assertEquals("public", tag.visibility) + } + + @Test + fun `GhostTagFull default optional fields are null`() { + val tag = GhostTagFull(name = "test") + assertNull(tag.slug) + assertNull(tag.description) + assertNull(tag.feature_image) + assertNull(tag.accent_color) + assertNull(tag.count) + assertNull(tag.created_at) + assertNull(tag.updated_at) + assertNull(tag.url) + } + + @Test + fun `GhostTagFull stores all fields`() { + val tag = GhostTagFull( + id = "tag-1", + name = "kotlin", + slug = "kotlin", + description = "Posts about Kotlin", + feature_image = "https://example.com/kotlin.png", + visibility = "public", + accent_color = "#FF5722", + count = TagCount(posts = 42), + created_at = "2024-01-01T00:00:00.000Z", + updated_at = "2024-06-15T12:00:00.000Z", + url = "https://blog.example.com/tag/kotlin/" + ) + assertEquals("tag-1", tag.id) + assertEquals("kotlin", tag.name) + assertEquals("kotlin", tag.slug) + assertEquals("Posts about Kotlin", tag.description) + assertEquals("https://example.com/kotlin.png", tag.feature_image) + assertEquals("public", tag.visibility) + assertEquals("#FF5722", tag.accent_color) + assertEquals(42, tag.count?.posts) + assertEquals("2024-01-01T00:00:00.000Z", tag.created_at) + assertEquals("2024-06-15T12:00:00.000Z", tag.updated_at) + assertEquals("https://blog.example.com/tag/kotlin/", tag.url) + } + + // --- TagCount --- + + @Test + fun `TagCount stores post count`() { + val count = TagCount(posts = 10) + assertEquals(10, count.posts) + } + + @Test + fun `TagCount allows null posts`() { + val count = TagCount(posts = null) + assertNull(count.posts) + } + + // --- GSON serialization --- + + @Test + fun `GhostTagFull serializes to JSON correctly`() { + val tag = GhostTagFull( + name = "android", + description = "Android development", + accent_color = "#3DDC84" + ) + val json = gson.toJson(tag) + assertTrue(json.contains("\"name\":\"android\"")) + assertTrue(json.contains("\"description\":\"Android development\"")) + assertTrue(json.contains("\"accent_color\":\"#3DDC84\"")) + } + + @Test + fun `GhostTagFull deserializes from JSON correctly`() { + val json = """{ + "id": "abc123", + "name": "tech", + "slug": "tech", + "description": "Technology posts", + "visibility": "public", + "accent_color": "#1E88E5", + "count": {"posts": 15}, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-06-01T00:00:00.000Z", + "url": "https://blog.example.com/tag/tech/" + }""" + val tag = gson.fromJson(json, GhostTagFull::class.java) + assertEquals("abc123", tag.id) + assertEquals("tech", tag.name) + assertEquals("tech", tag.slug) + assertEquals("Technology posts", tag.description) + assertEquals("public", tag.visibility) + assertEquals("#1E88E5", tag.accent_color) + assertEquals(15, tag.count?.posts) + assertEquals("2024-01-01T00:00:00.000Z", tag.created_at) + assertEquals("2024-06-01T00:00:00.000Z", tag.updated_at) + assertEquals("https://blog.example.com/tag/tech/", tag.url) + } + + @Test + fun `GhostTagFull deserializes with missing optional fields`() { + val json = """{"name": "minimal"}""" + val tag = gson.fromJson(json, GhostTagFull::class.java) + assertEquals("minimal", tag.name) + assertNull(tag.id) + assertNull(tag.slug) + assertNull(tag.description) + assertNull(tag.accent_color) + assertNull(tag.count) + } + + // --- TagsResponse --- + + @Test + fun `TagsResponse deserializes with tags and meta`() { + val json = """{ + "tags": [ + {"id": "1", "name": "news", "slug": "news", "count": {"posts": 5}}, + {"id": "2", "name": "tech", "slug": "tech", "count": {"posts": 12}} + ], + "meta": {"pagination": {"page": 1, "limit": 15, "pages": 1, "total": 2, "next": null, "prev": null}} + }""" + val response = gson.fromJson(json, TagsResponse::class.java) + assertEquals(2, response.tags.size) + assertEquals("news", response.tags[0].name) + assertEquals(5, response.tags[0].count?.posts) + assertEquals("tech", response.tags[1].name) + assertEquals(12, response.tags[1].count?.posts) + assertNotNull(response.meta) + assertEquals(1, response.meta?.pagination?.page) + assertEquals(2, response.meta?.pagination?.total) + } + + @Test + fun `TagsResponse deserializes with null meta`() { + val json = """{"tags": [{"name": "solo"}], "meta": null}""" + val response = gson.fromJson(json, TagsResponse::class.java) + assertEquals(1, response.tags.size) + assertNull(response.meta) + } + + // --- TagWrapper --- + + @Test + fun `TagWrapper wraps tags for API request`() { + val wrapper = TagWrapper(listOf(GhostTagFull(name = "new-tag", description = "A new tag"))) + val json = gson.toJson(wrapper) + assertTrue(json.contains("\"tags\"")) + assertTrue(json.contains("\"new-tag\"")) + assertTrue(json.contains("\"A new tag\"")) + } + + @Test + fun `TagWrapper serializes accent_color`() { + val wrapper = TagWrapper(listOf(GhostTagFull( + name = "colored", + accent_color = "#FF0000" + ))) + val json = gson.toJson(wrapper) + assertTrue(json.contains("\"accent_color\":\"#FF0000\"")) + } + + @Test + fun `TagCount zero posts`() { + val count = TagCount(posts = 0) + assertEquals(0, count.posts) + } + + @Test + fun `GhostTagFull with internal visibility`() { + val tag = GhostTagFull(name = "internal-tag", visibility = "internal") + assertEquals("internal", tag.visibility) + } +}