233 lines
8.3 KiB
Go
233 lines
8.3 KiB
Go
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"),
|
|
useCases.NewAuthenticateUserUseCase(userRepo, hasher),
|
|
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{}),
|
|
GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
|
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
|
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
|
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo),
|
|
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
|
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
|
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher),
|
|
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(userRepo),
|
|
ListUsersUseCase: useCases.NewListUsersUseCase(userRepo),
|
|
SendMailUseCase: useCases.NewSendMailUseCase(mailRepo),
|
|
CreateImageUseCase: useCases.NewCreateImageUseCase(imageRepo, newMockImageStorage()),
|
|
ListMediaUseCase: useCases.NewListMediaUseCase(imageRepo, articleRepo, userRepo),
|
|
RenameMediaUseCase: useCases.NewRenameMediaUseCase(imageRepo),
|
|
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(imageRepo, newMockImageStorage()),
|
|
MergeMediaUseCase: useCases.NewMergeMediaUseCase(imageRepo, articleRepo, newMockImageStorage()),
|
|
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")
|
|
}
|
|
}
|