Files
3x-ui/internal/web/service/client_lookup_test.go
T
Kim Fom 6af2995930 feat(api): add GET endpoint to look up clients by Telegram ID (#5945)
* feat(api): add GET endpoint to look up clients by Telegram ID

GET /panel/api/clients/getByTgId/:tgId returns all clients matching the given Telegram user ID. tgId is not unique, so the response is an array of {client, inboundIds, externalLinks, usedTraffic} objects.

* fix: guard tgId=0 sentinel, index tg_id, deduplicate enrichment in getByTgId

Three issues from the code review on the new GET /panel/api/clients/getByTgId/:tgId
endpoint: the lookup did not short-circuit tgId <= 0 (this codebase's sentinel
for 'no Telegram ID'), had no index on clients.tg_id causing a full table scan
on every call, and duplicated the per-record enrichment (inbound IDs, external
links, effective flow, traffic) identically between get and getByTgId.

- Reject tgId <= 0 in GetRecordsByTgId with a clear error, matching the
  '0 = none' convention used elsewhere in the codebase.
- Add index:idx_clients_tg_id to ClientRecord.TgID (struct tag + idempotent
  startup migration for existing databases).
- Extract buildClientPayload helper used by both get and getByTgId.
- Update client_lookup_test.go to verify sentinel rejection instead of
  expecting tgId=0 to be a valid lookup.

* refactor(api): move Telegram client lookup under /get/tgId/:tgId

Nest the Telegram-ID lookup beside the email lookup as /get/tgId/:tgId
instead of the flat /getByTgId/:tgId, so both client fetch routes share the
/get prefix. Gin resolves the static tgId segment ahead of the :email
wildcard, so /get/:email keeps matching plain email lookups, including a
literal 'tgId' email. The endpoint is unreleased, so no compatibility
concern.
2026-07-28 22:38:44 +02:00

81 lines
1.9 KiB
Go

package service
import (
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestGetRecordsByTgID(t *testing.T) {
setupBulkDB(t)
svc := &ClientService{}
db := database.GetDB()
records := []model.ClientRecord{
{Email: "alice@x", TgID: 100, SubID: "sa"},
{Email: "bob@x", TgID: 100, SubID: "sb"},
{Email: "carol@x", TgID: 200, SubID: "sc"},
{Email: "dave@x", TgID: 0, SubID: "sd"},
}
for _, r := range records {
if err := db.Create(&r).Error; err != nil {
t.Fatalf("create record %q: %v", r.Email, err)
}
}
t.Run("multiple clients share tgId", func(t *testing.T) {
got, err := svc.GetRecordsByTgID(100)
if err != nil {
t.Fatalf("GetRecordsByTgID(100): %v", err)
}
if len(got) != 2 {
t.Fatalf("expected 2 records, got %d", len(got))
}
emails := make(map[string]bool)
for _, r := range got {
emails[r.Email] = true
}
if !emails["alice@x"] || !emails["bob@x"] {
t.Fatalf("expected alice@x and bob@x, got %v", got)
}
})
t.Run("single client by tgId", func(t *testing.T) {
got, err := svc.GetRecordsByTgID(200)
if err != nil {
t.Fatalf("GetRecordsByTgID(200): %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 record, got %d", len(got))
}
if got[0].Email != "carol@x" {
t.Fatalf("expected carol@x, got %s", got[0].Email)
}
})
t.Run("tgId zero rejected as sentinel", func(t *testing.T) {
_, err := svc.GetRecordsByTgID(0)
if err == nil {
t.Fatal("expected error for tgId=0")
}
})
t.Run("negative tgId rejected", func(t *testing.T) {
_, err := svc.GetRecordsByTgID(-5)
if err == nil {
t.Fatal("expected error for tgId=-5")
}
})
t.Run("nonexistent tgId returns empty", func(t *testing.T) {
got, err := svc.GetRecordsByTgID(999)
if err != nil {
t.Fatalf("GetRecordsByTgID(999): %v", err)
}
if len(got) != 0 {
t.Fatalf("expected 0 records, got %d", len(got))
}
})
}