From 3cb49d005e64890707fa11bea059f36232b6e929 Mon Sep 17 00:00:00 2001 From: Joshua Ramon Enslin Date: Tue, 25 Feb 2025 04:00:30 +0100 Subject: [PATCH] Implement loading and storing config to / from file --- src/configloader/ValidateInstanceLink_test.go | 14 ++ src/configloader/ValidateInstitutionId.go | 2 - src/configloader/configloader.go | 209 +++++++++++++++++- src/configloader/configloader_test.go | 174 +++++++++++++++ 4 files changed, 386 insertions(+), 13 deletions(-) create mode 100644 src/configloader/configloader_test.go diff --git a/src/configloader/ValidateInstanceLink_test.go b/src/configloader/ValidateInstanceLink_test.go index 3bec0c6..1270e3e 100644 --- a/src/configloader/ValidateInstanceLink_test.go +++ b/src/configloader/ValidateInstanceLink_test.go @@ -51,5 +51,19 @@ func TestValidateInstanceWorks(t *testing.T) { t.Fatalf("Output of ValidateInstanceLink() is not https://hessen.museum-digital.de where it should be") } +} + +// Test that ValidateInstanceLink() works with a valid instance of md and cleans paths. +func TestValidateInstanceDoesCleanPathFromUrl(t *testing.T) { + + result, err := ValidateInstanceLink("https://hessen.museum-digital.de/home") + + if err != nil { + t.Fatalf("ValidateInstanceLink() returns an error where it should work") + } + if result != "https://hessen.museum-digital.de" { + t.Fatalf("Output of ValidateInstanceLink() is not https://hessen.museum-digital.de where it should be") + } + } diff --git a/src/configloader/ValidateInstitutionId.go b/src/configloader/ValidateInstitutionId.go index fc3e40c..4fd0bf7 100644 --- a/src/configloader/ValidateInstitutionId.go +++ b/src/configloader/ValidateInstitutionId.go @@ -25,8 +25,6 @@ func ValidateInstitutionId(institutionId int, instanceUrl string) (int, error) { return 0, errors.New("The institution page does not respond with HTTP 200, the institution does not seem to exist") } - - return institutionId, nil } diff --git a/src/configloader/configloader.go b/src/configloader/configloader.go index af751d3..3e12417 100644 --- a/src/configloader/configloader.go +++ b/src/configloader/configloader.go @@ -1,25 +1,212 @@ package configloader +import ( + "os" + "encoding/json" + "path/filepath" + "io/ioutil" +) + type MDWebDavUploaderConfig struct { - InstanceLink string `json:"instance"` - Mail string `json:"mail"` - WebDavAuthToken string `json:"token"` - InstitutionId int `json:"institution_id"` - Parser string `json:"parser"` - MetadataFolder string `json:"metadata_folder"` - MediaFolder string `json:"media_folder"` - PublishOnImport bool `json:"visible"` + InstanceLink string `json:"instance"` + Mail string `json:"mail"` + WebDavAuthToken string `json:"token"` + InstitutionId int `json:"institution_id"` + Parser string `json:"parser"` + MetadataFolder string `json:"metadata_folder"` + MediaFolder string `json:"media_folder"` + PublishOnImport bool `json:"visible"` + Settings map[string]string `json:"settings"` } -// Returns a uniform filepath for the configuration of this tool. +// Returns the file path of the configuration file within the supplied +// directory. Moved to a dedicated function to provide a consistent +// filename. +func getConfigFileNameByDir(folder string) string { + return filepath.Join(folder, "config.json") +} + +// Returns a uniform directory for the configuration of this tool. // To be compatible across operating systems, this will be a JSON // file in the same directory as the current programm. -func getConfigFilepath() string { +func getConfigFilepath() (string, error) { + + // Get the OS-dependent configuration directory. + generalConfigDir, dirErr := os.UserConfigDir() + if dirErr != nil { + return "", dirErr + } + + // Select a subdirectory of that directory to store application-specific + // settings in. Attempt to create it and return the config filepath as + // a file in that directory. + configDir := filepath.Join(generalConfigDir, "museum-digital-uploader") + generateConfigDirErr := os.Mkdir(configDir, 0700) + + // The config directory could be created, return path of the configuration + // file in it. + if generateConfigDirErr == nil { + return getConfigFileNameByDir(configDir), nil // Use this to create files + } + + // There has been an error creating the config directory. + // This may be either that the directory already exists, which is alright + // (and even expected on most runs). Or it might be, that the path is + // already occupied with a file. Or something else. In those cases, + // the error should be returned. + + // If the path is already occupied, inspect it more closely. + if os.IsExist(generateConfigDirErr) { + + info, err := os.Stat(configDir) + if err != nil { + return "", err + } + + // The directory already exists and is a directory. This is fine, + // so we return the config filename in the existing config directory. + if info.IsDir() { + return getConfigFileNameByDir(configDir), nil // Use this to create files + } + + } + + return "", generateConfigDirErr + +} + +// Wrapper around getConfigFilepath() allowing for an override for testing. +func getConfigFilepathOrOverride(overridePath string) (string, error) { + + // Override path is not validated, as it should only be used + // in test settings + if overridePath != "" { + return overridePath, nil + } + + configFilePath, err := getConfigFilepath() + if err != nil { + return "", err + } + + return configFilePath, nil + +} + +// Validates each of the values of MDWebDavUploaderConfig. +func ValidateConfig(conf MDWebDavUploaderConfig) (MDWebDavUploaderConfig, error) { + + // Validate and clean instance link + instanceLink, instanceErr := ValidateInstanceLink(conf.InstanceLink) + if instanceErr != nil { + return conf, instanceErr + } + conf.InstanceLink = instanceLink + + // Validate and clean mail + mailLink, mailErr := ValidateMail(conf.Mail) + if mailErr != nil { + return conf, mailErr + } + conf.Mail = mailLink + + // Validate and clean institution ID + institutionIdLink, institutionIdErr := ValidateInstitutionId(conf.InstitutionId, conf.InstanceLink) + if institutionIdErr != nil { + return conf, institutionIdErr + } + conf.InstitutionId = institutionIdLink + + // Validate and clean parser + parserLink, parserErr := ValidateParser(conf.Parser) + if parserErr != nil { + return conf, parserErr + } + conf.Parser = parserLink + + // Validate and clean metadata folder + metadataFolder, mFolderErr := ValidateUploadDir(conf.MetadataFolder) + if mFolderErr != nil { + return conf, mFolderErr + } + conf.MetadataFolder = metadataFolder + + // Validate and clean media folder + mediaFolder, mediaFolderErr := ValidateUploadDir(conf.MediaFolder) + if mediaFolderErr != nil { + return conf, mediaFolderErr + } + conf.MediaFolder = mediaFolder + + return conf, nil } // Loads configuration from the configuration file (located using // getConfigFilepath()). -func LoadFromFile() MDWebDavUploaderConfig { +// The function parameter overridePath is only useful for test settings. +// Outside those, set an empty string to use the default config path. +// Output parameters: +// 1: Config +// 2: Requirement for a re-run of the setup +// 3: Error +func LoadFromFile(overridePath string) (MDWebDavUploaderConfig, bool, error) { + + // Get config file path + configFile, configFileErr := getConfigFilepathOrOverride(overridePath) + if configFileErr != nil { + return MDWebDavUploaderConfig{}, false, configFileErr + } + + // Read the file + configFileBytes, ioErr := ioutil.ReadFile(configFile) + if ioErr != nil { + return MDWebDavUploaderConfig{}, true, ioErr + } + + // Parse the file into a configuration struct + var data MDWebDavUploaderConfig + unmarshalErr := json.Unmarshal(configFileBytes, &data) + + if unmarshalErr != nil { + return MDWebDavUploaderConfig{}, true, ioErr + } + + // Validate data - the configuration file may have been altered externally + config, validationErr := ValidateConfig(data) + if validationErr != nil { + return MDWebDavUploaderConfig{}, true, validationErr + } + + // Config could be parsed + return config, false, nil + +} + +// Stores a config from MDWebDavUploaderConfig to the config file. +// As with LoadFromFile(), it is possible to provide an overridePath for testing. +func StoreConfigToFile(conf MDWebDavUploaderConfig, overridePath string) error { + + config, validationErr := ValidateConfig(conf) + if validationErr != nil { + return validationErr + } + + configFilePath, configFileErr := getConfigFilepathOrOverride(overridePath) + if configFileErr != nil { + return configFileErr + } + + configJson, encodeErr := json.Marshal(config) + if encodeErr != nil { + return encodeErr + } + + writeErr := ioutil.WriteFile(configFilePath, configJson, 0644) + if writeErr != nil { + return writeErr + } + + return nil } diff --git a/src/configloader/configloader_test.go b/src/configloader/configloader_test.go new file mode 100644 index 0000000..22ff3d6 --- /dev/null +++ b/src/configloader/configloader_test.go @@ -0,0 +1,174 @@ +package configloader + +import ( + "testing" + "os" + "path/filepath" +) + +// Returns a generally valid config. Single values can then be +// replaced with the actually tested contents. +func getTestConfig() MDWebDavUploaderConfig { + + input := MDWebDavUploaderConfig{} + input.InstanceLink = "https://hessen.museum-digital.de/home" + input.Mail = "test@example.com" + input.InstitutionId = 1; + input.Parser = "Lido" + + tmpDir := os.TempDir() + testDir := filepath.Join(tmpDir, "/existing-dir-for-import") + mkdirErr := os.MkdirAll(testDir, os.ModePerm) + if mkdirErr != nil { + panic("Test failure: Failed to create test dir") + } + + input.MetadataFolder = testDir + input.MediaFolder = testDir + + return input + + +} + +// Test that ValidateConfig() fails on non-URLs. +func TestValidateUploaderConfigFailsOnInvalidInstanceUrl(t *testing.T) { + + input := getTestConfig() + input.InstanceLink = "abcmuseum-digital.org" + _, err := ValidateConfig(input) + if err == nil { + t.Fatalf("ValidateConfig() does not return an error on a non-URL (via ValidateInstanceLink)") + } + +} + +// Test that ValidateConfig cleans an instance URL as per ValidateInstanceLink(). +func TestValidateUploaderConfigCleansInstanceViaValidateInstanceLink(t *testing.T) { + + input := getTestConfig() + input.InstanceLink = "https://hessen.museum-digital.de/home" + returnVal, _ := ValidateConfig(input) + if returnVal.InstanceLink != "https://hessen.museum-digital.de" { + t.Fatalf("Failed to clean up input URL") + } + +} + +// Test that ValidateConfig() fails on invalid mail. +func TestValidateUploaderConfigFailsOnInvalidMail(t *testing.T) { + + input := getTestConfig() + input.Mail = "test" + _, err := ValidateConfig(input) + if err == nil { + t.Fatalf("ValidateConfig() does not return an error on a mail address") + } + +} + +// Test that ValidateConfig() accepts a valid mail address. +func TestValidateUploaderConfigAcceptsValidMail(t *testing.T) { + + input := getTestConfig() + input.Mail = "test@example.com" + returnVal, _ := ValidateConfig(input) + if returnVal.Mail != "test@example.com" { + t.Fatalf("Failed to accept valid mail") + } + +} + +// Test that ValidateConfig() fails on negative / invalid IDs. +func TestValidateUploaderConfigFailsOnInvalidInstitutionId(t *testing.T) { + + input := getTestConfig() + input.InstitutionId = -1 + _, err := ValidateConfig(input) + if err == nil { + t.Fatalf("ValidateConfig() does not return an error on an invalid institution ID") + } + +} + +// Test that ValidateConfig() accepts valid institution IDs. +func TestValidateUploaderConfigAcceptsValidInstitutionId(t *testing.T) { + + input := getTestConfig() + input.InstitutionId = 1 + returnVal, _ := ValidateConfig(input) + if returnVal.InstitutionId != 1 { + t.Fatalf("Failed to accept valid institution ID") + } + +} + +// Test that ValidateConfig() fails on negative / invalid IDs. +func TestValidateUploaderConfigFailsOnInvalidParser(t *testing.T) { + + input := getTestConfig() + input.Parser = "nonexistentparser" + _, err := ValidateConfig(input) + if err == nil { + t.Fatalf("ValidateConfig() does not return an error on an invalid parser") + } + +} + +// Test that ValidateConfig() accepts valid ParserDs. +func TestValidateUploaderConfigAcceptsValidParser(t *testing.T) { + + input := getTestConfig() + input.Parser = "ParserLido" + returnVal, _ := ValidateConfig(input) + if returnVal.Parser != "Lido" { + t.Fatalf("Failed to accept and clean valid parser") + } + +} + +// Test that ValidateConfig() fails on non-existent folder. +func TestValidateUploaderConfigFailsOnInvalidMetadataFolder(t *testing.T) { + + input := getTestConfig() + input.MetadataFolder = "nonexistentfolder" + _, err := ValidateConfig(input) + if err == nil { + t.Fatalf("ValidateConfig() does not return an error on an invalid metadata folder") + } + +} + +// Test that saving and reading config works. +func TestWritingAndReadingConfigWorks(t *testing.T) { + + input := getTestConfig() + input, _ = ValidateConfig(input) + + writeErr := StoreConfigToFile(input, input.MetadataFolder + "/config.json") + if writeErr != nil { + t.Log("Error:") + t.Log(writeErr) + t.Fatalf("Failed to write config to override path") + } + + loadedFromFile, setupRequired, err := LoadFromFile(input.MetadataFolder + "/config.json") + + if setupRequired != false { + t.Fatalf("Expected no setup to be required, but return value indicated thus") + } + if err != nil { + t.Fatalf("Returned an error on trying to load config file") + } + // Golang can't compare structs with slices or maps + if input.InstanceLink != loadedFromFile.InstanceLink || input.Mail != loadedFromFile.Mail || input.WebDavAuthToken != loadedFromFile.WebDavAuthToken || input.InstitutionId != loadedFromFile.InstitutionId || input.Parser != loadedFromFile.Parser || input.MetadataFolder != loadedFromFile.MetadataFolder || input.MediaFolder != loadedFromFile.MediaFolder || input.PublishOnImport != loadedFromFile.PublishOnImport { + t.Log("Input") + t.Log(input) + + t.Log("Loaded output") + t.Log(loadedFromFile) + + t.Fatalf("Failed to write and then load the same config") + } + +}