From e38139749158e2b6bbae6fee943124ede2e6e789 Mon Sep 17 00:00:00 2001
From: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com>
Date: Sun, 19 Apr 2026 15:30:20 +0000
Subject: [PATCH] fix: add sms provider
---
.prettierrc | 2 +
account-service/pom.xml | 5 +
.../com/drinkool/services/AccountService.java | 5 +-
.../com/drinkool/services/OtpService.java | 13 ++-
.../java/com/drinkool/utils/SpeedSMSAPI.java | 110 ++++++++++++++++++
.../services/CustomerServiceTest.java | 38 +++++-
.../com/drinkool/services/OtpServiceTest.java | 76 ++++++++++--
.../src/test/resources/application.properties | 3 +
k8s.yaml | 5 +
9 files changed, 236 insertions(+), 21 deletions(-)
create mode 100644 account-service/src/main/java/com/drinkool/utils/SpeedSMSAPI.java
diff --git a/.prettierrc b/.prettierrc
index 7c25d7e..fcb9da8 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -5,5 +5,7 @@
"tabWidth": 2,
"trailingComma": "all",
"useTabs": true,
+ "xmlSortAttributesByKey": true,
+ "xmlQuoteAttributes": "double",
"plugins": ["prettier-plugin-java", "@prettier/plugin-xml"]
}
diff --git a/account-service/pom.xml b/account-service/pom.xml
index 2a3da27..4d7f284 100644
--- a/account-service/pom.xml
+++ b/account-service/pom.xml
@@ -49,6 +49,11 @@
lib
${project.version}
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
io.quarkus
quarkus-jdbc-h2
diff --git a/account-service/src/main/java/com/drinkool/services/AccountService.java b/account-service/src/main/java/com/drinkool/services/AccountService.java
index 5c7cb22..3cd57ec 100644
--- a/account-service/src/main/java/com/drinkool/services/AccountService.java
+++ b/account-service/src/main/java/com/drinkool/services/AccountService.java
@@ -22,10 +22,7 @@ public class AccountService {
.entity("InvalidPhoneNumber")
.build();
- String otp = otpService.generateAndSaveOtp(input.phone);
-
- // Ở đây bạn sẽ gọi Kafka để gửi SMS thực tế
- System.out.println("OTP cho " + input.phone + " là: " + otp);
+ otpService.generateAndSaveOtp(input.phone);
return Response.accepted().build();
}
diff --git a/account-service/src/main/java/com/drinkool/services/OtpService.java b/account-service/src/main/java/com/drinkool/services/OtpService.java
index e97346b..edfb843 100644
--- a/account-service/src/main/java/com/drinkool/services/OtpService.java
+++ b/account-service/src/main/java/com/drinkool/services/OtpService.java
@@ -1,9 +1,11 @@
package com.drinkool.services;
+import com.drinkool.utils.SpeedSMSAPI;
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.value.ValueCommands;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
+import java.io.IOException;
import java.util.Random;
@ApplicationScoped
@@ -11,18 +13,25 @@ public class OtpService {
private final ValueCommands countCommands;
+ @Inject
+ SpeedSMSAPI smsApi;
+
@Inject
public OtpService(RedisDataSource ds) {
this.countCommands = ds.value(String.class);
}
- public String generateAndSaveOtp(String phone) {
+ public void generateAndSaveOtp(String phone) {
String otp = String.format("%06d", new Random().nextInt(1000000));
String key = "otp:" + phone;
countCommands.setex(key, 300, otp);
- return otp;
+ try {
+ smsApi.sendSMS(phone, "Mã OTP drinkool của bạn là " + otp, 2, "Drinkool");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
public boolean verifyOtp(String phone, String inputOtp) {
diff --git a/account-service/src/main/java/com/drinkool/utils/SpeedSMSAPI.java b/account-service/src/main/java/com/drinkool/utils/SpeedSMSAPI.java
new file mode 100644
index 0000000..f1d69a9
--- /dev/null
+++ b/account-service/src/main/java/com/drinkool/utils/SpeedSMSAPI.java
@@ -0,0 +1,110 @@
+package com.drinkool.utils;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.xml.bind.DatatypeConverter;
+import java.io.*;
+import java.net.*;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@ApplicationScoped
+public class SpeedSMSAPI {
+
+ public static final String API_URL = "https://api.speedsms.vn/index.php";
+ protected String mAccessToken;
+
+ public SpeedSMSAPI(
+ @ConfigProperty(name = "sms.api.token") String accessToken
+ ) {
+ this.mAccessToken = accessToken;
+ }
+
+ /**
+ * Get user information
+ * @param: none
+ * @return: json string
+ * */
+ public String getUserInfo() throws IOException {
+ URI uri = URI.create(API_URL + "/user/info");
+ HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
+ conn.setRequestMethod("GET");
+ String userCredentials = mAccessToken + ":x";
+ String basicAuth =
+ "Basic " +
+ DatatypeConverter.printBase64Binary(userCredentials.getBytes());
+ conn.setRequestProperty("Authorization", basicAuth);
+
+ BufferedReader in = new BufferedReader(
+ new InputStreamReader(conn.getInputStream())
+ );
+ String inputLine = "";
+ StringBuffer buffer = new StringBuffer();
+
+ while ((inputLine = in.readLine()) != null) {
+ buffer.append(inputLine);
+ }
+ in.close();
+ return buffer.toString();
+ }
+
+ /**
+ * Send SMS
+ * @param
+ * @return: json string
+ * */
+ public String sendSMS(String to, String content, int type, String sender)
+ throws IOException {
+ String json =
+ "{\"to\": [\"" +
+ to +
+ "\"], \"content\": \"" +
+ EncodeNonAsciiCharacters(content) +
+ "\", \"type\":" +
+ type +
+ ", \"brandname\":\"" +
+ sender +
+ "\"}";
+ URI uri = URI.create(API_URL + "/sms/send");
+ HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
+ conn.setRequestMethod("POST");
+ String userCredentials = mAccessToken + ":x";
+ String basicAuth =
+ "Basic " +
+ DatatypeConverter.printBase64Binary(userCredentials.getBytes());
+ conn.setRequestProperty("Authorization", basicAuth);
+ conn.setRequestProperty("Content-Type", "application/json");
+
+ conn.setDoOutput(true);
+ DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
+ wr.writeBytes(json);
+ wr.flush();
+ wr.close();
+
+ BufferedReader in = new BufferedReader(
+ new InputStreamReader(conn.getInputStream())
+ );
+ String inputLine = "";
+ StringBuffer buffer = new StringBuffer();
+
+ while ((inputLine = in.readLine()) != null) {
+ buffer.append(inputLine);
+ }
+ in.close();
+ return buffer.toString();
+ }
+
+ private String EncodeNonAsciiCharacters(String value) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ int unit = (int) c;
+ if (unit > 127) {
+ String hex = String.format("%04x", (int) unit);
+ String encodedValue = "\\u" + hex;
+ sb.append(encodedValue);
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/account-service/src/test/java/com/drinkool/services/CustomerServiceTest.java b/account-service/src/test/java/com/drinkool/services/CustomerServiceTest.java
index a823961..767f990 100644
--- a/account-service/src/test/java/com/drinkool/services/CustomerServiceTest.java
+++ b/account-service/src/test/java/com/drinkool/services/CustomerServiceTest.java
@@ -1,16 +1,19 @@
package com.drinkool.services;
import static io.restassured.RestAssured.*;
-import static org.hamcrest.CoreMatchers.*;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
import com.drinkool.*;
import com.drinkool.dtos.*;
import com.drinkool.entities.CustomerEntity;
-import io.quarkus.test.junit.QuarkusTest;
+import com.drinkool.utils.SpeedSMSAPI;
+import io.quarkus.test.junit.*;
import io.restassured.http.ContentType;
import jakarta.inject.Inject;
-import org.junit.jupiter.api.Test;
+import java.io.IOException;
+import org.junit.jupiter.api.*;
+import org.mockito.ArgumentCaptor;
@QuarkusTest
public class CustomerServiceTest {
@@ -18,6 +21,15 @@ public class CustomerServiceTest {
@Inject
OtpService otpService;
+ SpeedSMSAPI smsApiMock;
+
+ @BeforeEach
+ public void setup() {
+ SpeedSMSAPI mock = mock(SpeedSMSAPI.class);
+ QuarkusMock.installMockForType(mock, SpeedSMSAPI.class);
+ this.smsApiMock = mock;
+ }
+
@Test
void testSignup() {
String phone = Utils.generateRandomPhone();
@@ -114,7 +126,7 @@ public class CustomerServiceTest {
}
@Test
- public void testQuickLogin_Success() {
+ public void testQuickLogin_Success() throws IOException {
String phone = Utils.generateRandomPhone();
QuickSignup input = new QuickSignup();
@@ -129,7 +141,23 @@ public class CustomerServiceTest {
QuickLogin payload = new QuickLogin();
payload.phone = phone;
- payload.otp = otpService.generateAndSaveOtp(phone);
+ otpService.generateAndSaveOtp(phone);
+
+ ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(
+ String.class
+ );
+
+ verify(smsApiMock).sendSMS(
+ eq(phone),
+ contentCaptor.capture(),
+ anyInt(),
+ anyString()
+ );
+
+ // 4. Trích xuất mã OTP từ nội dung tin nhắn đã bắt được
+ // Giả sử tin nhắn có định dạng: "... là 123456"
+ String sentMessage = contentCaptor.getValue();
+ payload.otp = sentMessage.substring(sentMessage.length() - 6);
given()
.contentType(ContentType.JSON)
diff --git a/account-service/src/test/java/com/drinkool/services/OtpServiceTest.java b/account-service/src/test/java/com/drinkool/services/OtpServiceTest.java
index 0057f97..aa647d3 100644
--- a/account-service/src/test/java/com/drinkool/services/OtpServiceTest.java
+++ b/account-service/src/test/java/com/drinkool/services/OtpServiceTest.java
@@ -1,11 +1,17 @@
package com.drinkool.services;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
-import io.quarkus.test.junit.QuarkusTest;
+import com.drinkool.utils.SpeedSMSAPI;
+import io.quarkus.redis.datasource.RedisDataSource;
+import io.quarkus.test.junit.*;
import jakarta.inject.Inject;
+import java.io.IOException;
import java.util.Random;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.*;
+import org.mockito.ArgumentCaptor;
@QuarkusTest
public class OtpServiceTest {
@@ -13,24 +19,58 @@ public class OtpServiceTest {
@Inject
OtpService otpService;
+ @Inject
+ RedisDataSource redisDataSource;
+
+ SpeedSMSAPI smsApiMock;
+
+ @BeforeEach
+ public void setup() {
+ SpeedSMSAPI mock = mock(SpeedSMSAPI.class);
+ QuarkusMock.installMockForType(mock, SpeedSMSAPI.class);
+ this.smsApiMock = mock;
+ }
+
// 1. Test case: Tạo và lưu OTP thành công
@Test
public void testGenerateAndSaveOtp() {
String phone =
"+8477" + String.format("%07d", new Random().nextInt(10000000));
- String otp = otpService.generateAndSaveOtp(phone);
+ String redisKey = "otp:" + phone;
- assertNotNull(otp, "OTP sinh ra không được null");
- assertEquals(6, otp.length(), "OTP phải có độ dài đúng 6 ký tự");
- assertTrue(otp.matches("\\d{6}"), "OTP chỉ được chứa các chữ số");
+ otpService.generateAndSaveOtp(phone);
+
+ String otpFromRedis = redisDataSource.value(String.class).get(redisKey);
+
+ assertNotNull(otpFromRedis, "Mã OTP phải tồn tại trong Redis");
+ assertEquals(6, otpFromRedis.length(), "Mã OTP phải có 6 chữ số");
+
+ long ttl = redisDataSource.key().ttl(redisKey);
+ assertTrue(ttl > 0 && ttl <= 300, "TTL của OTP phải hợp lệ");
}
// 2. Test case: Xác thực OTP thành công
@Test
- public void testVerifyOtpSuccess() {
+ public void testVerifyOtpSuccess() throws IOException {
String phone =
"+8477" + String.format("%07d", new Random().nextInt(10000000));
- String generatedOtp = otpService.generateAndSaveOtp(phone);
+ otpService.generateAndSaveOtp(phone);
+
+ ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(
+ String.class
+ );
+
+ verify(smsApiMock).sendSMS(
+ eq(phone),
+ contentCaptor.capture(),
+ anyInt(),
+ anyString()
+ );
+
+ // 4. Trích xuất mã OTP từ nội dung tin nhắn đã bắt được
+ // Giả sử tin nhắn có định dạng: "... là 123456"
+ String sentMessage = contentCaptor.getValue();
+ String generatedOtp = sentMessage.substring(sentMessage.length() - 6);
// Xác thực với OTP vừa tạo ra
boolean verifyResult = otpService.verifyOtp(phone, generatedOtp);
@@ -68,10 +108,26 @@ public class OtpServiceTest {
// 5. Test case: Đảm bảo OTP chỉ được sử dụng 1 lần (đã bị xóa sau khi verify thành công bằng lệnh getdel)
@Test
- public void testVerifyOtpConsumedAfterSuccess() {
+ public void testVerifyOtpConsumedAfterSuccess() throws IOException {
String phone =
"+8477" + String.format("%07d", new Random().nextInt(10000000));
- String generatedOtp = otpService.generateAndSaveOtp(phone);
+ otpService.generateAndSaveOtp(phone);
+
+ ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(
+ String.class
+ );
+
+ verify(smsApiMock).sendSMS(
+ eq(phone),
+ contentCaptor.capture(),
+ anyInt(),
+ anyString()
+ );
+
+ // 4. Trích xuất mã OTP từ nội dung tin nhắn đã bắt được
+ // Giả sử tin nhắn có định dạng: "... là 123456"
+ String sentMessage = contentCaptor.getValue();
+ String generatedOtp = sentMessage.substring(sentMessage.length() - 6);
// Lần 1: Xác thực thành công
assertTrue(otpService.verifyOtp(phone, generatedOtp));
diff --git a/account-service/src/test/resources/application.properties b/account-service/src/test/resources/application.properties
index 59bc331..ddce574 100644
--- a/account-service/src/test/resources/application.properties
+++ b/account-service/src/test/resources/application.properties
@@ -8,3 +8,6 @@ quarkus.http.host=0.0.0.0
## Orm
%test.quarkus.hibernate-orm.database.generation=drop-and-create
+
+# Sms API
+sms.api.token=your_sms_api_token_here
\ No newline at end of file
diff --git a/k8s.yaml b/k8s.yaml
index 25dbdb6..decfebe 100644
--- a/k8s.yaml
+++ b/k8s.yaml
@@ -37,6 +37,11 @@ spec:
secretKeyRef:
name: postgres-credentials
key: url
+ - name: SMS_API_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: sms-api-token
+ key: value
resources:
requests:
memory: "32Mi"