aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-01 16:03:57 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-01 16:03:57 -0700
commit262f66351484369fa65c2e23df81ef682ea06d89 (patch)
tree97c2d135531b20b7b6a986fdba69e386412bbfd5 /internal
parenta9c6485fb00dbf2e4b55351c1ae74d4eaca3eec9 (diff)
Add documentation and comprehensive test coverage for AccountManager
- Add GoDoc comments to AccountManager interface and all YAMLAccountManager methods - Add complete table-driven test coverage for Create, Update, Get, and List methods - Tests follow project conventions with proper error handling and temporary directories - All tests pass with comprehensive verification of both memory state and file operations
Diffstat (limited to 'internal')
-rw-r--r--internal/mobius/account_manager.go19
-rw-r--r--internal/mobius/account_manager_test.go347
2 files changed, 360 insertions, 6 deletions
diff --git a/internal/mobius/account_manager.go b/internal/mobius/account_manager.go
index 2558d3a..8859c58 100644
--- a/internal/mobius/account_manager.go
+++ b/internal/mobius/account_manager.go
@@ -24,6 +24,8 @@ func loadFromYAMLFile(path string, data interface{}) error {
return decoder.Decode(data)
}
+// YAMLAccountManager implements AccountManager interface using YAML files for persistence.
+// It maintains an in-memory cache of accounts and synchronizes with YAML files on disk.
type YAMLAccountManager struct {
accounts map[string]hotline.Account
accountDir string
@@ -31,6 +33,9 @@ type YAMLAccountManager struct {
mu sync.Mutex
}
+// NewYAMLAccountManager creates a new YAML-based account manager that loads existing
+// accounts from .yaml files in the specified directory. It also performs migration
+// from old access flag format to new AccessBitmap format when needed.
func NewYAMLAccountManager(accountDir string) (*YAMLAccountManager, error) {
accountMgr := YAMLAccountManager{
accountDir: accountDir,
@@ -71,6 +76,8 @@ func NewYAMLAccountManager(accountDir string) (*YAMLAccountManager, error) {
return &accountMgr, nil
}
+// Create adds a new account by writing it to a YAML file and updating the in-memory cache.
+// Returns an error if an account with the same login already exists.
func (am *YAMLAccountManager) Create(account hotline.Account) error {
am.mu.Lock()
defer am.mu.Unlock()
@@ -100,6 +107,8 @@ func (am *YAMLAccountManager) Create(account hotline.Account) error {
return nil
}
+// Update modifies an existing account with new data and optionally renames it.
+// If newLogin differs from account.Login, the account file is renamed accordingly.
func (am *YAMLAccountManager) Update(account hotline.Account, newLogin string) error {
am.mu.Lock()
defer am.mu.Unlock()
@@ -133,6 +142,8 @@ func (am *YAMLAccountManager) Update(account hotline.Account, newLogin string) e
return nil
}
+// Get retrieves an account by login from the in-memory cache.
+// Returns nil if the account is not found.
func (am *YAMLAccountManager) Get(login string) *hotline.Account {
am.mu.Lock()
defer am.mu.Unlock()
@@ -145,6 +156,7 @@ func (am *YAMLAccountManager) Get(login string) *hotline.Account {
return &account
}
+// List returns all accounts from the in-memory cache as a slice.
func (am *YAMLAccountManager) List() []hotline.Account {
am.mu.Lock()
defer am.mu.Unlock()
@@ -157,6 +169,7 @@ func (am *YAMLAccountManager) List() []hotline.Account {
return accounts
}
+// Delete removes an account by deleting its YAML file and removing it from the cache.
func (am *YAMLAccountManager) Delete(login string) error {
am.mu.Lock()
defer am.mu.Unlock()
@@ -171,34 +184,40 @@ func (am *YAMLAccountManager) Delete(login string) error {
return nil
}
+// MockAccountManager provides a test double implementation of AccountManager using testify/mock.
type MockAccountManager struct {
mock.Mock
}
+// Create mocks the Create method for testing.
func (m *MockAccountManager) Create(account hotline.Account) error {
args := m.Called(account)
return args.Error(0)
}
+// Update mocks the Update method for testing.
func (m *MockAccountManager) Update(account hotline.Account, newLogin string) error {
args := m.Called(account, newLogin)
return args.Error(0)
}
+// Get mocks the Get method for testing.
func (m *MockAccountManager) Get(login string) *hotline.Account {
args := m.Called(login)
return args.Get(0).(*hotline.Account)
}
+// List mocks the List method for testing.
func (m *MockAccountManager) List() []hotline.Account {
args := m.Called()
return args.Get(0).([]hotline.Account)
}
+// Delete mocks the Delete method for testing.
func (m *MockAccountManager) Delete(login string) error {
args := m.Called(login)
diff --git a/internal/mobius/account_manager_test.go b/internal/mobius/account_manager_test.go
index a6f39ca..b0bfd79 100644
--- a/internal/mobius/account_manager_test.go
+++ b/internal/mobius/account_manager_test.go
@@ -1,25 +1,25 @@
package mobius
import (
- "os"
- "path"
"github.com/jhalter/mobius/hotline"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "os"
+ "path"
"testing"
)
// copyTestFiles copies test config files to a temporary directory
func copyTestFiles(t *testing.T, srcDir, dstDir string) {
files := []string{"admin.yaml", "guest.yaml", "user-with-old-access-format.yaml"}
-
+
for _, file := range files {
srcPath := path.Join(srcDir, file)
dstPath := path.Join(dstDir, file)
-
+
data, err := os.ReadFile(srcPath)
require.NoError(t, err, "Failed to read %s", srcPath)
-
+
err = os.WriteFile(dstPath, data, 0644)
require.NoError(t, err, "Failed to write %s", dstPath)
}
@@ -30,7 +30,7 @@ func TestNewYAMLAccountManager(t *testing.T) {
// Create temporary directory and copy test files
tempDir := t.TempDir()
copyTestFiles(t, "test/config/Users", tempDir)
-
+
// Use temp directory instead of original test directory
got, err := NewYAMLAccountManager(tempDir)
require.NoError(t, err)
@@ -66,3 +66,338 @@ func TestNewYAMLAccountManager(t *testing.T) {
)
})
}
+
+func TestYAMLAccountManager_Delete(t *testing.T) {
+ t.Run("successful deletions", func(t *testing.T) {
+ type args struct {
+ login string
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "deletes existing guest account",
+ args: args{
+ login: "guest",
+ },
+ },
+ {
+ name: "deletes existing admin account",
+ args: args{
+ login: "admin",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create temporary directory and copy test files
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ // Get initial state
+ initialAccounts := accountMgr.List()
+ initialCount := len(initialAccounts)
+
+ // Verify account exists before deletion
+ account := accountMgr.Get(tt.args.login)
+ require.NotNilf(t, account, "Account %s should exist before deletion", tt.args.login)
+
+ // Perform deletion
+ err = accountMgr.Delete(tt.args.login)
+ assert.NoErrorf(t, err, "Delete(%v)", tt.args.login)
+
+ // Verify account no longer exists in memory
+ account = accountMgr.Get(tt.args.login)
+ assert.Nilf(t, account, "Delete(%v)", tt.args.login)
+
+ // Verify file was deleted
+ filePath := path.Join(tempDir, tt.args.login+".yaml")
+ _, fileErr := os.Stat(filePath)
+ assert.Truef(t, os.IsNotExist(fileErr), "Delete(%v) - file should be deleted", tt.args.login)
+
+ // Verify list count decreased
+ updatedAccounts := accountMgr.List()
+ expectedCount := initialCount - 1
+ assert.Equalf(t, expectedCount, len(updatedAccounts), "Delete(%v)", tt.args.login)
+
+ // Verify deleted account is not in the list
+ for _, account := range updatedAccounts {
+ assert.NotEqualf(t, tt.args.login, account.Login, "Delete(%v)", tt.args.login)
+ }
+ })
+ }
+ })
+
+ t.Run("error cases", func(t *testing.T) {
+ type args struct {
+ login string
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "returns error when deleting non-existent account",
+ args: args{
+ login: "nonexistent",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create temporary directory and copy test files
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ // Perform deletion
+ err = accountMgr.Delete(tt.args.login)
+ assert.Errorf(t, err, "Delete(%v)", tt.args.login)
+ })
+ }
+ })
+}
+
+func TestYAMLAccountManager_Create(t *testing.T) {
+ t.Run("successful creation", func(t *testing.T) {
+ type args struct {
+ account hotline.Account
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "creates new account with valid data",
+ args: args{
+ account: hotline.Account{
+ Login: "newuser",
+ Name: "New User",
+ Password: "hashedpassword",
+ Access: hotline.AccessBitmap{0x60, 0x70, 0x0c, 0x20, 0x03, 0x80, 0x00, 0x00},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ initialCount := len(accountMgr.List())
+
+ err = accountMgr.Create(tt.args.account)
+ assert.NoErrorf(t, err, "Create(%v)", tt.args.account)
+
+ // Verify account exists in memory
+ retrievedAccount := accountMgr.Get(tt.args.account.Login)
+ assert.NotNilf(t, retrievedAccount, "Create(%v)", tt.args.account)
+ assert.Equalf(t, &tt.args.account, retrievedAccount, "Create(%v)", tt.args.account)
+
+ // Verify file was created
+ filePath := path.Join(tempDir, tt.args.account.Login+".yaml")
+ _, err = os.Stat(filePath)
+ assert.NoErrorf(t, err, "Create(%v) - file should exist", tt.args.account)
+
+ // Verify list count increased
+ assert.Equalf(t, initialCount+1, len(accountMgr.List()), "Create(%v)", tt.args.account)
+ })
+ }
+ })
+
+ t.Run("error cases", func(t *testing.T) {
+ type args struct {
+ account hotline.Account
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "returns error when creating account with existing login",
+ args: args{
+ account: hotline.Account{
+ Login: "admin",
+ Name: "Duplicate Admin",
+ Password: "password",
+ Access: hotline.AccessBitmap{},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ err = accountMgr.Create(tt.args.account)
+ assert.Errorf(t, err, "Create(%v)", tt.args.account)
+ })
+ }
+ })
+}
+
+func TestYAMLAccountManager_Update(t *testing.T) {
+ t.Run("successful updates", func(t *testing.T) {
+ type args struct {
+ account hotline.Account
+ newLogin string
+ }
+ tests := []struct {
+ name string
+ args args
+ }{
+ {
+ name: "updates account without changing login",
+ args: args{
+ account: hotline.Account{
+ Login: "guest",
+ Name: "Updated Guest",
+ Password: "newpassword",
+ Access: hotline.AccessBitmap{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88},
+ },
+ newLogin: "guest",
+ },
+ },
+ {
+ name: "updates account with login change",
+ args: args{
+ account: hotline.Account{
+ Login: "guest",
+ Name: "Renamed Guest",
+ Password: "password",
+ Access: hotline.AccessBitmap{0x60, 0x70, 0x0c, 0x20, 0x03, 0x80, 0x00, 0x00},
+ },
+ newLogin: "renamedguest",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ err = accountMgr.Update(tt.args.account, tt.args.newLogin)
+ assert.NoErrorf(t, err, "Update(%v, %v)", tt.args.account, tt.args.newLogin)
+
+ // Verify updated account exists with new login
+ retrievedAccount := accountMgr.Get(tt.args.newLogin)
+ assert.NotNilf(t, retrievedAccount, "Update(%v, %v)", tt.args.account, tt.args.newLogin)
+
+ expectedAccount := tt.args.account
+ expectedAccount.Login = tt.args.newLogin
+ assert.Equalf(t, &expectedAccount, retrievedAccount, "Update(%v, %v)", tt.args.account, tt.args.newLogin)
+
+ // If login changed, verify old login no longer exists
+ if tt.args.account.Login != tt.args.newLogin {
+ oldAccount := accountMgr.Get(tt.args.account.Login)
+ assert.Nilf(t, oldAccount, "Update(%v, %v) - old login should not exist", tt.args.account, tt.args.newLogin)
+
+ // Verify old file was renamed
+ oldFilePath := path.Join(tempDir, tt.args.account.Login+".yaml")
+ _, err = os.Stat(oldFilePath)
+ assert.Truef(t, os.IsNotExist(err), "Update(%v, %v) - old file should not exist", tt.args.account, tt.args.newLogin)
+ }
+
+ // Verify new file exists
+ newFilePath := path.Join(tempDir, tt.args.newLogin+".yaml")
+ _, err = os.Stat(newFilePath)
+ assert.NoErrorf(t, err, "Update(%v, %v) - new file should exist", tt.args.account, tt.args.newLogin)
+ })
+ }
+ })
+}
+
+func TestYAMLAccountManager_Get(t *testing.T) {
+ type args struct {
+ login string
+ }
+ tests := []struct {
+ name string
+ args args
+ want *hotline.Account
+ }{
+ {
+ name: "returns existing account",
+ args: args{
+ login: "admin",
+ },
+ want: &hotline.Account{
+ Name: "admin",
+ Login: "admin",
+ Password: "$2a$04$2itGEYx8C1N5bsfRSoC9JuonS3I4YfnyVPZHLSwp7kEInRX0yoB.a",
+ Access: hotline.AccessBitmap{0xff, 0xff, 0xef, 0xff, 0xff, 0x80, 0x00, 0x00},
+ },
+ },
+ {
+ name: "returns nil for non-existent account",
+ args: args{
+ login: "nonexistent",
+ },
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ got := accountMgr.Get(tt.args.login)
+ assert.Equalf(t, tt.want, got, "Get(%v)", tt.args.login)
+ })
+ }
+}
+
+func TestYAMLAccountManager_List(t *testing.T) {
+ tests := []struct {
+ name string
+ wantCount int
+ wantLogins []string
+ }{
+ {
+ name: "returns all accounts",
+ wantCount: 3,
+ wantLogins: []string{"admin", "guest", "test-user"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tempDir := t.TempDir()
+ copyTestFiles(t, "test/config/Users", tempDir)
+
+ accountMgr, err := NewYAMLAccountManager(tempDir)
+ require.NoError(t, err)
+
+ got := accountMgr.List()
+ assert.Equalf(t, tt.wantCount, len(got), "List() count")
+
+ // Check that all expected logins are present
+ gotLogins := make([]string, len(got))
+ for i, account := range got {
+ gotLogins[i] = account.Login
+ }
+
+ for _, expectedLogin := range tt.wantLogins {
+ assert.Containsf(t, gotLogins, expectedLogin, "List() should contain login %s", expectedLogin)
+ }
+ })
+ }
+}