Skip to content

Commit ec61d21

Browse files
committed
chore(services): refactor ocp Transactors to allow testability; add test coverage across controllers/transactors
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 735b9a3 commit ec61d21

27 files changed

Lines changed: 1727 additions & 1251 deletions

services/flipcash/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,7 @@ dependencies {
7171
implementation(libs.bugsnag)
7272

7373
implementation(libs.event.bus)
74+
75+
testImplementation(kotlin("test"))
76+
testImplementation(libs.bundles.unit.testing)
7477
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package com.flipcash.services.controllers
2+
3+
import com.flipcash.services.models.SocialAccount
4+
import com.flipcash.services.models.SocialAccountLinkRequest
5+
import com.flipcash.services.models.SocialAccountUnlinkRequest
6+
import com.flipcash.services.models.UserProfile
7+
import com.flipcash.services.repository.ProfileRepository
8+
import com.flipcash.services.user.UserManager
9+
import com.getcode.ed25519.Ed25519
10+
import com.getcode.opencode.model.accounts.AccountCluster
11+
import com.getcode.opencode.model.core.ID
12+
import io.mockk.every
13+
import io.mockk.mockk
14+
import io.mockk.mockkStatic
15+
import io.mockk.unmockkStatic
16+
import io.mockk.verify
17+
import kotlinx.coroutines.ExperimentalCoroutinesApi
18+
import kotlinx.coroutines.test.runTest
19+
import org.junit.After
20+
import org.junit.Before
21+
import org.junit.Test
22+
import kotlin.test.assertEquals
23+
import kotlin.test.assertTrue
24+
25+
@OptIn(ExperimentalCoroutinesApi::class)
26+
class ProfileControllerTest {
27+
28+
private val repository = FakeProfileRepository()
29+
private val userManager = mockk<UserManager>(relaxed = true)
30+
31+
private val controller = ProfileController(repository, userManager)
32+
33+
@Before
34+
fun setUp() {
35+
mockkStatic("com.getcode.utils.LoggingKt")
36+
every { com.getcode.utils.trace(any(), any(), any(), any(), any()) } returns Unit
37+
}
38+
39+
@After
40+
fun tearDown() {
41+
unmockkStatic("com.getcode.utils.LoggingKt")
42+
}
43+
44+
private fun stubOwner() {
45+
val keyPair = mockk<Ed25519.KeyPair>(relaxed = true)
46+
val cluster = mockk<AccountCluster>(relaxed = true) {
47+
every { authority } returns mockk { every { this@mockk.keyPair } returns keyPair }
48+
}
49+
every { userManager.accountCluster } returns cluster
50+
}
51+
52+
// region updateUserProfile
53+
54+
@Test
55+
fun `updateUserProfile fails when no account id`() = runTest {
56+
every { userManager.accountId } returns null
57+
58+
val result = controller.updateUserProfile()
59+
60+
assertTrue(result.isFailure)
61+
}
62+
63+
@Test
64+
fun `updateUserProfile fails when no account cluster`() = runTest {
65+
every { userManager.accountId } returns listOf(1)
66+
every { userManager.accountCluster } returns null
67+
68+
val result = controller.updateUserProfile()
69+
70+
assertTrue(result.isFailure)
71+
}
72+
73+
@Test
74+
fun `updateUserProfile caches profile on success`() = runTest {
75+
stubOwner()
76+
every { userManager.accountId } returns listOf(1)
77+
78+
val profile = stubProfile()
79+
repository.getProfileResult = Result.success(profile)
80+
81+
val result = controller.updateUserProfile()
82+
83+
assertTrue(result.isSuccess)
84+
assertEquals(profile, result.getOrThrow())
85+
verify { userManager.set(profile) }
86+
}
87+
88+
@Test
89+
fun `updateUserProfile propagates failure without caching`() = runTest {
90+
stubOwner()
91+
every { userManager.accountId } returns listOf(1)
92+
repository.getProfileResult = Result.failure(RuntimeException("not found"))
93+
94+
val result = controller.updateUserProfile()
95+
96+
assertTrue(result.isFailure)
97+
verify(exactly = 0) { userManager.set(any<UserProfile>()) }
98+
}
99+
100+
// endregion
101+
102+
// region getProfileForUser
103+
104+
@Test
105+
fun `getProfileForUser fails when no account cluster`() = runTest {
106+
every { userManager.accountCluster } returns null
107+
108+
val result = controller.getProfileForUser(listOf(1))
109+
110+
assertTrue(result.isFailure)
111+
}
112+
113+
@Test
114+
fun `getProfileForUser delegates to repository`() = runTest {
115+
stubOwner()
116+
val profile = stubProfile()
117+
repository.getProfileResult = Result.success(profile)
118+
119+
val result = controller.getProfileForUser(listOf(1))
120+
121+
assertTrue(result.isSuccess)
122+
assertEquals(profile, result.getOrThrow())
123+
}
124+
125+
// endregion
126+
127+
// region setDisplayName
128+
129+
@Test
130+
fun `setDisplayName fails when no account cluster`() = runTest {
131+
every { userManager.accountCluster } returns null
132+
133+
val result = controller.setDisplayName("Test")
134+
135+
assertTrue(result.isFailure)
136+
}
137+
138+
@Test
139+
fun `setDisplayName delegates to repository`() = runTest {
140+
stubOwner()
141+
repository.setDisplayNameResult = Result.success(Unit)
142+
143+
val result = controller.setDisplayName("Test")
144+
145+
assertTrue(result.isSuccess)
146+
}
147+
148+
// endregion
149+
150+
// region linkTwitterXAccount
151+
152+
@Test
153+
fun `linkTwitterXAccount fails when no account cluster`() = runTest {
154+
every { userManager.accountCluster } returns null
155+
156+
val result = controller.linkTwitterXAccount("token-123")
157+
158+
assertTrue(result.isFailure)
159+
}
160+
161+
@Test
162+
fun `linkTwitterXAccount delegates to repository`() = runTest {
163+
stubOwner()
164+
val account = stubTwitterXAccount()
165+
repository.linkSocialAccountResult = Result.success(account)
166+
167+
val result = controller.linkTwitterXAccount("token-123")
168+
169+
assertTrue(result.isSuccess)
170+
assertEquals(account, result.getOrThrow())
171+
}
172+
173+
@Test
174+
fun `linkTwitterXAccount propagates failure`() = runTest {
175+
stubOwner()
176+
repository.linkSocialAccountResult = Result.failure(RuntimeException("denied"))
177+
178+
val result = controller.linkTwitterXAccount("token-123")
179+
180+
assertTrue(result.isFailure)
181+
}
182+
183+
// endregion
184+
185+
// region unlinkTwitterXAccount
186+
187+
@Test
188+
fun `unlinkTwitterXAccount fails when no account cluster`() = runTest {
189+
every { userManager.accountCluster } returns null
190+
191+
val result = controller.unlinkTwitterXAccount(stubTwitterXAccount())
192+
193+
assertTrue(result.isFailure)
194+
}
195+
196+
@Test
197+
fun `unlinkTwitterXAccount delegates to repository`() = runTest {
198+
stubOwner()
199+
repository.unlinkSocialAccountResult = Result.success(Unit)
200+
201+
val result = controller.unlinkTwitterXAccount(stubTwitterXAccount())
202+
203+
assertTrue(result.isSuccess)
204+
}
205+
206+
// endregion
207+
208+
// region helpers
209+
210+
private fun stubProfile() = UserProfile(
211+
displayName = "Test User",
212+
socialAccounts = emptyList(),
213+
verifiedPhoneNumber = null,
214+
verifiedEmailAddress = null,
215+
)
216+
217+
private fun stubTwitterXAccount() = SocialAccount.TwitterX(
218+
id = "123",
219+
username = "testuser",
220+
name = "Test User",
221+
description = "",
222+
profilePicUrl = "",
223+
verifiedType = null,
224+
followerCount = 0,
225+
)
226+
227+
// endregion
228+
}
229+
230+
// region Fakes
231+
232+
private class FakeProfileRepository : ProfileRepository {
233+
var getProfileResult: Result<UserProfile> = Result.failure(RuntimeException("not configured"))
234+
var setDisplayNameResult: Result<Unit> = Result.success(Unit)
235+
var linkSocialAccountResult: Result<SocialAccount> = Result.failure(RuntimeException("not configured"))
236+
var unlinkSocialAccountResult: Result<Unit> = Result.success(Unit)
237+
238+
override suspend fun getProfile(userId: ID, owner: Ed25519.KeyPair) = getProfileResult
239+
override suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair) = setDisplayNameResult
240+
override suspend fun linkSocialAccount(request: SocialAccountLinkRequest, owner: Ed25519.KeyPair) =
241+
linkSocialAccountResult
242+
override suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest, owner: Ed25519.KeyPair) =
243+
unlinkSocialAccountResult
244+
}
245+
246+
// endregion
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.flipcash.services.internal.domain
2+
3+
import com.codeinc.flipcash.gen.profile.v1.socialProfile
4+
import com.codeinc.flipcash.gen.profile.v1.xProfile
5+
import com.codeinc.flipcash.gen.profile.v1.Model
6+
import com.flipcash.services.models.SocialAccount
7+
import org.junit.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertIs
10+
import kotlin.test.assertNull
11+
12+
class SocialAccountMapperTest {
13+
14+
private val mapper = SocialAccountMapper()
15+
16+
@Test
17+
fun `maps X profile fields`() {
18+
val proto = socialProfile {
19+
x = xProfile {
20+
id = "123"
21+
username = "testuser"
22+
name = "Test User"
23+
description = "Bio text"
24+
profilePicUrl = "https://example.com/pic.jpg"
25+
followerCount = 5000
26+
}
27+
}
28+
29+
val result = mapper.map(proto)
30+
31+
assertIs<SocialAccount.TwitterX>(result)
32+
assertEquals("123", result.id)
33+
assertEquals("testuser", result.username)
34+
assertEquals("Test User", result.name)
35+
assertEquals("Bio text", result.description)
36+
assertEquals("https://example.com/pic.jpg", result.profilePicUrl)
37+
assertEquals(5000, result.followerCount)
38+
}
39+
40+
@Test
41+
fun `maps verified type NONE`() {
42+
val proto = socialProfile {
43+
x = xProfile {
44+
verifiedType = Model.XProfile.VerifiedType.NONE
45+
}
46+
}
47+
48+
val result = mapper.map(proto) as SocialAccount.TwitterX
49+
assertEquals(SocialAccount.TwitterX.VerifiedType.NONE, result.verifiedType)
50+
}
51+
52+
@Test
53+
fun `maps verified type BLUE`() {
54+
val proto = socialProfile {
55+
x = xProfile {
56+
verifiedType = Model.XProfile.VerifiedType.BLUE
57+
}
58+
}
59+
60+
val result = mapper.map(proto) as SocialAccount.TwitterX
61+
assertEquals(SocialAccount.TwitterX.VerifiedType.BLUE, result.verifiedType)
62+
}
63+
64+
@Test
65+
fun `maps verified type BUSINESS`() {
66+
val proto = socialProfile {
67+
x = xProfile {
68+
verifiedType = Model.XProfile.VerifiedType.BUSINESS
69+
}
70+
}
71+
72+
val result = mapper.map(proto) as SocialAccount.TwitterX
73+
assertEquals(SocialAccount.TwitterX.VerifiedType.BUSINESS, result.verifiedType)
74+
}
75+
76+
@Test
77+
fun `maps verified type GOVERNMENT`() {
78+
val proto = socialProfile {
79+
x = xProfile {
80+
verifiedType = Model.XProfile.VerifiedType.GOVERNMENT
81+
}
82+
}
83+
84+
val result = mapper.map(proto) as SocialAccount.TwitterX
85+
assertEquals(SocialAccount.TwitterX.VerifiedType.GOVERNMENT, result.verifiedType)
86+
}
87+
88+
@Test
89+
fun `TYPE_NOT_SET returns null`() {
90+
val proto = socialProfile { }
91+
92+
val result = mapper.map(proto)
93+
94+
assertNull(result)
95+
}
96+
}

0 commit comments

Comments
 (0)