| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- package service
- import (
- "context"
- "sync/atomic"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- )
- // dirtyProbeRuntime reads a node's config_dirty flag at the moment an inbound
- // pushes, i.e. while the edit's other inbounds are still unapplied.
- type dirtyProbeRuntime struct {
- fakeNodeRuntime
- watch int
- sawSet atomic.Bool
- probed atomic.Bool
- }
- func (d *dirtyProbeRuntime) UpdateUser(ctx context.Context, ib *model.Inbound, oldEmail string, c model.Client) error {
- var node model.Node
- if err := database.GetDB().Where("id = ?", d.watch).First(&node).Error; err == nil {
- d.sawSet.Store(node.ConfigDirty)
- d.probed.Store(true)
- }
- return d.fakeNodeRuntime.UpdateUser(ctx, ib, oldEmail, c)
- }
- // TestEditFlagsOtherNodesBeforeApplying pins the ordering: every node is flagged
- // BEFORE any inbound applies, so no merge can slot into the fanout gap.
- func TestEditFlagsOtherNodesBeforeApplying(t *testing.T) {
- setupBulkDB(t)
- startSerializedWriter(t)
- mgr := useTestRuntimeManager(t)
- db := database.GetDB()
- const uuid = "eeeeeeee-1111-2222-3333-444444444444"
- probe := &dirtyProbeRuntime{}
- ids := fanoutNodeInbounds(t, mgr, probe, 2, 47200)
- // The watched node is the one whose apply is made to fail, so nothing but
- // the up-front marking can have flagged it when the other inbound pushes.
- var victim model.Inbound
- if err := db.Where("id = ?", ids[1]).First(&victim).Error; err != nil {
- t.Fatalf("read inbound %d: %v", ids[1], err)
- }
- probe.watch = *victim.NodeID
- if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
- Client: model.Client{Email: "carol", ID: uuid, SubID: "sub-carol", Enable: true},
- InboundIds: ids,
- }); err != nil {
- t.Fatalf("seed Create: %v", err)
- }
- if err := db.Model(&model.Inbound{}).Where("id = ?", ids[1]).
- Update("settings", `{"clients":`).Error; err != nil {
- t.Fatalf("corrupt inbound %d: %v", ids[1], err)
- }
- if err := db.Model(model.Node{}).Where("1 = 1").
- Update("config_dirty", false).Error; err != nil {
- t.Fatalf("clear config_dirty: %v", err)
- }
- rec := lookupClientRecord(t, "carol")
- if _, err := (&ClientService{}).Update(&InboundService{}, rec.Id, model.Client{
- Email: "carol-renamed", ID: uuid, SubID: "sub-carol", Enable: true,
- }, 0); err == nil {
- t.Fatal("Update on a corrupted inbound should report the failure")
- }
- if !probe.probed.Load() {
- t.Fatal("the healthy inbound never pushed, so the ordering was never observed")
- }
- if !probe.sawSet.Load() {
- t.Fatal("a node was still clean while another inbound of the same edit was applying: a snapshot merge in that gap would resurrect the pre-edit email")
- }
- }
- // TestEditFlagsNodesOutsideTheInboundFilter pins that the marking covers the
- // client's whole attachment set, not just the inbounds the filter applies.
- func TestEditFlagsNodesOutsideTheInboundFilter(t *testing.T) {
- setupBulkDB(t)
- startSerializedWriter(t)
- mgr := useTestRuntimeManager(t)
- db := database.GetDB()
- const uuid = "ffffffff-1111-2222-3333-444444444444"
- ids := fanoutNodeInbounds(t, mgr, &fakeNodeRuntime{}, 2, 47300)
- if _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
- Client: model.Client{Email: "dave", ID: uuid, SubID: "sub-dave", Enable: true},
- InboundIds: ids,
- }); err != nil {
- t.Fatalf("seed Create: %v", err)
- }
- if err := db.Model(model.Node{}).Where("1 = 1").
- Update("config_dirty", false).Error; err != nil {
- t.Fatalf("clear config_dirty: %v", err)
- }
- // Edit scoped to the first inbound only; the second one's node still holds
- // the old email once the shared record is renamed.
- rec := lookupClientRecord(t, "dave")
- if _, err := (&ClientService{}).Update(&InboundService{}, rec.Id, model.Client{
- Email: "dave-renamed", ID: uuid, SubID: "sub-dave", Enable: true,
- }, 0, ids[0]); err != nil {
- t.Fatalf("filtered Update: %v", err)
- }
- var excluded model.Inbound
- if err := db.Where("id = ?", ids[1]).First(&excluded).Error; err != nil {
- t.Fatalf("read inbound %d: %v", ids[1], err)
- }
- var node model.Node
- if err := db.Where("id = ?", *excluded.NodeID).First(&node).Error; err != nil {
- t.Fatalf("read node %d: %v", *excluded.NodeID, err)
- }
- if !node.ConfigDirty {
- t.Fatal("a node left out of the inboundIds filter stayed clean after the shared record was renamed, so its stale snapshot would be merged")
- }
- }
|