diff --git a/internal/clientsdb/db.go b/internal/clientsdb/db.go index b1fb44f..49bf3fc 100644 --- a/internal/clientsdb/db.go +++ b/internal/clientsdb/db.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "net" "os" "sync" @@ -61,16 +62,18 @@ func (db *DB) Add(clientID, comment string) error { db.mu.Lock() defer db.mu.Unlock() - db.data.Clients[clientID] = ClientInfo{Comment: comment} - return db.save() + next := Data{Clients: maps.Clone(db.data.Clients)} + next.Clients[clientID] = ClientInfo{Comment: comment} + return db.save(next) } func (db *DB) Remove(clientID string) error { db.mu.Lock() defer db.mu.Unlock() - delete(db.data.Clients, clientID) - return db.save() + next := Data{Clients: maps.Clone(db.data.Clients)} + delete(next.Clients, clientID) + return db.save(next) } func (db *DB) List() map[string]ClientInfo { @@ -126,8 +129,8 @@ func (db *DB) loadIfModified() { } } -func (db *DB) save() error { - b, err := json.MarshalIndent(db.data, "", " ") +func (db *DB) save(next Data) error { + b, err := json.MarshalIndent(next, "", " ") if err != nil { return err } @@ -138,6 +141,8 @@ func (db *DB) save() error { err = os.Rename(tmpFile, db.path) } if err == nil { + // Авторизация меняется только после успешной замены файла. + db.data = next stat, _ := os.Stat(db.path) if stat != nil { db.lastModified = stat.ModTime() diff --git a/internal/clientsdb/db_test.go b/internal/clientsdb/db_test.go index 45cd67c..adc79db 100644 --- a/internal/clientsdb/db_test.go +++ b/internal/clientsdb/db_test.go @@ -2,11 +2,55 @@ package clientsdb import ( "net" + "os" "path/filepath" "testing" "time" ) +func TestFailedSavePreservesAuthorization(t *testing.T) { + path := filepath.Join(t.TempDir(), "clients.json") + db, err := New(path) + if err != nil { + t.Fatal(err) + } + if err := db.Add("existing", "original"); err != nil { + t.Fatal(err) + } + // Каталог вместо временного файла воспроизводит отказ записи без прав root. + if err := os.Mkdir(path+".tmp", 0o700); err != nil { + t.Fatal(err) + } + operations := []struct { + name string + run func() error + }{ + {"add", func() error { return db.Add("new", "new") }}, + {"update", func() error { return db.Add("existing", "changed") }}, + {"remove", func() error { return db.Remove("existing") }}, + } + for _, op := range operations { + t.Run(op.name, func(t *testing.T) { + if err := op.run(); err == nil { + t.Fatal("save unexpectedly succeeded") + } + if !db.IsAuthorized("existing") || db.IsAuthorized("new") { + t.Fatal("failed save changed authorization") + } + if got := db.List(); len(got) != 1 || got["existing"].Comment != "original" { + t.Fatalf("failed save changed clients: %+v", got) + } + }) + } + loaded, err := New(path) + if err != nil { + t.Fatal(err) + } + if got := loaded.List(); len(got) != 1 || got["existing"].Comment != "original" { + t.Fatalf("disk changed: %+v", got) + } +} + func TestClientsDB(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "clients.json")