package authtoken import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) // fakeNotifier zeichnet auf, ob/mit welchem Token benachrichtigt wurde — // ersetzt LogNotifier im Test, damit der Test pruefen kann, OB ein Token // erzeugt wurde, ohne die HTTP-Antwort danach zu unterscheiden // (Akzeptanzkriterium 3: die Antwort selbst bleibt in beiden Faellen gleich). type fakeNotifier struct { calls []string // e-mail-adressen, fuer die notifiziert wurde } func (f *fakeNotifier) NotifyPasswordReset(_ context.Context, email, _ string) error { f.calls = append(f.calls, email) return nil } // Akzeptanzkriterium 3 + Pruefung 1: Antwort ist IDENTISCH, egal ob die // E-Mail-Adresse existiert — nur intern (Notifier-Aufruf) unterscheidet sich. func TestRequestReset_SameResponseRegardlessOfExistence(t *testing.T) { tokens, users, userID, cleanup := setupTest(t) defer cleanup() _ = userID u, err := users.Get(context.Background(), userID) if err != nil { t.Fatalf("benutzer laden: %v", err) } notifier := &fakeNotifier{} h := NewHandler(tokens, users, notifier) // Fall 1: existierende E-Mail. body1, _ := json.Marshal(requestResetRequest{Email: u.Email}) req1 := httptest.NewRequest(http.MethodPost, "/auth/password-reset/request", bytes.NewReader(body1)) rec1 := httptest.NewRecorder() h.RequestReset(rec1, req1) // Fall 2: nicht existierende E-Mail. body2, _ := json.Marshal(requestResetRequest{Email: "gibt-es-nicht@example.com"}) req2 := httptest.NewRequest(http.MethodPost, "/auth/password-reset/request", bytes.NewReader(body2)) rec2 := httptest.NewRecorder() h.RequestReset(rec2, req2) if rec1.Code != rec2.Code { t.Fatalf("status codes unterscheiden sich: %d vs %d — verraet Konto-Existenz", rec1.Code, rec2.Code) } if rec1.Body.String() != rec2.Body.String() { t.Fatalf("antwort-body unterscheidet sich: %q vs %q — verraet Konto-Existenz", rec1.Body.String(), rec2.Body.String()) } // Intern wurde aber nur fuer die existierende Adresse tatsaechlich benachrichtigt. if len(notifier.calls) != 1 || notifier.calls[0] != u.Email { t.Fatalf("erwartet genau 1 benachrichtigung fuer %q, habe %v", u.Email, notifier.calls) } } // Akzeptanzkriterium 3: vollstaendiger Reset-Flow ueber HTTP. func TestCompleteReset_Works(t *testing.T) { tokens, users, userID, cleanup := setupTest(t) defer cleanup() ctx := context.Background() token, err := tokens.Create(ctx, userID, PurposeReset, ResetTokenTTL) if err != nil { t.Fatalf("token erzeugen: %v", err) } h := NewHandler(tokens, users, &fakeNotifier{}) body, _ := json.Marshal(completeResetRequest{Token: token, NewPassword: "neues-sicheres-passwort"}) req := httptest.NewRequest(http.MethodPost, "/auth/password-reset/complete", bytes.NewReader(body)) rec := httptest.NewRecorder() h.CompleteReset(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body: %s", rec.Code, rec.Body.String()) } // Token ist jetzt verbraucht — zweiter Versuch schlaegt fehl. body2, _ := json.Marshal(completeResetRequest{Token: token, NewPassword: "noch-ein-passwort"}) req2 := httptest.NewRequest(http.MethodPost, "/auth/password-reset/complete", bytes.NewReader(body2)) rec2 := httptest.NewRecorder() h.CompleteReset(rec2, req2) if rec2.Code != http.StatusBadRequest { t.Fatalf("wiederverwendung: status = %d, want 400", rec2.Code) } }