diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2025-07-01 16:03:57 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2025-07-01 16:03:57 -0700 |
| commit | 262f66351484369fa65c2e23df81ef682ea06d89 (patch) | |
| tree | 97c2d135531b20b7b6a986fdba69e386412bbfd5 /internal | |
| parent | a9c6485fb00dbf2e4b55351c1ae74d4eaca3eec9 (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.go | 19 | ||||
| -rw-r--r-- | internal/mobius/account_manager_test.go | 347 |
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) + } + }) + } +} |