Integration: residence invite accept/decline APIs + wire notification actions

Adds acceptResidenceInvite / declineResidenceInvite to ResidenceApi
(POST /api/residences/{id}/invite/{accept|decline}) and exposes them via
APILayer. On accept success, myResidences is force-refreshed so the
newly-joined residence appears without a manual pull.

Wires NotificationActionReceiver's ACCEPT_INVITE / DECLINE_INVITE
handlers to the new APILayer calls, replacing the log-only TODOs left
behind by P4 Stream O. Notifications are now cleared only on API
success so a failed accept stays actionable.

Tests:
 - ResidenceApiInviteTest covers correct HTTP method/path + error surfacing.
 - NotificationActionReceiverTest invite cases updated to assert the new
   APILayer calls (were previously asserting the log-only path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 13:36:59 -05:00
parent 10b57aabaa
commit 485f70dfa1
5 changed files with 227 additions and 28 deletions
@@ -0,0 +1,127 @@
package com.tt.honeyDue.network
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.HttpHeaders
import io.ktor.http.headersOf
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Verifies the residence-invite accept/decline endpoints hit the correct
* paths. Wired from [com.tt.honeyDue.notifications.NotificationActionReceiver]
* when the user taps the Accept/Decline button on a `residence_invite`
* push notification (iOS parity with `NotificationCategories.swift`).
*/
class ResidenceApiInviteTest {
private fun mockClient(onRequest: (String) -> Unit): HttpClient =
HttpClient(MockEngine) {
engine {
addHandler { req ->
onRequest(req.url.encodedPath)
respond(
content = "",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "text/plain")
)
}
}
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
@Test
fun acceptResidenceInvite_hits_correct_endpoint() = runTest {
var method: HttpMethod? = null
var path: String? = null
val client = HttpClient(MockEngine) {
engine {
addHandler { req ->
method = req.method
path = req.url.encodedPath
respond(
content = "",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "text/plain")
)
}
}
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
val api = ResidenceApi(client)
val result = api.acceptResidenceInvite("test-token", residenceId = 42)
assertTrue(result is ApiResult.Success, "expected Success, got $result")
assertEquals(HttpMethod.Post, method)
assertTrue(
path?.endsWith("/residences/42/invite/accept/") == true,
"unexpected path: $path"
)
client.close()
}
@Test
fun declineResidenceInvite_hits_correct_endpoint() = runTest {
var method: HttpMethod? = null
var path: String? = null
val client = HttpClient(MockEngine) {
engine {
addHandler { req ->
method = req.method
path = req.url.encodedPath
respond(
content = "",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "text/plain")
)
}
}
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
val api = ResidenceApi(client)
val result = api.declineResidenceInvite("test-token", residenceId = 99)
assertTrue(result is ApiResult.Success, "expected Success, got $result")
assertEquals(HttpMethod.Post, method)
assertTrue(
path?.endsWith("/residences/99/invite/decline/") == true,
"unexpected path: $path"
)
client.close()
}
@Test
fun acceptResidenceInvite_surfaces_server_error() = runTest {
val client = HttpClient(MockEngine) {
engine {
addHandler {
respond(
content = """{"detail":"Invite not found"}""",
status = HttpStatusCode.NotFound,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
}
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
val api = ResidenceApi(client)
val result = api.acceptResidenceInvite("test-token", residenceId = 1)
assertTrue(result is ApiResult.Error, "expected Error, got $result")
assertEquals(404, result.code)
client.close()
}
}