From d38431d9e8ac1411e18191987faf7332dd6180c1 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sun, 18 Aug 2024 21:13:08 +0100 Subject: [PATCH] fix: error if home config directory not found Summary: If the user hasn't supplied the path to a configuration directory and Enbas fails to find the user's default home configuration directory, Enbas will now return an error message back to the user. Previously Enbas would try to, instead, calculate the configuration directory using the user's current directory. Changes: - Return an error if the user's home configuration directory cannot be found. - Add tests to the internal.config package. - Update the tests in internal.utilities package. PR: https://codeflow.dananglin.me.uk/apollo/enbas/pulls/54 --- internal/config/config.go | 38 ++++++--- internal/config/config_test.go | 83 ++++++++++++++++++ internal/config/credentials.go | 24 +++++- internal/config/credentials_test.go | 107 +++++++++++++++++++++++ internal/utilities/directories.go | 8 +- internal/utilities/directories_test.go | 112 +++++++++++++++++-------- internal/utilities/utilities_test.go | 2 - 7 files changed, 319 insertions(+), 55 deletions(-) create mode 100644 internal/config/config_test.go create mode 100644 internal/config/credentials_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 29cae4a..8946aa0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,7 +10,11 @@ import ( ) const ( - configFileName = "config.json" + configFileName string = "config.json" + + defaultHTTPTimeout int = 5 + defaultHTTPMediaTimeout int = 30 + defaultLineWrapMaxWidth int = 80 ) type Config struct { @@ -35,7 +39,10 @@ type Integrations struct { } func NewConfigFromFile(configDir string) (*Config, error) { - path := configFile(configDir) + path, err := configPath(configDir) + if err != nil { + return nil, fmt.Errorf("unable to calculate the path to your config file: %w", err) + } file, err := os.Open(path) if err != nil { @@ -53,13 +60,19 @@ func NewConfigFromFile(configDir string) (*Config, error) { } func FileExists(configDir string) (bool, error) { - path := configFile(configDir) + path, err := configPath(configDir) + if err != nil { + return false, fmt.Errorf("unable to calculate the path to your config file: %w", err) + } return utilities.FileExists(path) } func SaveDefaultConfigToFile(configDir string) error { - path := configFile(configDir) + path, err := configPath(configDir) + if err != nil { + return fmt.Errorf("unable to calculate the path to your config file: %w", err) + } file, err := os.Create(path) if err != nil { @@ -69,7 +82,7 @@ func SaveDefaultConfigToFile(configDir string) error { config := defaultConfig() - credentialsFilePath, err := utilities.AbsolutePath(defaultCredentialsConfigFile(configDir)) + credentialsFilePath, err := defaultCredentialsConfigFile(configDir) if err != nil { return fmt.Errorf("unable to calculate the path to the credentials file: %w", err) } @@ -86,8 +99,13 @@ func SaveDefaultConfigToFile(configDir string) error { return nil } -func configFile(configDir string) string { - return filepath.Join(utilities.CalculateConfigDir(configDir), configFileName) +func configPath(configDir string) (string, error) { + configDir, err := utilities.CalculateConfigDir(configDir) + if err != nil { + return "", fmt.Errorf("unable to get the config directory: %w", err) + } + + return filepath.Join(configDir, configFileName), nil } func defaultConfig() Config { @@ -95,10 +113,10 @@ func defaultConfig() Config { CredentialsFile: "", CacheDirectory: "", HTTP: HTTPConfig{ - Timeout: 5, - MediaTimeout: 30, + Timeout: defaultHTTPTimeout, + MediaTimeout: defaultHTTPMediaTimeout, }, - LineWrapMaxWidth: 80, + LineWrapMaxWidth: defaultLineWrapMaxWidth, Integrations: Integrations{ Browser: "", Editor: "", diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..795a060 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,83 @@ +package config_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" +) + +func TestConfigFile(t *testing.T) { + t.Log("Testing saving and loading the configuration") + + projectDir, err := projectRoot() + if err != nil { + t.Fatalf("Unable to get the project root directory: %v", err) + } + + configDir := filepath.Join(projectDir, "test", "config") + + t.Run("Save the default configuration to file", testSaveDefaultConfigToFile(configDir)) + t.Run("Load the configuration from file", testLoadConfigFromFile(configDir)) + + expectedConfigFile := filepath.Join(configDir, "config.json") + if err := os.Remove(expectedConfigFile); err != nil { + t.Fatalf( + "received an error after trying to clean up the test configuration at %q: %v", + expectedConfigFile, + err, + ) + } +} + +func testSaveDefaultConfigToFile(configDir string) func(t *testing.T) { + return func(t *testing.T) { + if err := config.SaveDefaultConfigToFile(configDir); err != nil { + t.Fatalf("Unable to save the default configuration within %q: %v", configDir, err) + } + + fileExists, err := config.FileExists(configDir) + if err != nil { + t.Fatalf("Unable to determine if the configuration exists within %q: %v", configDir, err) + } + + if !fileExists { + t.Fatalf("The configuration does not appear to exist within %q", configDir) + } + } +} + +func testLoadConfigFromFile(configDir string) func(t *testing.T) { + return func(t *testing.T) { + config, err := config.NewConfigFromFile(configDir) + if err != nil { + t.Fatalf("Unable to load the configuration from file: %v", err) + } + + expectedDefaultHTTPTimeout := 5 + + if config.HTTP.Timeout != expectedDefaultHTTPTimeout { + t.Errorf( + "Unexpected HTTP Timeout settings loaded from the configuration: want %d, got %d", + expectedDefaultHTTPTimeout, + config.HTTP.Timeout, + ) + } else { + t.Logf( + "Expected HTTP Timeout settings loaded from the configuration: got %d", + config.HTTP.Timeout, + ) + } + } +} + +func projectRoot() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("unable to get the current working directory, %w", err) + } + + return filepath.Join(cwd, "..", ".."), nil +} diff --git a/internal/config/credentials.go b/internal/config/credentials.go index 57e8ee5..4a2df20 100644 --- a/internal/config/credentials.go +++ b/internal/config/credentials.go @@ -38,9 +38,15 @@ func (e CredentialsNotFoundError) Error() string { // directory. If the directory is not specified then the default directory is used. If the directory // is not present, it will be created. func SaveCredentials(filePath, username string, credentials Credentials) (string, error) { - directory := filepath.Dir(filePath) + part := filepath.Dir(filePath) - if err := utilities.EnsureDirectory(utilities.CalculateConfigDir(directory)); err != nil { + // ensure that the directory exists. + credentialsDir, err := utilities.CalculateConfigDir(part) + if err != nil { + return "", fmt.Errorf("unable to calculate the directory to your credentials file: %w", err) + } + + if err := utilities.EnsureDirectory(credentialsDir); err != nil { return "", fmt.Errorf("unable to ensure the configuration directory: %w", err) } @@ -128,6 +134,16 @@ func saveCredentialsConfigFile(authConfig CredentialsConfig, filePath string) er return nil } -func defaultCredentialsConfigFile(configDir string) string { - return filepath.Join(utilities.CalculateConfigDir(configDir), defaultCredentialsFileName) +func defaultCredentialsConfigFile(configDir string) (string, error) { + dir, err := utilities.CalculateConfigDir(configDir) + if err != nil { + return "", fmt.Errorf("unable to calculate the config directory: %w", err) + } + + path, err := utilities.AbsolutePath(filepath.Join(dir, defaultCredentialsFileName)) + if err != nil { + return "", fmt.Errorf("unable to get the absolute path to the credentials config file: %w", err) + } + + return path, nil } diff --git a/internal/config/credentials_test.go b/internal/config/credentials_test.go new file mode 100644 index 0000000..96621dd --- /dev/null +++ b/internal/config/credentials_test.go @@ -0,0 +1,107 @@ +package config_test + +import ( + "maps" + "os" + "path/filepath" + "testing" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" +) + +func TestCredentialsFile(t *testing.T) { + t.Log("Testing saving and loading credentials from file") + + projectDir, err := projectRoot() + if err != nil { + t.Fatalf("Unable to get the project root directory: %v", err) + } + + credentialsFile := filepath.Join(projectDir, "test", "config", "credentials.json") + + credentialsMap := map[string]config.Credentials{ + "admin": { + Instance: "https://gts.red-crow.private", + ClientID: "01EOB91DVQGPA364QK32TM3LXR1998BMXSZE4", + ClientSecret: "ffd76025-4b23-4ce6-b8ea-077ce3cadf5a", + AccessToken: "C9VDXGGRPZ0448SH562N6N6893VNPGJMGJ336TXLMH8RXGWF4", + }, + "bobby": { + Instance: "https://gts.red-crow.private", + ClientID: "01CUVHR6LIST7Q6R25Z9Y14WZK780V91S9VQB", + ClientSecret: "379fc272-c7cc-4ccb-8461-f3f71207f798", + AccessToken: "F0YWQG1R4DDAMXGBZ514BCW7ATWN6JRGLDRUZO4RFAMTT6J38", + }, + "app": { + Instance: "https://gts.red-crow.private", + ClientID: "01HLZY7XCD60564OP3RG6FZTOAD3LGF0R8SEK", + ClientSecret: "dfd8e954-53b1-4f00-9c09-0b181f44bb79", + AccessToken: "JZ2PZ4YNE1BB38VMRIQ7DNWXKZE6B1EBV310RNC53KQCVHXGB", + }, + } + + t.Run("Saving credentials to file", testSaveCredentials(credentialsFile, credentialsMap)) + + expectedCurrentAccount := "bobby@gts.red-crow.private" + t.Run("Updating the current account in the credentials file", testUpdateCurrentAccount(expectedCurrentAccount, credentialsFile)) + + t.Run("Loading the credentials from file", testLoadCredentialsConfigFromFile(credentialsFile, expectedCurrentAccount)) + + if err := os.Remove(credentialsFile); err != nil { + t.Fatalf( + "received an error after trying to clean up the test configuration at %q: %v", + credentialsFile, + err, + ) + } +} + +func testSaveCredentials(credentialsFile string, credentialsMap map[string]config.Credentials) func(t *testing.T) { + return func(t *testing.T) { + for username, credentials := range maps.All(credentialsMap) { + if _, err := config.SaveCredentials(credentialsFile, username, credentials); err != nil { + t.Fatalf( + "Unable to save the credentials for %s to %q: %v", + username, + credentialsFile, + err, + ) + } + } + + t.Log("All credentials saved to file.") + } +} + +func testUpdateCurrentAccount(account, credentialsFile string) func(t *testing.T) { + return func(t *testing.T) { + if err := config.UpdateCurrentAccount(account, credentialsFile); err != nil { + t.Fatalf("Unable to update the current account to %q: %v", account, err) + } + + t.Logf("Successfully updated the current account.") + } +} + +func testLoadCredentialsConfigFromFile(credentialsFile string, expectedCurrentAccount string) func(t *testing.T) { + return func(t *testing.T) { + credentials, err := config.NewCredentialsConfigFromFile(credentialsFile) + if err != nil { + t.Fatalf( + "Unable to load the credentials configuration from %q: %v", + credentialsFile, + err, + ) + } + + if credentials.CurrentAccount != expectedCurrentAccount { + t.Errorf( + "Unexpected current account found in the credentials configuration file: want %s, got %s", + expectedCurrentAccount, + credentials.CurrentAccount, + ) + } else { + t.Logf("Expected current account found in the credentials configuration file: got %s", credentials.CurrentAccount) + } + } +} diff --git a/internal/utilities/directories.go b/internal/utilities/directories.go index f3cd194..eddecfd 100644 --- a/internal/utilities/directories.go +++ b/internal/utilities/directories.go @@ -14,17 +14,17 @@ const ( cacheStatusesDir = "statuses" ) -func CalculateConfigDir(configDir string) string { +func CalculateConfigDir(configDir string) (string, error) { if configDir != "" { - return configDir + return configDir, nil } configRoot, err := os.UserConfigDir() if err != nil { - return filepath.Join(os.Getenv("HOME"), "."+internal.ApplicationName, "config") + return "", fmt.Errorf("unable to get your default config diretory: %w", err) } - return filepath.Join(configRoot, internal.ApplicationName) + return filepath.Join(configRoot, internal.ApplicationName), nil } func CalculateMediaCacheDir(cacheRoot, instance string) (string, error) { diff --git a/internal/utilities/directories_test.go b/internal/utilities/directories_test.go index 53e04e7..b55cc22 100644 --- a/internal/utilities/directories_test.go +++ b/internal/utilities/directories_test.go @@ -7,34 +7,45 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) -func TestCalculateMediaCacheDir(t *testing.T) { - t.Parallel() +func TestDirectoryCalculations(t *testing.T) { + t.Log("Testing the directory calculations") projectDir, err := projectRoot() if err != nil { t.Fatalf("Unable to get the project root directory: %v", err) } - cacheRoot := filepath.Join(projectDir, "test", "cache") - instance := "http://gotosocial.yellow-desert.social" + t.Setenv("XDG_CACHE_HOME", "/home/enbas/.cache") + t.Setenv("XDG_CONFIG_HOME", "/home/enbas/.config") - got, err := utilities.CalculateMediaCacheDir(cacheRoot, instance) - if err != nil { - t.Fatalf("Unable to calculate the media cache directory: %v", err) - } + t.Run("Media Cache Directory Calculation", testCalculateMediaCacheDir(projectDir)) + t.Run("Media Cache Directory Calculation (with XDG_CACHE_HOME)", testCalculateMediaCacheDirWithXDG) + t.Run("Statuses Cache Directory Calculation", testCalculateStatusesCacheDir(projectDir)) + t.Run("Configuration Directory Calculation", testCalculateConfigDir(projectDir)) + t.Run("Configuration Directory Calculation (with XDG_CONFIG_HOME)", testCalculateConfigCacheDirWithXDG) +} - want := projectDir + "/test/cache/gotosocial.yellow-desert.social/media" +func testCalculateMediaCacheDir(projectDir string) func(t *testing.T) { + return func(t *testing.T) { + cacheRoot := filepath.Join(projectDir, "test", "cache") + instance := "http://gotosocial.yellow-desert.social" - if got != want { - t.Errorf("Unexpected media cache directory calculated: want %s, got %s", want, got) - } else { - t.Logf("Expected media cache directory calculated: got %s", got) + got, err := utilities.CalculateMediaCacheDir(cacheRoot, instance) + if err != nil { + t.Fatalf("Unable to calculate the media cache directory: %v", err) + } + + want := projectDir + "/test/cache/gotosocial.yellow-desert.social/media" + + if got != want { + t.Errorf("Unexpected media cache directory calculated: want %s, got %s", want, got) + } else { + t.Logf("Expected media cache directory calculated: got %s", got) + } } } -func TestCalculateMediaCacheDirWithXDG(t *testing.T) { - t.Setenv("XDG_CACHE_HOME", "/home/enbas/.cache") - +func testCalculateMediaCacheDirWithXDG(t *testing.T) { cacheRoot := "" instance := "https://gotosocial.yellow-desert.social" @@ -52,27 +63,58 @@ func TestCalculateMediaCacheDirWithXDG(t *testing.T) { } } -func TestCalculateStatusesCacheDir(t *testing.T) { - t.Parallel() +func testCalculateStatusesCacheDir(projectDir string) func(t *testing.T) { + return func(t *testing.T) { + cacheRoot := filepath.Join(projectDir, "test", "cache") + instance := "https://fedi.blue-mammoth.party" - projectDir, err := projectRoot() - if err != nil { - t.Fatalf("Unable to get the project root directory: %v", err) - } + got, err := utilities.CalculateStatusesCacheDir(cacheRoot, instance) + if err != nil { + t.Fatalf("Unable to calculate the statuses cache directory: %v", err) + } - cacheRoot := filepath.Join(projectDir, "test", "cache") - instance := "https://fedi.blue-mammoth.party" + want := projectDir + "/test/cache/fedi.blue-mammoth.party/statuses" - got, err := utilities.CalculateStatusesCacheDir(cacheRoot, instance) - if err != nil { - t.Fatalf("Unable to calculate the statuses cache directory: %v", err) - } - - want := projectDir + "/test/cache/fedi.blue-mammoth.party/statuses" - - if got != want { - t.Errorf("Unexpected statuses cache directory calculated: want %s, got %s", want, got) - } else { - t.Logf("Expected statuses cache directory calculated: got %s", got) + if got != want { + t.Errorf("Unexpected statuses cache directory calculated: want %s, got %s", want, got) + } else { + t.Logf("Expected statuses cache directory calculated: got %s", got) + } + } +} + +func testCalculateConfigDir(projectDir string) func(t *testing.T) { + return func(t *testing.T) { + configDir := filepath.Join(projectDir, "test", "config") + + got, err := utilities.CalculateConfigDir(configDir) + if err != nil { + t.Fatalf("Unable to calculate the config directory: %v", err) + } + + want := projectDir + "/test/config" + + if got != want { + t.Errorf("Unexpected config directory calculated: want %s, got %s", want, got) + } else { + t.Logf("Expected config directory calculated: got %s", got) + } + } +} + +func testCalculateConfigCacheDirWithXDG(t *testing.T) { + configDir := "" + + got, err := utilities.CalculateConfigDir(configDir) + if err != nil { + t.Fatalf("Unable to calculate the config directory: %v", err) + } + + want := "/home/enbas/.config/enbas" + + if got != want { + t.Errorf("Unexpected config directory calculated: want %s, got %s", want, got) + } else { + t.Logf("Expected config directory calculated: got %s", got) } } diff --git a/internal/utilities/utilities_test.go b/internal/utilities/utilities_test.go index b947314..6d16a25 100644 --- a/internal/utilities/utilities_test.go +++ b/internal/utilities/utilities_test.go @@ -7,8 +7,6 @@ import ( ) func TestGetFQDN(t *testing.T) { - t.Parallel() - cases := []struct { instance string want string