mattermost-plugin-bridge-xmpp/server/testhelpers_test.go

424 lines
12 KiB
Go

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