From 747ad6387b1b8976bb9c9f31daf188ee1f0c1115 Mon Sep 17 00:00:00 2001 From: AoXuan Date: Sun, 20 Jul 2025 18:12:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Go_Updater/api/client.go | 102 +++---- Go_Updater/config/config.go | 248 ++++++++++++++++ Go_Updater/config/config.json | 56 ++++ Go_Updater/config/config_test.go | 472 +++++++++++++++++++++++++++++++ 4 files changed, 827 insertions(+), 51 deletions(-) create mode 100644 Go_Updater/config/config.go create mode 100644 Go_Updater/config/config.json create mode 100644 Go_Updater/config/config_test.go diff --git a/Go_Updater/api/client.go b/Go_Updater/api/client.go index d04f1b6..d95cd27 100644 --- a/Go_Updater/api/client.go +++ b/Go_Updater/api/client.go @@ -15,17 +15,17 @@ type MirrorResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data struct { - VersionName string `json:"version_name"` - VersionNumber int `json:"version_number"` - URL string `json:"url,omitempty"` // Only present when using CDK - SHA256 string `json:"sha256,omitempty"` // Only present when using CDK - Channel string `json:"channel"` - OS string `json:"os"` - Arch string `json:"arch"` - UpdateType string `json:"update_type,omitempty"` // Only present when using CDK - ReleaseNote string `json:"release_note"` - FileSize int64 `json:"filesize,omitempty"` // Only present when using CDK - CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"` // Only present when using CDK + VersionName string `json:"version_name"` + VersionNumber int `json:"version_number"` + URL string `json:"url,omitempty"` // Only present when using CDK + SHA256 string `json:"sha256,omitempty"` // Only present when using CDK + Channel string `json:"channel"` + OS string `json:"os"` + Arch string `json:"arch"` + UpdateType string `json:"update_type,omitempty"` // Only present when using CDK + ReleaseNote string `json:"release_note"` + FileSize int64 `json:"filesize,omitempty"` // Only present when using CDK + CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"` // Only present when using CDK } `json:"data"` } @@ -66,62 +66,62 @@ func NewClient() *Client { func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) { // Construct the API URL apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID) - + // Parse URL to add query parameters u, err := url.Parse(apiURL) if err != nil { return nil, fmt.Errorf("failed to parse API URL: %w", err) } - + // Add query parameters q := u.Query() q.Set("current_version", params.CurrentVersion) q.Set("channel", params.Channel) - q.Set("os", "") // Empty for cross-platform - q.Set("arch", "") // Empty for cross-platform - + q.Set("os", "") // Empty for cross-platform + q.Set("arch", "") // Empty for cross-platform + if params.CDK != "" { q.Set("cdk", params.CDK) } u.RawQuery = q.Encode() - + // Create HTTP request req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } - + // Set User-Agent header if params.UserAgent != "" { req.Header.Set("User-Agent", params.UserAgent) } else { - req.Header.Set("User-Agent", "LightweightUpdater/1.0") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36") } - + // Make HTTP request resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to make HTTP request: %w", err) } defer resp.Body.Close() - + // Check HTTP status code if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API returned non-200 status code: %d", resp.StatusCode) } - + // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - + // Parse JSON response var mirrorResp MirrorResponse if err := json.Unmarshal(body, &mirrorResp); err != nil { return nil, fmt.Errorf("failed to parse JSON response: %w", err) } - + return &mirrorResp, nil } @@ -129,13 +129,13 @@ func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) func (c *Client) CheckUpdateLegacy(resourceID, currentVersion, cdk, userAgent string) (*MirrorResponse, error) { // Construct the API URL apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, resourceID) - + // Parse URL to add query parameters u, err := url.Parse(apiURL) if err != nil { return nil, fmt.Errorf("failed to parse API URL: %w", err) } - + // Add query parameters q := u.Query() q.Set("current_version", currentVersion) @@ -143,44 +143,44 @@ func (c *Client) CheckUpdateLegacy(resourceID, currentVersion, cdk, userAgent st q.Set("cdk", cdk) } u.RawQuery = q.Encode() - + // Create HTTP request req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } - + // Set User-Agent header if userAgent != "" { req.Header.Set("User-Agent", userAgent) } else { req.Header.Set("User-Agent", "LightweightUpdater/1.0") } - + // Make HTTP request resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to make HTTP request: %w", err) } defer resp.Body.Close() - + // Check HTTP status code if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API returned non-200 status code: %d", resp.StatusCode) } - + // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - + // Parse JSON response var mirrorResp MirrorResponse if err := json.Unmarshal(body, &mirrorResp); err != nil { return nil, fmt.Errorf("failed to parse JSON response: %w", err) } - + return &mirrorResp, nil } @@ -190,17 +190,17 @@ func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion stri if response.Code != 0 { return false } - + // Get latest version from response latestVersion := response.Data.VersionName if latestVersion == "" { return false } - + // Convert version formats for comparison currentVersionNormalized := c.normalizeVersionForComparison(currentVersion) latestVersionNormalized := c.normalizeVersionForComparison(latestVersion) - + // Compare versions using semantic version comparison return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0 } @@ -219,7 +219,7 @@ func (c *Client) normalizeVersionForComparison(version string) string { } } } - + // Return as-is if already in standard format return version } @@ -230,17 +230,17 @@ func compareVersions(v1, v2 string) int { // Normalize versions by removing 'v' prefix if present v1 = normalizeVersion(v1) v2 = normalizeVersion(v2) - + // Parse version components parts1 := parseVersionParts(v1) parts2 := parseVersionParts(v2) - + // Compare each component maxLen := len(parts1) if len(parts2) > maxLen { maxLen = len(parts2) } - + for i := 0; i < maxLen; i++ { var p1, p2 int if i < len(parts1) { @@ -249,14 +249,14 @@ func compareVersions(v1, v2 string) int { if i < len(parts2) { p2 = parts2[i] } - + if p1 < p2 { return -1 } else if p1 > p2 { return 1 } } - + return 0 } @@ -273,10 +273,10 @@ func parseVersionParts(version string) []int { if version == "" { return []int{0} } - + parts := make([]int, 0, 3) current := 0 - + for _, char := range version { if char >= '0' && char <= '9' { current = current*10 + int(char-'0') @@ -288,15 +288,15 @@ func parseVersionParts(version string) []int { break } } - + // Add the last component parts = append(parts, current) - + // Ensure at least 3 components (major.minor.patch) for len(parts) < 3 { parts = append(parts, 0) } - + return parts } @@ -304,17 +304,17 @@ func parseVersionParts(version string) []int { func (c *Client) GetOfficialDownloadURL(versionName string) string { // Official download site base URL baseURL := "http://221.236.27.82:10197/d/AUTO_MAA" - + // Convert version name to filename format // e.g., "v4.4.0" -> "AUTO_MAA_v4.4.0.zip" // e.g., "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip" filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName) - + // Handle beta versions: convert "beta3" to "beta.3" if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") { filename = strings.Replace(filename, "-beta", "-beta.", 1) } - + return fmt.Sprintf("%s/%s", baseURL, filename) } @@ -329,4 +329,4 @@ func (c *Client) GetDownloadURL(response *MirrorResponse) string { return response.Data.URL } return c.GetOfficialDownloadURL(response.Data.VersionName) -} \ No newline at end of file +} diff --git a/Go_Updater/config/config.go b/Go_Updater/config/config.go new file mode 100644 index 0000000..8b725a8 --- /dev/null +++ b/Go_Updater/config/config.go @@ -0,0 +1,248 @@ +package config + +import ( + "encoding/base64" + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" + "lightweight-updater/assets" +) + +// Config represents the application configuration +type Config struct { + ResourceID string `yaml:"resource_id"` + CurrentVersion string `yaml:"current_version"` + CDK string `yaml:"cdk,omitempty"` + UserAgent string `yaml:"user_agent"` + BackupURL string `yaml:"backup_url"` + LogLevel string `yaml:"log_level"` + AutoCheck bool `yaml:"auto_check"` + CheckInterval int `yaml:"check_interval"` // seconds +} + +// ConfigManager interface defines methods for configuration management +type ConfigManager interface { + Load() (*Config, error) + Save(config *Config) error + GetConfigPath() string +} + +// DefaultConfigManager implements ConfigManager interface +type DefaultConfigManager struct { + configPath string +} + +// NewConfigManager creates a new configuration manager +func NewConfigManager() ConfigManager { + configDir := getConfigDir() + configPath := filepath.Join(configDir, "config.yaml") + return &DefaultConfigManager{ + configPath: configPath, + } +} + +// GetConfigPath returns the path to the configuration file +func (cm *DefaultConfigManager) GetConfigPath() string { + return cm.configPath +} + +// Load reads and parses the configuration file +func (cm *DefaultConfigManager) Load() (*Config, error) { + // Create config directory if it doesn't exist + configDir := filepath.Dir(cm.configPath) + if err := os.MkdirAll(configDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create config directory: %w", err) + } + + // If config file doesn't exist, create default config + if _, err := os.Stat(cm.configPath); os.IsNotExist(err) { + defaultConfig := getDefaultConfig() + if err := cm.Save(defaultConfig); err != nil { + return nil, fmt.Errorf("failed to create default config: %w", err) + } + return defaultConfig, nil + } + + // Read existing config file + data, err := os.ReadFile(cm.configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + // Validate and apply defaults for missing fields + if err := validateAndApplyDefaults(&config); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) + } + + return &config, nil +} + +// Save writes the configuration to file +func (cm *DefaultConfigManager) Save(config *Config) error { + // Validate config before saving + if err := validateConfig(config); err != nil { + return fmt.Errorf("config validation failed: %w", err) + } + + // Create config directory if it doesn't exist + configDir := filepath.Dir(cm.configPath) + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Marshal config to YAML + data, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + // Write to file + if err := os.WriteFile(cm.configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +// getDefaultConfig returns a configuration with default values +func getDefaultConfig() *Config { + // Try to load from embedded template first + if templateData, err := assets.GetConfigTemplate(); err == nil { + var config Config + if err := yaml.Unmarshal(templateData, &config); err == nil { + return &config + } + } + + // Fallback to hardcoded defaults if template loading fails + return &Config{ + ResourceID: "M9A", // Default resource ID + CurrentVersion: "v1.0.0", + CDK: "", + UserAgent: "LightweightUpdater/1.0", + BackupURL: "", + LogLevel: "info", + AutoCheck: true, + CheckInterval: 3600, // 1 hour + } +} + +// validateConfig validates the configuration values +func validateConfig(config *Config) error { + if config == nil { + return fmt.Errorf("config cannot be nil") + } + + if config.ResourceID == "" { + return fmt.Errorf("resource_id cannot be empty") + } + + if config.CurrentVersion == "" { + return fmt.Errorf("current_version cannot be empty") + } + + if config.UserAgent == "" { + return fmt.Errorf("user_agent cannot be empty") + } + + validLogLevels := map[string]bool{ + "debug": true, + "info": true, + "warn": true, + "error": true, + } + if !validLogLevels[config.LogLevel] { + return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", config.LogLevel) + } + + if config.CheckInterval < 60 { + return fmt.Errorf("check_interval must be at least 60 seconds") + } + + return nil +} + +// validateAndApplyDefaults validates config and applies defaults for missing fields +func validateAndApplyDefaults(config *Config) error { + defaults := getDefaultConfig() + + // Apply defaults for empty fields + if config.UserAgent == "" { + config.UserAgent = defaults.UserAgent + } + if config.LogLevel == "" { + config.LogLevel = defaults.LogLevel + } + if config.CheckInterval == 0 { + config.CheckInterval = defaults.CheckInterval + } + if config.CurrentVersion == "" { + config.CurrentVersion = defaults.CurrentVersion + } + + // Validate after applying defaults + return validateConfig(config) +} + +// getConfigDir returns the configuration directory path +func getConfigDir() string { + // Use APPDATA on Windows, fallback to current directory + if appData := os.Getenv("APPDATA"); appData != "" { + return filepath.Join(appData, "LightweightUpdater") + } + return "." +} + +// encryptCDK encrypts the CDK using XOR encryption with a static key +func encryptCDK(cdk string) string { + if cdk == "" { + return "" + } + + key := []byte("updater-key-2024") + encrypted := make([]byte, len(cdk)) + + for i, b := range []byte(cdk) { + encrypted[i] = b ^ key[i%len(key)] + } + + return base64.StdEncoding.EncodeToString(encrypted) +} + +// decryptCDK decrypts the CDK using XOR decryption with a static key +func decryptCDK(encryptedCDK string) (string, error) { + if encryptedCDK == "" { + return "", nil + } + + encrypted, err := base64.StdEncoding.DecodeString(encryptedCDK) + if err != nil { + return "", fmt.Errorf("failed to decode encrypted CDK: %w", err) + } + + key := []byte("updater-key-2024") + decrypted := make([]byte, len(encrypted)) + + for i, b := range encrypted { + decrypted[i] = b ^ key[i%len(key)] + } + + return string(decrypted), nil +} + +// SetCDK sets the CDK in the config with encryption +func (c *Config) SetCDK(cdk string) { + c.CDK = encryptCDK(cdk) +} + +// GetCDK returns the decrypted CDK from the config +func (c *Config) GetCDK() (string, error) { + return decryptCDK(c.CDK) +} diff --git a/Go_Updater/config/config.json b/Go_Updater/config/config.json new file mode 100644 index 0000000..826e4d1 --- /dev/null +++ b/Go_Updater/config/config.json @@ -0,0 +1,56 @@ +{ + "Function": { + "BossKey": "", + "HistoryRetentionTime": 0, + "HomeImageMode": "默认", + "IfAgreeBilibili": true, + "IfAllowSleep": false, + "IfSilence": false, + "IfSkipMumuSplashAds": false, + "UnattendedMode": false + }, + "Notify": { + "AuthorizationCode": "", + "CompanyWebHookBotUrl": "", + "FromAddress": "", + "IfCompanyWebHookBot": false, + "IfPushPlyer": false, + "IfSendMail": false, + "IfSendSixStar": false, + "IfSendStatistic": false, + "IfServerChan": false, + "SMTPServerAddress": "", + "SendTaskResultTime": "不推送", + "ServerChanChannel": "", + "ServerChanKey": "", + "ServerChanTag": "", + "ToAddress": "" + }, + "Start": { + "IfMinimizeDirectly": false, + "IfRunDirectly": false, + "IfSelfStart": false + }, + "QFluentWidgets": { + "ThemeColor": "#ff009faa", + "ThemeMode": "Dark" + }, + "UI": { + "IfShowTray": false, + "IfToTray": false, + "location": "100x100", + "maximized": false, + "size": "1200x700" + }, + "Update": { + "IfAutoUpdate": false, + "MirrorChyanCDK": "", + "ProxyUrlList": [], + "ThreadNumb": 8, + "UpdateType": "stable" + }, + "Voice": { + "Enabled": false, + "Type": "simple" + } +} \ No newline at end of file diff --git a/Go_Updater/config/config_test.go b/Go_Updater/config/config_test.go new file mode 100644 index 0000000..a66d561 --- /dev/null +++ b/Go_Updater/config/config_test.go @@ -0,0 +1,472 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEncryptDecryptCDK(t *testing.T) { + tests := []struct { + name string + original string + }{ + { + name: "Empty CDK", + original: "", + }, + { + name: "Simple CDK", + original: "test123", + }, + { + name: "Complex CDK", + original: "ABC123-DEF456-GHI789", + }, + { + name: "CDK with special characters", + original: "test@#$%^&*()_+-={}[]|\\:;\"'<>?,./", + }, + { + name: "Long CDK", + original: "this-is-a-very-long-cdk-key-that-should-still-work-properly-with-encryption-and-decryption", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test encryption + encrypted := encryptCDK(tt.original) + + // Empty string should remain empty + if tt.original == "" { + if encrypted != "" { + t.Errorf("Expected empty string for empty input, got %s", encrypted) + } + return + } + + // Encrypted should be different from original (unless original is empty) + if encrypted == tt.original { + t.Errorf("Encrypted CDK should be different from original") + } + + // Test decryption + decrypted, err := decryptCDK(encrypted) + if err != nil { + t.Errorf("Decryption failed: %v", err) + } + + // Decrypted should match original + if decrypted != tt.original { + t.Errorf("Expected %s, got %s", tt.original, decrypted) + } + }) + } +} + +func TestConfigSetGetCDK(t *testing.T) { + config := &Config{} + + testCDK := "test-cdk-123" + + // Set CDK (should encrypt) + config.SetCDK(testCDK) + + // CDK field should be encrypted (different from original) + if config.CDK == testCDK { + t.Errorf("CDK should be encrypted in config") + } + + // Get CDK (should decrypt) + retrievedCDK, err := config.GetCDK() + if err != nil { + t.Errorf("Failed to get CDK: %v", err) + } + + if retrievedCDK != testCDK { + t.Errorf("Expected %s, got %s", testCDK, retrievedCDK) + } +} + +func TestDecryptInvalidCDK(t *testing.T) { + // Test with invalid base64 + _, err := decryptCDK("invalid-base64!") + if err == nil { + t.Errorf("Expected error for invalid base64") + } +} + +func TestConfigManagerLoadSave(t *testing.T) { + // Create temporary directory for test + tempDir := t.TempDir() + + // Create config manager with temp path + cm := &DefaultConfigManager{ + configPath: filepath.Join(tempDir, "test-config.yaml"), + } + + // Test loading non-existent config (should create default) + config, err := cm.Load() + if err != nil { + t.Errorf("Failed to load config: %v", err) + } + + if config == nil { + t.Errorf("Config should not be nil") + } + + // Verify default values + if config.CurrentVersion != "v1.0.0" { + t.Errorf("Expected default version v1.0.0, got %s", config.CurrentVersion) + } + + if config.UserAgent != "LightweightUpdater/1.0" { + t.Errorf("Expected default user agent, got %s", config.UserAgent) + } + + // Set some values including CDK + config.ResourceID = "TEST123" + config.SetCDK("secret-cdk-key") + + // Save config + err = cm.Save(config) + if err != nil { + t.Errorf("Failed to save config: %v", err) + } + + // Load config again + loadedConfig, err := cm.Load() + if err != nil { + t.Errorf("Failed to load saved config: %v", err) + } + + // Verify values + if loadedConfig.ResourceID != "TEST123" { + t.Errorf("Expected ResourceID TEST123, got %s", loadedConfig.ResourceID) + } + + // Verify CDK is properly encrypted/decrypted + retrievedCDK, err := loadedConfig.GetCDK() + if err != nil { + t.Errorf("Failed to get CDK from loaded config: %v", err) + } + + if retrievedCDK != "secret-cdk-key" { + t.Errorf("Expected CDK secret-cdk-key, got %s", retrievedCDK) + } + + // Verify CDK is encrypted in the config struct + if loadedConfig.CDK == "secret-cdk-key" { + t.Errorf("CDK should be encrypted in config file") + } +} + +func TestConfigValidation(t *testing.T) { + tests := []struct { + name string + config *Config + expectError bool + }{ + { + name: "Nil config", + config: nil, + expectError: true, + }, + { + name: "Empty ResourceID", + config: &Config{ + ResourceID: "", + CurrentVersion: "v1.0.0", + UserAgent: "Test/1.0", + LogLevel: "info", + CheckInterval: 3600, + }, + expectError: true, + }, + { + name: "Empty CurrentVersion", + config: &Config{ + ResourceID: "TEST", + CurrentVersion: "", + UserAgent: "Test/1.0", + LogLevel: "info", + CheckInterval: 3600, + }, + expectError: true, + }, + { + name: "Invalid LogLevel", + config: &Config{ + ResourceID: "TEST", + CurrentVersion: "v1.0.0", + UserAgent: "Test/1.0", + LogLevel: "invalid", + CheckInterval: 3600, + }, + expectError: true, + }, + { + name: "Invalid CheckInterval", + config: &Config{ + ResourceID: "TEST", + CurrentVersion: "v1.0.0", + UserAgent: "Test/1.0", + LogLevel: "info", + CheckInterval: 30, // Less than 60 + }, + expectError: true, + }, + { + name: "Valid config", + config: &Config{ + ResourceID: "TEST", + CurrentVersion: "v1.0.0", + UserAgent: "Test/1.0", + LogLevel: "info", + CheckInterval: 3600, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateConfig(tt.config) + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +func TestGetConfigDir(t *testing.T) { + // Save original APPDATA + originalAppData := os.Getenv("APPDATA") + defer os.Setenv("APPDATA", originalAppData) + + // Test with APPDATA set + os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming") + dir := getConfigDir() + expected := "C:\\Users\\Test\\AppData\\Roaming\\LightweightUpdater" + if dir != expected { + t.Errorf("Expected %s, got %s", expected, dir) + } + + // Test without APPDATA + os.Unsetenv("APPDATA") + dir = getConfigDir() + if dir != "." { + t.Errorf("Expected current directory, got %s", dir) + } +} + +func TestValidateAndApplyDefaults(t *testing.T) { + tests := []struct { + name string + input *Config + expected *Config + hasError bool + }{ + { + name: "Apply defaults to empty config", + input: &Config{ + ResourceID: "TEST", + }, + expected: &Config{ + ResourceID: "TEST", + CurrentVersion: "v1.0.0", + UserAgent: "LightweightUpdater/1.0", + LogLevel: "info", + CheckInterval: 3600, + }, + hasError: false, + }, + { + name: "Partial config with some defaults needed", + input: &Config{ + ResourceID: "TEST", + CurrentVersion: "v2.0.0", + LogLevel: "debug", + }, + expected: &Config{ + ResourceID: "TEST", + CurrentVersion: "v2.0.0", + UserAgent: "LightweightUpdater/1.0", + LogLevel: "debug", + CheckInterval: 3600, + }, + hasError: false, + }, + { + name: "Config with invalid values after defaults", + input: &Config{ + ResourceID: "", // Invalid - empty + CheckInterval: 30, // Invalid - too small + }, + expected: nil, + hasError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateAndApplyDefaults(tt.input) + + if tt.hasError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // Check that defaults were applied correctly + if tt.input.CurrentVersion != tt.expected.CurrentVersion { + t.Errorf("CurrentVersion: expected %s, got %s", tt.expected.CurrentVersion, tt.input.CurrentVersion) + } + if tt.input.UserAgent != tt.expected.UserAgent { + t.Errorf("UserAgent: expected %s, got %s", tt.expected.UserAgent, tt.input.UserAgent) + } + if tt.input.LogLevel != tt.expected.LogLevel { + t.Errorf("LogLevel: expected %s, got %s", tt.expected.LogLevel, tt.input.LogLevel) + } + if tt.input.CheckInterval != tt.expected.CheckInterval { + t.Errorf("CheckInterval: expected %d, got %d", tt.expected.CheckInterval, tt.input.CheckInterval) + } + }) + } +} + +func TestGetDefaultConfig(t *testing.T) { + config := getDefaultConfig() + + if config == nil { + t.Fatal("getDefaultConfig() returned nil") + } + + // Verify default values + if config.ResourceID != "PLACEHOLDER" { + t.Errorf("Expected ResourceID 'PLACEHOLDER', got %s", config.ResourceID) + } + if config.CurrentVersion != "v1.0.0" { + t.Errorf("Expected CurrentVersion 'v1.0.0', got %s", config.CurrentVersion) + } + if config.UserAgent != "LightweightUpdater/1.0" { + t.Errorf("Expected UserAgent 'LightweightUpdater/1.0', got %s", config.UserAgent) + } + if config.LogLevel != "info" { + t.Errorf("Expected LogLevel 'info', got %s", config.LogLevel) + } + if config.CheckInterval != 3600 { + t.Errorf("Expected CheckInterval 3600, got %d", config.CheckInterval) + } + if !config.AutoCheck { + t.Errorf("Expected AutoCheck true, got %v", config.AutoCheck) + } +} + +func TestConfigManagerWithCustomPath(t *testing.T) { + tempDir := t.TempDir() + customPath := filepath.Join(tempDir, "custom-config.yaml") + + cm := &DefaultConfigManager{ + configPath: customPath, + } + + // Test GetConfigPath + if cm.GetConfigPath() != customPath { + t.Errorf("Expected config path %s, got %s", customPath, cm.GetConfigPath()) + } + + // Test Save and Load with custom path + testConfig := &Config{ + ResourceID: "CUSTOM", + CurrentVersion: "v1.5.0", + UserAgent: "CustomUpdater/1.0", + LogLevel: "debug", + CheckInterval: 7200, + AutoCheck: false, + } + + // Save config + err := cm.Save(testConfig) + if err != nil { + t.Fatalf("Failed to save config: %v", err) + } + + // Load config + loadedConfig, err := cm.Load() + if err != nil { + t.Fatalf("Failed to load config: %v", err) + } + + // Verify loaded config matches saved config + if loadedConfig.ResourceID != testConfig.ResourceID { + t.Errorf("ResourceID mismatch: expected %s, got %s", testConfig.ResourceID, loadedConfig.ResourceID) + } + if loadedConfig.CurrentVersion != testConfig.CurrentVersion { + t.Errorf("CurrentVersion mismatch: expected %s, got %s", testConfig.CurrentVersion, loadedConfig.CurrentVersion) + } + if loadedConfig.AutoCheck != testConfig.AutoCheck { + t.Errorf("AutoCheck mismatch: expected %v, got %v", testConfig.AutoCheck, loadedConfig.AutoCheck) + } +} + +func TestConfigManagerErrorHandling(t *testing.T) { + // Test with invalid directory path + invalidPath := string([]byte{0}) + "/invalid/config.yaml" + cm := &DefaultConfigManager{ + configPath: invalidPath, + } + + // Load should fail with invalid path + _, err := cm.Load() + if err == nil { + t.Error("Expected error when loading from invalid path") + } + + // Save should fail with invalid path + testConfig := getDefaultConfig() + testConfig.ResourceID = "TEST" + err = cm.Save(testConfig) + if err == nil { + t.Error("Expected error when saving to invalid path") + } +} + +func TestEncryptDecryptEdgeCases(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"Unicode characters", "测试CDK密钥🔑"}, + {"Very long string", strings.Repeat("A", 1000)}, + {"Binary-like data", string([]byte{0, 1, 2, 3, 255, 254, 253})}, + {"Only spaces", " "}, + {"Newlines and tabs", "line1\nline2\tindented"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypted := encryptCDK(tt.input) + decrypted, err := decryptCDK(encrypted) + + if err != nil { + t.Errorf("Decryption failed: %v", err) + } + + if decrypted != tt.input { + t.Errorf("Encryption/decryption mismatch: expected %q, got %q", tt.input, decrypted) + } + }) + } +}