mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03:00
feat(clients): bind host rules to individual clients in subscriptions (#6620)
Host rules applied per inbound only, so every client of an inbound got the same subscription endpoints (#6618). A client can now be bound to one or more host rules, and its raw, JSON and Clash subscriptions render only those hosts, in either subscription order; an unbound client is unchanged. A binding whose hosts are deleted, disabled or excluded from the format fails closed: the client gets no links there but stays in traffic accounting. Host edits keep row ids, so bindings survive them; nil leaves a stored binding alone and an empty list clears it.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
func setupHostBindingDB(t *testing.T) {
|
||||
t.Helper()
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
||||
}
|
||||
|
||||
func TestUpdate_OmittedPreservesAndEmptyClearsHostBindings(t *testing.T) {
|
||||
setupHostBindingDB(t)
|
||||
svc := &ClientService{}
|
||||
inboundSvc := &InboundService{}
|
||||
|
||||
rec := &model.ClientRecord{
|
||||
Email: "hostbind@x",
|
||||
UUID: "11111111-1111-1111-1111-111111111111",
|
||||
SubID: "hostbind@x",
|
||||
Enable: true,
|
||||
ClientHostRuleIds: []int{7},
|
||||
}
|
||||
if err := database.GetDB().Create(rec).Error; err != nil {
|
||||
t.Fatalf("create record: %v", err)
|
||||
}
|
||||
|
||||
omitted := rec.ToClient()
|
||||
omitted.ClientHostRuleIds = nil
|
||||
omitted.Comment = "touch"
|
||||
if _, err := svc.Update(inboundSvc, rec.Id, *omitted, 0); err != nil {
|
||||
t.Fatalf("Update omitted: %v", err)
|
||||
}
|
||||
got, err := svc.GetByID(rec.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID: %v", err)
|
||||
}
|
||||
if got.ClientHostRuleIds == nil || len(got.ClientHostRuleIds) != 1 || got.ClientHostRuleIds[0] != 7 {
|
||||
t.Fatalf("omitted must preserve list, got %v", got.ClientHostRuleIds)
|
||||
}
|
||||
|
||||
cleared := got.ToClient()
|
||||
cleared.ClientHostRuleIds = []int{}
|
||||
if _, err := svc.Update(inboundSvc, rec.Id, *cleared, 0); err != nil {
|
||||
t.Fatalf("Update clear: %v", err)
|
||||
}
|
||||
after, err := svc.GetByID(rec.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID: %v", err)
|
||||
}
|
||||
if after.ClientHostRuleIds == nil {
|
||||
t.Fatalf("empty must clear to non-nil empty, got nil")
|
||||
}
|
||||
if len(after.ClientHostRuleIds) != 0 {
|
||||
t.Fatalf("empty must clear list, got %v", after.ClientHostRuleIds)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user