summaryrefslogtreecommitdiff
path: root/kauth/backend_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'kauth/backend_test.go')
-rw-r--r--kauth/backend_test.go252
1 files changed, 252 insertions, 0 deletions
diff --git a/kauth/backend_test.go b/kauth/backend_test.go
new file mode 100644
index 0000000..fb18374
--- /dev/null
+++ b/kauth/backend_test.go
@@ -0,0 +1,252 @@
+package kauth
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+ "github.com/stretchr/testify/assert"
+)
+
+// Test structures
+type testUser struct {
+ id kcore.ID
+ password string
+}
+
+func (u testUser) ID() kcore.ID {
+ return u.id
+}
+
+type store struct {
+ users map[string]testUser
+}
+
+func (s *store) Authenticate(ctx context.Context, username, password string) (testUser, bool) {
+ if u, ok := s.users[username]; ok {
+ if password == u.password {
+ return u, true
+ }
+ }
+ return testUser{}, false
+}
+
+func (s *store) LoadUser(ctx context.Context, id kcore.ID) (testUser, error) {
+ for _, u := range s.users {
+ if u.id == id {
+ return u, nil
+ }
+ }
+ return testUser{}, errors.New("user not found")
+}
+
+func newUser(password string) testUser {
+ return testUser{id: kcore.NewID(), password: password}
+}
+
+func newBackend() (*Backend[testUser], *store) {
+ s := store{users: map[string]testUser{}}
+ return &Backend[testUser]{
+ UserStore: &s,
+ Domain: "localhost",
+ CookieSecret: GenerateCookieSecret(),
+ Now: time.Now,
+ }, &s
+}
+
+// Helper to extract authentication cookie value from a response recorder
+func getAuthCookie(rr *httptest.ResponseRecorder) *http.Cookie {
+ for _, c := range rr.Result().Cookies() {
+ if c.Name == "authentication" {
+ return c
+ }
+ }
+ return nil
+}
+
+// Test Backend.Login sets cookie on success
+func TestBackend_Login_Success(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+
+ rr := httptest.NewRecorder()
+ ctx := context.Background()
+ _, err := backend.Login(rr, ctx, "user", "pass")
+ assert.NoError(t, err)
+
+ c := getAuthCookie(rr)
+ assert.NotNil(t, c, "authentication cookie should be set")
+ assert.Equal(t, backend.Domain, c.Domain)
+ assert.Equal(t, "/", c.Path)
+ assert.True(t, time.Until(c.Expires) >= 23*time.Hour && time.Until(c.Expires) <= 25*time.Hour, "expected expires about 24h from now")
+}
+
+// Test Backend.Login returns error on invalid credentials
+func TestBackend_Login_InvalidCredentials(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+
+ rr := httptest.NewRecorder()
+ ctx := context.Background()
+ _, err := backend.Login(rr, ctx, "baduser", "badpass")
+ assert.Equal(t, ErrInvalidCredentials, err)
+
+ c := getAuthCookie(rr)
+ assert.Nil(t, c, "did not expect authentication cookie to be set")
+}
+
+// Test Backend.Logout deletes the authentication cookie
+func TestBackend_Logout(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+
+ rr := httptest.NewRecorder()
+ backend.Logout(rr)
+
+ c := getAuthCookie(rr)
+ assert.NotNil(t, c, "authentication cookie should be set for deletion")
+ assert.Equal(t, "", c.Value)
+ assert.Equal(t, "/", c.Path)
+ assert.Equal(t, backend.Domain, c.Domain)
+ assert.Equal(t, int(-1), c.MaxAge)
+ assert.True(t, c.Expires.Equal(time.Unix(0, 0)), "Expires should be Unix epoch")
+ assert.True(t, c.HttpOnly)
+ assert.True(t, c.Secure)
+}
+
+func TestCookieAuthMiddleware_ValidCookie_SetsUserInContext(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+
+ rrLogin := httptest.NewRecorder()
+ ctx := context.Background()
+ _, _ = backend.Login(rrLogin, ctx, "user", "pass")
+ cookie := getAuthCookie(rrLogin)
+ req := httptest.NewRequest("GET", "/", nil)
+ req.AddCookie(cookie)
+
+ var userInHandler testUser
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ u, ok := backend.User(r.Context())
+ assert.True(t, ok, "user should be in context")
+ userInHandler = u
+ w.WriteHeader(http.StatusOK)
+ }))
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.Equal(t, user.id, userInHandler.id)
+ assert.Equal(t, http.StatusOK, rr.Code)
+}
+
+func TestCookieAuthMiddleware_NoCookie_PassesThrough(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+ req := httptest.NewRequest("GET", "/", nil)
+
+ called := false
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ called = true
+ w.WriteHeader(http.StatusOK)
+ }))
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.True(t, called, "handler should be called")
+ assert.Equal(t, http.StatusOK, rr.Code)
+}
+
+func TestCookieAuthMiddleware_InvalidCookie_DecryptionFails(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+ req := httptest.NewRequest("GET", "/", nil)
+ req.AddCookie(&http.Cookie{Name: "authentication", Value: "not-encrypted"})
+
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Error("handler should not be called on decryption failure")
+ }))
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.Equal(t, http.StatusBadRequest, rr.Code)
+}
+
+func TestCookieAuthMiddleware_BadFormatCookie(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+ // Encrypt a string that doesn't have the expected "id:expires" format
+ badValue := encrypt(backend.CookieSecret, "badformat")
+ req := httptest.NewRequest("GET", "/", nil)
+ req.AddCookie(&http.Cookie{Name: "authentication", Value: badValue})
+
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Error("handler should not be called on bad format")
+ }))
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.Equal(t, http.StatusBadRequest, rr.Code)
+}
+
+func Must[T any](val T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+ return val
+}
+
+func TestCookieAuthMiddleware_ExpiredCookie(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+
+ backend.Now = func() time.Time { return Must(time.Parse(time.RFC3339, "2025-12-23T12:00:00Z")) }
+ rrLogin := httptest.NewRecorder()
+ ctx := context.Background()
+ _, _ = backend.Login(rrLogin, ctx, "user", "pass")
+ cookie := getAuthCookie(rrLogin)
+ assert.NotNil(t, cookie, "authentication cookie should be set")
+ req := httptest.NewRequest("GET", "/", nil)
+ req.AddCookie(cookie)
+
+ var userSet bool
+ backend.Now = func() time.Time { return Must(time.Parse(time.RFC3339, "2025-12-24T13:00:00Z")) } // after expiry
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ _, ok := backend.User(r.Context())
+ userSet = ok
+ w.WriteHeader(http.StatusUnauthorized)
+ }))
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.False(t, userSet, "user should not be set in context for expired cookie")
+ assert.Equal(t, http.StatusUnauthorized, rr.Code)
+}
+
+func TestCookieAuthMiddleware_InvalidUserID(t *testing.T) {
+ user := newUser("pass")
+ backend, store := newBackend()
+ store.users["user"] = user
+ rrLogin := httptest.NewRecorder()
+ ctx := context.Background()
+ _, _ = backend.Login(rrLogin, ctx, "user", "pass")
+ cookie := getAuthCookie(rrLogin)
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.AddCookie(cookie)
+ delete(store.users, "user") // User is no longer valid
+
+ handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Error("handler should not be called on invalid user id")
+ }))
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+}