package httpapi import ( "net/http" "net/http/httptest" "sync" "testing" "time" ) func TestExtractBearerToken(testInstance *testing.T) { validHeaders := map[string]string{ "Bearer abc123": "abc123", "bearer abc123": "abc123", "BEARER abc123": "abc123", "Bearer abc123": "abc123", } for headerValue, expectedToken := range validHeaders { testRequest := httptest.NewRequest(http.MethodGet, "/api/v1/users", nil) testRequest.Header.Set("Authorization", headerValue) extractedToken, extractError := extractBearerToken(testRequest) if extractError != nil { testInstance.Errorf("der Header %q wurde abgelehnt: %v", headerValue, extractError) continue } if extractedToken != expectedToken { testInstance.Errorf("der Header %q lieferte %q statt %q", headerValue, extractedToken, expectedToken) } } } func TestExtractBearerTokenRejectsInvalidHeaders(testInstance *testing.T) { invalidHeaders := map[string]string{ "leer": "", "nur Schema": "Bearer", "leeres Token": "Bearer ", "falsches Schema": "Basic abc123", "ohne Schema": "abc123", } for headerDescription, headerValue := range invalidHeaders { testRequest := httptest.NewRequest(http.MethodGet, "/api/v1/users", nil) if headerValue != "" { testRequest.Header.Set("Authorization", headerValue) } // Ein unbrauchbarer Header darf niemals als gültiger Nachweis durchgehen. if _, extractError := extractBearerToken(testRequest); extractError == nil { testInstance.Errorf("%s: der Header %q wurde akzeptiert", headerDescription, headerValue) } } } func TestClientIPAddressIgnoresForwardedHeaders(testInstance *testing.T) { testRequest := httptest.NewRequest(http.MethodGet, "/api/v1/users", nil) testRequest.RemoteAddr = "192.0.2.10:54321" // Diese Header sind frei fälschbar. Würden sie ausgewertet, könnte ein // Angreifer die Herkunft im Auditprotokoll beliebig verfälschen. testRequest.Header.Set("X-Forwarded-For", "10.0.0.1") testRequest.Header.Set("X-Real-IP", "10.0.0.2") determinedAddress := clientIPAddress(testRequest) if determinedAddress != "192.0.2.10" { testInstance.Fatalf("es soll die echte Absenderadresse verwendet werden, war %q", determinedAddress) } } func TestRateLimiterAllowsUpToLimit(testInstance *testing.T) { testLimiter := NewRateLimiter(3, time.Minute) for attemptIndex := 1; attemptIndex <= 3; attemptIndex++ { if !testLimiter.Allow("192.0.2.1") { testInstance.Fatalf("Versuch %d soll erlaubt sein", attemptIndex) } } // Der vierte Versuch überschreitet das Limit. if testLimiter.Allow("192.0.2.1") { testInstance.Fatal("der vierte Versuch soll abgelehnt werden") } } func TestRateLimiterSeparatesSenders(testInstance *testing.T) { testLimiter := NewRateLimiter(2, time.Minute) testLimiter.Allow("192.0.2.1") testLimiter.Allow("192.0.2.1") // Ein anderer Absender darf durch fremde Versuche nicht ausgesperrt werden. if !testLimiter.Allow("192.0.2.2") { testInstance.Fatal("ein anderer Absender soll unabhängig gezählt werden") } } func TestRateLimiterForgetsOldAttempts(testInstance *testing.T) { // Ein sehr kurzes Fenster macht das Ablaufen im Test beobachtbar. testLimiter := NewRateLimiter(1, 50*time.Millisecond) if !testLimiter.Allow("192.0.2.1") { testInstance.Fatal("der erste Versuch soll erlaubt sein") } if testLimiter.Allow("192.0.2.1") { testInstance.Fatal("der zweite Versuch soll im selben Fenster abgelehnt werden") } time.Sleep(60 * time.Millisecond) if !testLimiter.Allow("192.0.2.1") { testInstance.Fatal("nach Ablauf des Fensters soll wieder ein Versuch erlaubt sein") } } func TestRateLimiterCleanupRemovesStaleEntries(testInstance *testing.T) { testLimiter := NewRateLimiter(5, 50*time.Millisecond) testLimiter.Allow("192.0.2.1") testLimiter.Allow("192.0.2.2") time.Sleep(60 * time.Millisecond) testLimiter.Cleanup() testLimiter.mutex.Lock() remainingEntries := len(testLimiter.attemptsByKey) testLimiter.mutex.Unlock() // Ohne Bereinigung wüchse die Karte mit jeder neuen Absenderadresse. if remainingEntries != 0 { testInstance.Fatalf("die Bereinigung soll abgelaufene Einträge entfernen, es blieben %d", remainingEntries) } } func TestRateLimiterIsSafeForConcurrentUse(testInstance *testing.T) { testLimiter := NewRateLimiter(1000, time.Minute) // Der Begrenzer wird aus vielen Requests gleichzeitig aufgerufen. var waitGroup sync.WaitGroup for goroutineIndex := 0; goroutineIndex < 50; goroutineIndex++ { waitGroup.Add(1) go func() { defer waitGroup.Done() for callIndex := 0; callIndex < 20; callIndex++ { testLimiter.Allow("192.0.2.1") } }() } waitGroup.Wait() testLimiter.mutex.Lock() recordedAttempts := len(testLimiter.attemptsByKey["192.0.2.1"]) testLimiter.mutex.Unlock() if recordedAttempts != 1000 { testInstance.Fatalf("es sollen 1000 Versuche gezählt sein, waren %d", recordedAttempts) } } func TestAuthenticatedUserFromContextWithoutUser(testInstance *testing.T) { testRequest := httptest.NewRequest(http.MethodGet, "/api/v1/users", nil) // Ohne Authentifizierung darf kein Benutzer im Context liegen. if _, isPresent := AuthenticatedUserFromContext(testRequest.Context()); isPresent { testInstance.Fatal("ohne Anmeldung darf kein Benutzer gefunden werden") } }