diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/handlers_auth.go | 69 | ||||
| -rw-r--r-- | internal/api/handlers_more_test.go | 149 | ||||
| -rw-r--r-- | internal/api/handlers_test.go | 85 | ||||
| -rw-r--r-- | internal/api/server.go | 5 | ||||
| -rw-r--r-- | internal/repository/mock.go | 1 | ||||
| -rw-r--r-- | internal/repository/repository.go | 5 | ||||
| -rw-r--r-- | internal/service/auth.go | 86 | ||||
| -rw-r--r-- | internal/service/media.go | 2 | ||||
| -rw-r--r-- | internal/service/mock.go | 20 | ||||
| -rw-r--r-- | internal/service/service.go | 12 |
10 files changed, 272 insertions, 162 deletions
diff --git a/internal/api/handlers_auth.go b/internal/api/handlers_auth.go index 96991e6..5bf5f39 100644 --- a/internal/api/handlers_auth.go +++ b/internal/api/handlers_auth.go @@ -1,10 +1,11 @@ package api import ( + "errors" "net/http" "time" - "codeberg.org/snonux/player/internal/model" + "codeberg.org/snonux/player/internal/service" ) type bootstrapRequest struct { @@ -22,6 +23,9 @@ type loginRequest struct { // ------------------------------------------------------------------ func (s *Server) handleBootstrap(w http.ResponseWriter, r *http.Request) { + if !requireService(w, s.authSvc) { + return + } var req bootstrapRequest if err := readJSON(r, &req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) @@ -32,41 +36,24 @@ func (s *Server) handleBootstrap(w http.ResponseWriter, r *http.Request) { return } - ctx := r.Context() - count, err := s.store.CountUsers(ctx) + res, err := s.authSvc.Bootstrap(r.Context(), req.Username, req.Password) if err != nil { + if errors.Is(err, service.ErrAlreadyBootstrapped) { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "bootstrap already complete"}) + return + } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) return } - if count > 0 { - writeJSON(w, http.StatusForbidden, map[string]string{"error": "bootstrap already complete"}) - return - } - hash, err := s.hasher.Hash(req.Password) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) - return - } - - user := &model.User{Username: req.Username, PasswordHash: hash, IsAdmin: true, CreatedAt: time.Now()} - id, err := s.store.CreateUser(ctx, user) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) - return - } - user.ID = id - - sessID, err := s.sm.CreateSession(ctx, id) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) - return - } - s.setSessionCookie(w, sessID) - writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin}) + s.setSessionCookie(w, res.SessionID) + writeJSON(w, http.StatusOK, map[string]interface{}{"id": res.User.ID, "username": res.User.Username, "is_admin": res.User.IsAdmin}) } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { + if !requireService(w, s.authSvc) { + return + } var req loginRequest if err := readJSON(r, &req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) @@ -77,28 +64,18 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { return } - ctx := r.Context() - user, err := s.store.GetUserByUsername(ctx, req.Username) - if err != nil { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) - return - } - if user == nil { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) - return - } - if err := s.hasher.Compare(user.PasswordHash, req.Password); err != nil { - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) - return - } - - sessID, err := s.sm.CreateSession(ctx, user.ID) + res, err := s.authSvc.Login(r.Context(), req.Username, req.Password) if err != nil { + if errors.Is(err, service.ErrInvalidCredentials) { + writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + return + } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"}) return } - s.setSessionCookie(w, sessID) - writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin}) + + s.setSessionCookie(w, res.SessionID) + writeJSON(w, http.StatusOK, map[string]interface{}{"id": res.User.ID, "username": res.User.Username, "is_admin": res.User.IsAdmin}) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/handlers_more_test.go b/internal/api/handlers_more_test.go index 61838cb..a4fd3c4 100644 --- a/internal/api/handlers_more_test.go +++ b/internal/api/handlers_more_test.go @@ -89,7 +89,7 @@ func TestNewGracefulServer(t *testing.T) { func TestPingStore_nonPinger(t *testing.T) { store := &repository.MockStore{} - srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil, nil) if err := srv.pingStore(context.Background()); err != nil { t.Fatal("expected nil for non-pinger") } @@ -97,7 +97,7 @@ func TestPingStore_nonPinger(t *testing.T) { func TestPingStore_pingerError(t *testing.T) { store := &mockPingStore{err: errors.New("down")} - srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil, nil) if err := srv.pingStore(context.Background()); err == nil { t.Fatal("expected error") } @@ -159,7 +159,7 @@ func TestServer_ServeFile_success(t *testing.T) { }, } sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) - srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) @@ -179,7 +179,7 @@ func TestServer_ServeFile_notFound(t *testing.T) { }, } sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) - srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, fs) + srv := newTestServer(t, store, nil, sm, &internal.Config{SessionTimeoutHours: 24}, nil, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) @@ -219,14 +219,13 @@ func TestServer_Bootstrap_negativePaths(t *testing.T) { } func TestServer_Bootstrap_hashError(t *testing.T) { - store := &repository.MockStore{ - UserRepo: repository.MockUserRepo{ - CountUsersFunc: func(ctx context.Context) (int, error) { return 0, nil }, + cfg := &internal.Config{SessionTimeoutHours: 24} + authSvc := &service.MockAuthService{ + BootstrapFunc: func(ctx context.Context, username, password string) (*service.AuthResult, error) { + return nil, errors.New("hash err") }, } - cfg := &internal.Config{SessionTimeoutHours: 24} - hasher := &errHasher{} - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, nil, nil, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"u","password":"p"}` req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -238,14 +237,13 @@ func TestServer_Bootstrap_hashError(t *testing.T) { } func TestServer_Bootstrap_createUserError(t *testing.T) { - store := &repository.MockStore{ - UserRepo: repository.MockUserRepo{ - CountUsersFunc: func(ctx context.Context) (int, error) { return 0, nil }, - CreateUserFunc: func(ctx context.Context, user *model.User) (int64, error) { return 0, errors.New("boom") }, + cfg := &internal.Config{SessionTimeoutHours: 24} + authSvc := &service.MockAuthService{ + BootstrapFunc: func(ctx context.Context, username, password string) (*service.AuthResult, error) { + return nil, errors.New("boom") }, } - cfg := &internal.Config{SessionTimeoutHours: 24} - srv := newTestServer(t, store, &staticHasher{fixed: "h"}, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, nil, nil, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"u","password":"p"}` req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -257,18 +255,13 @@ func TestServer_Bootstrap_createUserError(t *testing.T) { } func TestServer_Bootstrap_createSessionError(t *testing.T) { - store := &repository.MockStore{ - UserRepo: repository.MockUserRepo{ - CountUsersFunc: func(ctx context.Context) (int, error) { return 0, nil }, - CreateUserFunc: func(ctx context.Context, user *model.User) (int64, error) { return 1, nil }, + cfg := &internal.Config{SessionTimeoutHours: 24} + authSvc := &service.MockAuthService{ + BootstrapFunc: func(ctx context.Context, username, password string) (*service.AuthResult, error) { + return nil, errors.New("boom") }, } - repo := repository.MockSessionRepo{ - CreateSessionFunc: func(ctx context.Context, session *model.Session) error { return errors.New("boom") }, - } - sm := auth.NewSessionManager(&repo, &clock.MockClock{T: time.Now()}, time.Hour) - cfg := &internal.Config{SessionTimeoutHours: 24} - srv := newTestServer(t, store, &staticHasher{fixed: "h"}, sm, cfg, nil, nil, nil, nil) + srv := newTestServer(t, nil, nil, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"u","password":"p"}` req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -284,12 +277,11 @@ func TestServer_Bootstrap_createSessionError(t *testing.T) { // ------------------------------------------------------------------ func TestServer_Login_negativePaths(t *testing.T) { - hasher := &staticHasher{fixed: "hashed"} cfg := &internal.Config{SessionTimeoutHours: 24} t.Run("invalid json", func(t *testing.T) { - store := buildSessionStore(1) - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + authSvc := &service.MockAuthService{} + srv := newTestServer(t, nil, nil, nil, cfg, nil, nil, nil, authSvc, nil) req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(`bad`))) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() @@ -300,18 +292,19 @@ func TestServer_Login_negativePaths(t *testing.T) { }) t.Run("db error", func(t *testing.T) { - store := buildSessionStore(1) - store.UserRepo.GetUserByUsernameFunc = func(ctx context.Context, username string) (*model.User, error) { - return nil, errors.New("boom") + authSvc := &service.MockAuthService{ + LoginFunc: func(ctx context.Context, username, password string) (*service.AuthResult, error) { + return nil, errors.New("boom") + }, } - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, nil, nil, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"alice","password":"correct"}` req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) - if rr.Code != http.StatusUnauthorized { - t.Fatalf("expected %d, got %d", http.StatusUnauthorized, rr.Code) + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected %d, got %d", http.StatusInternalServerError, rr.Code) } }) } @@ -350,7 +343,7 @@ func TestServer_SetCover(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/sets/"+tt.id+"/cover", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -368,7 +361,7 @@ func TestServer_ListSets_negative(t *testing.T) { cfg := &internal.Config{SessionTimeoutHours: 24} t.Run("nil service", func(t *testing.T) { - srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/sets", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -384,7 +377,7 @@ func TestServer_ListSets_negative(t *testing.T) { return nil, errors.New("boom") }, } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/sets", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -434,7 +427,7 @@ func TestServer_Upload(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) var req *http.Request if tt.noFile { var buf bytes.Buffer @@ -471,7 +464,7 @@ func TestServer_MediaDetail_nilService(t *testing.T) { store := buildSessionStore(1) sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) cfg := &internal.Config{SessionTimeoutHours: 24} - srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/media/1", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -508,7 +501,7 @@ func TestServer_Favorite_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/"+tt.id+"/favorite", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -524,7 +517,7 @@ func TestServer_AddTag_nilService(t *testing.T) { store := buildSessionStore(1) sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour) cfg := &internal.Config{SessionTimeoutHours: 24} - srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/1/tags", strings.NewReader(`{"tag":"x"}`)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") @@ -563,7 +556,7 @@ func TestServer_RemoveTag_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/media/%s/tags/%s", tt.id, tt.tag), nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -611,7 +604,7 @@ func TestServer_Stream(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/media/"+tt.id+"/stream", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -656,7 +649,7 @@ func TestServer_Download(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/media/"+tt.id+"/download", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -706,7 +699,7 @@ func TestServer_Thumbnail(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/media/"+tt.id+"/thumbnail", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -746,7 +739,7 @@ func TestServer_RegenThumbnail(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/"+tt.id+"/thumbnail", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -781,7 +774,7 @@ func TestServer_RegenThumbnail_errorMapping(t *testing.T) { return tt.svcErr }, } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/1/thumbnail", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -824,7 +817,7 @@ func TestServer_CreateShare_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/"+tt.id+"/shares", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -863,7 +856,7 @@ func TestServer_ListShares_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/media/"+tt.id+"/shares", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -902,7 +895,7 @@ func TestServer_RevokeShare(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/api/shares/"+tt.token, nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -924,7 +917,7 @@ func TestServer_SharePage(t *testing.T) { }) t.Run("nil service", func(t *testing.T) { - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, nil, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, nil, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -939,7 +932,7 @@ func TestServer_SharePage(t *testing.T) { return nil, errors.New("boom") }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -954,7 +947,7 @@ func TestServer_SharePage(t *testing.T) { return nil, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -969,7 +962,7 @@ func TestServer_SharePage(t *testing.T) { return nil, service.ErrShareExpired }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -984,7 +977,7 @@ func TestServer_SharePage(t *testing.T) { return &model.Share{Token: "abc", MediaID: 1}, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1007,7 +1000,7 @@ func TestServer_SharePage(t *testing.T) { return &model.Share{Token: "abc", MediaID: 1}, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) req.Header.Set("Accept", "text/html") rr := httptest.NewRecorder() @@ -1027,7 +1020,7 @@ func TestServer_SharePage(t *testing.T) { return &model.Share{Token: "abc", MediaID: 1}, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, fs) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/s/abc", nil) req.Header.Set("Accept", "application/json") rr := httptest.NewRecorder() @@ -1054,7 +1047,7 @@ func TestServer_ShareStream(t *testing.T) { cfg := &internal.Config{SessionTimeoutHours: 24} t.Run("nil service", func(t *testing.T) { - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1069,7 +1062,7 @@ func TestServer_ShareStream(t *testing.T) { return nil, errors.New("boom") }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1084,7 +1077,7 @@ func TestServer_ShareStream(t *testing.T) { return nil, service.ErrShareNotFound }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1099,7 +1092,7 @@ func TestServer_ShareStream(t *testing.T) { return nil, service.ErrShareExpired }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1114,7 +1107,7 @@ func TestServer_ShareStream(t *testing.T) { return nil, service.ErrMediaNotFound }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1129,7 +1122,7 @@ func TestServer_ShareStream(t *testing.T) { return &service.FileResult{Path: "/nonexistent", FileName: "a.mp4"}, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1144,7 +1137,7 @@ func TestServer_ShareStream(t *testing.T) { return &service.FileResult{Path: path, FileName: "a.mp4"}, nil }, } - srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil) + srv := newTestServer(t, buildSessionStore(1), nil, nil, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/s/abc/stream", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -1186,7 +1179,7 @@ func TestServer_SoftDelete_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/api/media/"+tt.id, nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1226,7 +1219,7 @@ func TestServer_Restore_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/"+tt.id+"/restore", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1272,7 +1265,7 @@ func TestServer_UpsertNote(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/media/"+tt.id+"/notes", strings.NewReader(tt.body)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") @@ -1313,7 +1306,7 @@ func TestServer_DeleteNote(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, ms, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/api/media/"+tt.id+"/notes", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1357,7 +1350,7 @@ func TestServer_Progress_negative(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, nil, ps, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, nil, ps, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/progress", strings.NewReader(tt.body)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") @@ -1398,7 +1391,7 @@ func TestServer_AdminRescan(t *testing.T) { TriggerRescanFunc: func(ctx context.Context) error { return tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/admin/rescan", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1434,7 +1427,7 @@ func TestServer_AdminListTrash(t *testing.T) { ListTrashFunc: func(ctx context.Context) ([]model.Media, error) { return nil, tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/trash", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1470,7 +1463,7 @@ func TestServer_AdminListUsers(t *testing.T) { ListUsersFunc: func(ctx context.Context) ([]model.User, error) { return nil, tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/users", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1511,7 +1504,7 @@ func TestServer_AdminCreateUser(t *testing.T) { }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/admin/users", strings.NewReader(tt.body)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") @@ -1550,7 +1543,7 @@ func TestServer_AdminDeleteUser(t *testing.T) { DeleteUserFunc: func(ctx context.Context, id int64) error { return tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/api/admin/users/"+tt.id, nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1586,7 +1579,7 @@ func TestServer_AdminListPermissions(t *testing.T) { ListPermissionsFunc: func(ctx context.Context) (*service.PermissionsMatrix, error) { return nil, tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/permissions", nil) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) rr := httptest.NewRecorder() @@ -1624,7 +1617,7 @@ func TestServer_AdminGrantPermission(t *testing.T) { GrantPermissionFunc: func(ctx context.Context, setID, userID int64, role model.Role) error { return tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/admin/permissions", strings.NewReader(tt.body)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") @@ -1663,7 +1656,7 @@ func TestServer_AdminRevokePermission(t *testing.T) { RevokePermissionFunc: func(ctx context.Context, setID, userID int64) error { return tt.svcErr }, } } - srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil) + srv := newTestServer(t, store, nil, sm, cfg, nil, as, nil, nil, nil) req := httptest.NewRequest(http.MethodDelete, "/api/admin/permissions", strings.NewReader(tt.body)) req.AddCookie(sessionCookieForStore(t, store, sm, 1)) req.Header.Set("Content-Type", "application/json") diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 42aea48..41215c9 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -35,6 +35,7 @@ func newTestFS(files map[string]string) http.FileSystem { func newTestServer(t *testing.T, store repository.Store, hasher auth.Hasher, sm *auth.SessionManager, cfg *internal.Config, mediaSvc service.MediaService, adminSvc service.AdminService, progressSvc service.ProgressService, + authSvc service.AuthService, fs http.FileSystem, ) *Server { t.Helper() @@ -46,7 +47,7 @@ func newTestServer(t *testing.T, store repository.Store, hasher auth.Hasher, sm "share.html": "share", }) } - return NewServer(store, hasher, sm, cfg, mediaSvc, adminSvc, progressSvc, fs) + return NewServer(store, hasher, sm, cfg, mediaSvc, adminSvc, progressSvc, authSvc, fs) } func addSessionCookie(t *testing.T, store repository.Store, sm *auth.SessionManager, userID int64) *http.Cookie { @@ -246,7 +247,7 @@ func TestServer_StaticPages(t *testing.T) { } t.Run("index requires session", func(t *testing.T) { - srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -256,7 +257,7 @@ func TestServer_StaticPages(t *testing.T) { }) t.Run("login public", func(t *testing.T) { - srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/login.html", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -269,7 +270,7 @@ func TestServer_StaticPages(t *testing.T) { }) t.Run("bootstrap public", func(t *testing.T) { - srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/bootstrap.html", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -280,7 +281,7 @@ func TestServer_StaticPages(t *testing.T) { t.Run("css public", func(t *testing.T) { fs := newTestFS(map[string]string{"css/theme.css": "body{}"}) - srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, fs) + srv := newTestServer(t, store, nil, nil, cfg, nil, nil, nil, nil, fs) req := httptest.NewRequest(http.MethodGet, "/css/theme.css", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -312,6 +313,7 @@ func (h *staticHasher) Compare(hash, password string) error { func TestServer_Bootstrap(t *testing.T) { hasher := &staticHasher{fixed: "hashed"} cfg := &internal.Config{SessionTimeoutHours: 24} + clk := &clock.MockClock{T: time.Now()} t.Run("create first admin", func(t *testing.T) { store := &repository.MockStore{ @@ -323,8 +325,9 @@ func TestServer_Bootstrap(t *testing.T) { repo := repository.MockSessionRepo{ CreateSessionFunc: func(ctx context.Context, session *model.Session) error { return nil }, } - sm := auth.NewSessionManager(&repo, &clock.MockClock{T: time.Now()}, time.Hour) - srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, nil) + sm := auth.NewSessionManager(&repo, clk, time.Hour) + authSvc := service.NewAuthService(store, clk, hasher, sm) + srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"admin","password":"secret"}` req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(body))) @@ -351,7 +354,8 @@ func TestServer_Bootstrap(t *testing.T) { CountUsersFunc: func(ctx context.Context) (int, error) { return 1, nil }, }, } - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + authSvc := service.NewAuthService(store, clk, hasher, nil) + srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"admin","password":"secret"}` req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -364,7 +368,8 @@ func TestServer_Bootstrap(t *testing.T) { t.Run("missing fields", func(t *testing.T) { store := &repository.MockStore{UserRepo: repository.MockUserRepo{CountUsersFunc: func(ctx context.Context) (int, error) { return 0, nil }}} - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + authSvc := service.NewAuthService(store, clk, hasher, nil) + srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, authSvc, nil) req := httptest.NewRequest(http.MethodPost, "/api/bootstrap", bytes.NewReader([]byte(`{"username":""}`))) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -374,7 +379,7 @@ func TestServer_Bootstrap(t *testing.T) { }) t.Run("wrong method", func(t *testing.T) { - srv := newTestServer(t, nil, hasher, nil, cfg, nil, nil, nil, nil) + srv := newTestServer(t, nil, hasher, nil, cfg, nil, nil, nil, nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/bootstrap", nil) rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) @@ -387,6 +392,7 @@ func TestServer_Bootstrap(t *testing.T) { func TestServer_Login(t *testing.T) { hasher := &staticHasher{fixed: "hashed"} cfg := &internal.Config{SessionTimeoutHours: 24} + clk := &clock.MockClock{T: time.Now()} t.Run("valid credentials", func(t *testing.T) { store := &repository.MockStore{ @@ -400,8 +406,9 @@ func TestServer_Login(t *testing.T) { repo := repository.MockSessionRepo{ CreateSessionFunc: func(ctx context.Context, session *model.Session) error { return nil }, } - sm := auth.NewSessionManager(&repo, &clock.MockClock{T: time.Now()}, time.Hour) - srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, nil) + sm := auth.NewSessionManager(&repo, clk, time.Hour) + authSvc := service.NewAuthService(store, clk, hasher, sm) + srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"alice","password":"correct"}` req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -427,7 +434,8 @@ func TestServer_Login(t *testing.T) { }, }, } - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + authSvc := service.NewAuthService(store, clk, hasher, nil) + srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"alice","password":"wrong"}` req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -447,7 +455,8 @@ func TestServer_Login(t *testing.T) { }, }, } - srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, nil) + authSvc := service.NewAuthService(store, clk, hasher, nil) + srv := newTestServer(t, store, hasher, nil, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"nobody","password":"pass"}` req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) rr := httptest.NewRecorder() @@ -471,11 +480,13 @@ func TestServer_SessionCookieSecure(t *testing.T) { repo := repository.MockSessionRepo{ CreateSessionFunc: func(ctx context.Context, session *model.Session) error { return nil }, } - sm := auth.NewSessionManager(&repo, &clock.MockClock{T: time.Now()}, time.Hour) + clk := &clock.MockClock{T: time.Now()} + sm := auth.NewSessionManager(&repo, clk, time.Hour) + authSvc := service.NewAuthService(store, clk, hasher, sm) t.Run("Secure=true by default", func(t *testing.T) { cfg := &internal.Config{SessionTimeoutHours: 24, SecureCookies: true} - srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, nil) + srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, authSvc, nil) body := `{"username":"alice","password":"correct"}` req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") @@ -493,7 +504,7 @@ func TestServer_SessionCook |
