A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
792 lines
24 KiB
Go
792 lines
24 KiB
Go
package matrix
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ========================================
|
|
// Test Helpers
|
|
// ========================================
|
|
|
|
// createTestServer creates a mock Matrix server for testing
|
|
func createTestServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *MatrixService) {
|
|
server := httptest.NewServer(handler)
|
|
service := NewMatrixService(Config{
|
|
HomeserverURL: server.URL,
|
|
AccessToken: "test-access-token",
|
|
ServerName: "test.local",
|
|
})
|
|
return server, service
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Service Creation
|
|
// ========================================
|
|
|
|
func TestNewMatrixService_ValidConfig_CreatesService(t *testing.T) {
|
|
cfg := Config{
|
|
HomeserverURL: "http://localhost:8008",
|
|
AccessToken: "test-token",
|
|
ServerName: "breakpilot.local",
|
|
}
|
|
|
|
service := NewMatrixService(cfg)
|
|
|
|
if service == nil {
|
|
t.Fatal("Expected service to be created, got nil")
|
|
}
|
|
if service.homeserverURL != cfg.HomeserverURL {
|
|
t.Errorf("Expected homeserverURL %s, got %s", cfg.HomeserverURL, service.homeserverURL)
|
|
}
|
|
if service.accessToken != cfg.AccessToken {
|
|
t.Errorf("Expected accessToken %s, got %s", cfg.AccessToken, service.accessToken)
|
|
}
|
|
if service.serverName != cfg.ServerName {
|
|
t.Errorf("Expected serverName %s, got %s", cfg.ServerName, service.serverName)
|
|
}
|
|
if service.httpClient == nil {
|
|
t.Error("Expected httpClient to be initialized")
|
|
}
|
|
if service.httpClient.Timeout != 30*time.Second {
|
|
t.Errorf("Expected timeout 30s, got %v", service.httpClient.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestGetServerName_ReturnsConfiguredName(t *testing.T) {
|
|
service := NewMatrixService(Config{
|
|
HomeserverURL: "http://localhost:8008",
|
|
AccessToken: "test-token",
|
|
ServerName: "school.example.com",
|
|
})
|
|
|
|
result := service.GetServerName()
|
|
|
|
if result != "school.example.com" {
|
|
t.Errorf("Expected 'school.example.com', got '%s'", result)
|
|
}
|
|
}
|
|
|
|
func TestGenerateUserID_ValidUsername_ReturnsFormattedID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
username string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "simple username",
|
|
serverName: "breakpilot.local",
|
|
username: "max.mustermann",
|
|
expected: "@max.mustermann:breakpilot.local",
|
|
},
|
|
{
|
|
name: "teacher username",
|
|
serverName: "school.de",
|
|
username: "lehrer_mueller",
|
|
expected: "@lehrer_mueller:school.de",
|
|
},
|
|
{
|
|
name: "parent username with numbers",
|
|
serverName: "test.local",
|
|
username: "eltern123",
|
|
expected: "@eltern123:test.local",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
service := NewMatrixService(Config{
|
|
HomeserverURL: "http://localhost:8008",
|
|
AccessToken: "test-token",
|
|
ServerName: tt.serverName,
|
|
})
|
|
|
|
result := service.GenerateUserID(tt.username)
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Health Check
|
|
// ========================================
|
|
|
|
func TestHealthCheck_ServerHealthy_ReturnsNil(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/_matrix/client/versions" {
|
|
t.Errorf("Expected path /_matrix/client/versions, got %s", r.URL.Path)
|
|
}
|
|
if r.Method != "GET" {
|
|
t.Errorf("Expected GET method, got %s", r.Method)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"versions": []string{"v1.1", "v1.2"},
|
|
})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.HealthCheck(context.Background())
|
|
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheck_ServerUnreachable_ReturnsError(t *testing.T) {
|
|
service := NewMatrixService(Config{
|
|
HomeserverURL: "http://localhost:59999", // Non-existent server
|
|
AccessToken: "test-token",
|
|
ServerName: "test.local",
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
err := service.HealthCheck(ctx)
|
|
|
|
if err == nil {
|
|
t.Error("Expected error for unreachable server, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "unreachable") {
|
|
t.Errorf("Expected 'unreachable' in error message, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheck_ServerReturns500_ReturnsError(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.HealthCheck(context.Background())
|
|
|
|
if err == nil {
|
|
t.Error("Expected error for 500 response, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "500") {
|
|
t.Errorf("Expected '500' in error message, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Room Creation
|
|
// ========================================
|
|
|
|
func TestCreateRoom_ValidRequest_ReturnsRoomID(t *testing.T) {
|
|
expectedRoomID := "!abc123:test.local"
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/_matrix/client/v3/createRoom" {
|
|
t.Errorf("Expected path /_matrix/client/v3/createRoom, got %s", r.URL.Path)
|
|
}
|
|
if r.Method != "POST" {
|
|
t.Errorf("Expected POST method, got %s", r.Method)
|
|
}
|
|
|
|
// Verify authorization header
|
|
auth := r.Header.Get("Authorization")
|
|
if auth != "Bearer test-access-token" {
|
|
t.Errorf("Expected 'Bearer test-access-token', got '%s'", auth)
|
|
}
|
|
|
|
// Verify content type
|
|
contentType := r.Header.Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Expected 'application/json', got '%s'", contentType)
|
|
}
|
|
|
|
// Decode and verify request body
|
|
var req CreateRoomRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Fatalf("Failed to decode request body: %v", err)
|
|
}
|
|
|
|
if req.Name != "Test Room" {
|
|
t.Errorf("Expected name 'Test Room', got '%s'", req.Name)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(CreateRoomResponse{
|
|
RoomID: expectedRoomID,
|
|
})
|
|
})
|
|
defer server.Close()
|
|
|
|
req := CreateRoomRequest{
|
|
Name: "Test Room",
|
|
Visibility: "private",
|
|
}
|
|
|
|
result, err := service.CreateRoom(context.Background(), req)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if result.RoomID != expectedRoomID {
|
|
t.Errorf("Expected room ID '%s', got '%s'", expectedRoomID, result.RoomID)
|
|
}
|
|
}
|
|
|
|
func TestCreateRoom_ServerError_ReturnsError(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"errcode": "M_FORBIDDEN",
|
|
"error": "Not allowed to create rooms",
|
|
})
|
|
})
|
|
defer server.Close()
|
|
|
|
req := CreateRoomRequest{Name: "Test"}
|
|
|
|
_, err := service.CreateRoom(context.Background(), req)
|
|
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "M_FORBIDDEN") {
|
|
t.Errorf("Expected 'M_FORBIDDEN' in error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateClassInfoRoom_ValidInput_CreatesRoomWithCorrectPowerLevels(t *testing.T) {
|
|
var receivedRequest CreateRoomRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedRequest)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(CreateRoomResponse{RoomID: "!class:test.local"})
|
|
})
|
|
defer server.Close()
|
|
|
|
teacherIDs := []string{"@lehrer1:test.local", "@lehrer2:test.local"}
|
|
result, err := service.CreateClassInfoRoom(context.Background(), "5a", "Grundschule Musterstadt", teacherIDs)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if result.RoomID != "!class:test.local" {
|
|
t.Errorf("Expected room ID '!class:test.local', got '%s'", result.RoomID)
|
|
}
|
|
|
|
// Verify room name format
|
|
expectedName := "5a - Grundschule Musterstadt (Info)"
|
|
if receivedRequest.Name != expectedName {
|
|
t.Errorf("Expected name '%s', got '%s'", expectedName, receivedRequest.Name)
|
|
}
|
|
|
|
// Verify power levels
|
|
if receivedRequest.PowerLevelContentOverride == nil {
|
|
t.Fatal("Expected power level override, got nil")
|
|
}
|
|
if receivedRequest.PowerLevelContentOverride.EventsDefault != 50 {
|
|
t.Errorf("Expected EventsDefault 50, got %d", receivedRequest.PowerLevelContentOverride.EventsDefault)
|
|
}
|
|
if receivedRequest.PowerLevelContentOverride.UsersDefault != 0 {
|
|
t.Errorf("Expected UsersDefault 0, got %d", receivedRequest.PowerLevelContentOverride.UsersDefault)
|
|
}
|
|
|
|
// Verify teachers have power level 50
|
|
for _, teacherID := range teacherIDs {
|
|
if level, ok := receivedRequest.PowerLevelContentOverride.Users[teacherID]; !ok || level != 50 {
|
|
t.Errorf("Expected teacher %s to have power level 50, got %d", teacherID, level)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateStudentDMRoom_ValidInput_CreatesEncryptedRoom(t *testing.T) {
|
|
var receivedRequest CreateRoomRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedRequest)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(CreateRoomResponse{RoomID: "!dm:test.local"})
|
|
})
|
|
defer server.Close()
|
|
|
|
teacherIDs := []string{"@lehrer:test.local"}
|
|
parentIDs := []string{"@eltern1:test.local", "@eltern2:test.local"}
|
|
|
|
result, err := service.CreateStudentDMRoom(context.Background(), "Max Mustermann", "5a", teacherIDs, parentIDs)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if result.RoomID != "!dm:test.local" {
|
|
t.Errorf("Expected room ID '!dm:test.local', got '%s'", result.RoomID)
|
|
}
|
|
|
|
// Verify room name
|
|
expectedName := "Max Mustermann (5a) - Dialog"
|
|
if receivedRequest.Name != expectedName {
|
|
t.Errorf("Expected name '%s', got '%s'", expectedName, receivedRequest.Name)
|
|
}
|
|
|
|
// Verify encryption is enabled
|
|
foundEncryption := false
|
|
for _, state := range receivedRequest.InitialState {
|
|
if state.Type == "m.room.encryption" {
|
|
foundEncryption = true
|
|
// Content comes as map[string]interface{} from JSON unmarshaling
|
|
content, ok := state.Content.(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("Expected encryption content to be map[string]interface{}, got %T", state.Content)
|
|
continue
|
|
}
|
|
if algo, ok := content["algorithm"].(string); !ok || algo != "m.megolm.v1.aes-sha2" {
|
|
t.Errorf("Expected algorithm 'm.megolm.v1.aes-sha2', got '%v'", content["algorithm"])
|
|
}
|
|
}
|
|
}
|
|
if !foundEncryption {
|
|
t.Error("Expected encryption state event, not found")
|
|
}
|
|
|
|
// Verify all users are invited
|
|
expectedInvites := append(teacherIDs, parentIDs...)
|
|
for _, expected := range expectedInvites {
|
|
found := false
|
|
for _, invited := range receivedRequest.Invite {
|
|
if invited == expected {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected user %s to be invited", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateParentRepRoom_ValidInput_CreatesRoom(t *testing.T) {
|
|
var receivedRequest CreateRoomRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedRequest)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(CreateRoomResponse{RoomID: "!rep:test.local"})
|
|
})
|
|
defer server.Close()
|
|
|
|
teacherIDs := []string{"@lehrer:test.local"}
|
|
repIDs := []string{"@elternvertreter1:test.local", "@elternvertreter2:test.local"}
|
|
|
|
result, err := service.CreateParentRepRoom(context.Background(), "5a", teacherIDs, repIDs)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if result.RoomID != "!rep:test.local" {
|
|
t.Errorf("Expected room ID '!rep:test.local', got '%s'", result.RoomID)
|
|
}
|
|
|
|
// Verify room name
|
|
expectedName := "5a - Elternvertreter"
|
|
if receivedRequest.Name != expectedName {
|
|
t.Errorf("Expected name '%s', got '%s'", expectedName, receivedRequest.Name)
|
|
}
|
|
|
|
// Verify all participants can write (power level 50)
|
|
allUsers := append(teacherIDs, repIDs...)
|
|
for _, userID := range allUsers {
|
|
if level, ok := receivedRequest.PowerLevelContentOverride.Users[userID]; !ok || level != 50 {
|
|
t.Errorf("Expected user %s to have power level 50, got %d", userID, level)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: User Management
|
|
// ========================================
|
|
|
|
func TestSetDisplayName_ValidRequest_Succeeds(t *testing.T) {
|
|
var receivedPath string
|
|
var receivedBody map[string]string
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
receivedPath = r.URL.Path
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SetDisplayName(context.Background(), "@user:test.local", "Max Mustermann")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
// Path may or may not be URL-encoded depending on Go version
|
|
if !strings.Contains(receivedPath, "/profile/") || !strings.Contains(receivedPath, "/displayname") {
|
|
t.Errorf("Expected path to contain '/profile/' and '/displayname', got '%s'", receivedPath)
|
|
}
|
|
|
|
if receivedBody["displayname"] != "Max Mustermann" {
|
|
t.Errorf("Expected displayname 'Max Mustermann', got '%s'", receivedBody["displayname"])
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Room Membership
|
|
// ========================================
|
|
|
|
func TestInviteUser_ValidRequest_Succeeds(t *testing.T) {
|
|
var receivedBody InviteRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.URL.Path, "/invite") {
|
|
t.Errorf("Expected path to contain '/invite', got '%s'", r.URL.Path)
|
|
}
|
|
if r.Method != "POST" {
|
|
t.Errorf("Expected POST method, got %s", r.Method)
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.InviteUser(context.Background(), "!room:test.local", "@user:test.local")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if receivedBody.UserID != "@user:test.local" {
|
|
t.Errorf("Expected user_id '@user:test.local', got '%s'", receivedBody.UserID)
|
|
}
|
|
}
|
|
|
|
func TestInviteUser_UserAlreadyInRoom_ReturnsError(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"errcode": "M_FORBIDDEN",
|
|
"error": "User is already in the room",
|
|
})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.InviteUser(context.Background(), "!room:test.local", "@user:test.local")
|
|
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJoinRoom_ValidRequest_Succeeds(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.URL.Path, "/join/") {
|
|
t.Errorf("Expected path to contain '/join/', got '%s'", r.URL.Path)
|
|
}
|
|
if r.Method != "POST" {
|
|
t.Errorf("Expected POST method, got %s", r.Method)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"room_id": "!room:test.local"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.JoinRoom(context.Background(), "!room:test.local")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Messaging
|
|
// ========================================
|
|
|
|
func TestSendMessage_ValidRequest_Succeeds(t *testing.T) {
|
|
var receivedBody SendMessageRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.URL.Path, "/send/m.room.message/") {
|
|
t.Errorf("Expected path to contain '/send/m.room.message/', got '%s'", r.URL.Path)
|
|
}
|
|
if r.Method != "PUT" {
|
|
t.Errorf("Expected PUT method, got %s", r.Method)
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"event_id": "$event123"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SendMessage(context.Background(), "!room:test.local", "Hello, World!")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if receivedBody.MsgType != "m.text" {
|
|
t.Errorf("Expected msgtype 'm.text', got '%s'", receivedBody.MsgType)
|
|
}
|
|
if receivedBody.Body != "Hello, World!" {
|
|
t.Errorf("Expected body 'Hello, World!', got '%s'", receivedBody.Body)
|
|
}
|
|
}
|
|
|
|
func TestSendHTMLMessage_ValidRequest_IncludesFormattedBody(t *testing.T) {
|
|
var receivedBody SendMessageRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"event_id": "$event123"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SendHTMLMessage(context.Background(), "!room:test.local", "Plain text", "<b>Bold text</b>")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if receivedBody.Format != "org.matrix.custom.html" {
|
|
t.Errorf("Expected format 'org.matrix.custom.html', got '%s'", receivedBody.Format)
|
|
}
|
|
if receivedBody.Body != "Plain text" {
|
|
t.Errorf("Expected body 'Plain text', got '%s'", receivedBody.Body)
|
|
}
|
|
if receivedBody.FormattedBody != "<b>Bold text</b>" {
|
|
t.Errorf("Expected formatted_body '<b>Bold text</b>', got '%s'", receivedBody.FormattedBody)
|
|
}
|
|
}
|
|
|
|
func TestSendAbsenceNotification_ValidRequest_FormatsCorrectly(t *testing.T) {
|
|
var receivedBody SendMessageRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"event_id": "$event123"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SendAbsenceNotification(context.Background(), "!room:test.local", "Max Mustermann", "15.12.2025", 3)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
// Verify plain text contains key information
|
|
if !strings.Contains(receivedBody.Body, "Max Mustermann") {
|
|
t.Error("Expected body to contain student name")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "15.12.2025") {
|
|
t.Error("Expected body to contain date")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "3. Stunde") {
|
|
t.Error("Expected body to contain lesson number")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "Abwesenheitsmeldung") {
|
|
t.Error("Expected body to contain 'Abwesenheitsmeldung'")
|
|
}
|
|
|
|
// Verify HTML is set
|
|
if receivedBody.FormattedBody == "" {
|
|
t.Error("Expected formatted body to be set")
|
|
}
|
|
}
|
|
|
|
func TestSendGradeNotification_ValidRequest_FormatsCorrectly(t *testing.T) {
|
|
var receivedBody SendMessageRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"event_id": "$event123"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SendGradeNotification(context.Background(), "!room:test.local", "Max Mustermann", "Mathematik", "Klassenarbeit", 2.3)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
if !strings.Contains(receivedBody.Body, "Max Mustermann") {
|
|
t.Error("Expected body to contain student name")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "Mathematik") {
|
|
t.Error("Expected body to contain subject")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "Klassenarbeit") {
|
|
t.Error("Expected body to contain grade type")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "2.3") {
|
|
t.Error("Expected body to contain grade")
|
|
}
|
|
}
|
|
|
|
func TestSendClassAnnouncement_ValidRequest_FormatsCorrectly(t *testing.T) {
|
|
var receivedBody SendMessageRequest
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]string{"event_id": "$event123"})
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SendClassAnnouncement(context.Background(), "!room:test.local", "Elternabend", "Am 20.12. findet der Elternabend statt.", "Frau Müller")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
|
|
if !strings.Contains(receivedBody.Body, "Elternabend") {
|
|
t.Error("Expected body to contain title")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "20.12.") {
|
|
t.Error("Expected body to contain content")
|
|
}
|
|
if !strings.Contains(receivedBody.Body, "Frau Müller") {
|
|
t.Error("Expected body to contain teacher name")
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Power Levels
|
|
// ========================================
|
|
|
|
func TestSetUserPowerLevel_ValidRequest_UpdatesPowerLevel(t *testing.T) {
|
|
callCount := 0
|
|
var putBody PowerLevels
|
|
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
if r.Method == "GET" {
|
|
// Return current power levels
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(PowerLevels{
|
|
Users: map[string]int{
|
|
"@admin:test.local": 100,
|
|
},
|
|
UsersDefault: 0,
|
|
})
|
|
} else if r.Method == "PUT" {
|
|
// Update power levels
|
|
json.NewDecoder(r.Body).Decode(&putBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
})
|
|
defer server.Close()
|
|
|
|
err := service.SetUserPowerLevel(context.Background(), "!room:test.local", "@newuser:test.local", 50)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got %v", err)
|
|
}
|
|
if callCount != 2 {
|
|
t.Errorf("Expected 2 API calls (GET then PUT), got %d", callCount)
|
|
}
|
|
if putBody.Users["@newuser:test.local"] != 50 {
|
|
t.Errorf("Expected user power level 50, got %d", putBody.Users["@newuser:test.local"])
|
|
}
|
|
// Verify existing users are preserved
|
|
if putBody.Users["@admin:test.local"] != 100 {
|
|
t.Errorf("Expected admin power level 100 to be preserved, got %d", putBody.Users["@admin:test.local"])
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Error Handling
|
|
// ========================================
|
|
|
|
func TestParseError_MatrixError_ExtractsFields(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"errcode": "M_UNKNOWN",
|
|
"error": "Something went wrong",
|
|
})
|
|
})
|
|
defer server.Close()
|
|
|
|
_, err := service.CreateRoom(context.Background(), CreateRoomRequest{Name: "Test"})
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "M_UNKNOWN") {
|
|
t.Errorf("Expected error to contain 'M_UNKNOWN', got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "Something went wrong") {
|
|
t.Errorf("Expected error to contain 'Something went wrong', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseError_NonJSONError_ReturnsRawBody(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("Internal Server Error"))
|
|
})
|
|
defer server.Close()
|
|
|
|
_, err := service.CreateRoom(context.Background(), CreateRoomRequest{Name: "Test"})
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "500") {
|
|
t.Errorf("Expected error to contain '500', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Unit Tests: Context Handling
|
|
// ========================================
|
|
|
|
func TestCreateRoom_ContextCanceled_ReturnsError(t *testing.T) {
|
|
server, service := createTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
defer server.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
_, err := service.CreateRoom(ctx, CreateRoomRequest{Name: "Test"})
|
|
|
|
if err == nil {
|
|
t.Error("Expected error for canceled context, got nil")
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Integration Tests (require running Synapse)
|
|
// ========================================
|
|
|
|
// These tests are skipped by default as they require a running Matrix server
|
|
// Run with: go test -tags=integration ./...
|
|
|
|
func TestIntegration_HealthCheck(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
service := NewMatrixService(Config{
|
|
HomeserverURL: "http://localhost:8008",
|
|
AccessToken: "", // Not needed for health check
|
|
ServerName: "breakpilot.local",
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err := service.HealthCheck(ctx)
|
|
if err != nil {
|
|
t.Skipf("Matrix server not available: %v", err)
|
|
}
|
|
}
|