feat: add extended tag model (GhostTagFull) and tag CRUD API endpoints

Add TagModels.kt with GhostTagFull, TagsResponse, TagWrapper, TagCount
data classes for full Ghost tag management. Add getTags, getTag,
createTag, updateTag, deleteTag endpoints to GhostApiService.
This commit is contained in:
Paweł Orzech 2026-03-20 00:25:34 +01:00
parent 0891013df6
commit d0019947f8
3 changed files with 244 additions and 0 deletions

View file

@ -2,6 +2,8 @@ package com.swoosh.microblog.data.api
import com.swoosh.microblog.data.model.PostWrapper import com.swoosh.microblog.data.model.PostWrapper
import com.swoosh.microblog.data.model.PostsResponse 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.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.Response import retrofit2.Response
@ -40,6 +42,26 @@ interface GhostApiService {
@GET("ghost/api/admin/users/me/") @GET("ghost/api/admin/users/me/")
suspend fun getCurrentUser(): Response<UsersResponse> suspend fun getCurrentUser(): Response<UsersResponse>
@GET("ghost/api/admin/tags/")
suspend fun getTags(
@Query("limit") limit: String = "all",
@Query("include") include: String = "count.posts"
): Response<TagsResponse>
@GET("ghost/api/admin/tags/{id}/")
suspend fun getTag(@Path("id") id: String): Response<TagsResponse>
@POST("ghost/api/admin/tags/")
@Headers("Content-Type: application/json")
suspend fun createTag(@Body body: TagWrapper): Response<TagsResponse>
@PUT("ghost/api/admin/tags/{id}/")
@Headers("Content-Type: application/json")
suspend fun updateTag(@Path("id") id: String, @Body body: TagWrapper): Response<TagsResponse>
@DELETE("ghost/api/admin/tags/{id}/")
suspend fun deleteTag(@Path("id") id: String): Response<Unit>
@Multipart @Multipart
@POST("ghost/api/admin/images/upload/") @POST("ghost/api/admin/images/upload/")
suspend fun uploadImage( suspend fun uploadImage(

View file

@ -0,0 +1,26 @@
package com.swoosh.microblog.data.model
data class TagsResponse(
val tags: List<GhostTagFull>,
val meta: Meta?
)
data class TagWrapper(
val tags: List<GhostTagFull>
)
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?)

View file

@ -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)
}
}