From aacf1a04b977b07c4d66786b63d807049e7fff51 Mon Sep 17 00:00:00 2001 From: sokari Date: Sun, 19 May 2024 09:52:14 +0100 Subject: [PATCH 1/4] fix: embedded db duplicate index --- pom.xml | 24 +++++++++-- src/main/kotlin/ng/cove/web/App.kt | 3 +- .../kotlin/ng/cove/web/config/MongoConfig.kt | 10 ++++- .../kotlin/ng/cove/web/data/model/PhoneOtp.kt | 15 +++---- .../cove/web/data/repo/MemberPhoneOtpRepo.kt | 2 +- .../kotlin/ng/cove/web/service/UserService.kt | 15 ++++--- src/main/resources/application.properties | 4 +- .../ng/cove/web/{AppTests.kt => AppTest.kt} | 41 +++++++++++-------- .../http/controller/AdminControllerTest.kt | 5 +-- .../http/controller/DefaultControllerTest.kt | 23 ++++++++--- .../ng/cove/web/service/LevyServiceTest.kt | 4 +- 11 files changed, 98 insertions(+), 48 deletions(-) rename src/test/kotlin/ng/cove/web/{AppTests.kt => AppTest.kt} (80%) diff --git a/pom.xml b/pom.xml index 8534a2e..d009444 100644 --- a/pom.xml +++ b/pom.xml @@ -130,9 +130,15 @@ de.flapdoodle.embed - de.flapdoodle.embed.mongo - 4.13.1 - + de.flapdoodle.embed.mongo.spring30x + 4.11.0 + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test @@ -152,6 +158,16 @@ src/main/kotlin src/test/kotlin + + + + + + + + + + com.google.cloud.tools @@ -226,7 +242,7 @@ - src/test/java + src/test/kotlin target/generated-test-sources/test-annotations diff --git a/src/main/kotlin/ng/cove/web/App.kt b/src/main/kotlin/ng/cove/web/App.kt index cac0dc1..265841d 100644 --- a/src/main/kotlin/ng/cove/web/App.kt +++ b/src/main/kotlin/ng/cove/web/App.kt @@ -6,6 +6,7 @@ import com.google.cloud.secretmanager.v1.SecretManagerServiceClient import com.google.cloud.secretmanager.v1.SecretVersionName import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions +import de.flapdoodle.embed.mongo.spring.autoconfigure.EmbeddedMongoAutoConfiguration import ng.cove.web.component.SmsOtpService import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication @@ -23,7 +24,7 @@ import java.util.concurrent.TimeUnit @EnableAsync @EnableCaching @EnableScheduling -@SpringBootApplication(exclude = [MongoDataAutoConfiguration::class]) +@SpringBootApplication(exclude = [MongoDataAutoConfiguration::class, EmbeddedMongoAutoConfiguration::class]) class App { @Bean diff --git a/src/main/kotlin/ng/cove/web/config/MongoConfig.kt b/src/main/kotlin/ng/cove/web/config/MongoConfig.kt index 2d2f4a2..2ba93d8 100644 --- a/src/main/kotlin/ng/cove/web/config/MongoConfig.kt +++ b/src/main/kotlin/ng/cove/web/config/MongoConfig.kt @@ -22,7 +22,14 @@ class MongoConfig(val context: WebApplicationContext) : AbstractMongoClientConfi var embeddedMongo: TransitionWalker.ReachedState? = null - override fun getDatabaseName(): String = "dev" + override fun getDatabaseName(): String { + val profiles = context.environment.activeProfiles + return if (profiles.getOrNull(0) == "test"){ + "test" + }else { + "dev" + } + } override fun mongoClient(): MongoClient { val profiles = context.environment.activeProfiles @@ -44,6 +51,7 @@ class MongoConfig(val context: WebApplicationContext) : AbstractMongoClientConfi } } else -> { + // Embedded Mongo instance for testing embeddedMongo = Mongod.instance().start(Version.Main.V7_0) val serverAddress: ServerAddress = embeddedMongo!!.current().serverAddress diff --git a/src/main/kotlin/ng/cove/web/data/model/PhoneOtp.kt b/src/main/kotlin/ng/cove/web/data/model/PhoneOtp.kt index 3d060da..2b44f1b 100644 --- a/src/main/kotlin/ng/cove/web/data/model/PhoneOtp.kt +++ b/src/main/kotlin/ng/cove/web/data/model/PhoneOtp.kt @@ -1,31 +1,28 @@ package ng.cove.web.data.model +import com.mongodb.lang.NonNull import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.CompoundIndex import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Field import java.util.* @Document("phone_otp") +@CompoundIndex(name = "phone_ref", def = "{'phone': 1, 'ref': 1}", unique = true) class PhoneOtp { - constructor(phone: String, ref: String, type: UserType, expireAt: Date?){ - this.phone = phone - this.ref = ref - this.type = type - this.expireAt = expireAt - } - @Id var id: String? = null + @field:NonNull var phone: String? = null - @Indexed(unique = true, sparse = true) + @field:NonNull var ref: String? = null - private var type: UserType = UserType.Member + var type: UserType = UserType.Member @field:Field("expire_at") var expireAt: Date? = null diff --git a/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt b/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt index d576ba1..f513366 100644 --- a/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt +++ b/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt @@ -6,5 +6,5 @@ import java.util.Date interface MemberPhoneOtpRepo: MongoRepository { fun countByPhoneAndCreatedAtIsAfter(phone: String, createdAt: Date): Long - fun countByPhone(phone: String): Long + fun deleteAllByPhone(phone: String) } \ No newline at end of file diff --git a/src/main/kotlin/ng/cove/web/service/UserService.kt b/src/main/kotlin/ng/cove/web/service/UserService.kt index 4f60ae5..5fec402 100644 --- a/src/main/kotlin/ng/cove/web/service/UserService.kt +++ b/src/main/kotlin/ng/cove/web/service/UserService.kt @@ -1,6 +1,5 @@ package ng.cove.web.service -import com.fasterxml.jackson.databind.ObjectMapper import com.google.firebase.auth.FirebaseAuth import ng.cove.web.component.SmsOtpService import ng.cove.web.data.model.PhoneOtp @@ -20,8 +19,6 @@ import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service import java.time.Duration import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId import java.util.* @@ -75,7 +72,12 @@ class UserService { return if (otpResult != null) { trialCount++ otpResult.dailyTrialLeft = maxDailyOtpTrial - trialCount - otpRepo.save(PhoneOtp(phone, otpResult.ref, userType, otpResult.expireAt)) + val phoneOtp = PhoneOtp() + phoneOtp.phone = phone + phoneOtp.ref = otpResult.ref + phoneOtp.type = userType + phoneOtp.expireAt = otpResult.expireAt + otpRepo.save(phoneOtp) ResponseEntity.ok().body(otpResult) } else { ResponseEntity.internalServerError().body("OTP provider error") @@ -130,9 +132,12 @@ class UserService { } val firebaseAuth = FirebaseAuth.getInstance() - //Revoke refresh token for old devices if any + try { + //Revoke refresh token for old devices if any firebaseAuth.revokeRefreshTokens(userId) + // Clear OTP limit for this user + otpRepo.deleteAllByPhone(phone) } catch (_: Exception) { } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c0e6836..80c0651 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,4 +12,6 @@ spring.cloud.gcp.secretmanager.enabled=true # GCP Cloud Logging spring.cloud.gcp.logging.enabled=true #This resolves CORS issues on Swagger UI -server.forward-headers-strategy=framework \ No newline at end of file +server.forward-headers-strategy=framework + +#de.flapdoodle.mongodb.embedded.version=6.0.5 \ No newline at end of file diff --git a/src/test/kotlin/ng/cove/web/AppTests.kt b/src/test/kotlin/ng/cove/web/AppTest.kt similarity index 80% rename from src/test/kotlin/ng/cove/web/AppTests.kt rename to src/test/kotlin/ng/cove/web/AppTest.kt index 98e4d25..559d4b3 100644 --- a/src/test/kotlin/ng/cove/web/AppTests.kt +++ b/src/test/kotlin/ng/cove/web/AppTest.kt @@ -11,8 +11,11 @@ import ng.cove.web.data.repo.CommunityRepo import ng.cove.web.data.repo.JoinRequestRepo import ng.cove.web.data.repo.MemberPhoneOtpRepo import ng.cove.web.data.repo.MemberRepo +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInstance import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.Mockito.mockStatic @@ -21,6 +24,8 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.getCollectionName import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc @@ -28,7 +33,8 @@ import org.springframework.test.web.servlet.MockMvc @SpringBootTest(classes = [App::class]) @ActiveProfiles("test") @AutoConfigureMockMvc -class AppTests { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AppTest { @Autowired lateinit var communityRepo: CommunityRepo @@ -42,18 +48,14 @@ class AppTests { @Autowired lateinit var memberPhoneOtpRepo: MemberPhoneOtpRepo - - lateinit var member: Member - lateinit var community: Community - @MockBean lateinit var smsOtpService: SmsOtpService @Autowired lateinit var mockMvc: MockMvc - lateinit var staticFirebaseAuth: MockedStatic + lateinit var staticFirebaseAuth: MockedStatic // Mocked FirebaseAuth for testing val auth: FirebaseAuth = Mockito.mock(FirebaseAuth::class.java) @@ -63,9 +65,15 @@ class AppTests { final val faker = Faker() val mapper = ObjectMapper().apply { propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE } + lateinit var member: Member + lateinit var community: Community + + @Autowired + lateinit var mongoTemplate : MongoTemplate @BeforeEach fun setUp() { + community = Community() community.id = faker.random().hex(20) community.name = "${faker.address().state()} Community" @@ -81,23 +89,24 @@ class AppTests { community.adminIds = setOf(member.id!!) member.community = community + } - + @BeforeAll + fun setupAll(){ // Mock FirebaseAuth staticFirebaseAuth = mockStatic(FirebaseAuth::class.java) staticFirebaseAuth.`when`(FirebaseAuth::getInstance).thenReturn(auth) + + //TODO: Investigate why this index is always duplicate in Embedded db + mongoTemplate.collectionNames.forEach { + mongoTemplate.getCollection(it).dropIndexes() + mongoTemplate.getCollection(it).drop() + } } - @AfterEach - fun tearDown() { + @AfterAll + fun tearDownAll(){ staticFirebaseAuth.close() - -// communityRepo.deleteAll() -// memberRepo.deleteAll() -// joinRequestRepo.deleteAll() -// memberPhoneOtpRepo.deleteAll() } - - } diff --git a/src/test/kotlin/ng/cove/web/http/controller/AdminControllerTest.kt b/src/test/kotlin/ng/cove/web/http/controller/AdminControllerTest.kt index 8c01f15..c80fbcd 100644 --- a/src/test/kotlin/ng/cove/web/http/controller/AdminControllerTest.kt +++ b/src/test/kotlin/ng/cove/web/http/controller/AdminControllerTest.kt @@ -2,7 +2,7 @@ package ng.cove.web.http.controller import com.google.firebase.auth.FirebaseToken -import ng.cove.web.AppTests +import ng.cove.web.AppTest import ng.cove.web.data.model.* import ng.cove.web.service.CacheService import org.junit.jupiter.api.Assertions.assertEquals @@ -18,10 +18,9 @@ import org.springframework.http.MediaType import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post -import java.util.* @ActiveProfiles("test") -class AdminControllerTest : AppTests() { +class AdminControllerTest : AppTest() { @MockBean lateinit var cacheService: CacheService diff --git a/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt b/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt index 1a7e4b7..bf935ac 100644 --- a/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt +++ b/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt @@ -1,6 +1,6 @@ package ng.cove.web.http.controller -import ng.cove.web.AppTests +import ng.cove.web.AppTest import ng.cove.web.data.model.PhoneOtp import ng.cove.web.data.model.UserType import ng.cove.web.http.body.OtpRefBody @@ -22,14 +22,18 @@ import java.util.* //@WebAppConfiguration("") -class DefaultControllerTest : AppTests() { +class DefaultControllerTest : AppTest() { @Test fun givenPhoneRegistered_whenUserPhoneGetOtp_thenSuccess() { val phone = member.phone!! memberRepo.save(member) val ref = faker.random().hex(20) - val phoneOtp = PhoneOtp(phone, ref, UserType.Member, Date()) + val phoneOtp = PhoneOtp() + phoneOtp.phone = phone + phoneOtp.ref = ref + phoneOtp.type = UserType.Member + phoneOtp.expireAt = Date() memberPhoneOtpRepo.save(phoneOtp) val otpRefBody = OtpRefBody(ref, phone, Date(), 2) Mockito.`when`(smsOtpService.sendOtp(member.phone!!)).thenReturn(otpRefBody) @@ -44,7 +48,11 @@ class DefaultControllerTest : AppTests() { fun givenPhoneNotRegistered_whenUserPhoneGetOtp_thenError() { val phone = member.phone!! val ref = faker.random().hex(20) - val phoneOtp = PhoneOtp(phone, ref, UserType.Member, Date()) + val phoneOtp = PhoneOtp() + phoneOtp.phone = phone + phoneOtp.ref = ref + phoneOtp.type = UserType.Member + phoneOtp.expireAt = Date() memberPhoneOtpRepo.save(phoneOtp) val otpRefBody = OtpRefBody(ref, phone, Date(), 2) Mockito.`when`(smsOtpService.sendOtp(member.phone!!)).thenReturn(otpRefBody) @@ -65,7 +73,12 @@ class DefaultControllerTest : AppTests() { val phoneOtps = buildList { repeat(maxDailyOtpTrial) { val momentsAgo = Date.from(Instant.now().minus(Duration.ofHours(faker.random().nextLong(1, 8)))) - add(PhoneOtp(member.phone!!, faker.random().hex(20), UserType.Member, momentsAgo)) + val phoneOtp = PhoneOtp() + phoneOtp.phone = member.phone!! + phoneOtp.ref = faker.random().hex(20) + phoneOtp.type = UserType.Member + phoneOtp.expireAt = momentsAgo + add(phoneOtp) } } memberPhoneOtpRepo.saveAll(phoneOtps) diff --git a/src/test/kotlin/ng/cove/web/service/LevyServiceTest.kt b/src/test/kotlin/ng/cove/web/service/LevyServiceTest.kt index dcd4aaa..ec17293 100644 --- a/src/test/kotlin/ng/cove/web/service/LevyServiceTest.kt +++ b/src/test/kotlin/ng/cove/web/service/LevyServiceTest.kt @@ -1,6 +1,6 @@ package ng.cove.web.service -import ng.cove.web.AppTests +import ng.cove.web.AppTest import ng.cove.web.data.model.Community import ng.cove.web.data.model.Levy import ng.cove.web.data.model.LevyType @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired import java.util.* -class LevyServiceTest : AppTests() { +class LevyServiceTest : AppTest() { @Autowired private lateinit var levyService: LevyService From fdc97ba3a9d6f058134e111dfc0b88eb011c3130 Mon Sep 17 00:00:00 2001 From: sokari Date: Sun, 19 May 2024 11:19:10 +0100 Subject: [PATCH 2/4] chore: test.yml Github workflow --- .github/workflows/deploy.yml | 2 - .github/workflows/test.yml | 22 ++++ README.md | 1 + pom.xml | 10 -- .../cove/web/data/repo/MemberPhoneOtpRepo.kt | 1 + src/test/kotlin/ng/cove/web/AppTest.kt | 4 +- .../http/controller/DefaultControllerTest.kt | 107 ++++++++++-------- 7 files changed, 87 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 558b3c1..eeeb538 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,8 +47,6 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Test - run: mvn test - name: Build with Maven run: mvn -B package -DskipTests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..40be9ba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: test.yml +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Run integration test + run: mvn clean test diff --git a/README.md b/README.md index 8299926..e89f098 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Cove Web Service Repo ![Deployment](https://github.com/sprinthubmobile/cove_web/actions/workflows/deploy.yml/badge.svg?branch=main) +![Test](https://github.com/sprinthubmobile/cove_web/actions/workflows/test.yml/badge.svg) ## Before diving in 🙌 - Our recommend IDE for this project is IntelliJ, but you can use any IDE that supports Springboot diff --git a/pom.xml b/pom.xml index d009444..e8ef037 100644 --- a/pom.xml +++ b/pom.xml @@ -158,16 +158,6 @@ src/main/kotlin src/test/kotlin - - - - - - - - - - com.google.cloud.tools diff --git a/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt b/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt index f513366..73d628e 100644 --- a/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt +++ b/src/main/kotlin/ng/cove/web/data/repo/MemberPhoneOtpRepo.kt @@ -7,4 +7,5 @@ import java.util.Date interface MemberPhoneOtpRepo: MongoRepository { fun countByPhoneAndCreatedAtIsAfter(phone: String, createdAt: Date): Long fun deleteAllByPhone(phone: String) + fun countByPhone(phone: String): Long } \ No newline at end of file diff --git a/src/test/kotlin/ng/cove/web/AppTest.kt b/src/test/kotlin/ng/cove/web/AppTest.kt index 559d4b3..9dcc489 100644 --- a/src/test/kotlin/ng/cove/web/AppTest.kt +++ b/src/test/kotlin/ng/cove/web/AppTest.kt @@ -12,7 +12,6 @@ import ng.cove.web.data.repo.JoinRequestRepo import ng.cove.web.data.repo.MemberPhoneOtpRepo import ng.cove.web.data.repo.MemberRepo import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInstance @@ -25,7 +24,6 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.data.mongodb.core.MongoTemplate -import org.springframework.data.mongodb.core.getCollectionName import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc @@ -97,7 +95,7 @@ class AppTest { staticFirebaseAuth = mockStatic(FirebaseAuth::class.java) staticFirebaseAuth.`when`(FirebaseAuth::getInstance).thenReturn(auth) - //TODO: Investigate why this index is always duplicate in Embedded db + //TODO: Investigate why 'phone_otp' collection index is always duplicate in Embedded db mongoTemplate.collectionNames.forEach { mongoTemplate.getCollection(it).dropIndexes() mongoTemplate.getCollection(it).drop() diff --git a/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt b/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt index bf935ac..076a40d 100644 --- a/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt +++ b/src/test/kotlin/ng/cove/web/http/controller/DefaultControllerTest.kt @@ -4,8 +4,11 @@ import ng.cove.web.AppTest import ng.cove.web.data.model.PhoneOtp import ng.cove.web.data.model.UserType import ng.cove.web.http.body.OtpRefBody +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.mockito.Mockito import org.mockito.kotlin.any @@ -13,6 +16,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.springframework.http.MediaType +import org.springframework.test.context.event.annotation.AfterTestClass import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -67,57 +71,70 @@ class DefaultControllerTest : AppTest() { assertTrue(result.status == 400, "Should return 400") } - @Test - fun givenDailyOtpLimitReached_whenUserPhoneGetOtp_thenError() { - memberRepo.save(member) - val phoneOtps = buildList { - repeat(maxDailyOtpTrial) { - val momentsAgo = Date.from(Instant.now().minus(Duration.ofHours(faker.random().nextLong(1, 8)))) - val phoneOtp = PhoneOtp() - phoneOtp.phone = member.phone!! - phoneOtp.ref = faker.random().hex(20) - phoneOtp.type = UserType.Member - phoneOtp.expireAt = momentsAgo - add(phoneOtp) + @Nested + inner class OTPVerificationTest{ + + @BeforeEach + fun setUp(){ + memberRepo.save(member) + val phoneOtps = buildList { + repeat(maxDailyOtpTrial) { + val momentsAgo = Instant.now().minus(Duration.ofHours(faker.random().nextLong(1, 8))) + val phoneOtp = PhoneOtp() + phoneOtp.phone = member.phone!! + phoneOtp.ref = faker.random().hex(20) + phoneOtp.type = UserType.Member + phoneOtp.expireAt = Date.from(momentsAgo) + add(phoneOtp) + } } + memberPhoneOtpRepo.saveAll(phoneOtps) } - memberPhoneOtpRepo.saveAll(phoneOtps) + @AfterEach + fun tearDown(){ + memberPhoneOtpRepo.deleteAllByPhone(member.phone!!) + } + @Test + fun givenDailyOtpLimitReached_whenUserPhoneGetOtp_thenError() { - val result = mockMvc.get("/user/login?phone={phone}", member.phone!!).andReturn().response + val result = mockMvc.get("/user/login?phone={phone}", member.phone!!).andReturn().response - assertEquals(400, result.status, "Should return 400") - verifyNoInteractions(smsOtpService) - } + assertEquals(400, result.status, "Should return 400") + verifyNoInteractions(smsOtpService) + } - @Test - fun givenValidOtp_whenLoginVerifyOtp_return200() { - val phone = member.phone!! - val otp = faker.number().randomNumber(6, true).toString() - val ref = faker.random().hex(15) - val token = faker.random().hex(55) - Mockito.`when`(smsOtpService.verifyOtp(otp, ref)).thenReturn(phone) - memberRepo.save(member) + @Test + fun givenValidOtp_whenLoginVerifyOtp_return200() { + + val phone = member.phone!! + val otp = faker.number().randomNumber(6, true).toString() + val ref = faker.random().hex(15) + Mockito.`when`(smsOtpService.verifyOtp(otp, ref)).thenReturn(phone) + + val customJWT = faker.random().hex(55) + Mockito.`when`(auth.createCustomToken(member.id!!, mapOf("type" to "Member"))) + .thenReturn(customJWT) + + val login = mapOf( + "otp" to otp, + "ref" to ref, + "device_id" to faker.random().hex(30), + "device_name" to faker.device().modelName() + ) + val result = mockMvc.post("/user/login/verify") { + contentType = MediaType.APPLICATION_JSON + content = mapper.writeValueAsString(login) + }.andReturn().response - Mockito.`when`(auth.createCustomToken(member.id!!, mapOf("type" to "Member"))) - .thenReturn(token) - - val login = mapOf( - "otp" to otp, - "ref" to ref, - "device_id" to faker.random().hex(30), - "device_name" to faker.device().modelName() - ) - val result = mockMvc.post("/user/login/verify") { - contentType = MediaType.APPLICATION_JSON - content = mapper.writeValueAsString(login) - }.andReturn().response - - assertEquals(200, result.status) - assertEquals(token, result.contentAsString) - - verify(smsOtpService, times(1)).verifyOtp(otp, ref) -// verify(memberRepo, times(1)).findByPhone(phone) - verify(auth, times(1)).createCustomToken(any(), any()) + assertEquals(200, result.status) + assertEquals(customJWT, result.contentAsString, "Custom JWT is returned") + assertEquals(0, memberPhoneOtpRepo.countByPhone(phone), "OTP limit data is cleared") + + verify(smsOtpService, times(1)).verifyOtp(otp, ref) + verify(auth, times(1)).createCustomToken(any(), any()) + } } + + } \ No newline at end of file From b2b72971d6cc64cbe25ba129ed473efcda902490 Mon Sep 17 00:00:00 2001 From: sokari Date: Sun, 19 May 2024 11:39:54 +0100 Subject: [PATCH 3/4] fix: Skip Firebase init in test profile --- .github/workflows/test.yml | 2 +- src/main/kotlin/ng/cove/web/App.kt | 51 +++++++++++++++--------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40be9ba..bb846cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: test.yml +name: Test on: pull_request: branches: diff --git a/src/main/kotlin/ng/cove/web/App.kt b/src/main/kotlin/ng/cove/web/App.kt index 265841d..6dc9e94 100644 --- a/src/main/kotlin/ng/cove/web/App.kt +++ b/src/main/kotlin/ng/cove/web/App.kt @@ -16,7 +16,6 @@ import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.caffeine.CaffeineCacheManager import org.springframework.context.ApplicationListener import org.springframework.context.annotation.Bean -import org.springframework.core.env.StandardEnvironment import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableScheduling import java.util.concurrent.TimeUnit @@ -45,35 +44,35 @@ fun main(args: Array) { val startedEvent = ApplicationListener { event -> - val profiles = StandardEnvironment().activeProfiles + val profiles = event.applicationContext.environment.activeProfiles // profiles is sometimes empty when running on a production server because it // has not been loaded from the application.properties file at this point - val runningOnProd = profiles.isEmpty() || profiles[0] == "prod" + val profile = profiles.getOrNull(0) ?: "prod" - if (!runningOnProd) { - // Init Firebase with Application default credentials from Gcloud or Firebase CLI - FirebaseApp.initializeApp() - } else { + when (profile) { + "test" -> {} + "dev" -> FirebaseApp.initializeApp() + "prod" -> { + // Get secrets from GCP + val gcpProjectId = "gatedaccessdev" + val firebaseSecret = SecretVersionName.of(gcpProjectId, "firebase-service-account", "1") + val termiiSecret = SecretVersionName.of(gcpProjectId, "termii-key", "1") - // Get secrets from GCP - val gcpProjectId = "gatedaccessdev" - val firebaseSecret = SecretVersionName.of(gcpProjectId, "firebase-service-account", "1") - val termiiSecret = SecretVersionName.of(gcpProjectId, "termii-key", "1") - - // Auto-closable - SecretManagerServiceClient.create().use { - // Set Termii API Key to service - val termiiSecretPayload = - it.accessSecretVersion(termiiSecret).payload.data.toByteArray().inputStream() - val smsOtpService = event.applicationContext.getBean(SmsOtpService::class.java) - smsOtpService.termiiApiKey = String(termiiSecretPayload.readAllBytes()) - // Init Firebase with service account - val serviceAccountStream = - it.accessSecretVersion(firebaseSecret).payload.data.toByteArray().inputStream() - val options = FirebaseOptions.builder() - .setCredentials(GoogleCredentials.fromStream(serviceAccountStream)) - .build() - FirebaseApp.initializeApp(options) + // Auto-closable + SecretManagerServiceClient.create().use { + // Set Termii API Key to service + val termiiSecretPayload = + it.accessSecretVersion(termiiSecret).payload.data.toByteArray().inputStream() + val smsOtpService = event.applicationContext.getBean(SmsOtpService::class.java) + smsOtpService.termiiApiKey = String(termiiSecretPayload.readAllBytes()) + // Init Firebase with service account + val serviceAccountStream = + it.accessSecretVersion(firebaseSecret).payload.data.toByteArray().inputStream() + val options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccountStream)) + .build() + FirebaseApp.initializeApp(options) + } } } From b13e4e849f94b7a1506677748b739724d6582cb9 Mon Sep 17 00:00:00 2001 From: sokari Date: Sun, 19 May 2024 12:05:30 +0100 Subject: [PATCH 4/4] fix: Disable GCP autoconfiguration in test --- src/main/resources/application-prod.properties | 8 ++++++-- src/main/resources/application.properties | 4 ---- .../resources/application\342\200\223test.properties" | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 "src/test/resources/application\342\200\223test.properties" diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 68e9124..c2b2dc0 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -2,6 +2,10 @@ spring.mvc.async.request-timeout=20000 server.tomcat.threads.max=80 server.port=80 - # Disable Swagger UI on production -springdoc.swagger-ui.enabled=false \ No newline at end of file +springdoc.swagger-ui.enabled=false + +# GCP Secret Manager +spring.cloud.gcp.secretmanager.enabled=true +# GCP Cloud Logging +spring.cloud.gcp.logging.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 80c0651..ce4cb49 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,10 +7,6 @@ spring.profiles.active=prod otp.trial-limit=3 otp.expiry-mins=10 visitor.access-code.length=8 -# GCP Secret Manager -spring.cloud.gcp.secretmanager.enabled=true -# GCP Cloud Logging -spring.cloud.gcp.logging.enabled=true #This resolves CORS issues on Swagger UI server.forward-headers-strategy=framework diff --git "a/src/test/resources/application\342\200\223test.properties" "b/src/test/resources/application\342\200\223test.properties" new file mode 100644 index 0000000..3c16b64 --- /dev/null +++ "b/src/test/resources/application\342\200\223test.properties" @@ -0,0 +1,2 @@ +spring.cloud.gcp.core.enabled=false +spring.cloud.gcp.config.enabled=false \ No newline at end of file