package jitsi import ( "context" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) // ======================================== // Test Helpers // ======================================== func createTestService() *JitsiService { return NewJitsiService(Config{ BaseURL: "http://localhost:8443", AppID: "breakpilot", AppSecret: "test-secret-key", }) } func createTestServiceWithoutAuth() *JitsiService { return NewJitsiService(Config{ BaseURL: "http://localhost:8443", }) } // ======================================== // Unit Tests: Service Creation // ======================================== func TestNewJitsiService_ValidConfig_CreatesService(t *testing.T) { cfg := Config{ BaseURL: "http://localhost:8443", AppID: "test-app", AppSecret: "test-secret", } service := NewJitsiService(cfg) if service == nil { t.Fatal("Expected service to be created, got nil") } if service.baseURL != cfg.BaseURL { t.Errorf("Expected baseURL %s, got %s", cfg.BaseURL, service.baseURL) } if service.appID != cfg.AppID { t.Errorf("Expected appID %s, got %s", cfg.AppID, service.appID) } if service.appSecret != cfg.AppSecret { t.Errorf("Expected appSecret %s, got %s", cfg.AppSecret, service.appSecret) } if service.httpClient == nil { t.Error("Expected httpClient to be initialized") } } func TestNewJitsiService_TrailingSlash_Removed(t *testing.T) { service := NewJitsiService(Config{ BaseURL: "http://localhost:8443/", }) if service.baseURL != "http://localhost:8443" { t.Errorf("Expected trailing slash to be removed, got %s", service.baseURL) } } func TestGetBaseURL_ReturnsConfiguredURL(t *testing.T) { service := createTestService() result := service.GetBaseURL() if result != "http://localhost:8443" { t.Errorf("Expected 'http://localhost:8443', got '%s'", result) } } func TestIsAuthEnabled_WithSecret_ReturnsTrue(t *testing.T) { service := createTestService() if !service.IsAuthEnabled() { t.Error("Expected auth to be enabled when secret is configured") } } func TestIsAuthEnabled_WithoutSecret_ReturnsFalse(t *testing.T) { service := createTestServiceWithoutAuth() if service.IsAuthEnabled() { t.Error("Expected auth to be disabled when secret is not configured") } } // ======================================== // Unit Tests: Room Name Generation // ======================================== func TestSanitizeRoomName_ValidInput_ReturnsCleanName(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "simple name", input: "meeting", expected: "meeting", }, { name: "with spaces", input: "My Meeting Room", expected: "my-meeting-room", }, { name: "german umlauts", input: "Schüler Müller", expected: "schueler-mueller", }, { name: "special characters", input: "Test@#$%Meeting!", expected: "testmeeting", }, { name: "consecutive hyphens", input: "test---meeting", expected: "test-meeting", }, { name: "leading trailing hyphens", input: "-test-meeting-", expected: "test-meeting", }, { name: "eszett", input: "Straße", expected: "strasse", }, { name: "numbers", input: "Klasse 5a", expected: "klasse-5a", }, } service := createTestService() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := service.sanitizeRoomName(tt.input) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } func TestSanitizeRoomName_LongName_Truncated(t *testing.T) { service := createTestService() longName := strings.Repeat("a", 100) result := service.sanitizeRoomName(longName) if len(result) > 50 { t.Errorf("Expected max 50 chars, got %d", len(result)) } } func TestGenerateRoomName_ReturnsUniqueNames(t *testing.T) { service := createTestService() name1 := service.generateRoomName() name2 := service.generateRoomName() if name1 == name2 { t.Error("Expected unique room names") } if !strings.HasPrefix(name1, "breakpilot-") { t.Errorf("Expected prefix 'breakpilot-', got '%s'", name1) } } func TestGenerateTrainingRoomName_IncludesTitle(t *testing.T) { service := createTestService() result := service.generateTrainingRoomName("Go Workshop") if !strings.HasPrefix(result, "go-workshop-") { t.Errorf("Expected to start with 'go-workshop-', got '%s'", result) } } func TestGeneratePassword_ReturnsValidPassword(t *testing.T) { service := createTestService() password := service.generatePassword() if len(password) != 8 { t.Errorf("Expected 8 char password, got %d", len(password)) } } // ======================================== // Unit Tests: Meeting Link Creation // ======================================== func TestCreateMeetingLink_BasicMeeting_ReturnsValidLink(t *testing.T) { service := createTestServiceWithoutAuth() meeting := Meeting{ RoomName: "test-room", DisplayName: "Test User", } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.RoomName != "test-room" { t.Errorf("Expected room name 'test-room', got '%s'", link.RoomName) } if link.URL != "http://localhost:8443/test-room" { t.Errorf("Expected URL 'http://localhost:8443/test-room', got '%s'", link.URL) } if link.JoinURL != "http://localhost:8443/test-room" { t.Errorf("Expected JoinURL 'http://localhost:8443/test-room', got '%s'", link.JoinURL) } } func TestCreateMeetingLink_NoRoomName_GeneratesName(t *testing.T) { service := createTestServiceWithoutAuth() meeting := Meeting{ DisplayName: "Test User", } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.RoomName == "" { t.Error("Expected room name to be generated") } if !strings.HasPrefix(link.RoomName, "breakpilot-") { t.Errorf("Expected generated room name to start with 'breakpilot-', got '%s'", link.RoomName) } } func TestCreateMeetingLink_WithPassword_IncludesPassword(t *testing.T) { service := createTestServiceWithoutAuth() meeting := Meeting{ RoomName: "test-room", Password: "secret123", } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.Password != "secret123" { t.Errorf("Expected password 'secret123', got '%s'", link.Password) } } func TestCreateMeetingLink_WithAuth_IncludesJWT(t *testing.T) { service := createTestService() meeting := Meeting{ RoomName: "test-room", DisplayName: "Test User", Email: "test@example.com", } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.JWT == "" { t.Error("Expected JWT to be generated") } if !strings.Contains(link.JoinURL, "jwt=") { t.Error("Expected JoinURL to contain JWT parameter") } if link.ExpiresAt == nil { t.Error("Expected ExpiresAt to be set") } } func TestCreateMeetingLink_WithConfig_IncludesParams(t *testing.T) { service := createTestServiceWithoutAuth() meeting := Meeting{ RoomName: "test-room", Config: &MeetingConfig{ StartWithAudioMuted: true, StartWithVideoMuted: true, RequireDisplayName: true, }, } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !strings.Contains(link.JoinURL, "startWithAudioMuted=true") { t.Error("Expected JoinURL to contain audio muted config") } if !strings.Contains(link.JoinURL, "startWithVideoMuted=true") { t.Error("Expected JoinURL to contain video muted config") } } func TestCreateMeetingLink_Moderator_SetsModeratorURL(t *testing.T) { service := createTestService() meeting := Meeting{ RoomName: "test-room", DisplayName: "Admin", Moderator: true, } link, err := service.CreateMeetingLink(context.Background(), meeting) if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.ModeratorURL == "" { t.Error("Expected ModeratorURL to be set for moderator") } } // ======================================== // Unit Tests: Specialized Meeting Types // ======================================== func TestCreateTrainingSession_ReturnsOptimizedConfig(t *testing.T) { service := createTestServiceWithoutAuth() link, err := service.CreateTrainingSession( context.Background(), "Go Grundlagen", "Max Trainer", "trainer@example.com", 60, ) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !strings.Contains(link.RoomName, "go-grundlagen") { t.Errorf("Expected room name to contain 'go-grundlagen', got '%s'", link.RoomName) } // Config should have lobby enabled for training if !strings.Contains(link.JoinURL, "enableLobby=true") { t.Error("Expected training to have lobby enabled") } } func TestCreateQuickMeeting_ReturnsSimpleMeeting(t *testing.T) { service := createTestServiceWithoutAuth() link, err := service.CreateQuickMeeting(context.Background(), "Quick User") if err != nil { t.Fatalf("Expected no error, got %v", err) } if link.RoomName == "" { t.Error("Expected room name to be generated") } } func TestCreateParentTeacherMeeting_ReturnsSecureMeeting(t *testing.T) { service := createTestServiceWithoutAuth() scheduledTime := time.Now().Add(24 * time.Hour) link, err := service.CreateParentTeacherMeeting( context.Background(), "Frau Müller", "Herr Schmidt", "Max Mustermann", scheduledTime, ) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !strings.Contains(link.RoomName, "elterngespraech") { t.Errorf("Expected room name to contain 'elterngespraech', got '%s'", link.RoomName) } if link.Password == "" { t.Error("Expected password for parent-teacher meeting") } if !strings.Contains(link.JoinURL, "enableLobby=true") { t.Error("Expected lobby to be enabled") } } func TestCreateClassMeeting_ReturnsMeetingForClass(t *testing.T) { service := createTestServiceWithoutAuth() link, err := service.CreateClassMeeting( context.Background(), "5a", "Herr Lehrer", "Mathematik", ) if err != nil { t.Fatalf("Expected no error, got %v", err) } if !strings.Contains(link.RoomName, "klasse-5a") { t.Errorf("Expected room name to contain 'klasse-5a', got '%s'", link.RoomName) } // Students should be muted by default if !strings.Contains(link.JoinURL, "startWithAudioMuted=true") { t.Error("Expected students to start muted") } } // ======================================== // Unit Tests: JWT Generation // ======================================== func TestGenerateJWT_ValidClaims_ReturnsValidToken(t *testing.T) { service := createTestService() meeting := Meeting{ RoomName: "test-room", DisplayName: "Test User", Email: "test@example.com", Moderator: true, Duration: 60, } token, expiresAt, err := service.generateJWT(meeting, "test-room") if err != nil { t.Fatalf("Expected no error, got %v", err) } if token == "" { t.Error("Expected token to be generated") } if expiresAt == nil { t.Error("Expected expiration time to be set") } // Verify token structure (header.payload.signature) parts := strings.Split(token, ".") if len(parts) != 3 { t.Errorf("Expected 3 JWT parts, got %d", len(parts)) } // Decode and verify payload payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { t.Fatalf("Failed to decode payload: %v", err) } var claims JWTClaims if err := json.Unmarshal(payloadJSON, &claims); err != nil { t.Fatalf("Failed to unmarshal claims: %v", err) } if claims.Room != "test-room" { t.Errorf("Expected room 'test-room', got '%s'", claims.Room) } if !claims.Moderator { t.Error("Expected moderator to be true") } if claims.Context == nil || claims.Context.User == nil { t.Error("Expected user context to be set") } if claims.Context.User.Name != "Test User" { t.Errorf("Expected user name 'Test User', got '%s'", claims.Context.User.Name) } } func TestGenerateJWT_WithFeatures_IncludesFeatures(t *testing.T) { service := createTestService() meeting := Meeting{ RoomName: "test-room", Features: &MeetingFeatures{ Recording: true, Transcription: true, }, } token, _, err := service.generateJWT(meeting, "test-room") if err != nil { t.Fatalf("Expected no error, got %v", err) } parts := strings.Split(token, ".") payloadJSON, _ := base64.RawURLEncoding.DecodeString(parts[1]) var claims JWTClaims json.Unmarshal(payloadJSON, &claims) if claims.Features == nil { t.Error("Expected features to be set") } if claims.Features.Recording != "true" { t.Errorf("Expected recording 'true', got '%s'", claims.Features.Recording) } } func TestGenerateJWT_NoSecret_ReturnsError(t *testing.T) { service := createTestServiceWithoutAuth() meeting := Meeting{RoomName: "test"} _, _, err := service.generateJWT(meeting, "test") if err == nil { t.Error("Expected error when secret is not configured") } } // ======================================== // Unit Tests: Health Check // ======================================== func TestHealthCheck_ServerAvailable_ReturnsNil(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() service := NewJitsiService(Config{BaseURL: server.URL}) err := service.HealthCheck(context.Background()) if err != nil { t.Errorf("Expected no error, got %v", err) } } func TestHealthCheck_ServerError_ReturnsError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() service := NewJitsiService(Config{BaseURL: server.URL}) err := service.HealthCheck(context.Background()) if err == nil { t.Error("Expected error for server error response") } } func TestHealthCheck_ServerUnreachable_ReturnsError(t *testing.T) { service := NewJitsiService(Config{BaseURL: "http://localhost:59999"}) 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") } } // ======================================== // Unit Tests: URL Building // ======================================== func TestBuildEmbedURL_BasicRoom_ReturnsURL(t *testing.T) { service := createTestService() url := service.BuildEmbedURL("test-room", "", nil) if url != "http://localhost:8443/test-room" { t.Errorf("Expected 'http://localhost:8443/test-room', got '%s'", url) } } func TestBuildEmbedURL_WithDisplayName_IncludesParam(t *testing.T) { service := createTestService() url := service.BuildEmbedURL("test-room", "Max Mustermann", nil) if !strings.Contains(url, "displayName=Max") { t.Errorf("Expected URL to contain display name, got '%s'", url) } } func TestBuildEmbedURL_WithConfig_IncludesParams(t *testing.T) { service := createTestService() config := &MeetingConfig{ StartWithAudioMuted: true, StartWithVideoMuted: true, } url := service.BuildEmbedURL("test-room", "", config) if !strings.Contains(url, "startWithAudioMuted=true") { t.Error("Expected URL to contain audio muted config") } if !strings.Contains(url, "startWithVideoMuted=true") { t.Error("Expected URL to contain video muted config") } } func TestBuildIFrameCode_DefaultSize_Returns800x600(t *testing.T) { service := createTestService() code := service.BuildIFrameCode("test-room", 0, 0) if !strings.Contains(code, "width=\"800\"") { t.Error("Expected default width 800") } if !strings.Contains(code, "height=\"600\"") { t.Error("Expected default height 600") } if !strings.Contains(code, "test-room") { t.Error("Expected room name in iframe") } if !strings.Contains(code, "allow=\"camera; microphone") { t.Error("Expected camera/microphone permissions") } } func TestBuildIFrameCode_CustomSize_ReturnsCorrectDimensions(t *testing.T) { service := createTestService() code := service.BuildIFrameCode("test-room", 1920, 1080) if !strings.Contains(code, "width=\"1920\"") { t.Error("Expected width 1920") } if !strings.Contains(code, "height=\"1080\"") { t.Error("Expected height 1080") } } // ======================================== // Unit Tests: Server Info // ======================================== func TestGetServerInfo_ReturnsInfo(t *testing.T) { service := createTestService() info := service.GetServerInfo() if info["base_url"] != "http://localhost:8443" { t.Errorf("Expected base_url, got '%s'", info["base_url"]) } if info["app_id"] != "breakpilot" { t.Errorf("Expected app_id 'breakpilot', got '%s'", info["app_id"]) } if info["auth_enabled"] != "true" { t.Errorf("Expected auth_enabled 'true', got '%s'", info["auth_enabled"]) } } // ======================================== // Unit Tests: Helper Functions // ======================================== func TestBoolToString_True_ReturnsTrue(t *testing.T) { result := boolToString(true) if result != "true" { t.Errorf("Expected 'true', got '%s'", result) } } func TestBoolToString_False_ReturnsFalse(t *testing.T) { result := boolToString(false) if result != "false" { t.Errorf("Expected 'false', got '%s'", result) } }