[messages] explicit device selection

This commit is contained in:
Aleksandr Soloshenko 2025-07-03 09:36:31 +07:00 committed by Aleksandr
parent 8c88067543
commit 289a3b2ca2
8 changed files with 554 additions and 428 deletions

2
go.sum
View File

@ -39,8 +39,6 @@ github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/capcom6/go-helpers v0.2.1-0.20250630235533-8457c7435058 h1:tt64ezShwdmcUk04gBVL1BD49FDAfVZ4ELiw2rrJp+I=
github.com/capcom6/go-helpers v0.2.1-0.20250630235533-8457c7435058/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
github.com/capcom6/go-helpers v0.3.0 h1:ae18fLfluoPubiB2V+j4cIpfZaTuK4acS2entamaDkE=
github.com/capcom6/go-helpers v0.3.0/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
github.com/capcom6/go-infra-fx v0.2.1 h1:8rqr2ZV+YC2R07amHMdlE1XKLUhMe5yO+ffCJ/xXlNY=

View File

@ -63,19 +63,36 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error {
skipPhoneValidation := c.QueryBool("skipPhoneValidation", false)
devices, err := h.devicesSvc.Select(user.ID)
if err != nil {
h.Logger.Error("Failed to select devices", zap.Error(err), zap.String("user_id", user.ID))
return fiber.NewError(fiber.StatusInternalServerError, "Can't select devices. Please contact support")
}
var device models.Device
var err error
if len(devices) < 1 {
return fiber.NewError(fiber.StatusBadRequest, "No devices registered")
}
// Check if device_id is provided
if req.DeviceID != "" {
// Validate device ownership
device, err = h.devicesSvc.Get(user.ID, devices.WithID(req.DeviceID))
if err != nil {
if errors.Is(err, devices.ErrNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "Invalid device ID")
}
h.Logger.Error("Failed to get device", zap.Error(err), zap.String("user_id", user.ID), zap.String("device_id", req.DeviceID))
return fiber.NewError(fiber.StatusInternalServerError, "Can't select device. Please contact support")
}
} else {
// Fallback to random selection
devices, err := h.devicesSvc.Select(user.ID)
if err != nil {
h.Logger.Error("Failed to select devices", zap.Error(err), zap.String("user_id", user.ID))
return fiber.NewError(fiber.StatusInternalServerError, "Can't select devices. Please contact support")
}
device, err := slices.Random(devices)
if err != nil {
return fmt.Errorf("can't get random device: %w", err)
if len(devices) < 1 {
return fiber.NewError(fiber.StatusBadRequest, "No devices registered")
}
device, err = slices.Random(devices)
if err != nil {
return fmt.Errorf("can't get random device: %w", err)
}
}
var textContent *messages.TextMessageContent

View File

@ -154,7 +154,7 @@ func (s *Service) GetState(user models.User, ID string) (smsgateway.MessageState
func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueOptions) (smsgateway.MessageState, error) {
state := smsgateway.MessageState{
ID: "",
DeviceID: device.ID,
State: smsgateway.ProcessingStatePending,
Recipients: make([]smsgateway.RecipientState, len(message.PhoneNumbers)),
}
@ -296,6 +296,7 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash
func modelToMessageState(input Message) smsgateway.MessageState {
return smsgateway.MessageState{
ID: input.ExtID,
DeviceID: input.DeviceID,
State: smsgateway.ProcessingState(input.State),
IsHashed: input.IsHashed,
IsEncrypted: input.IsEncrypted,

View File

@ -1412,6 +1412,12 @@
}
]
},
"deviceId": {
"description": "Optional device ID for explicit selection",
"type": "string",
"maxLength": 21,
"example": "PyDmBQZZXYmyxMwED8Fzy"
},
"id": {
"description": "ID (if not set - will be generated)",
"type": "string",
@ -1506,10 +1512,17 @@
"smsgateway.MessageState": {
"type": "object",
"required": [
"deviceId",
"recipients",
"state"
],
"properties": {
"deviceId": {
"description": "Device ID",
"type": "string",
"maxLength": 21,
"example": "PyDmBQZZXYmyxMwED8Fzy"
},
"id": {
"description": "Message ID",
"type": "string",
@ -1634,6 +1647,12 @@
}
]
},
"deviceId": {
"description": "Optional device ID for explicit selection",
"type": "string",
"maxLength": 21,
"example": "PyDmBQZZXYmyxMwED8Fzy"
},
"id": {
"description": "ID (if not set - will be generated)",
"type": "string",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
package e2e
import (
"testing"
"github.com/capcom6/go-helpers/anys"
)
func TestDeviceSelection(t *testing.T) {
// Register first device
firstDevice := mobileDeviceRegister(t, publicMobileClient)
client := publicUserClient.SetBasicAuth(firstDevice.Login, firstDevice.Password)
// Register a second device to test explicit device selection
secondDevice := mobileDeviceRegister(
t,
publicMobileClient,
(&mobileDeviceRegisterOptions{}).
withCredentials(firstDevice.Login, firstDevice.Password),
)
cases := []struct {
name string
deviceID *string
expectedStatusCode int
}{
{
name: "explicit device selection",
deviceID: anys.AsPointer(secondDevice.ID),
expectedStatusCode: 202,
},
{
name: "invalid device ID",
deviceID: anys.AsPointer("invalid-device-id"),
expectedStatusCode: 400,
},
{
name: "no device ID (random selection)",
deviceID: nil,
expectedStatusCode: 202,
},
}
req := map[string]any{
"message": "test",
"phoneNumbers": []string{
"+79999999999",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if c.deviceID != nil {
req["deviceId"] = *c.deviceID
} else {
delete(req, "deviceId")
}
res, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(req).
Post("messages")
if err != nil {
t.Fatal(err)
}
if res.StatusCode() != c.expectedStatusCode {
t.Fatal(res.StatusCode(), res.String())
}
})
}
}

View File

@ -7,6 +7,7 @@ import (
)
type mobileRegisterResponse struct {
ID string `json:"id"`
Token string `json:"token"`
Login string `json:"login"`
Password string `json:"password"`

View File

@ -7,8 +7,26 @@ import (
"github.com/go-resty/resty/v2"
)
func mobileDeviceRegister(t *testing.T, client *resty.Client) mobileRegisterResponse {
res, err := client.R().
type mobileDeviceRegisterOptions struct {
username string
password string
}
func (o *mobileDeviceRegisterOptions) withCredentials(username, password string) *mobileDeviceRegisterOptions {
o.username = username
o.password = password
return o
}
func mobileDeviceRegister(t *testing.T, client *resty.Client, opts ...*mobileDeviceRegisterOptions) mobileRegisterResponse {
req := client.R()
for _, opt := range opts {
if opt.username != "" && opt.password != "" {
req.SetBasicAuth(opt.username, opt.password)
}
}
res, err := req.
SetHeader("Content-Type", "application/json").
SetBody(`{"name": "Public Device Name", "pushToken": "token"}`).
Post("device")