From 202622f2c40aef1734ebcae2df1c635e25f28edb Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Thu, 31 Jul 2025 12:17:24 +0200 Subject: [PATCH] chore: retrieved base logic files from matrix bridge --- server/logger.go | 41 +++ server/store/kvstore/constants.go | 79 +++++ server/store/kvstore/kvstore.go | 7 + server/store/kvstore/startertemplate.go | 50 ++- server/testhelpers_test.go | 424 ++++++++++++++++++++++++ 5 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 server/logger.go create mode 100644 server/store/kvstore/constants.go create mode 100644 server/testhelpers_test.go diff --git a/server/logger.go b/server/logger.go new file mode 100644 index 0000000..56265b1 --- /dev/null +++ b/server/logger.go @@ -0,0 +1,41 @@ +package main + +import "github.com/mattermost/mattermost/server/public/plugin" + +// Logger interface for logging operations +type Logger interface { + LogDebug(message string, keyValuePairs ...any) + LogInfo(message string, keyValuePairs ...any) + LogWarn(message string, keyValuePairs ...any) + LogError(message string, keyValuePairs ...any) +} + +// PluginAPILogger adapts the plugin.API to implement the Logger interface +type PluginAPILogger struct { + api plugin.API +} + +// NewPluginAPILogger creates a new PluginAPILogger +func NewPluginAPILogger(api plugin.API) Logger { + return &PluginAPILogger{api: api} +} + +// LogDebug logs a debug message +func (l *PluginAPILogger) LogDebug(message string, keyValuePairs ...any) { + l.api.LogDebug(message, keyValuePairs...) +} + +// LogInfo logs an info message +func (l *PluginAPILogger) LogInfo(message string, keyValuePairs ...any) { + l.api.LogInfo(message, keyValuePairs...) +} + +// LogWarn logs a warning message +func (l *PluginAPILogger) LogWarn(message string, keyValuePairs ...any) { + l.api.LogWarn(message, keyValuePairs...) +} + +// LogError logs an error message +func (l *PluginAPILogger) LogError(message string, keyValuePairs ...any) { + l.api.LogError(message, keyValuePairs...) +} diff --git a/server/store/kvstore/constants.go b/server/store/kvstore/constants.go new file mode 100644 index 0000000..a100e21 --- /dev/null +++ b/server/store/kvstore/constants.go @@ -0,0 +1,79 @@ +package kvstore + +// KV Store key prefixes and constants +// This file centralizes all KV store key patterns used throughout the plugin +// to ensure consistency and avoid key conflicts. + +const ( + // CurrentKVStoreVersion is the current version requiring migrations + CurrentKVStoreVersion = 2 + // KeyPrefixMatrixUser is the prefix for Matrix user ID -> Mattermost user ID mappings + KeyPrefixMatrixUser = "matrix_user_" + // KeyPrefixMattermostUser is the prefix for Mattermost user ID -> Matrix user ID mappings + KeyPrefixMattermostUser = "mattermost_user_" + + // KeyPrefixChannelMapping is the prefix for Mattermost channel ID -> Matrix room mappings + KeyPrefixChannelMapping = "channel_mapping_" + // KeyPrefixRoomMapping is the prefix for Matrix room identifier -> Mattermost channel ID mappings + KeyPrefixRoomMapping = "room_mapping_" + + // KeyPrefixGhostUser is the prefix for Mattermost user ID -> Matrix ghost user ID cache + KeyPrefixGhostUser = "ghost_user_" + // KeyPrefixGhostRoom is the prefix for ghost user room membership tracking + KeyPrefixGhostRoom = "ghost_room_" + + // KeyPrefixMatrixEventPost is the prefix for Matrix event ID -> Mattermost post ID mappings + KeyPrefixMatrixEventPost = "matrix_event_post_" + // KeyPrefixMatrixReaction is the prefix for Matrix reaction event ID -> reaction info mappings + KeyPrefixMatrixReaction = "matrix_reaction_" + + // KeyStoreVersion is the key for tracking the current KV store schema version + KeyStoreVersion = "kv_store_version" + + // KeyPrefixLegacyDMMapping was the old prefix for DM mappings (migrated to channel_mapping_) + KeyPrefixLegacyDMMapping = "dm_mapping_" + // KeyPrefixLegacyMatrixDMMapping was the old prefix for Matrix DM mappings (migrated to room_mapping_) + KeyPrefixLegacyMatrixDMMapping = "matrix_dm_mapping_" +) + +// Helper functions for building KV store keys + +// BuildMatrixUserKey creates a key for Matrix user -> Mattermost user mapping +func BuildMatrixUserKey(matrixUserID string) string { + return KeyPrefixMatrixUser + matrixUserID +} + +// BuildMattermostUserKey creates a key for Mattermost user -> Matrix user mapping +func BuildMattermostUserKey(mattermostUserID string) string { + return KeyPrefixMattermostUser + mattermostUserID +} + +// BuildChannelMappingKey creates a key for channel -> room mapping +func BuildChannelMappingKey(channelID string) string { + return KeyPrefixChannelMapping + channelID +} + +// BuildRoomMappingKey creates a key for room -> channel mapping +func BuildRoomMappingKey(roomIdentifier string) string { + return KeyPrefixRoomMapping + roomIdentifier +} + +// BuildGhostUserKey creates a key for ghost user cache +func BuildGhostUserKey(mattermostUserID string) string { + return KeyPrefixGhostUser + mattermostUserID +} + +// BuildGhostRoomKey creates a key for ghost user room membership +func BuildGhostRoomKey(mattermostUserID, roomID string) string { + return KeyPrefixGhostRoom + mattermostUserID + "_" + roomID +} + +// BuildMatrixEventPostKey creates a key for Matrix event -> post mapping +func BuildMatrixEventPostKey(matrixEventID string) string { + return KeyPrefixMatrixEventPost + matrixEventID +} + +// BuildMatrixReactionKey creates a key for Matrix reaction storage +func BuildMatrixReactionKey(reactionEventID string) string { + return KeyPrefixMatrixReaction + reactionEventID +} diff --git a/server/store/kvstore/kvstore.go b/server/store/kvstore/kvstore.go index f284a2e..6bb5cb2 100644 --- a/server/store/kvstore/kvstore.go +++ b/server/store/kvstore/kvstore.go @@ -1,6 +1,13 @@ +// Package kvstore provides a key-value store interface for plugin data persistence. package kvstore +// KVStore provides an interface for key-value storage operations. type KVStore interface { // Define your methods here. This package is used to access the KVStore pluginapi methods. GetTemplateData(userID string) (string, error) + Get(key string) ([]byte, error) + Set(key string, value []byte) error + Delete(key string) error + ListKeys(page, perPage int) ([]string, error) + ListKeysWithPrefix(page, perPage int, prefix string) ([]string, error) } diff --git a/server/store/kvstore/startertemplate.go b/server/store/kvstore/startertemplate.go index 0c6711b..d328acc 100644 --- a/server/store/kvstore/startertemplate.go +++ b/server/store/kvstore/startertemplate.go @@ -8,17 +8,19 @@ import ( // We expose our calls to the KVStore pluginapi methods through this interface for testability and stability. // This allows us to better control which values are stored with which keys. +// Client wraps the Mattermost plugin API client for KV store operations. type Client struct { client *pluginapi.Client } +// NewKVStore creates a new KVStore implementation using the provided plugin API client. func NewKVStore(client *pluginapi.Client) KVStore { return Client{ client: client, } } -// Sample method to get a key-value pair in the KV store +// GetTemplateData retrieves template data for a specific user from the KV store. func (kv Client) GetTemplateData(userID string) (string, error) { var templateData string err := kv.client.KV.Get("template_key-"+userID, &templateData) @@ -27,3 +29,49 @@ func (kv Client) GetTemplateData(userID string) (string, error) { } return templateData, nil } + +// Get retrieves a value from the KV store by key. +func (kv Client) Get(key string) ([]byte, error) { + var data []byte + err := kv.client.KV.Get(key, &data) + if err != nil { + return nil, errors.Wrap(err, "failed to get key from KV store") + } + return data, nil +} + +// Set stores a key-value pair in the KV store. +func (kv Client) Set(key string, value []byte) error { + _, appErr := kv.client.KV.Set(key, value) + if appErr != nil { + return errors.Wrap(appErr, "failed to set key in KV store") + } + return nil +} + +// Delete removes a key-value pair from the KV store. +func (kv Client) Delete(key string) error { + appErr := kv.client.KV.Delete(key) + if appErr != nil { + return errors.Wrap(appErr, "failed to delete key from KV store") + } + return nil +} + +// ListKeys retrieves a paginated list of keys from the KV store. +func (kv Client) ListKeys(page, perPage int) ([]string, error) { + keys, appErr := kv.client.KV.ListKeys(page, perPage) + if appErr != nil { + return nil, errors.Wrap(appErr, "failed to list keys from KV store") + } + return keys, nil +} + +// ListKeysWithPrefix retrieves a paginated list of keys with a specific prefix from the KV store. +func (kv Client) ListKeysWithPrefix(page, perPage int, prefix string) ([]string, error) { + keys, appErr := kv.client.KV.ListKeys(page, perPage, pluginapi.WithPrefix(prefix)) + if appErr != nil { + return nil, errors.Wrap(appErr, "failed to list keys with prefix from KV store") + } + return keys, nil +} diff --git a/server/testhelpers_test.go b/server/testhelpers_test.go new file mode 100644 index 0000000..f16ecd5 --- /dev/null +++ b/server/testhelpers_test.go @@ -0,0 +1,424 @@ +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) +}