From 069ed9ca1cd3acac59d37f4c0d836ca5e1882475 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 17:16:17 +0700 Subject: [PATCH] test: Add unit tests for chat storage and environment utilities --- src/pkg/utils/chat_storage_test.go | 155 ++++++++++++++++++++++++ src/pkg/utils/environment_test.go | 185 +++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/pkg/utils/chat_storage_test.go create mode 100644 src/pkg/utils/environment_test.go diff --git a/src/pkg/utils/chat_storage_test.go b/src/pkg/utils/chat_storage_test.go new file mode 100644 index 0000000..73ce445 --- /dev/null +++ b/src/pkg/utils/chat_storage_test.go @@ -0,0 +1,155 @@ +package utils_test + +import ( + "encoding/csv" + "os" + "path/filepath" + "testing" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" + . "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ChatStorageTestSuite struct { + suite.Suite + tempDir string + origStorage bool + origPath string +} + +func (suite *ChatStorageTestSuite) SetupTest() { + // Create a temporary directory for test files + tempDir, err := os.MkdirTemp("", "chat_storage_test") + assert.NoError(suite.T(), err) + suite.tempDir = tempDir + + // Save original config values + suite.origStorage = config.WhatsappChatStorage + suite.origPath = config.PathChatStorage + + // Set test config values + config.WhatsappChatStorage = true + config.PathChatStorage = filepath.Join(tempDir, "chat_storage.csv") +} + +func (suite *ChatStorageTestSuite) TearDownTest() { + // Restore original config values + config.WhatsappChatStorage = suite.origStorage + config.PathChatStorage = suite.origPath + + // Clean up temp directory + os.RemoveAll(suite.tempDir) +} + +func (suite *ChatStorageTestSuite) createTestData() { + // Create test CSV data + file, err := os.Create(config.PathChatStorage) + assert.NoError(suite.T(), err) + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + testData := [][]string{ + {"msg1", "user1@test.com", "Hello world"}, + {"msg2", "user2@test.com", "Test message"}, + } + + err = writer.WriteAll(testData) + assert.NoError(suite.T(), err) +} + +func (suite *ChatStorageTestSuite) TestFindRecordFromStorage() { + // Test case: Record found + suite.createTestData() + record, err := FindRecordFromStorage("msg1") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "msg1", record.MessageID) + assert.Equal(suite.T(), "user1@test.com", record.JID) + assert.Equal(suite.T(), "Hello world", record.MessageContent) + + // Test case: Record not found + _, err = FindRecordFromStorage("non_existent") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "not found in storage") + + // Test case: Empty file - should still report message not found + os.Remove(config.PathChatStorage) + _, err = FindRecordFromStorage("msg1") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "not found in storage") + + // Test case: Corrupted CSV file - should return CSV parsing error + err = os.WriteFile(config.PathChatStorage, []byte("corrupted,csv,data\nwith,no,proper,format"), 0644) + assert.NoError(suite.T(), err) + _, err = FindRecordFromStorage("msg1") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to read CSV records") + + // Test case: File permissions issue + // Create an unreadable directory for testing file permission issues + unreadableDir := filepath.Join(suite.tempDir, "unreadable") + err = os.Mkdir(unreadableDir, 0000) + assert.NoError(suite.T(), err) + defer os.Chmod(unreadableDir, 0755) // So it can be deleted during teardown + + // Temporarily change path to unreadable location + origPath := config.PathChatStorage + config.PathChatStorage = filepath.Join(unreadableDir, "inaccessible.csv") + _, err = FindRecordFromStorage("anything") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to open storage file") + + // Restore path + config.PathChatStorage = origPath +} + +func (suite *ChatStorageTestSuite) TestRecordMessage() { + // Test case: Normal recording + err := RecordMessage("newMsg", "user@test.com", "New test message") + assert.NoError(suite.T(), err) + + // Verify the message was recorded + record, err := FindRecordFromStorage("newMsg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "newMsg", record.MessageID) + assert.Equal(suite.T(), "user@test.com", record.JID) + assert.Equal(suite.T(), "New test message", record.MessageContent) + + // Test case: Duplicate message ID + err = RecordMessage("newMsg", "user@test.com", "Duplicate message") + assert.NoError(suite.T(), err) + + // Verify the duplicate wasn't added + record, err = FindRecordFromStorage("newMsg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "New test message", record.MessageContent, "Should not update existing record") + + // Test case: Disabled storage + config.WhatsappChatStorage = false + err = RecordMessage("anotherMsg", "user@test.com", "Should not be stored") + assert.NoError(suite.T(), err) + + config.WhatsappChatStorage = true // Re-enable for next tests + _, err = FindRecordFromStorage("anotherMsg") + assert.Error(suite.T(), err, "Message should not be found when storage is disabled") + + // Test case: Write permission error - Alternative approach to avoid platform-specific issues + // Instead of creating an unwritable file, we'll temporarily set PathChatStorage to a non-existent directory + nonExistentPath := filepath.Join(suite.tempDir, "non-existent-dir", "test.csv") + origPath := config.PathChatStorage + config.PathChatStorage = nonExistentPath + + err = RecordMessage("failMsg", "user@test.com", "Should fail to write") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to open file for writing") + + // Restore path + config.PathChatStorage = origPath +} + +func TestChatStorageTestSuite(t *testing.T) { + suite.Run(t, new(ChatStorageTestSuite)) +} diff --git a/src/pkg/utils/environment_test.go b/src/pkg/utils/environment_test.go new file mode 100644 index 0000000..fc3c4b4 --- /dev/null +++ b/src/pkg/utils/environment_test.go @@ -0,0 +1,185 @@ +package utils_test + +import ( + "os" + "testing" + "time" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type EnvironmentTestSuite struct { + suite.Suite +} + +func (suite *EnvironmentTestSuite) SetupTest() { + // Clear any existing viper configs + viper.Reset() + // Set up automatic environment variable reading + viper.AutomaticEnv() +} + +func (suite *EnvironmentTestSuite) TearDownTest() { + viper.Reset() +} + +func (suite *EnvironmentTestSuite) TestIsLocal() { + tests := []struct { + name string + envValue string + expected bool + }{ + {"Production environment", "production", false}, + {"Staging environment", "staging", false}, + {"Integration environment", "integration", false}, + {"Development environment", "development", true}, + {"Local environment", "local", true}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + // Set the environment value + if tt.envValue != "" { + viper.Set("APP_ENV", tt.envValue) + } else { + viper.Set("APP_ENV", nil) // Explicitly clear the value + } + + result := utils.IsLocal() + assert.Equal(t, tt.expected, result) + }) + } +} + +func (suite *EnvironmentTestSuite) TestEnv() { + // Test with existing value + viper.Set("TEST_KEY", "test_value") + result := utils.Env[string]("TEST_KEY") + assert.Equal(suite.T(), "test_value", result) + + // Test with default value + result = utils.Env("NON_EXISTENT_KEY", "default_value") + assert.Equal(suite.T(), "default_value", result) + + // Test with integer + viper.Set("TEST_INT", 42) + intResult := utils.Env[int]("TEST_INT") + assert.Equal(suite.T(), 42, intResult) + + // Test with default integer + intResult = utils.Env("NON_EXISTENT_INT", 100) + assert.Equal(suite.T(), 100, intResult) + + // Test with boolean + viper.Set("TEST_BOOL", true) + boolResult := utils.Env[bool]("TEST_BOOL") + assert.Equal(suite.T(), true, boolResult) +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnv() { + // Test with value present + viper.Set("REQUIRED_ENV", "required_value") + result := utils.MustHaveEnv("REQUIRED_ENV") + assert.Equal(suite.T(), "required_value", result) + + // Create a temporary .env file for testing + tempEnvContent := []byte("ENV_FROM_FILE=env_file_value\n") + err := os.WriteFile(".env", tempEnvContent, 0644) + assert.NoError(suite.T(), err) + defer os.Remove(".env") + + // Test reading from .env file + result = utils.MustHaveEnv("ENV_FROM_FILE") + assert.Equal(suite.T(), "env_file_value", result) + + // We can't easily test the fatal log scenario in a unit test + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvBool() { + // Test true value + viper.Set("BOOL_TRUE", "true") + result := utils.MustHaveEnvBool("BOOL_TRUE") + assert.True(suite.T(), result) + + // Test false value + viper.Set("BOOL_FALSE", "false") + result = utils.MustHaveEnvBool("BOOL_FALSE") + assert.False(suite.T(), result) +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvInt() { + // Test valid integer + viper.Set("INT_VALUE", "42") + result := utils.MustHaveEnvInt("INT_VALUE") + assert.Equal(suite.T(), 42, result) + + // Test zero + viper.Set("ZERO_INT", "0") + result = utils.MustHaveEnvInt("ZERO_INT") + assert.Equal(suite.T(), 0, result) + + // Test negative number + viper.Set("NEG_INT", "-10") + result = utils.MustHaveEnvInt("NEG_INT") + assert.Equal(suite.T(), -10, result) + + // We can't easily test the fatal log scenario with invalid int + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvMinuteDuration() { + // Test valid duration + viper.Set("DURATION_MIN", "5") + result := utils.MustHaveEnvMinuteDuration("DURATION_MIN") + assert.Equal(suite.T(), 5*time.Minute, result) + + // Test zero duration + viper.Set("ZERO_DURATION", "0") + result = utils.MustHaveEnvMinuteDuration("ZERO_DURATION") + assert.Equal(suite.T(), 0*time.Minute, result) + + // We can't easily test the fatal log scenario with invalid duration + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestLoadConfig() { + // Create a temporary config file for testing + tempDir, err := os.MkdirTemp("", "config_test") + assert.NoError(suite.T(), err) + defer os.RemoveAll(tempDir) + + // Create test config file + configContent := []byte("TEST_CONFIG=config_value\n") + configPath := tempDir + "/.env" + err = os.WriteFile(configPath, configContent, 0644) + assert.NoError(suite.T(), err) + + // Test loading config with default name + err = utils.LoadConfig(tempDir) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "config_value", viper.GetString("TEST_CONFIG")) + + // Test loading config with custom name + customConfigContent := []byte("CUSTOM_CONFIG=custom_value\n") + customConfigPath := tempDir + "/custom.env" + err = os.WriteFile(customConfigPath, customConfigContent, 0644) + assert.NoError(suite.T(), err) + + viper.Reset() + err = utils.LoadConfig(tempDir, "custom") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "custom_value", viper.GetString("CUSTOM_CONFIG")) + + // Test error case - non-existent directory + viper.Reset() + err = utils.LoadConfig("/non/existent/directory") + assert.Error(suite.T(), err) +} + +func TestEnvironmentTestSuite(t *testing.T) { + suite.Run(t, new(EnvironmentTestSuite)) +}