mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-26 06:16:12 +00:00
a652cb8cea
The subId collision check in Update ran on every save, unlike the email
check above it. Because Update defaults an omitted subId to the stored
one, any client already sharing a subId was rejected on every later edit
-- even a pure totalGB or expiry change that never mentions subId.
Gate the check on an actual change. Pre-existing duplicates are reachable
because SyncInbound has no such check, and 88a36773 meant to leave them
untouched. Editing a client onto another subscriber's subId is still
rejected, so the typo guard is intact.
158 lines
5.0 KiB
Go
158 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
func countClientRecords(t *testing.T) int64 {
|
|
t.Helper()
|
|
var n int64
|
|
if err := database.GetDB().Model(&model.ClientRecord{}).Count(&n).Error; err != nil {
|
|
t.Fatalf("count client records: %v", err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestUpdateInboundClientRenameDoesNotDuplicateRecord(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{{Email: "old@x", ID: "aaaaaaaa-0000-0000-0000-000000000001", SubID: "sub-old", Enable: true}}
|
|
ib := mkInbound(t, 22001, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
origId := lookupClientRecord(t, "old@x").Id
|
|
|
|
renamed := source
|
|
renamed[0].Email = "new@x"
|
|
if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{
|
|
Id: ib.Id,
|
|
Settings: clientsSettings(t, renamed),
|
|
}, "old@x"); err != nil {
|
|
t.Fatalf("UpdateInboundClient: %v", err)
|
|
}
|
|
|
|
if n := countClientRecords(t); n != 1 {
|
|
t.Fatalf("client records after rename = %d, want 1", n)
|
|
}
|
|
rec := lookupClientRecord(t, "new@x")
|
|
if rec.Id != origId {
|
|
t.Fatalf("record id after rename = %d, want %d", rec.Id, origId)
|
|
}
|
|
}
|
|
|
|
func TestUpdateInboundClientCaseOnlyRenameDoesNotDuplicateRecord(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{{Email: "test", ID: "aaaaaaaa-0000-0000-0000-000000000002", SubID: "sub-case", Enable: true}}
|
|
ib := mkInbound(t, 22002, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
origId := lookupClientRecord(t, "test").Id
|
|
|
|
updated := source[0]
|
|
updated.Email = "Test"
|
|
if _, err := svc.Update(inboundSvc, origId, updated); err != nil {
|
|
t.Fatalf("Update case-only email: %v", err)
|
|
}
|
|
|
|
if n := countClientRecords(t); n != 1 {
|
|
t.Fatalf("client records after case-only rename = %d, want 1", n)
|
|
}
|
|
rec := lookupClientRecord(t, "Test")
|
|
if rec.Id != origId {
|
|
t.Fatalf("record id after case-only rename = %d, want %d", rec.Id, origId)
|
|
}
|
|
if rec.Email != "Test" {
|
|
t.Fatalf("email after case-only rename = %q, want %q", rec.Email, "Test")
|
|
}
|
|
}
|
|
|
|
func TestClientUpdateDuplicateSubIDDoesNotRenameEmail(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{
|
|
{Email: "keep@x", ID: "aaaaaaaa-0000-0000-0000-000000000003", SubID: "sub-keep", Enable: true},
|
|
{Email: "other@x", ID: "aaaaaaaa-0000-0000-0000-000000000004", SubID: "sub-other", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 22003, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
origId := lookupClientRecord(t, "keep@x").Id
|
|
origSettings := mustInboundSettings(t, inboundSvc, ib.Id)
|
|
|
|
updated := source[0]
|
|
updated.Email = "kept@x"
|
|
updated.SubID = "sub-other"
|
|
if _, err := svc.Update(inboundSvc, origId, updated); err == nil {
|
|
t.Fatalf("Update with colliding subId succeeded, want error")
|
|
}
|
|
|
|
rec := lookupClientRecord(t, "keep@x")
|
|
if rec.Id != origId {
|
|
t.Fatalf("record id changed after rejected update")
|
|
}
|
|
if got := mustInboundSettings(t, inboundSvc, ib.Id); got != origSettings {
|
|
t.Fatalf("inbound settings changed after rejected update")
|
|
}
|
|
}
|
|
|
|
func TestClientUpdateKeepsSharedSubIDEditable(t *testing.T) {
|
|
setupBulkDB(t)
|
|
svc := &ClientService{}
|
|
inboundSvc := &InboundService{}
|
|
|
|
source := []model.Client{
|
|
{Email: "a@node", ID: "aaaaaaaa-0000-0000-0000-000000000005", SubID: "sub-shared", Enable: true},
|
|
{Email: "b@node", ID: "aaaaaaaa-0000-0000-0000-000000000006", SubID: "sub-shared", Enable: true},
|
|
}
|
|
ib := mkInbound(t, 22004, model.VLESS, clientsSettings(t, source))
|
|
if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
|
|
t.Fatalf("seed linkage: %v", err)
|
|
}
|
|
first := lookupClientRecord(t, "a@node")
|
|
if first.SubID != "sub-shared" || lookupClientRecord(t, "b@node").SubID != "sub-shared" {
|
|
t.Fatalf("seed did not produce a shared subId")
|
|
}
|
|
|
|
updated := source[0]
|
|
updated.TotalGB = 42
|
|
if _, err := svc.Update(inboundSvc, first.Id, updated); err != nil {
|
|
t.Fatalf("Update of a client whose subId is already shared: %v", err)
|
|
}
|
|
if got := lookupClientRecord(t, "a@node").TotalGB; got != 42 {
|
|
t.Fatalf("totalGB after update = %d, want 42", got)
|
|
}
|
|
|
|
omitted := source[0]
|
|
omitted.SubID = ""
|
|
omitted.TotalGB = 43
|
|
if _, err := svc.Update(inboundSvc, first.Id, omitted); err != nil {
|
|
t.Fatalf("Update with subId omitted entirely: %v", err)
|
|
}
|
|
other := lookupClientRecord(t, "b@node")
|
|
if other.SubID != "sub-shared" {
|
|
t.Fatalf("other client subId = %q, want %q", other.SubID, "sub-shared")
|
|
}
|
|
}
|
|
|
|
func mustInboundSettings(t *testing.T, inboundSvc *InboundService, id int) string {
|
|
t.Helper()
|
|
ib, err := inboundSvc.GetInbound(id)
|
|
if err != nil {
|
|
t.Fatalf("GetInbound %d: %v", id, err)
|
|
}
|
|
return ib.Settings
|
|
}
|