mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add Member model, API endpoints, and model parsing tests
Add MemberModels.kt with GhostMember, MemberLabel, MemberNewsletter, MemberSubscription, SubscriptionPrice, and SubscriptionTier data classes. Add getMembers() and getMember() endpoints to GhostApiService. Add comprehensive JSON parsing tests for all member model types.
This commit is contained in:
parent
0891013df6
commit
689b8cc8c2
3 changed files with 331 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
package com.swoosh.microblog.data.api
|
package com.swoosh.microblog.data.api
|
||||||
|
|
||||||
|
import com.swoosh.microblog.data.model.MembersResponse
|
||||||
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 okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
|
@ -37,6 +38,21 @@ interface GhostApiService {
|
||||||
@Path("id") id: String
|
@Path("id") id: String
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@GET("ghost/api/admin/members/")
|
||||||
|
suspend fun getMembers(
|
||||||
|
@Query("limit") limit: Int = 15,
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("order") order: String = "created_at desc",
|
||||||
|
@Query("filter") filter: String? = null,
|
||||||
|
@Query("include") include: String = "newsletters,labels"
|
||||||
|
): Response<MembersResponse>
|
||||||
|
|
||||||
|
@GET("ghost/api/admin/members/{id}/")
|
||||||
|
suspend fun getMember(
|
||||||
|
@Path("id") id: String,
|
||||||
|
@Query("include") include: String = "newsletters,labels"
|
||||||
|
): Response<MembersResponse>
|
||||||
|
|
||||||
@GET("ghost/api/admin/users/me/")
|
@GET("ghost/api/admin/users/me/")
|
||||||
suspend fun getCurrentUser(): Response<UsersResponse>
|
suspend fun getCurrentUser(): Response<UsersResponse>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.swoosh.microblog.data.model
|
||||||
|
|
||||||
|
data class MembersResponse(
|
||||||
|
val members: List<GhostMember>,
|
||||||
|
val meta: Meta?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GhostMember(
|
||||||
|
val id: String,
|
||||||
|
val email: String?,
|
||||||
|
val name: String?,
|
||||||
|
val status: String?, // "free" or "paid"
|
||||||
|
val avatar_image: String?,
|
||||||
|
val email_count: Int?,
|
||||||
|
val email_opened_count: Int?,
|
||||||
|
val email_open_rate: Double?,
|
||||||
|
val last_seen_at: String?,
|
||||||
|
val created_at: String?,
|
||||||
|
val updated_at: String?,
|
||||||
|
val labels: List<MemberLabel>?,
|
||||||
|
val newsletters: List<MemberNewsletter>?,
|
||||||
|
val subscriptions: List<MemberSubscription>?,
|
||||||
|
val note: String?,
|
||||||
|
val geolocation: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MemberLabel(val id: String?, val name: String, val slug: String?)
|
||||||
|
data class MemberNewsletter(val id: String, val name: String?, val slug: String?)
|
||||||
|
data class MemberSubscription(
|
||||||
|
val id: String?, val status: String?, val start_date: String?,
|
||||||
|
val current_period_end: String?, val cancel_at_period_end: Boolean?,
|
||||||
|
val price: SubscriptionPrice?, val tier: SubscriptionTier?
|
||||||
|
)
|
||||||
|
data class SubscriptionPrice(val amount: Int?, val currency: String?, val interval: String?)
|
||||||
|
data class SubscriptionTier(val id: String?, val name: String?)
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
package com.swoosh.microblog.data.model
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MemberModelsTest {
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember deserializes from JSON correctly`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "member1",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"name": "John Doe",
|
||||||
|
"status": "free",
|
||||||
|
"avatar_image": "https://example.com/avatar.jpg",
|
||||||
|
"email_count": 10,
|
||||||
|
"email_opened_count": 5,
|
||||||
|
"email_open_rate": 50.0,
|
||||||
|
"last_seen_at": "2026-03-15T10:00:00.000Z",
|
||||||
|
"created_at": "2026-01-01T00:00:00.000Z",
|
||||||
|
"updated_at": "2026-03-15T10:00:00.000Z",
|
||||||
|
"note": "VIP member",
|
||||||
|
"geolocation": "Warsaw, Poland"
|
||||||
|
}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals("member1", member.id)
|
||||||
|
assertEquals("test@example.com", member.email)
|
||||||
|
assertEquals("John Doe", member.name)
|
||||||
|
assertEquals("free", member.status)
|
||||||
|
assertEquals("https://example.com/avatar.jpg", member.avatar_image)
|
||||||
|
assertEquals(10, member.email_count)
|
||||||
|
assertEquals(5, member.email_opened_count)
|
||||||
|
assertEquals(50.0, member.email_open_rate!!, 0.001)
|
||||||
|
assertEquals("VIP member", member.note)
|
||||||
|
assertEquals("Warsaw, Poland", member.geolocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember deserializes with missing optional fields`() {
|
||||||
|
val json = """{"id": "member2"}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals("member2", member.id)
|
||||||
|
assertNull(member.email)
|
||||||
|
assertNull(member.name)
|
||||||
|
assertNull(member.status)
|
||||||
|
assertNull(member.avatar_image)
|
||||||
|
assertNull(member.email_count)
|
||||||
|
assertNull(member.email_opened_count)
|
||||||
|
assertNull(member.email_open_rate)
|
||||||
|
assertNull(member.last_seen_at)
|
||||||
|
assertNull(member.created_at)
|
||||||
|
assertNull(member.updated_at)
|
||||||
|
assertNull(member.labels)
|
||||||
|
assertNull(member.newsletters)
|
||||||
|
assertNull(member.subscriptions)
|
||||||
|
assertNull(member.note)
|
||||||
|
assertNull(member.geolocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember deserializes with labels`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "member3",
|
||||||
|
"labels": [
|
||||||
|
{"id": "label1", "name": "VIP", "slug": "vip"},
|
||||||
|
{"id": "label2", "name": "Beta", "slug": "beta"}
|
||||||
|
]
|
||||||
|
}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals(2, member.labels?.size)
|
||||||
|
assertEquals("VIP", member.labels?.get(0)?.name)
|
||||||
|
assertEquals("vip", member.labels?.get(0)?.slug)
|
||||||
|
assertEquals("Beta", member.labels?.get(1)?.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember deserializes with newsletters`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "member4",
|
||||||
|
"newsletters": [
|
||||||
|
{"id": "nl1", "name": "Weekly Digest", "slug": "weekly-digest"},
|
||||||
|
{"id": "nl2", "name": "Product Updates", "slug": "product-updates"}
|
||||||
|
]
|
||||||
|
}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals(2, member.newsletters?.size)
|
||||||
|
assertEquals("Weekly Digest", member.newsletters?.get(0)?.name)
|
||||||
|
assertEquals("nl2", member.newsletters?.get(1)?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember deserializes with subscriptions`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "member5",
|
||||||
|
"status": "paid",
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"id": "sub1",
|
||||||
|
"status": "active",
|
||||||
|
"start_date": "2026-01-01T00:00:00.000Z",
|
||||||
|
"current_period_end": "2026-04-01T00:00:00.000Z",
|
||||||
|
"cancel_at_period_end": false,
|
||||||
|
"price": {"amount": 500, "currency": "USD", "interval": "month"},
|
||||||
|
"tier": {"id": "tier1", "name": "Gold"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals("paid", member.status)
|
||||||
|
assertEquals(1, member.subscriptions?.size)
|
||||||
|
val sub = member.subscriptions!![0]
|
||||||
|
assertEquals("sub1", sub.id)
|
||||||
|
assertEquals("active", sub.status)
|
||||||
|
assertEquals(false, sub.cancel_at_period_end)
|
||||||
|
assertEquals(500, sub.price?.amount)
|
||||||
|
assertEquals("USD", sub.price?.currency)
|
||||||
|
assertEquals("month", sub.price?.interval)
|
||||||
|
assertEquals("Gold", sub.tier?.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MembersResponse deserializes with members and pagination`() {
|
||||||
|
val json = """{
|
||||||
|
"members": [
|
||||||
|
{"id": "m1", "email": "a@example.com", "name": "Alice", "status": "free"},
|
||||||
|
{"id": "m2", "email": "b@example.com", "name": "Bob", "status": "paid"}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"pagination": {
|
||||||
|
"page": 1, "limit": 15, "pages": 3, "total": 42, "next": 2, "prev": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
val response = gson.fromJson(json, MembersResponse::class.java)
|
||||||
|
assertEquals(2, response.members.size)
|
||||||
|
assertEquals("m1", response.members[0].id)
|
||||||
|
assertEquals("Alice", response.members[0].name)
|
||||||
|
assertEquals("free", response.members[0].status)
|
||||||
|
assertEquals("Bob", response.members[1].name)
|
||||||
|
assertEquals("paid", response.members[1].status)
|
||||||
|
assertEquals(1, response.meta?.pagination?.page)
|
||||||
|
assertEquals(3, response.meta?.pagination?.pages)
|
||||||
|
assertEquals(42, response.meta?.pagination?.total)
|
||||||
|
assertEquals(2, response.meta?.pagination?.next)
|
||||||
|
assertNull(response.meta?.pagination?.prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MembersResponse deserializes with empty members list`() {
|
||||||
|
val json = """{
|
||||||
|
"members": [],
|
||||||
|
"meta": {
|
||||||
|
"pagination": {
|
||||||
|
"page": 1, "limit": 15, "pages": 0, "total": 0, "next": null, "prev": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
val response = gson.fromJson(json, MembersResponse::class.java)
|
||||||
|
assertTrue(response.members.isEmpty())
|
||||||
|
assertEquals(0, response.meta?.pagination?.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MemberLabel deserializes correctly`() {
|
||||||
|
val json = """{"id": "l1", "name": "Premium", "slug": "premium"}"""
|
||||||
|
val label = gson.fromJson(json, MemberLabel::class.java)
|
||||||
|
assertEquals("l1", label.id)
|
||||||
|
assertEquals("Premium", label.name)
|
||||||
|
assertEquals("premium", label.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MemberLabel allows null id and slug`() {
|
||||||
|
val json = """{"name": "Test"}"""
|
||||||
|
val label = gson.fromJson(json, MemberLabel::class.java)
|
||||||
|
assertNull(label.id)
|
||||||
|
assertEquals("Test", label.name)
|
||||||
|
assertNull(label.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MemberNewsletter deserializes correctly`() {
|
||||||
|
val json = """{"id": "n1", "name": "Daily News", "slug": "daily-news"}"""
|
||||||
|
val newsletter = gson.fromJson(json, MemberNewsletter::class.java)
|
||||||
|
assertEquals("n1", newsletter.id)
|
||||||
|
assertEquals("Daily News", newsletter.name)
|
||||||
|
assertEquals("daily-news", newsletter.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubscriptionPrice deserializes correctly`() {
|
||||||
|
val json = """{"amount": 1000, "currency": "EUR", "interval": "year"}"""
|
||||||
|
val price = gson.fromJson(json, SubscriptionPrice::class.java)
|
||||||
|
assertEquals(1000, price.amount)
|
||||||
|
assertEquals("EUR", price.currency)
|
||||||
|
assertEquals("year", price.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubscriptionTier deserializes correctly`() {
|
||||||
|
val json = """{"id": "t1", "name": "Premium"}"""
|
||||||
|
val tier = gson.fromJson(json, SubscriptionTier::class.java)
|
||||||
|
assertEquals("t1", tier.id)
|
||||||
|
assertEquals("Premium", tier.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember serializes to JSON correctly`() {
|
||||||
|
val member = GhostMember(
|
||||||
|
id = "m1",
|
||||||
|
email = "test@test.com",
|
||||||
|
name = "Test User",
|
||||||
|
status = "free",
|
||||||
|
avatar_image = null,
|
||||||
|
email_count = 5,
|
||||||
|
email_opened_count = 3,
|
||||||
|
email_open_rate = 60.0,
|
||||||
|
last_seen_at = null,
|
||||||
|
created_at = "2026-01-01T00:00:00.000Z",
|
||||||
|
updated_at = null,
|
||||||
|
labels = emptyList(),
|
||||||
|
newsletters = emptyList(),
|
||||||
|
subscriptions = null,
|
||||||
|
note = null,
|
||||||
|
geolocation = null
|
||||||
|
)
|
||||||
|
val json = gson.toJson(member)
|
||||||
|
assertTrue(json.contains("\"id\":\"m1\""))
|
||||||
|
assertTrue(json.contains("\"email\":\"test@test.com\""))
|
||||||
|
assertTrue(json.contains("\"status\":\"free\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MembersResponse reuses Meta and Pagination from GhostModels`() {
|
||||||
|
// Verify MembersResponse uses the same Meta/Pagination as PostsResponse
|
||||||
|
val json = """{
|
||||||
|
"members": [{"id": "m1"}],
|
||||||
|
"meta": {"pagination": {"page": 2, "limit": 50, "pages": 5, "total": 250, "next": 3, "prev": 1}}
|
||||||
|
}"""
|
||||||
|
val response = gson.fromJson(json, MembersResponse::class.java)
|
||||||
|
val pagination = response.meta?.pagination!!
|
||||||
|
assertEquals(2, pagination.page)
|
||||||
|
assertEquals(50, pagination.limit)
|
||||||
|
assertEquals(5, pagination.pages)
|
||||||
|
assertEquals(250, pagination.total)
|
||||||
|
assertEquals(3, pagination.next)
|
||||||
|
assertEquals(1, pagination.prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GhostMember with zero email stats`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "m1",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"email_open_rate": null
|
||||||
|
}"""
|
||||||
|
val member = gson.fromJson(json, GhostMember::class.java)
|
||||||
|
assertEquals(0, member.email_count)
|
||||||
|
assertEquals(0, member.email_opened_count)
|
||||||
|
assertNull(member.email_open_rate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MemberSubscription with cancel_at_period_end true`() {
|
||||||
|
val json = """{
|
||||||
|
"id": "sub1",
|
||||||
|
"status": "active",
|
||||||
|
"cancel_at_period_end": true,
|
||||||
|
"price": {"amount": 900, "currency": "USD", "interval": "month"},
|
||||||
|
"tier": {"id": "t1", "name": "Basic"}
|
||||||
|
}"""
|
||||||
|
val sub = gson.fromJson(json, MemberSubscription::class.java)
|
||||||
|
assertEquals(true, sub.cancel_at_period_end)
|
||||||
|
assertEquals(900, sub.price?.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue