chore: retrieved base logic files from matrix bridge

This commit is contained in:
Felipe M 2025-07-31 12:17:24 +02:00
parent b10a439a29
commit 202622f2c4
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
5 changed files with 600 additions and 1 deletions

41
server/logger.go Normal file
View file

@ -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...)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

424
server/testhelpers_test.go Normal file
View file

@ -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)
}