package main import ( "os" "sort" "strings" "sync" "testing" "time" "github.com/mattermost/mattermost-plugin-matrix-bridge/server/matrix" "github.com/mattermost/mattermost-plugin-matrix-bridge/server/store/kvstore" matrixtest "github.com/mattermost/mattermost-plugin-matrix-bridge/testcontainers/matrix" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/pkg/errors" "github.com/stretchr/testify/mock" ) // testLogger implements Logger interface for testing type testLogger struct { t *testing.T } func (l *testLogger) LogDebug(message string, keyValuePairs ...any) { if l.t != nil { l.t.Logf("[DEBUG] %s %v", message, keyValuePairs) } } func (l *testLogger) LogInfo(message string, keyValuePairs ...any) { if l.t != nil { l.t.Logf("[INFO] %s %v", message, keyValuePairs) } } func (l *testLogger) LogWarn(message string, keyValuePairs ...any) { if l.t != nil { l.t.Logf("[WARN] %s %v", message, keyValuePairs) } } func (l *testLogger) LogError(message string, keyValuePairs ...any) { if l.t != nil { l.t.Logf("[ERROR] %s %v", message, keyValuePairs) } } // TestSetup contains common test setup data for integration tests type TestSetup struct { Plugin *Plugin ChannelID string UserID string RoomID string GhostUserID string API *plugintest.API } // setupPluginForTest creates a basic plugin instance with mock API for unit tests func setupPluginForTest() *Plugin { api := &plugintest.API{} // Allow any logging calls since we're not testing logging behavior api.On("LogDebug", mock.Anything, mock.Anything).Maybe() api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe() api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe() plugin := &Plugin{} plugin.SetAPI(api) plugin.logger = &testLogger{} return plugin } // setupPluginForTestWithLogger creates a plugin instance with test logger that logs to testing.T func setupPluginForTestWithLogger(t *testing.T, api plugin.API) *Plugin { plugin := &Plugin{} plugin.API = api plugin.logger = &testLogger{t: t} return plugin } // setupPluginForTestWithKVStore creates a plugin instance with test logger, API, and KV store func setupPluginForTestWithKVStore(t *testing.T, api plugin.API, kvstore kvstore.KVStore) *Plugin { plugin := &Plugin{} plugin.API = api plugin.kvstore = kvstore plugin.logger = &testLogger{t: t} return plugin } // createMatrixClientWithTestLogger creates a matrix client with test logger for testing func createMatrixClientWithTestLogger(t *testing.T, serverURL, asToken, remoteID string) *matrix.Client { testLogger := matrix.NewTestLogger(t) return matrix.NewClientWithLogger(serverURL, asToken, remoteID, testLogger) } // TestMatrixClientTestLogger verifies that matrix client uses test logger correctly func TestMatrixClientTestLogger(t *testing.T) { // Create a matrix client with test logger client := createMatrixClientWithTestLogger(t, "https://test.example.com", "test_token", "test_remote") // This would trigger logging if the matrix client were to log something // Since we can't easily test actual HTTP calls without a server, this test mainly // verifies that the client is created correctly with a test logger if client == nil { t.Error("Matrix client should not be nil") } // Log success - this confirms the test logger interface is working t.Log("Matrix client created successfully with test logger") } // setupTestPlugin creates a test plugin instance with Matrix container for integration tests func setupTestPlugin(t *testing.T, matrixContainer *matrixtest.MatrixContainer) *TestSetup { api := &plugintest.API{} testChannelID := model.NewId() testUserID := model.NewId() testRoomID := matrixContainer.CreateRoom(t, "Test Room") testGhostUserID := "@_mattermost_" + testUserID + ":" + matrixContainer.ServerDomain plugin := &Plugin{remoteID: "test-remote-id"} plugin.SetAPI(api) // Initialize kvstore with in-memory implementation for testing plugin.kvstore = NewMemoryKVStore() // Initialize required plugin components plugin.pendingFiles = NewPendingFileTracker() plugin.postTracker = NewPostTracker(DefaultPostTrackerMaxEntries) plugin.matrixClient = createMatrixClientWithTestLogger( t, matrixContainer.ServerURL, matrixContainer.ASToken, plugin.remoteID, ) // Set explicit server domain for testing plugin.matrixClient.SetServerDomain(matrixContainer.ServerDomain) config := &configuration{ MatrixServerURL: matrixContainer.ServerURL, MatrixASToken: matrixContainer.ASToken, MatrixHSToken: matrixContainer.HSToken, } plugin.configuration = config // Set up basic mocks setupBasicMocks(api, testUserID) // Set up test data in KV store setupTestKVData(plugin.kvstore, testChannelID, testRoomID) // Initialize the logger with test implementation plugin.logger = &testLogger{t: t} // Initialize bridges for testing plugin.initBridges() return &TestSetup{ Plugin: plugin, ChannelID: testChannelID, UserID: testUserID, RoomID: testRoomID, GhostUserID: testGhostUserID, API: api, } } // setupBasicMocks sets up common API mocks for integration tests func setupBasicMocks(api *plugintest.API, testUserID string) { // Basic user mock testUser := &model.User{ Id: testUserID, Username: "testuser", Email: "test@example.com", Nickname: "Test User", } api.On("GetUser", testUserID).Return(testUser, nil) api.On("GetUser", mock.AnythingOfType("string")).Return(&model.User{Id: "default", Username: "default"}, nil) // Mock profile image for ghost user creation api.On("GetProfileImage", testUserID).Return([]byte("fake-image-data"), nil) // Post update mock - return the updated post with current timestamp api.On("UpdatePost", mock.AnythingOfType("*model.Post")).Return(func(post *model.Post) *model.Post { // Simulate what Mattermost does - update the UpdateAt timestamp updatedPost := post.Clone() // Copy the post updatedPost.UpdateAt = time.Now().UnixMilli() return updatedPost }, nil) // Logging mocks - handle variable argument types api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() api.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() } // setupTestKVData sets up initial test data in the KV store func setupTestKVData(kvstore kvstore.KVStore, testChannelID, testRoomID string) { // Set up channel mapping _ = kvstore.Set("channel_mapping_"+testChannelID, []byte(testRoomID)) // Ghost users and ghost rooms are intentionally not set up here // to trigger creation during tests, which validates the creation logic } // setupMentionMocks sets up mocks for testing user mentions func setupMentionMocks(api *plugintest.API, userID, username string) { user := &model.User{Id: userID, Username: username, Email: username + "@example.com"} api.On("GetUserByUsername", username).Return(user, nil) // Mock profile image for ghost user creation api.On("GetProfileImage", userID).Return([]byte("fake-image-data"), nil) } // clearMockExpectations clears all previous mock expectations for reuse in subtests func clearMockExpectations(api *plugintest.API) { api.ExpectedCalls = nil } // Helper function to compare file attachment arrays (moved from sync_to_matrix_test.go) func compareFileAttachmentArrays(currentFiles, newFiles []matrix.FileAttachment) bool { if len(currentFiles) != len(newFiles) { return false } for i, newFile := range newFiles { if i >= len(currentFiles) { return false } currentFile := currentFiles[i] if currentFile.Filename != newFile.Filename || currentFile.MxcURI != newFile.MxcURI || currentFile.MimeType != newFile.MimeType || currentFile.Size != newFile.Size { return false } } return true } // MemoryKVStore provides an in-memory implementation of the KVStore interface for testing. type MemoryKVStore struct { data map[string][]byte mu sync.RWMutex } // NewMemoryKVStore creates a new in-memory KV store for testing. func NewMemoryKVStore() kvstore.KVStore { return &MemoryKVStore{ data: make(map[string][]byte), } } // GetTemplateData retrieves template data for a specific user from the KV store. func (m *MemoryKVStore) GetTemplateData(userID string) (string, error) { m.mu.RLock() defer m.mu.RUnlock() key := "template_key-" + userID if data, exists := m.data[key]; exists { return string(data), nil } return "", errors.New("key not found") } // Get retrieves a value from the KV store by key. func (m *MemoryKVStore) Get(key string) ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() if data, exists := m.data[key]; exists { // Return a copy to prevent external modification result := make([]byte, len(data)) copy(result, data) return result, nil } return nil, errors.New("key not found") } // Set stores a key-value pair in the KV store. func (m *MemoryKVStore) Set(key string, value []byte) error { m.mu.Lock() defer m.mu.Unlock() // Store a copy to prevent external modification data := make([]byte, len(value)) copy(data, value) m.data[key] = data return nil } // Delete removes a key-value pair from the KV store. func (m *MemoryKVStore) Delete(key string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.data, key) return nil } // ListKeys retrieves a paginated list of keys from the KV store. func (m *MemoryKVStore) ListKeys(page, perPage int) ([]string, error) { m.mu.RLock() defer m.mu.RUnlock() // Collect all keys keys := make([]string, 0, len(m.data)) for key := range m.data { keys = append(keys, key) } // Sort keys for consistent ordering sort.Strings(keys) // Apply pagination start := page * perPage if start >= len(keys) { return []string{}, nil } end := start + perPage if end > len(keys) { end = len(keys) } return keys[start:end], nil } // ListKeysWithPrefix retrieves a paginated list of keys with a specific prefix from the KV store. func (m *MemoryKVStore) ListKeysWithPrefix(page, perPage int, prefix string) ([]string, error) { m.mu.RLock() defer m.mu.RUnlock() // Collect keys with the specified prefix keys := make([]string, 0, len(m.data)) for key := range m.data { if strings.HasPrefix(key, prefix) { keys = append(keys, key) } } // Sort keys for consistent ordering sort.Strings(keys) // Apply pagination start := page * perPage if start >= len(keys) { return []string{}, nil } end := start + perPage if end > len(keys) { end = len(keys) } return keys[start:end], nil } // Clear removes all data from the store (useful for test cleanup). func (m *MemoryKVStore) Clear() { m.mu.Lock() defer m.mu.Unlock() m.data = make(map[string][]byte) } // Size returns the number of key-value pairs in the store. func (m *MemoryKVStore) Size() int { m.mu.RLock() defer m.mu.RUnlock() return len(m.data) } // TestMemoryKVStore tests the in-memory KV store implementation func TestMemoryKVStore(t *testing.T) { store := NewMemoryKVStore() // Test Set and Get err := store.Set("test-key", []byte("test-value")) if err != nil { t.Errorf("Expected no error, got %v", err) } value, err := store.Get("test-key") if err != nil { t.Errorf("Expected no error, got %v", err) } if string(value) != "test-value" { t.Errorf("Expected 'test-value', got '%s'", string(value)) } // Test Get non-existent key _, err = store.Get("non-existent") if err == nil { t.Error("Expected error for non-existent key") } // Test Delete err = store.Delete("test-key") if err != nil { t.Errorf("Expected no error, got %v", err) } _, err = store.Get("test-key") if err == nil { t.Error("Expected error for deleted key") } } // TestMain provides global test setup and cleanup func TestMain(m *testing.M) { // Run tests code := m.Run() // Ensure all Matrix containers are cleaned up matrixtest.CleanupAllContainers() os.Exit(code) }