package test import ( "RenewCMS/api" "RenewCMS/api/handlers/auth" validator "RenewCMS/api/pkg/validator" domainUser "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/config" "RenewCMS/internal/infrastructure/hashing" "RenewCMS/internal/infrastructure/mailer" "RenewCMS/internal/infrastructure/storage" "RenewCMS/internal/infrastructure/useCases" "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" ) func newSecureCookieAuthRouter(t *testing.T, isProduction bool) (*chi.Mux, *mockUserRepo) { t.Helper() token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() hasher := hashing.NewBcryptHasher() hashed, err := hasher.Hash("hunter2example") if err != nil { t.Fatalf("failed to hash password: %v", err) } userRepo.users[1] = domainUser.User{ID: 1, Username: "alice", Password: hashed, IsVerified: true} loginHandler := auth.NewLoginHandler( token, useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder), useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), isProduction, ) r := chi.NewRouter() r.Post("/login", loginHandler.Login) return r, userRepo } func performLoginRequest(t *testing.T, router *chi.Mux) *httptest.ResponseRecorder { t.Helper() body, err := json.Marshal(map[string]string{"username": "alice", "password": "hunter2example"}) if err != nil { t.Fatalf("failed to marshal body: %v", err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } func secureAttributeFromResponse(t *testing.T, rec *httptest.ResponseRecorder) bool { t.Helper() values := rec.Result().Header.Values("Set-Cookie") for _, value := range values { cookie, err := http.ParseSetCookie(value) if err != nil { t.Fatalf("failed to parse Set-Cookie header %q: %v", value, err) } if cookie.Name == "jwt" { return cookie.Secure } } t.Fatalf("expected a jwt Set-Cookie header, got %v", values) return false } func TestRuntimeSettingsInjectionSecureCookie(t *testing.T) { validator.InitValidator() outline := []struct { injectedEnvironment string processEnvironment string secureExpected bool }{ {"production", "production", true}, {"production", "development", true}, {"development", "production", false}, {"development", "development", false}, } for _, tc := range outline { t.Run("Secure follows the injected environment "+tc.injectedEnvironment+"/"+tc.processEnvironment, func(t *testing.T) { isProduction := tc.injectedEnvironment == "production" router, _ := newSecureCookieAuthRouter(t, isProduction) t.Setenv("ENVIRONMENT", tc.processEnvironment) rec := performLoginRequest(t, router) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d, body %q", rec.Code, rec.Body.String()) } if got := secureAttributeFromResponse(t, rec); got != tc.secureExpected { t.Errorf("expected Secure=%v, got %v", tc.secureExpected, got) } }) } t.Run("Changing the process environment after construction does not change already-built behaviour", func(t *testing.T) { router, _ := newSecureCookieAuthRouter(t, true) t.Setenv("ENVIRONMENT", "development") rec := performLoginRequest(t, router) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d, body %q", rec.Code, rec.Body.String()) } if got := secureAttributeFromResponse(t, rec); !got { t.Errorf("expected Secure=true, got %v", got) } }) } func newRuntimeSettingsContainer(t *testing.T) *api.UseCases { t.Helper() articleRepo := newMockArticleRepo() userRepo := newMockUserRepo() imageRepo := newMockImageRepo() mailRepo := newMockMailRepo() hasher := hashing.NewBcryptHasher() token := jwtauth.New("HS256", []byte("test-secret"), nil) return &api.UseCases{ Token: token, CreateArticleUseCase: useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()), GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}), ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()), UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()), DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder), GetUserUseCase: useCases.NewGetUserUseCase(userRepo), GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()), CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder), AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(userRepo), ListUsersUseCase: useCases.NewListUsersUseCase(userRepo), SendMailUseCase: useCases.NewSendMailUseCase(mailRepo), CreateImageUseCase: useCases.NewCreateImageUseCase(imageRepo, newMockImageStorage(), discardRecorder), GenerateVariantsOnUploadUseCase: newInertGenerateOnUploadUseCase(), ListMediaUseCase: useCases.NewListMediaUseCase(imageRepo, articleRepo, userRepo, newMockVariantFileRepo(), plainSites(), plainAuthors()), RenameMediaUseCase: useCases.NewRenameMediaUseCase(imageRepo, discardRecorder), DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(imageRepo, newMockImageStorage(), newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()), MergeMediaUseCase: useCases.NewMergeMediaUseCase(imageRepo, articleRepo, newMockImageStorage(), newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()), GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(imageRepo, articleRepo), } } func TestRuntimeSettingsInjectionCors(t *testing.T) { validator.InitValidator() container := newRuntimeSettingsContainer(t) t.Run("CORS allows a request from an injected allowed origin", func(t *testing.T) { settings := config.Settings{CorsAllowedOrigins: []string{"https://cms.example.com"}} router := api.InitRoutes(container, settings) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) req.Header.Set("Origin", "https://cms.example.com") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if got := rec.Result().Header.Get("Access-Control-Allow-Origin"); got != "https://cms.example.com" { t.Errorf("expected Access-Control-Allow-Origin %q, got %q", "https://cms.example.com", got) } }) t.Run("CORS does not allow a request from an origin outside the injected list", func(t *testing.T) { settings := config.Settings{CorsAllowedOrigins: []string{"https://cms.example.com"}} router := api.InitRoutes(container, settings) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) req.Header.Set("Origin", "https://attacker.example.com") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if got := rec.Result().Header.Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("expected no Access-Control-Allow-Origin header, got %q", got) } }) t.Run("An empty injected origins list disables CORS entirely", func(t *testing.T) { settings := config.Settings{CorsAllowedOrigins: nil} router := api.InitRoutes(container, settings) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) req.Header.Set("Origin", "https://cms.example.com") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if got := rec.Result().Header.Get("Access-Control-Allow-Origin"); got != "" { t.Errorf("expected no Access-Control-Allow-Origin header, got %q", got) } }) } func TestRuntimeSettingsInjectionUploadDirectory(t *testing.T) { injectedUploadDir := t.TempDir() store := storage.NewLocalStorage(injectedUploadDir) processUploadDir := t.TempDir() t.Setenv("UPLOAD_DIR", processUploadDir) storedName, err := store.Put("image/png", bytes.NewReader([]byte("fake-image-data"))) if err != nil { t.Fatalf("Put failed: %v", err) } if _, err := os.Stat(filepath.Join(injectedUploadDir, storedName)); err != nil { t.Errorf("expected the file to exist under the injected upload dir: %v", err) } if _, err := os.Stat(filepath.Join(processUploadDir, storedName)); !os.IsNotExist(err) { t.Errorf("expected the file to not exist under the process UPLOAD_DIR") } } func TestRuntimeSettingsInjectionMailer(t *testing.T) { smtp := config.SmtpSettings{ Login: "smtp-login@example.com", Sender: "notify@example.com", Host: "smtp.example.com", Port: 587, Password: "unused-in-test", } repo := mailer.NewMailRepository(smtp) if repo == nil { t.Fatalf("expected a non-nil MailRepository") } }