refac: removed 7 direct and 12 indirect go modules for custom code instead
This commit is contained in:
1 parent
cfcf108488
commit
c68e9a7858
73 files changed
+2455
-428
No files matched your search
@@ -16,9 +16,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
@@ -117,7 +117,7 @@ func newAtomicAcceptFixture(t *testing.T, wrap func(db *gorm.DB, repo invite.Rep
|
||||
handler := auth.NewAcceptInviteHandler(
|
||||
useCases.NewGetInviteUseCase(invites, hasher),
|
||||
useCases.NewAcceptInviteUseCase(users, invites, hasher, discardRecorder),
|
||||
jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
authtoken.New([]byte("test-secret")),
|
||||
true,
|
||||
)
|
||||
router := chi.NewRouter()
|
||||
|
||||
@@ -17,9 +17,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/v3/transform"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,7 +38,7 @@ type acceptInviteFixture struct {
|
||||
mail *mockMailRepo
|
||||
hasher *stubHasher
|
||||
router *chi.Mux
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
}
|
||||
|
||||
func newAcceptInviteFixture(t *testing.T, maxAttempts int) *acceptInviteFixture {
|
||||
@@ -50,7 +49,7 @@ func newAcceptInviteFixture(t *testing.T, maxAttempts int) *acceptInviteFixture
|
||||
invites: newInviteFakeRepo(),
|
||||
mail: newMockMailRepo(),
|
||||
hasher: &stubHasher{},
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
}
|
||||
f.invites.users = f.users
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
@@ -85,15 +84,15 @@ func newAcceptInviteFixture(t *testing.T, maxAttempts int) *acceptInviteFixture
|
||||
r.Post("/invite/accept", acceptHandler.AcceptInvite)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Get("/me", meHandler.Me)
|
||||
})
|
||||
})
|
||||
f.router.Route("/v1/user", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Use(middleware.RequireVerifiedUser(getUser))
|
||||
r.Get("/", listHandler.ListUsers)
|
||||
@@ -258,12 +257,10 @@ func TestAcceptInvitationHttp(t *testing.T) {
|
||||
if lifetime := time.Until(cookie.Expires); lifetime < 23*time.Hour || lifetime > 24*time.Hour+time.Minute {
|
||||
t.Errorf("expected a 24 hour cookie, got %v", lifetime)
|
||||
}
|
||||
decoded, err := f.token.Decode(cookie.Value)
|
||||
claims, err := f.token.Decode(cookie.Value)
|
||||
if err != nil {
|
||||
t.Fatalf("cookie does not decode: %v", err)
|
||||
}
|
||||
claims := map[string]interface{}{}
|
||||
_ = transform.AsMap(decoded, claims)
|
||||
if claims["user_id"] != float64(created.ID) || claims["token_version"] != float64(created.TokenVersion) {
|
||||
t.Errorf("unexpected claims %v", claims)
|
||||
}
|
||||
@@ -414,12 +411,10 @@ func TestAcceptInvitationHttp(t *testing.T) {
|
||||
}
|
||||
created, _ := f.userNamed("lea")
|
||||
cookie := jwtCookieOf(rec)
|
||||
decoded, err := f.token.Decode(cookie.Value)
|
||||
claims, err := f.token.Decode(cookie.Value)
|
||||
if err != nil {
|
||||
t.Fatalf("cookie does not decode: %v", err)
|
||||
}
|
||||
claims := map[string]interface{}{}
|
||||
_ = transform.AsMap(decoded, claims)
|
||||
if claims["user_id"] != float64(created.ID) || created.ID == 7 {
|
||||
t.Errorf("cookie must carry the new user's id, got %v", claims["user_id"])
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newAccountVerificationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newAccountVerificationRouter(userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder),
|
||||
@@ -29,8 +29,8 @@ func newAccountVerificationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Post("/verify", verificationHandler.Verify)
|
||||
})
|
||||
})
|
||||
@@ -51,11 +51,11 @@ func newAccountVerificationTestUserRepo() *mockUserRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func sendVerifyRequest(router *chi.Mux, token *jwtauth.JWTAuth, claims map[string]any, code string) *httptest.ResponseRecorder {
|
||||
func sendVerifyRequest(router *chi.Mux, token *authtoken.Auth, claims map[string]any, code string) *httptest.ResponseRecorder {
|
||||
body, _ := json.Marshal(map[string]string{"code": code})
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/verify", bytes.NewReader(body))
|
||||
if claims != nil {
|
||||
_, tokenString, _ := token.Encode(claims)
|
||||
tokenString, _ := token.Encode(claims)
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -65,7 +65,7 @@ func sendVerifyRequest(router *chi.Mux, token *jwtauth.JWTAuth, claims map[strin
|
||||
|
||||
func TestAccountVerificationHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("A logged-in user verifies their account with the correct emailed code", func(t *testing.T) {
|
||||
repo := newAccountVerificationTestUserRepo()
|
||||
@@ -200,7 +200,7 @@ func TestAccountVerificationHttp(t *testing.T) {
|
||||
router := newAccountVerificationRouter(repo, token)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/verify", bytes.NewReader([]byte("not-json")))
|
||||
_, tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
@@ -224,7 +224,7 @@ func TestAccountVerificationHttp(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A request with no jwt cookie is answered 401 by jwtauth itself", func(t *testing.T) {
|
||||
t.Run("A request with no jwt cookie is answered 401 by the authenticator itself", func(t *testing.T) {
|
||||
repo := newAccountVerificationTestUserRepo()
|
||||
router := newAccountVerificationRouter(repo, token)
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,7 +44,7 @@ type activityHTTPEnv struct {
|
||||
keys *mockAPIKeyRepo
|
||||
articles *mockArticleRepo
|
||||
mail *mockMailRepo
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
generator *hashing.SHA256KeyGenerator
|
||||
router http.Handler
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func newActivityHTTPEnv(t *testing.T, attempts int) *activityHTTPEnv {
|
||||
keys: newMockAPIKeyRepo(),
|
||||
articles: newMockArticleRepo(),
|
||||
mail: newMockMailRepo(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
generator: hashing.NewSHA256KeyGenerator(),
|
||||
}
|
||||
e.users.users[activityOwnerID] = user.User{ID: activityOwnerID, Username: "marta", DisplayName: "Marta Alves", Email: "marta@example.com", Password: "hashed:" + activityPassword, IsVerified: true, Role: user.RoleOwner}
|
||||
@@ -99,7 +99,7 @@ type activityRequest func(*http.Request)
|
||||
|
||||
func asUser(e *activityHTTPEnv, id uint32) activityRequest {
|
||||
return func(r *http.Request) {
|
||||
_, tokenString, _ := e.token.Encode(map[string]any{"user_id": id, "token_version": 0})
|
||||
tokenString, _ := e.token.Encode(map[string]any{"user_id": id, "token_version": 0})
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +1037,7 @@ func TestActivityLogBackendAuditContext(t *testing.T) {
|
||||
t.Helper()
|
||||
probe := &auditOriginProbe{}
|
||||
r := chi.NewRouter()
|
||||
r.Use(jwtauth.Verifier(e.token))
|
||||
r.Use(authtoken.Verifier(e.token))
|
||||
r.Use(middleware.APIKeyAuth(useCases.NewAuthenticateAPIKeyUseCase(e.keys, e.generator, e.users, e.clock), 100, time.Minute))
|
||||
r.Use(middleware.AuditContext(useCases.NewGetUserUseCase(e.users), trusted))
|
||||
r.Get("/probe", func(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -51,7 +51,7 @@ type apiKeyEnv struct {
|
||||
mail *mockMailRepo
|
||||
feed *fakeReleaseFeed
|
||||
updates *fakeUpdateStore
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
generator *hashing.SHA256KeyGenerator
|
||||
now time.Time
|
||||
settings config.Settings
|
||||
@@ -76,7 +76,7 @@ func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
|
||||
images: newMockImageRepo(),
|
||||
invites: newInviteFakeRepo(),
|
||||
updates: &fakeUpdateStore{},
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
generator: hashing.NewSHA256KeyGenerator(),
|
||||
now: time.Date(2026, time.October, 4, 12, 0, 0, 0, time.UTC),
|
||||
settings: config.Settings{AuthRateLimit: config.AuthRateLimitSettings{MaxAttempts: 1000, Window: time.Minute}},
|
||||
@@ -172,7 +172,7 @@ type apiKeyRequest struct {
|
||||
|
||||
func (e *apiKeyEnv) cookie(t *testing.T, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
type fakeBodyRenderer struct {
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
bodyFormatArticleOneJSON = `[{"type":"heading","level":2,"children":[{"type":"text","text":"Plan","marks":[]}]},{"type":"paragraph","children":[{"type":"text","text":"Replace the ","marks":[]},{"type":"text","text":"old","marks":[{"type":"bold"}]},{"type":"text","text":" deck.","marks":[]}]}]`
|
||||
)
|
||||
|
||||
func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, renderer domainArticle.BodyRenderer, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, renderer domainArticle.BodyRenderer, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, renderer, discardRecorder, plainSites(), plainAuthors()),
|
||||
@@ -54,11 +54,11 @@ func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, r
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Patch("/{id}", articleHandler.UpdateArticleBody)
|
||||
@@ -145,7 +145,7 @@ func assertSameJSON(t *testing.T, actual, expected string) {
|
||||
|
||||
func TestArticleBodyFormatHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
newRouter := func(repo *mockArticleRepo, users *mockUserRepo, renderer domainArticle.BodyRenderer) *chi.Mux {
|
||||
return newBodyFormatRouter(repo, users, renderer, token)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(newVerifiedUserRepo())
|
||||
renderer := markdown.NewConverter()
|
||||
articleHandler := article.NewHandler(
|
||||
@@ -34,9 +34,9 @@ func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
@@ -48,7 +48,7 @@ func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
return r
|
||||
}
|
||||
|
||||
func serveUnlimitedBody(t *testing.T, router *chi.Mux, token *jwtauth.JWTAuth, method, target, payload string) *httptest.ResponseRecorder {
|
||||
func serveUnlimitedBody(t *testing.T, router *chi.Mux, token *authtoken.Auth, method, target, payload string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := newEditArticleBodyRequest(method, target, payload)
|
||||
setEditArticleBodyJwtCookie(t, req, token)
|
||||
@@ -59,7 +59,7 @@ func serveUnlimitedBody(t *testing.T, router *chi.Mux, token *jwtauth.JWTAuth, m
|
||||
|
||||
func TestArticleBodyUnlimitedHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
const title = "Choosing tiles for a small bathroom"
|
||||
|
||||
createCases := []struct {
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -35,10 +35,10 @@ func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWT
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
@@ -59,9 +59,9 @@ func newPublishingTestArticleRepo() *mockArticleRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func setPublishingJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTAuth) {
|
||||
func setPublishingJwtCookie(t *testing.T, req *http.Request, token *authtoken.Auth) {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": uint32(1)})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": uint32(1)})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func setPublishingJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTA
|
||||
|
||||
func TestArticlePublishingHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("Publishing an offline article makes it online", func(t *testing.T) {
|
||||
repo := newPublishingTestArticleRepo()
|
||||
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newArticleTitleRulesRouter(repo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -37,8 +37,8 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
@@ -48,7 +48,7 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *
|
||||
return r
|
||||
}
|
||||
|
||||
func postArticleRequest(t *testing.T, token *jwtauth.JWTAuth, title string, body string) *http.Request {
|
||||
func postArticleRequest(t *testing.T, token *authtoken.Auth, title string, body string) *http.Request {
|
||||
t.Helper()
|
||||
payload, err := json.Marshal(map[string]string{"title": title, "body": body})
|
||||
if err != nil {
|
||||
@@ -122,7 +122,7 @@ func TestArticleTitleRunesInUseCase(t *testing.T) {
|
||||
|
||||
func TestArticleTitleRunesOverHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
cases := []struct {
|
||||
length int
|
||||
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
)
|
||||
|
||||
func newArticlesListRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newArticlesListRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -31,7 +31,7 @@ func newArticlesListRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth)
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
})
|
||||
})
|
||||
@@ -40,7 +40,7 @@ func newArticlesListRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth)
|
||||
|
||||
func TestArticlesListHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("GET /v1/article/ items carry no images field now that images belong to the library", func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
)
|
||||
@@ -71,7 +71,7 @@ func itemIds(envelope paginationEnvelope) []int {
|
||||
|
||||
func TestArticlesPaginationApiHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
get := func(t *testing.T, repo *mockArticleRepo, query string, verified bool) (*httptest.ResponseRecorder, paginationEnvelope) {
|
||||
t.Helper()
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type authTokenProbe struct {
|
||||
reached bool
|
||||
claims authtoken.Claims
|
||||
err error
|
||||
}
|
||||
|
||||
func authTokenRouter(auth *authtoken.Auth, probe *authTokenProbe, guarded bool) http.Handler {
|
||||
final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
probe.reached = true
|
||||
probe.claims, probe.err = authtoken.FromContext(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
if guarded {
|
||||
return authtoken.Verifier(auth)(authtoken.Authenticator(final))
|
||||
}
|
||||
return authtoken.Verifier(auth)(final)
|
||||
}
|
||||
|
||||
func serveAuthToken(handler http.Handler, configure func(*http.Request)) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
configure(req)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func TestAuthTokenVerifierAndAuthenticator(t *testing.T) {
|
||||
auth := authtoken.New([]byte("test-secret"))
|
||||
valid, _ := auth.Encode(map[string]any{"user_id": uint32(5)})
|
||||
other, _ := auth.Encode(map[string]any{"user_id": uint32(9)})
|
||||
|
||||
t.Run("A valid jwt cookie reaches the handler with its claims", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: valid})
|
||||
})
|
||||
|
||||
if rec.Code != http.StatusOK || probe.claims["user_id"] != float64(5) {
|
||||
t.Errorf("expected 200 with user 5, got %d and %v", rec.Code, probe.claims)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A bearer header in any letter case is accepted", func(t *testing.T) {
|
||||
for _, scheme := range []string{"Bearer ", "BEARER ", "bearer "} {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
|
||||
r.Header.Set("Authorization", scheme+valid)
|
||||
})
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("scheme %q: expected 200, got %d", scheme, rec.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The bearer header wins over the cookie", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
|
||||
r.Header.Set("Authorization", "Bearer "+valid)
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: other})
|
||||
})
|
||||
|
||||
if probe.claims["user_id"] != float64(5) {
|
||||
t.Errorf("expected the header token, got %v", probe.claims)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A malformed Authorization header falls back to the cookie", func(t *testing.T) {
|
||||
for _, header := range []string{"Bearer", "Bearer ", "Basic abc", "Token " + valid} {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
|
||||
r.Header.Set("Authorization", header)
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: valid})
|
||||
})
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("header %q: expected the cookie to be used, got %d", header, rec.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The authenticator answers 401 with the standard message when no token is sent", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(*http.Request) {})
|
||||
|
||||
if rec.Code != http.StatusUnauthorized || probe.reached {
|
||||
t.Fatalf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached)
|
||||
}
|
||||
if got := rec.Body.String(); got != apperr.UnauthorizedErrorMessage+"\n" {
|
||||
t.Errorf("unexpected body %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The authenticator rejects a forged token", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
forged, _ := authtoken.New([]byte("another-secret")).Encode(map[string]any{"user_id": 1})
|
||||
|
||||
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: forged})
|
||||
})
|
||||
|
||||
if rec.Code != http.StatusUnauthorized || probe.reached {
|
||||
t.Errorf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Without the authenticator the handler sees the verification error", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
|
||||
serveAuthToken(authTokenRouter(auth, probe, false), func(r *http.Request) {
|
||||
r.AddCookie(&http.Cookie{Name: "jwt", Value: "garbage"})
|
||||
})
|
||||
|
||||
if !probe.reached || probe.err == nil || probe.claims != nil {
|
||||
t.Errorf("expected an error and no claims, got %v / %v", probe.claims, probe.err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A context that never passed the verifier reports no token", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
if _, err := authtoken.FromContext(req.Context()); err != authtoken.ErrNoToken {
|
||||
t.Errorf("expected ErrNoToken, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A context carrying nil claims and no error still reports no token", func(t *testing.T) {
|
||||
ctx := authtoken.NewContext(httptest.NewRequest(http.MethodGet, "/", nil).Context(), nil, nil)
|
||||
|
||||
if _, err := authtoken.FromContext(ctx); err != authtoken.ErrNoToken {
|
||||
t.Errorf("expected ErrNoToken, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A context injected with claims is authenticated", func(t *testing.T) {
|
||||
probe := &authTokenProbe{}
|
||||
handler := authtoken.Authenticator(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probe.reached = true }))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req = req.WithContext(authtoken.NewContext(req.Context(), authtoken.Claims{"user_id": float64(1)}, nil))
|
||||
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
if !probe.reached {
|
||||
t.Error("expected the handler to be reached")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var authTokenEpoch = time.Date(2026, time.March, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
func authTokenAt(now time.Time) *authtoken.Auth {
|
||||
return authtoken.New([]byte("test-secret"), authtoken.WithClock(func() time.Time { return now }))
|
||||
}
|
||||
|
||||
func signedAuthTokenSegments(secret string, header string, payload string) string {
|
||||
encode := base64.RawURLEncoding.EncodeToString
|
||||
signingInput := encode([]byte(header)) + "." + encode([]byte(payload))
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(signingInput))
|
||||
return signingInput + "." + encode(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func TestAuthTokenEncodeDecode(t *testing.T) {
|
||||
t.Run("A token round-trips its claims", func(t *testing.T) {
|
||||
auth := authTokenAt(authTokenEpoch)
|
||||
|
||||
encoded, err := auth.Encode(map[string]any{"user_id": uint32(7), "token_version": uint32(3)})
|
||||
if err != nil {
|
||||
t.Fatalf("encode failed: %v", err)
|
||||
}
|
||||
claims, err := auth.Decode(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decode failed: %v", err)
|
||||
}
|
||||
|
||||
if claims["user_id"] != float64(7) || claims["token_version"] != float64(3) {
|
||||
t.Errorf("unexpected claims %v", claims)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encoding stamps iat and an exp one session lifetime ahead", func(t *testing.T) {
|
||||
auth := authTokenAt(authTokenEpoch)
|
||||
|
||||
encoded, _ := auth.Encode(map[string]any{"user_id": 1})
|
||||
claims, _ := auth.Decode(encoded)
|
||||
|
||||
if claims["iat"] != float64(authTokenEpoch.Unix()) {
|
||||
t.Errorf("expected iat %d, got %v", authTokenEpoch.Unix(), claims["iat"])
|
||||
}
|
||||
if claims["exp"] != float64(authTokenEpoch.Add(authtoken.SessionLifetime).Unix()) {
|
||||
t.Errorf("unexpected exp %v", claims["exp"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encoding keeps an explicit exp", func(t *testing.T) {
|
||||
auth := authTokenAt(authTokenEpoch)
|
||||
explicit := authTokenEpoch.Add(time.Minute).Unix()
|
||||
|
||||
encoded, _ := auth.Encode(map[string]any{"exp": explicit})
|
||||
claims, _ := auth.Decode(encoded)
|
||||
|
||||
if claims["exp"] != float64(explicit) {
|
||||
t.Errorf("expected exp %d, got %v", explicit, claims["exp"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encoding does not mutate the caller's claims", func(t *testing.T) {
|
||||
input := map[string]any{"user_id": 1}
|
||||
|
||||
_, _ = authTokenAt(authTokenEpoch).Encode(input)
|
||||
|
||||
if len(input) != 1 {
|
||||
t.Errorf("input was mutated: %v", input)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encoding nil claims still yields a decodable token", func(t *testing.T) {
|
||||
auth := authTokenAt(authTokenEpoch)
|
||||
|
||||
encoded, err := auth.Encode(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("encode failed: %v", err)
|
||||
}
|
||||
if _, err := auth.Decode(encoded); err != nil {
|
||||
t.Errorf("decode failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encoding reports claims that cannot be serialized", func(t *testing.T) {
|
||||
if _, err := authTokenAt(authTokenEpoch).Encode(map[string]any{"bad": make(chan int)}); err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthTokenDecodeRejections(t *testing.T) {
|
||||
auth := authTokenAt(authTokenEpoch)
|
||||
valid, _ := auth.Encode(map[string]any{"user_id": 1})
|
||||
segments := strings.Split(valid, ".")
|
||||
header := `{"alg":"HS256","typ":"JWT"}`
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
want error
|
||||
}{
|
||||
"empty": {"", authtoken.ErrNoToken},
|
||||
"one segment": {"abc", authtoken.ErrInvalid},
|
||||
"four segments": {valid + ".x", authtoken.ErrInvalid},
|
||||
"not base64 header": {"!!!." + segments[1] + "." + segments[2], authtoken.ErrInvalid},
|
||||
"signed with another secret": {signedAuthTokenSegments("other", header, `{"user_id":1}`), authtoken.ErrInvalid},
|
||||
"alg none": {signedAuthTokenSegments("test-secret", `{"alg":"none"}`, `{"user_id":1}`), authtoken.ErrInvalid},
|
||||
"alg HS512": {signedAuthTokenSegments("test-secret", `{"alg":"HS512"}`, `{"user_id":1}`), authtoken.ErrInvalid},
|
||||
"alg missing": {signedAuthTokenSegments("test-secret", `{}`, `{"user_id":1}`), authtoken.ErrInvalid},
|
||||
"tampered payload": {segments[0] + "." + base64.RawURLEncoding.EncodeToString([]byte(`{"user_id":2}`)) + "." + segments[2], authtoken.ErrInvalid},
|
||||
"truncated signature": {segments[0] + "." + segments[1] + "." + segments[2][:10], authtoken.ErrInvalid},
|
||||
"empty signature": {segments[0] + "." + segments[1] + ".", authtoken.ErrInvalid},
|
||||
"payload not an object": {signedAuthTokenSegments("test-secret", header, `[1]`), authtoken.ErrInvalid},
|
||||
"payload null": {signedAuthTokenSegments("test-secret", header, `null`), authtoken.ErrInvalid},
|
||||
"exp not a number": {signedAuthTokenSegments("test-secret", header, `{"exp":"soon"}`), authtoken.ErrInvalid},
|
||||
"nbf not a number": {signedAuthTokenSegments("test-secret", header, `{"nbf":"soon"}`), authtoken.ErrInvalid},
|
||||
"expired": {signedAuthTokenSegments("test-secret", header, `{"exp":`+authTokenUnix(authTokenEpoch.Add(-time.Second).Unix())+`}`), authtoken.ErrExpired},
|
||||
"expiring exactly now": {signedAuthTokenSegments("test-secret", header, `{"exp":`+authTokenUnix(authTokenEpoch.Unix())+`}`), authtoken.ErrExpired},
|
||||
"not yet valid": {signedAuthTokenSegments("test-secret", header, `{"nbf":`+authTokenUnix(authTokenEpoch.Add(time.Second).Unix())+`}`), authtoken.ErrNotYetValid},
|
||||
"larger than the size budget": {strings.Repeat("a", 5000), authtoken.ErrInvalid},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run("Rejects "+name, func(t *testing.T) {
|
||||
claims, err := auth.Decode(tc.token)
|
||||
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Errorf("expected %v, got %v", tc.want, err)
|
||||
}
|
||||
if claims != nil {
|
||||
t.Errorf("expected no claims, got %v", claims)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Accepts a token with no exp so sessions issued before expiry existed keep working", func(t *testing.T) {
|
||||
legacy := signedAuthTokenSegments("test-secret", header, `{"user_id":4,"token_version":1}`)
|
||||
|
||||
claims, err := auth.Decode(legacy)
|
||||
|
||||
if err != nil || claims["user_id"] != float64(4) {
|
||||
t.Errorf("expected legacy token accepted, got %v, %v", claims, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Accepts a token whose nbf has passed", func(t *testing.T) {
|
||||
token := signedAuthTokenSegments("test-secret", header, `{"nbf":`+authTokenUnix(authTokenEpoch.Add(-time.Second).Unix())+`}`)
|
||||
|
||||
if _, err := auth.Decode(token); err != nil {
|
||||
t.Errorf("expected acceptance, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A token expires once the clock passes its lifetime", func(t *testing.T) {
|
||||
encoded, _ := auth.Encode(map[string]any{"user_id": 1})
|
||||
|
||||
justBefore := authTokenAt(authTokenEpoch.Add(authtoken.SessionLifetime - time.Second))
|
||||
justAfter := authTokenAt(authTokenEpoch.Add(authtoken.SessionLifetime))
|
||||
|
||||
if _, err := justBefore.Decode(encoded); err != nil {
|
||||
t.Errorf("expected valid before expiry, got %v", err)
|
||||
}
|
||||
if _, err := justAfter.Decode(encoded); !errors.Is(err, authtoken.ErrExpired) {
|
||||
t.Errorf("expected expired, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func authTokenUnix(value int64) string {
|
||||
return strconv.FormatInt(value, 10)
|
||||
}
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newCancelRegistrationRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newCancelRegistrationRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
@@ -34,8 +34,8 @@ func newCancelRegistrationRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo,
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/register", loginHandler.Register)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Post("/verify", verificationHandler.Verify)
|
||||
r.Delete("/register", verificationHandler.CancelRegistration)
|
||||
})
|
||||
@@ -56,10 +56,10 @@ func newCancelRegistrationTestUserRepo(isVerified bool) *mockUserRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func sendCancelRegistrationRequest(router *chi.Mux, token *jwtauth.JWTAuth, claims map[string]any) *httptest.ResponseRecorder {
|
||||
func sendCancelRegistrationRequest(router *chi.Mux, token *authtoken.Auth, claims map[string]any) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodDelete, "/v1/auth/register", nil)
|
||||
if claims != nil {
|
||||
_, tokenString, _ := token.Encode(claims)
|
||||
tokenString, _ := token.Encode(claims)
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -83,7 +83,7 @@ func jwtSetCookie(t *testing.T, rec *httptest.ResponseRecorder) (*http.Cookie, b
|
||||
|
||||
func TestCancelPendingRegistrationHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("An unverified user cancels their pending registration", func(t *testing.T) {
|
||||
repo := newCancelRegistrationTestUserRepo(false)
|
||||
|
||||
+6
-6
@@ -7,12 +7,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
)
|
||||
|
||||
func requestWithJwtClaims(t *testing.T, token *jwtauth.JWTAuth, claimsToEncode map[string]any) *http.Request {
|
||||
func requestWithJwtClaims(t *testing.T, token *authtoken.Auth, claimsToEncode map[string]any) *http.Request {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(claimsToEncode)
|
||||
tokenString, err := token.Encode(claimsToEncode)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -21,11 +21,11 @@ func requestWithJwtClaims(t *testing.T, token *jwtauth.JWTAuth, claimsToEncode m
|
||||
t.Fatalf("failed to decode jwt: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
return req.WithContext(jwtauth.NewContext(req.Context(), decoded, nil))
|
||||
return req.WithContext(authtoken.NewContext(req.Context(), decoded, nil))
|
||||
}
|
||||
|
||||
func TestClaimsTokenVersion(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("returns the numeric token_version claim", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": uint32(7), "token_version": uint32(3)})
|
||||
@@ -68,7 +68,7 @@ func TestClaimsTokenVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClaimsUserId(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("returns the numeric user_id claim", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": uint32(7)})
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
)
|
||||
|
||||
type containerHarness struct {
|
||||
@@ -28,7 +28,7 @@ func newContainerHarness(t *testing.T, container bool, helperInstalled bool) *co
|
||||
rig.helper.installed = helperInstalled
|
||||
helper := infraupdate.NewContainerHelperDetector(rig.helper, container)
|
||||
now := func() time.Time { return updateTestCheckedAt }
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
feed := &fakeReleaseFeed{release: update.Release{
|
||||
Tag: "v2.3.0-beta",
|
||||
URL: "https://git.example/fsylvain/RenewCMS/releases/tag/v2.3.0-beta",
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/middleware"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type corsExchange struct {
|
||||
rec *httptest.ResponseRecorder
|
||||
reached bool
|
||||
}
|
||||
|
||||
func corsRequest(origins []string, method string, headers map[string]string) corsExchange {
|
||||
exchange := corsExchange{}
|
||||
handler := middleware.CORS(origins)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
exchange.reached = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
req := httptest.NewRequest(method, "/v1/article", nil)
|
||||
for name, value := range headers {
|
||||
req.Header.Set(name, value)
|
||||
}
|
||||
exchange.rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(exchange.rec, req)
|
||||
return exchange
|
||||
}
|
||||
|
||||
func corsPreflight(origins []string, origin string, method string, requestHeaders string) corsExchange {
|
||||
headers := map[string]string{"Origin": origin, "Access-Control-Request-Method": method}
|
||||
if requestHeaders != "" {
|
||||
headers["Access-Control-Request-Headers"] = requestHeaders
|
||||
}
|
||||
return corsRequest(origins, http.MethodOptions, headers)
|
||||
}
|
||||
|
||||
func TestCORSActualRequests(t *testing.T) {
|
||||
origins := []string{"https://app.example.com", "https://*.preview.example.com"}
|
||||
|
||||
t.Run("An allowed origin is echoed and the exposed headers are listed", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://app.example.com" {
|
||||
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
if got.rec.Header().Get("Access-Control-Expose-Headers") != "Link, X-Total-Count" {
|
||||
t.Errorf("unexpected expose-headers %q", got.rec.Header().Get("Access-Control-Expose-Headers"))
|
||||
}
|
||||
if !got.reached {
|
||||
t.Error("the request must reach the handler")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Credentials are never allowed", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Credentials") != "" {
|
||||
t.Error("Access-Control-Allow-Credentials must not be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The match is case-insensitive", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "HTTPS://APP.EXAMPLE.COM"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") == "" {
|
||||
t.Error("expected the origin to be allowed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A wildcard subdomain pattern matches", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://pr-12.preview.example.com"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://pr-12.preview.example.com" {
|
||||
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A wildcard pattern does not match a look-alike", func(t *testing.T) {
|
||||
for _, origin := range []string{"https://preview.example.com", "https://x.preview.example.com.evil.net", "http://x.preview.example.com"} {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": origin})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
|
||||
t.Errorf("origin %q must not be allowed", origin)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unknown origin gets no CORS headers but still reaches the handler", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached {
|
||||
t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A request with no Origin is untouched", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, nil)
|
||||
|
||||
if got.rec.Header().Get("Vary") != "" || got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached {
|
||||
t.Errorf("unexpected headers %v", got.rec.Header())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Responses vary on Origin so shared caches stay correct", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"})
|
||||
|
||||
if got.rec.Header().Get("Vary") != "Origin" {
|
||||
t.Errorf("unexpected Vary %q", got.rec.Header().Get("Vary"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A lone star allows any origin", func(t *testing.T) {
|
||||
got := corsRequest([]string{"*"}, http.MethodGet, map[string]string{"Origin": "https://anything.example.net"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://anything.example.net" {
|
||||
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Blank entries never match the empty origin", func(t *testing.T) {
|
||||
got := corsRequest([]string{"", " "}, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
|
||||
t.Error("blank entries must be ignored")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCORSPreflight(t *testing.T) {
|
||||
origins := []string{"https://app.example.com"}
|
||||
|
||||
t.Run("An allowed preflight is answered 204 without reaching the handler", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", http.MethodPatch, "Content-Type, Authorization")
|
||||
|
||||
header := got.rec.Header()
|
||||
if got.rec.Code != http.StatusNoContent || got.reached {
|
||||
t.Fatalf("expected 204 and no handler, got %d reached=%v", got.rec.Code, got.reached)
|
||||
}
|
||||
if header.Get("Access-Control-Allow-Origin") != "https://app.example.com" {
|
||||
t.Errorf("unexpected allow-origin %q", header.Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
if header.Get("Access-Control-Allow-Methods") != "PATCH" {
|
||||
t.Errorf("unexpected allow-methods %q", header.Get("Access-Control-Allow-Methods"))
|
||||
}
|
||||
if header.Get("Access-Control-Max-Age") != "300" {
|
||||
t.Errorf("unexpected max-age %q", header.Get("Access-Control-Max-Age"))
|
||||
}
|
||||
for _, allowed := range []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"} {
|
||||
if !strings.Contains(header.Get("Access-Control-Allow-Headers"), allowed) {
|
||||
t.Errorf("allow-headers misses %s: %q", allowed, header.Get("Access-Control-Allow-Headers"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Every supported method is allowed", func(t *testing.T) {
|
||||
for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
|
||||
got := corsPreflight(origins, "https://app.example.com", method, "")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Methods") != method {
|
||||
t.Errorf("method %s not allowed", method)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The requested method is matched case-insensitively", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", "patch", "")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Methods") != "PATCH" {
|
||||
t.Errorf("unexpected allow-methods %q", got.rec.Header().Get("Access-Control-Allow-Methods"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A method outside the list is refused", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", "TRACE", "")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.rec.Header().Get("Access-Control-Allow-Methods") != "" {
|
||||
t.Errorf("unexpected headers %v", got.rec.Header())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A header outside the list is refused", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", "GET", "Content-Type, X-Evil")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
|
||||
t.Errorf("unexpected headers %v", got.rec.Header())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Requested headers are matched case-insensitively and tolerate spacing", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", "GET", "content-type ,AUTHORIZATION,")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") == "" {
|
||||
t.Error("expected the preflight to be allowed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unknown origin is refused", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://evil.example.net", "GET", "")
|
||||
|
||||
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.reached {
|
||||
t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The preflight response varies on every request header it depends on", func(t *testing.T) {
|
||||
got := corsPreflight(origins, "https://app.example.com", "GET", "")
|
||||
|
||||
vary := strings.Join(got.rec.Header().Values("Vary"), ",")
|
||||
for _, field := range []string{"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"} {
|
||||
if !strings.Contains(vary, field) {
|
||||
t.Errorf("Vary misses %s: %q", field, vary)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An OPTIONS request without a requested method is treated as a normal request", func(t *testing.T) {
|
||||
got := corsRequest(origins, http.MethodOptions, map[string]string{"Origin": "https://app.example.com"})
|
||||
|
||||
if !got.reached {
|
||||
t.Error("expected the handler to be reached")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const supportRecipeKey = "recipe"
|
||||
@@ -377,7 +377,7 @@ func TestBuiltinTypeHTTP(t *testing.T) {
|
||||
t.Run("The built-in routes are read-only and restricted", func(t *testing.T) {
|
||||
env := newSupportEnv(t)
|
||||
stale := func(req *http.Request) {
|
||||
_, tokenString, _ := env.token.Encode(map[string]any{"user_id": uint32(ctStaleId), "token_version": 0})
|
||||
tokenString, _ := env.token.Encode(map[string]any{"user_id": uint32(ctStaleId), "token_version": 0})
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
}
|
||||
apiKey := env.issueKey(ctAdminId)
|
||||
@@ -414,7 +414,7 @@ func TestBuiltinTypeHTTP(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
users := newMockUserRepo()
|
||||
users.users[ctAdminId] = user.User{ID: ctAdminId, Username: "dev", IsVerified: true, Role: user.RoleAdmin}
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
now := func() time.Time { return time.Now() }
|
||||
generator := hashing.NewSHA256KeyGenerator()
|
||||
container := &api.UseCases{
|
||||
@@ -428,7 +428,7 @@ func TestBuiltinTypeHTTP(t *testing.T) {
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
router.Mount("/v1", api.InitBackendRoutes(container, config.Settings{}))
|
||||
_, tokenString, _ := token.Encode(map[string]any{"user_id": uint32(ctAdminId), "token_version": 0})
|
||||
tokenString, _ := token.Encode(map[string]any{"user_id": uint32(ctAdminId), "token_version": 0})
|
||||
for _, path := range []string{"/v1/builtin-type", "/v1/builtin-type/site"} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
@@ -576,7 +576,7 @@ func TestUserLookupHTTP(t *testing.T) {
|
||||
env := newSupportEnv(t)
|
||||
apiKey := env.issueKey(ctAdminId)
|
||||
stale := func(req *http.Request) {
|
||||
_, tokenString, _ := env.token.Encode(map[string]any{"user_id": uint32(ctStaleId), "token_version": 0})
|
||||
tokenString, _ := env.token.Encode(map[string]any{"user_id": uint32(ctStaleId), "token_version": 0})
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
}
|
||||
cases := []struct {
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -238,7 +238,7 @@ type contentTypeEnv struct {
|
||||
users *mockUserRepo
|
||||
keys *mockAPIKeyRepo
|
||||
generator *hashing.SHA256KeyGenerator
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
clock time.Time
|
||||
router http.Handler
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func newContentTypeEnv(t *testing.T) *contentTypeEnv {
|
||||
users: newMockUserRepo(),
|
||||
keys: newMockAPIKeyRepo(),
|
||||
generator: hashing.NewSHA256KeyGenerator(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
clock: time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC),
|
||||
}
|
||||
env.users.users[ctOwnerId] = user.User{ID: ctOwnerId, Username: "marta", IsVerified: true, Role: user.RoleOwner}
|
||||
@@ -313,7 +313,7 @@ func (e *contentTypeEnv) asUser(t *testing.T, userId uint32) func(*http.Request)
|
||||
if userId == 0 {
|
||||
return
|
||||
}
|
||||
_, tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": 0})
|
||||
tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": 0})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/infrastructure/config"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseDotenv(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
want map[string]string
|
||||
}{
|
||||
"plain assignment": {"A=1", map[string]string{"A": "1"}},
|
||||
"spaces around the equals sign": {"A = 1", map[string]string{"A": "1"}},
|
||||
"export prefix": {"export A=1", map[string]string{"A": "1"}},
|
||||
"empty value": {"A=", map[string]string{"A": ""}},
|
||||
"blank and comment lines": {"\n# note\n \nA=1\n # indented\n", map[string]string{"A": "1"}},
|
||||
"inline comment on an unquoted value": {"A=value # trailing", map[string]string{"A": "value"}},
|
||||
"hash without a leading space": {"A=pass#word", map[string]string{"A": "pass#word"}},
|
||||
"equals sign inside the value": {"A=b=c", map[string]string{"A": "b=c"}},
|
||||
"URL value": {"A=http://localhost:5173/x?y=1", map[string]string{"A": "http://localhost:5173/x?y=1"}},
|
||||
"double quoted keeps spaces and hash": {`A=" a # b "`, map[string]string{"A": " a # b "}},
|
||||
"double quoted escapes": {`A="l1\nl2\t\"q\" \\ \x"`, map[string]string{"A": "l1\nl2\t\"q\" \\ \\x"}},
|
||||
"double quoted then comment": {`A="v" # note`, map[string]string{"A": "v"}},
|
||||
"single quoted is literal": {`A='a\nb # c'`, map[string]string{"A": `a\nb # c`}},
|
||||
"dollar signs are not expanded": {"A=pa$$word${B}", map[string]string{"A": "pa$$word${B}"}},
|
||||
"dotted names": {"a.b_c1=2", map[string]string{"a.b_c1": "2"}},
|
||||
"windows line endings": {"A=1\r\nB=2\r\n", map[string]string{"A": "1", "B": "2"}},
|
||||
"byte order mark": {"\xef\xbb\xbfA=1", map[string]string{"A": "1"}},
|
||||
"last duplicate wins": {"A=1\nA=2", map[string]string{"A": "2"}},
|
||||
"no trailing newline": {"A=1\nB=2", map[string]string{"A": "1", "B": "2"}},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run("Parses "+name, func(t *testing.T) {
|
||||
got, err := config.ParseDotenv(strings.NewReader(tc.input))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
for key, value := range tc.want {
|
||||
if got[key] != value {
|
||||
t.Errorf("%s: expected %q, got %q", key, value, got[key])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
rejected := map[string]string{
|
||||
"a line with no equals sign": "JUST_A_WORD",
|
||||
"an empty name": "=value",
|
||||
"a name with a space": "MY VAR=1",
|
||||
"a name with a dash": "MY-VAR=1",
|
||||
"an unterminated double quote": `A="open`,
|
||||
"a trailing backslash": `A="open\`,
|
||||
"an unterminated single quote": `A='open`,
|
||||
}
|
||||
for name, input := range rejected {
|
||||
t.Run("Rejects "+name+" and names the line", func(t *testing.T) {
|
||||
_, err := config.ParseDotenv(strings.NewReader("OK=1\n" + input))
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), "line 2") {
|
||||
t.Errorf("expected a line 2 error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Rejects a line longer than the budget", func(t *testing.T) {
|
||||
_, err := config.ParseDotenv(strings.NewReader("A=" + strings.Repeat("x", 2<<20)))
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadDotenv(t *testing.T) {
|
||||
write := func(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), ".env")
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
t.Run("Sets variables that are not already set", func(t *testing.T) {
|
||||
t.Setenv("DOTENV_TEST_NEW", "")
|
||||
_ = os.Unsetenv("DOTENV_TEST_NEW")
|
||||
|
||||
err := config.LoadDotenv(write(t, "DOTENV_TEST_NEW=from-file"))
|
||||
|
||||
if err != nil || os.Getenv("DOTENV_TEST_NEW") != "from-file" {
|
||||
t.Errorf("expected from-file, got %q (%v)", os.Getenv("DOTENV_TEST_NEW"), err)
|
||||
}
|
||||
_ = os.Unsetenv("DOTENV_TEST_NEW")
|
||||
})
|
||||
|
||||
t.Run("Never overrides a variable already in the environment", func(t *testing.T) {
|
||||
t.Setenv("DOTENV_TEST_KEEP", "from-env")
|
||||
|
||||
_ = config.LoadDotenv(write(t, "DOTENV_TEST_KEEP=from-file"))
|
||||
|
||||
if os.Getenv("DOTENV_TEST_KEEP") != "from-env" {
|
||||
t.Errorf("environment was overridden: %q", os.Getenv("DOTENV_TEST_KEEP"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Keeps an explicitly empty environment variable", func(t *testing.T) {
|
||||
t.Setenv("DOTENV_TEST_EMPTY", "")
|
||||
|
||||
_ = config.LoadDotenv(write(t, "DOTENV_TEST_EMPTY=from-file"))
|
||||
|
||||
if value, set := os.LookupEnv("DOTENV_TEST_EMPTY"); !set || value != "" {
|
||||
t.Errorf("expected an empty value, got %q set=%v", value, set)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Applies nothing when any line is malformed", func(t *testing.T) {
|
||||
_ = os.Unsetenv("DOTENV_TEST_ATOMIC")
|
||||
|
||||
err := config.LoadDotenv(write(t, "DOTENV_TEST_ATOMIC=1\nbroken line"))
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if _, set := os.LookupEnv("DOTENV_TEST_ATOMIC"); set {
|
||||
t.Error("a partially applied file must not leak variables")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Names the file in a parse error", func(t *testing.T) {
|
||||
path := write(t, "broken line")
|
||||
|
||||
err := config.LoadDotenv(path)
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), path) {
|
||||
t.Errorf("expected the path in the error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Reports a missing file as fs.ErrNotExist", func(t *testing.T) {
|
||||
err := config.LoadDotenv(filepath.Join(t.TempDir(), "absent.env"))
|
||||
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Errorf("expected fs.ErrNotExist, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -36,9 +36,9 @@ func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAu
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Patch("/{id}", articleHandler.UpdateArticleBody)
|
||||
})
|
||||
@@ -55,9 +55,9 @@ func newEditArticleBodyTestRepo() *mockArticleRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func setEditArticleBodyJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTAuth) {
|
||||
func setEditArticleBodyJwtCookie(t *testing.T, req *http.Request, token *authtoken.Auth) {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": uint32(1)})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": uint32(1)})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func newEditArticleBodyRequest(method, target string, body string) *http.Request
|
||||
|
||||
func TestEditArticleBodyHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("Editing an existing article's body updates only the body", func(t *testing.T) {
|
||||
repo := newEditArticleBodyTestRepo()
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
meHandler := auth.NewMeHandler(getUserUseCase, useCases.NewGetMyAuthorUseCase(plainAuthors()))
|
||||
verificationHandler := auth.NewVerificationHandler(
|
||||
@@ -29,8 +29,8 @@ func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1/auth", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUserUseCase))
|
||||
r.Get("/me", meHandler.Me)
|
||||
r.Delete("/register", verificationHandler.CancelRegistration)
|
||||
@@ -39,9 +39,9 @@ func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *
|
||||
return r
|
||||
}
|
||||
|
||||
func mintGetCurrentUserCookie(t *testing.T, token *jwtauth.JWTAuth, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
func mintGetCurrentUserCookie(t *testing.T, token *authtoken.Auth, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
|
||||
t.Run("A verified user fetches their own profile", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{
|
||||
ID: 7,
|
||||
@@ -92,7 +92,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("An unverified user still fetches their own profile, with is_verified false, not a 403", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[8] = user.User{
|
||||
ID: 8,
|
||||
@@ -122,7 +122,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("No jwt cookie at all is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0}
|
||||
router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token)
|
||||
@@ -135,7 +135,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A jwt whose token_version no longer matches the stored user is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 1}
|
||||
router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token)
|
||||
@@ -152,7 +152,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A jwt minted for a since-deleted account is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: false, TokenVersion: 0}
|
||||
router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token)
|
||||
@@ -173,7 +173,7 @@ func TestGetCurrentUserHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("The route is never rate-limited, unlike login and password reset", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0}
|
||||
router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token)
|
||||
|
||||
+3
-3
@@ -15,8 +15,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
var errStubHashFailure = errors.New("stub hash failure")
|
||||
@@ -153,7 +153,7 @@ func TestBcryptHasherHashAndCompare(t *testing.T) {
|
||||
}
|
||||
|
||||
func newHasherRouter(userRepo *mockUserRepo, hasher *stubHasher) *chi.Mux {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
||||
@@ -170,7 +170,7 @@ func newHasherRouter(userRepo *mockUserRepo, hasher *stubHasher) *chi.Mux {
|
||||
}
|
||||
|
||||
func newRealHasherRouter(userRepo *mockUserRepo) *chi.Mux {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,7 +49,7 @@ type variantEnv struct {
|
||||
articles *mockArticleRepo
|
||||
keys *mockAPIKeyRepo
|
||||
logs *bytes.Buffer
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
now time.Time
|
||||
usage *mockVariantUsageRepo
|
||||
tracker *usage.Tracker
|
||||
@@ -82,7 +82,7 @@ func newVariantEnvLimits(t *testing.T, renderer *countingRenderer, limits useCas
|
||||
articles: newMockArticleRepo(),
|
||||
keys: newMockAPIKeyRepo(),
|
||||
logs: &bytes.Buffer{},
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
now: time.Date(2026, time.October, 8, 9, 0, 0, 0, time.UTC),
|
||||
usage: newMockVariantUsageRepo(),
|
||||
}
|
||||
@@ -152,7 +152,7 @@ func (e *variantEnv) issueAPIKey() string {
|
||||
|
||||
func (e *variantEnv) cookie(userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
e.t.Helper()
|
||||
_, signed, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
signed, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
e.t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const invitePublicURL = "https://cms.example.com"
|
||||
@@ -137,7 +137,7 @@ type inviteFixture struct {
|
||||
mail *mockMailRepo
|
||||
hasher *stubHasher
|
||||
router *chi.Mux
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
}
|
||||
|
||||
func newInviteFixture(t *testing.T, maxAttempts int) *inviteFixture {
|
||||
@@ -148,7 +148,7 @@ func newInviteFixture(t *testing.T, maxAttempts int) *inviteFixture {
|
||||
invites: newInviteFakeRepo(),
|
||||
mail: newMockMailRepo(),
|
||||
hasher: &stubHasher{},
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
}
|
||||
getUser := useCases.NewGetUserUseCase(f.users)
|
||||
handler := userhandler.NewInviteHandler(
|
||||
@@ -159,8 +159,8 @@ func newInviteFixture(t *testing.T, maxAttempts int) *inviteFixture {
|
||||
)
|
||||
f.router = chi.NewRouter()
|
||||
f.router.Route("/v1/user", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Use(middleware.RequireVerifiedUser(getUser))
|
||||
r.Get("/", handler.ListUsers)
|
||||
@@ -535,7 +535,7 @@ func TestInvitationsDoNotChangeRegistration(t *testing.T) {
|
||||
newRegistration := func(users *mockUserRepo, mail *mockMailRepo) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
handler := auth.NewLoginHandler(
|
||||
jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
authtoken.New([]byte("test-secret")),
|
||||
useCases.NewCreateUserUseCase(users, hasher, mail, invitePublicURL, discardRecorder),
|
||||
useCases.NewAuthenticateUserUseCase(users, hasher, discardRecorder),
|
||||
true,
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
@@ -34,7 +34,7 @@ type inviteManagementFixture struct {
|
||||
mail *mockMailRepo
|
||||
hasher *stubHasher
|
||||
router *chi.Mux
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
}
|
||||
|
||||
func newInviteManagementFixture(t *testing.T, maxAttempts int) *inviteManagementFixture {
|
||||
@@ -45,7 +45,7 @@ func newInviteManagementFixture(t *testing.T, maxAttempts int) *inviteManagement
|
||||
invites: newInviteFakeRepo(),
|
||||
mail: newMockMailRepo(),
|
||||
hasher: &stubHasher{},
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
}
|
||||
f.invites.users = f.users
|
||||
f.users.users[9] = user.User{ID: 9, Username: "jonas", Email: "jonas@renew.example", IsVerified: true, Role: user.RoleAdmin}
|
||||
@@ -71,8 +71,8 @@ func newInviteManagementFixture(t *testing.T, maxAttempts int) *inviteManagement
|
||||
f.router.Get("/v1/auth/invite", acceptHandler.GetInvite)
|
||||
f.router.Post("/v1/auth/invite/accept", acceptHandler.AcceptInvite)
|
||||
f.router.Route("/v1/user", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Use(middleware.RequireVerifiedUser(getUser))
|
||||
r.Get("/", handler.ListUsers)
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
type countingAuthenticateUserRepo struct {
|
||||
@@ -33,7 +33,7 @@ func (r *countingAuthenticateUserRepo) GetByUsername(username string) (user.User
|
||||
func newRateLimitedAuthRouter(t *testing.T, userRepo user.Repository, mailRepo *mockMailRepo, maxAttempts int, window time.Duration) *chi.Mux {
|
||||
t.Helper()
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newLoginLogoutCookieRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newLoginLogoutCookieRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
@@ -32,7 +32,7 @@ func newLoginLogoutCookieRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo,
|
||||
|
||||
func TestLoginSetsA24HourJwtCookieAndLogoutClearsIt(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("Login sets a jwt cookie that expires about 24 hours from now", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,7 +38,7 @@ type mediaEnv struct {
|
||||
purgeSettings *mockPurgeSettingsRepo
|
||||
clock time.Time
|
||||
logs *bytes.Buffer
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
variants: newMockVariantFileRepo(),
|
||||
storage: newMockImageStorage(),
|
||||
users: newMockUserRepo(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
purgeSettings: newMockPurgeSettingsRepo(),
|
||||
clock: time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC),
|
||||
logs: &bytes.Buffer{},
|
||||
@@ -93,7 +93,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
|
||||
func (e *mediaEnv) cookie(t *testing.T, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := e.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -71,7 +71,7 @@ type hardeningEnv struct {
|
||||
settings *mockPurgeSettingsRepo
|
||||
logs *bytes.Buffer
|
||||
users *mockUserRepo
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func newHardeningEnv(t *testing.T) *hardeningEnv {
|
||||
settings: newMockPurgeSettingsRepo(),
|
||||
logs: &bytes.Buffer{},
|
||||
users: newMockUserRepo(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
}
|
||||
env.users.users[hardeningAdminID] = user.User{ID: hardeningAdminID, Username: "marta", IsVerified: true, Role: user.RoleAdmin}
|
||||
env.transactor = newMergeTransactor(env.images, env.articles, env.sites, env.authors, env.entries)
|
||||
@@ -124,7 +124,7 @@ func (e *hardeningEnv) send(t *testing.T, method string, path string, body strin
|
||||
if body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
_, tokenString, err := e.token.Encode(map[string]any{"user_id": hardeningAdminID, "token_version": 0})
|
||||
tokenString, err := e.token.Encode(map[string]any{"user_id": hardeningAdminID, "token_version": 0})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ func TestUserRepositoryReportsMissingUserAsDomainError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
@@ -144,7 +144,7 @@ func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRep
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
})
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
@@ -154,9 +154,9 @@ func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRep
|
||||
return r
|
||||
}
|
||||
|
||||
func setJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTAuth, userId uint32) {
|
||||
func setJwtCookie(t *testing.T, req *http.Request, token *authtoken.Auth, userId uint32) {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": userId})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": userId})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func setJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTAuth, userI
|
||||
|
||||
func TestNotFoundErrorsHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
newArticleRepo := func() *mockArticleRepo {
|
||||
repo := newMockArticleRepo()
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
)
|
||||
@@ -37,7 +37,7 @@ func (w *erroringResponseWriter) Write([]byte) (int, error) {
|
||||
|
||||
func (w *erroringResponseWriter) WriteHeader(int) {}
|
||||
|
||||
func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -53,13 +53,13 @@ func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *jwtauth.
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Delete("/{id}", articleHandler.DeleteArticle)
|
||||
@@ -78,7 +78,7 @@ func seedPublicArticleListingRepo() *mockArticleRepo {
|
||||
|
||||
func TestPublicArticleListingHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("An anonymous visitor only sees online articles", func(t *testing.T) {
|
||||
router := newPublicArticleListingRouter(seedPublicArticleListingRepo(), token)
|
||||
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
@@ -35,11 +35,11 @@ func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUser
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Delete("/{id}", articleHandler.DeleteArticle)
|
||||
@@ -65,9 +65,9 @@ func newPublicArticleReadUserRepo(isVerified bool) *mockUserRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func mintPublicArticleReadCookie(t *testing.T, token *jwtauth.JWTAuth, userId uint32) *http.Cookie {
|
||||
func mintPublicArticleReadCookie(t *testing.T, token *authtoken.Auth, userId uint32) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": userId})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": userId})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func assertPublicArticleReadNotFound(t *testing.T, rec *httptest.ResponseRecorde
|
||||
|
||||
func TestPublicArticleReadHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("An anonymous visitor can read an online article", func(t *testing.T) {
|
||||
router := newPublicArticleReadRouter(seedPublicArticleReadRepo(), newMockUserRepo(), token)
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
type sequentialHasher struct {
|
||||
@@ -38,7 +38,7 @@ func (s *sequentialHasher) Compare(hashed string, value string) error {
|
||||
}
|
||||
|
||||
func newRegistrationGateRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, publicURL string) *chi.Mux {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newRegistrationStatusRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo) (*chi.Mux, *useCases.CreateUserUseCase) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
hasher := &stubHasher{}
|
||||
createUseCase := useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
loginHandler := auth.NewLoginHandler(token, createUseCase, useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), true)
|
||||
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"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)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
hashed, err := hasher.Hash("hunter2example")
|
||||
@@ -125,7 +125,7 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
imageRepo := newMockImageRepo()
|
||||
mailRepo := newMockMailRepo()
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
return &api.UseCases{
|
||||
Token: token,
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newSettingsRouter(t *testing.T, userRepo *mockUserRepo, token *jwtauth.JWTAuth, version string) *chi.Mux {
|
||||
func newSettingsRouter(t *testing.T, userRepo *mockUserRepo, token *authtoken.Auth, version string) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
settingsHandler := settings.NewHandler(
|
||||
useCases.NewGetSettingsUseCase(version),
|
||||
@@ -25,8 +25,8 @@ func newSettingsRouter(t *testing.T, userRepo *mockUserRepo, token *jwtauth.JWTA
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1/settings", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUserUseCase))
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Get("/", settingsHandler.GetSettings)
|
||||
@@ -43,7 +43,7 @@ func TestSettingsHttp(t *testing.T) {
|
||||
|
||||
for _, version := range []string{"v2.2.0-beta", "dev"} {
|
||||
t.Run("A verified caller gets the running build's version: "+version, func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0}
|
||||
router := newSettingsRouter(t, userRepo, token, version)
|
||||
@@ -61,7 +61,7 @@ func TestSettingsHttp(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("The response does not depend on the environment at request time", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0}
|
||||
router := newSettingsRouter(t, userRepo, token, "v2.2.0-beta")
|
||||
@@ -79,7 +79,7 @@ func TestSettingsHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A request without a jwt cookie is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
router := newSettingsRouter(t, newMockUserRepo(), token, "v2.2.0-beta")
|
||||
|
||||
rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/settings", nil)
|
||||
@@ -93,7 +93,7 @@ func TestSettingsHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A token whose version no longer matches the account is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 1}
|
||||
router := newSettingsRouter(t, userRepo, token, "v2.2.0-beta")
|
||||
@@ -110,7 +110,7 @@ func TestSettingsHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("An unverified account is refused", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[8] = user.User{ID: 8, Username: "bob", IsVerified: false, TokenVersion: 0}
|
||||
router := newSettingsRouter(t, userRepo, token, "v2.2.0-beta")
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/infrastructure/config"
|
||||
"RenewCMS/internal/infrastructure/mailer"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var smtpMailerClock = time.Date(2026, time.March, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
func smtpMailerFor(server *fakeSMTPServer, settings config.SmtpSettings) *mailer.MailRepository {
|
||||
if settings.Host == "" {
|
||||
settings.Host = "127.0.0.1"
|
||||
}
|
||||
settings.Port = server.port()
|
||||
if settings.Sender == "" {
|
||||
settings.Sender = "notify@example.com"
|
||||
}
|
||||
return mailer.NewMailRepository(settings, mailer.WithTLSConfig(server.clientTLS()), mailer.WithClock(func() time.Time { return smtpMailerClock }))
|
||||
}
|
||||
|
||||
func verificationData() useCases.MailValidationData {
|
||||
return useCases.MailValidationData{Host: "https://cms.example.com", VerificationCode: "abc123"}
|
||||
}
|
||||
|
||||
func parseDeliveredMail(t *testing.T, raw string) (*mail.Message, string) {
|
||||
t.Helper()
|
||||
message, err := mail.ReadMessage(strings.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("delivered data is not a valid message: %v\n%s", err, raw)
|
||||
}
|
||||
decoded, err := io.ReadAll(quotedprintable.NewReader(message.Body))
|
||||
if err != nil {
|
||||
t.Fatalf("body is not valid quoted-printable: %v", err)
|
||||
}
|
||||
return message, string(decoded)
|
||||
}
|
||||
|
||||
func TestSMTPMailerDelivery(t *testing.T) {
|
||||
t.Run("Delivers a well-formed HTML message to a plain local server", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
|
||||
err := smtpMailerFor(server, config.SmtpSettings{}).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("send failed: %v", err)
|
||||
}
|
||||
sessions := server.received()
|
||||
if len(sessions) != 1 || sessions[0].from != "notify@example.com" || sessions[0].to != "reader@example.org" {
|
||||
t.Fatalf("unexpected envelope %+v", sessions)
|
||||
}
|
||||
message, body := parseDeliveredMail(t, sessions[0].data)
|
||||
if got := message.Header.Get("From"); got != `"RenewCMS" <notify@example.com>` {
|
||||
t.Errorf("unexpected From %q", got)
|
||||
}
|
||||
if got := message.Header.Get("To"); got != "<reader@example.org>" {
|
||||
t.Errorf("unexpected To %q", got)
|
||||
}
|
||||
subject, _ := new(mime.WordDecoder).DecodeHeader(message.Header.Get("Subject"))
|
||||
if subject != "Verify your RenewCMS account" {
|
||||
t.Errorf("unexpected Subject %q", subject)
|
||||
}
|
||||
if got := message.Header.Get("Content-Type"); got != "text/html; charset=UTF-8" {
|
||||
t.Errorf("unexpected Content-Type %q", got)
|
||||
}
|
||||
if got := message.Header.Get("MIME-Version"); got != "1.0" {
|
||||
t.Errorf("unexpected MIME-Version %q", got)
|
||||
}
|
||||
if !strings.Contains(body, "https://cms.example.com/register/validate?c=abc123") {
|
||||
t.Errorf("verification link missing from body %q", body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Stamps a Date from the clock and a unique Message-ID on the sender's domain", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
repo := smtpMailerFor(server, config.SmtpSettings{})
|
||||
|
||||
_ = repo.Send("a@example.org", "verification", verificationData())
|
||||
_ = repo.Send("a@example.org", "verification", verificationData())
|
||||
|
||||
sessions := server.received()
|
||||
first, _ := parseDeliveredMail(t, sessions[0].data)
|
||||
second, _ := parseDeliveredMail(t, sessions[1].data)
|
||||
date, err := first.Header.Date()
|
||||
if err != nil || !date.Equal(smtpMailerClock) {
|
||||
t.Errorf("expected Date %v, got %v (%v)", smtpMailerClock, date, err)
|
||||
}
|
||||
id := first.Header.Get("Message-ID")
|
||||
if !strings.HasPrefix(id, "<") || !strings.HasSuffix(id, "@example.com>") {
|
||||
t.Errorf("unexpected Message-ID %q", id)
|
||||
}
|
||||
if id == second.Header.Get("Message-ID") {
|
||||
t.Error("Message-IDs must be unique")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Encodes a non-ASCII subject and body without corrupting them", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
repo := smtpMailerFor(server, config.SmtpSettings{})
|
||||
|
||||
err := repo.Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := server.received()[0].data
|
||||
headerBlock, _, _ := strings.Cut(raw, "\r\n\r\n")
|
||||
for _, r := range headerBlock {
|
||||
if r > 127 {
|
||||
t.Fatalf("header contains a raw non-ASCII rune %q", r)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Authenticates with PLAIN over STARTTLS", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{startTLS: true, authMechanisms: "PLAIN LOGIN"})
|
||||
settings := config.SmtpSettings{Login: "user@example.com", Password: "s3cret"}
|
||||
|
||||
err := smtpMailerFor(server, settings).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("send failed: %v", err)
|
||||
}
|
||||
session := server.received()[0]
|
||||
if !session.usedTLS || session.username != "user@example.com" || session.password != "s3cret" {
|
||||
t.Errorf("unexpected session %+v", session)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Falls back to LOGIN when PLAIN is not offered", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{startTLS: true, authMechanisms: "LOGIN"})
|
||||
settings := config.SmtpSettings{Login: "user@example.com", Password: "s3cret"}
|
||||
|
||||
err := smtpMailerFor(server, settings).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("send failed: %v", err)
|
||||
}
|
||||
session := server.received()[0]
|
||||
if session.username != "user@example.com" || session.password != "s3cret" {
|
||||
t.Errorf("unexpected credentials %+v", session)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Skips authentication when no login is configured", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{authMechanisms: "PLAIN"})
|
||||
|
||||
err := smtpMailerFor(server, config.SmtpSettings{}).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, command := range server.received()[0].commands {
|
||||
if strings.HasPrefix(strings.ToUpper(command), "AUTH") {
|
||||
t.Errorf("unexpected %q", command)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for _, mechanisms := range []string{"PLAIN", "LOGIN"} {
|
||||
t.Run("Refuses to send "+mechanisms+" credentials in clear text to a non-loopback server", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{authMechanisms: mechanisms, listenAddress: "127.0.0.2:0"})
|
||||
settings := config.SmtpSettings{Login: "user@example.com", Password: "s3cret", Host: "127.0.0.2"}
|
||||
|
||||
err := smtpMailerFor(server, settings).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if session := server.received()[0]; session.password != "" || session.data != "" {
|
||||
t.Errorf("credentials or data leaked: %+v", session)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Fails when credentials are configured but the server offers no AUTH", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
settings := config.SmtpSettings{Login: "user@example.com", Password: "s3cret"}
|
||||
|
||||
err := smtpMailerFor(server, settings).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if len(server.received()[0].data) != 0 {
|
||||
t.Error("no message may be delivered")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Fails the STARTTLS handshake against an untrusted certificate", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{startTLS: true})
|
||||
repo := mailer.NewMailRepository(config.SmtpSettings{Host: "127.0.0.1", Port: server.port(), Sender: "notify@example.com"})
|
||||
|
||||
err := repo.Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected a certificate error")
|
||||
}
|
||||
if len(server.received()[0].data) != 0 {
|
||||
t.Error("no message may be delivered")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Uses implicit TLS on port 465", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{implicitTLS: true, listenAddress: "127.0.0.1:465"})
|
||||
repo := mailer.NewMailRepository(config.SmtpSettings{Host: "127.0.0.1", Port: 465, Sender: "notify@example.com"}, mailer.WithTLSConfig(server.clientTLS()))
|
||||
|
||||
err := repo.Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("send failed: %v", err)
|
||||
}
|
||||
if !server.received()[0].usedTLS {
|
||||
t.Error("expected a TLS session")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Surfaces a rejected recipient", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{rejectRcpt: true})
|
||||
|
||||
err := smtpMailerFor(server, config.SmtpSettings{}).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Surfaces a rejected DATA command", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{rejectData: true})
|
||||
|
||||
err := smtpMailerFor(server, config.SmtpSettings{}).Send("reader@example.org", "verification", verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Surfaces an unreachable server", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
repo := smtpMailerFor(server, config.SmtpSettings{})
|
||||
_ = server.listener.Close()
|
||||
|
||||
if err := repo.Send("reader@example.org", "verification", verificationData()); err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSMTPMailerRejectsUnsafeInput(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
sender string
|
||||
receiver string
|
||||
template string
|
||||
}{
|
||||
"receiver with an injected header": {"notify@example.com", "a@example.org\r\nBcc: evil@example.net", "verification"},
|
||||
"receiver that is not an address": {"notify@example.com", "not-an-address", "verification"},
|
||||
"empty receiver": {"notify@example.com", "", "verification"},
|
||||
"sender that is not an address": {"nope", "a@example.org", "verification"},
|
||||
"unknown template": {"notify@example.com", "a@example.org", "does-not-exist"},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run("Rejects a "+name+" before opening a connection", func(t *testing.T) {
|
||||
server := newFakeSMTPServer(t, fakeSMTPOptions{})
|
||||
repo := smtpMailerFor(server, config.SmtpSettings{Sender: tc.sender})
|
||||
|
||||
err := repo.Send(tc.receiver, tc.template, verificationData())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if len(server.received()) != 0 {
|
||||
t.Errorf("no connection may be made, got %d", len(server.received()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeSMTPOptions struct {
|
||||
authMechanisms string
|
||||
startTLS bool
|
||||
rejectRcpt bool
|
||||
rejectData bool
|
||||
implicitTLS bool
|
||||
listenAddress string
|
||||
}
|
||||
|
||||
type fakeSMTPSession struct {
|
||||
commands []string
|
||||
from string
|
||||
to string
|
||||
data string
|
||||
username string
|
||||
password string
|
||||
usedTLS bool
|
||||
}
|
||||
|
||||
type fakeSMTPServer struct {
|
||||
listener net.Listener
|
||||
options fakeSMTPOptions
|
||||
cert tls.Certificate
|
||||
roots *x509.CertPool
|
||||
mu sync.Mutex
|
||||
sessions []*fakeSMTPSession
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newFakeSMTPServer(t *testing.T, options fakeSMTPOptions) *fakeSMTPServer {
|
||||
t.Helper()
|
||||
address := options.listenAddress
|
||||
if address == "" {
|
||||
address = "127.0.0.1:0"
|
||||
}
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
t.Skipf("cannot listen on %s: %v", address, err)
|
||||
}
|
||||
cert, roots := selfSignedLoopbackCert(t)
|
||||
server := &fakeSMTPServer{listener: listener, options: options, cert: cert, roots: roots}
|
||||
server.wg.Add(1)
|
||||
go server.acceptLoop()
|
||||
t.Cleanup(func() {
|
||||
_ = listener.Close()
|
||||
server.wg.Wait()
|
||||
})
|
||||
return server
|
||||
}
|
||||
|
||||
func selfSignedLoopbackCert(t *testing.T) (tls.Certificate, *x509.CertPool) {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "127.0.0.1"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
parsed, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(parsed)
|
||||
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, roots
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) port() int {
|
||||
return s.listener.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) clientTLS() *tls.Config {
|
||||
return &tls.Config{RootCAs: s.roots}
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) received() []*fakeSMTPSession {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]*fakeSMTPSession(nil), s.sessions...)
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) acceptLoop() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
connection, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
s.serve(connection)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) serve(connection net.Conn) {
|
||||
defer func() { _ = connection.Close() }()
|
||||
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
session := &fakeSMTPSession{}
|
||||
s.mu.Lock()
|
||||
s.sessions = append(s.sessions, session)
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.options.implicitTLS {
|
||||
connection = tls.Server(connection, &tls.Config{Certificates: []tls.Certificate{s.cert}})
|
||||
session.usedTLS = true
|
||||
}
|
||||
reader := bufio.NewReader(connection)
|
||||
reply := func(line string) { _, _ = connection.Write([]byte(line + "\r\n")) }
|
||||
reply("220 fake ESMTP")
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
s.record(session, line)
|
||||
verb := strings.ToUpper(strings.Fields(line + " ")[0])
|
||||
|
||||
switch verb {
|
||||
case "EHLO":
|
||||
s.replyEhlo(reply, session)
|
||||
case "STARTTLS":
|
||||
reply("220 ready")
|
||||
connection = tls.Server(connection, &tls.Config{Certificates: []tls.Certificate{s.cert}})
|
||||
reader = bufio.NewReader(connection)
|
||||
reply = func(line string) { _, _ = connection.Write([]byte(line + "\r\n")) }
|
||||
session.usedTLS = true
|
||||
case "AUTH":
|
||||
if !s.authenticate(line, reader, reply, session) {
|
||||
return
|
||||
}
|
||||
case "MAIL":
|
||||
session.from = smtpAngleContent(line, "<", ">")
|
||||
reply("250 ok")
|
||||
case "RCPT":
|
||||
if s.options.rejectRcpt {
|
||||
reply("550 no such user")
|
||||
continue
|
||||
}
|
||||
session.to = smtpAngleContent(line, "<", ">")
|
||||
reply("250 ok")
|
||||
case "DATA":
|
||||
if s.options.rejectData {
|
||||
reply("554 refused")
|
||||
continue
|
||||
}
|
||||
reply("354 go ahead")
|
||||
session.data = readSMTPData(reader)
|
||||
reply("250 queued")
|
||||
case "QUIT":
|
||||
reply("221 bye")
|
||||
return
|
||||
default:
|
||||
reply("250 ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) record(session *fakeSMTPSession, line string) {
|
||||
s.mu.Lock()
|
||||
session.commands = append(session.commands, line)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) replyEhlo(reply func(string), session *fakeSMTPSession) {
|
||||
lines := []string{"250-fake"}
|
||||
if s.options.startTLS && !session.usedTLS {
|
||||
lines = append(lines, "250-STARTTLS")
|
||||
}
|
||||
if s.options.authMechanisms != "" {
|
||||
lines = append(lines, "250-AUTH "+s.options.authMechanisms)
|
||||
}
|
||||
lines = append(lines, "250 8BITMIME")
|
||||
for _, line := range lines {
|
||||
reply(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeSMTPServer) authenticate(line string, reader *bufio.Reader, reply func(string), session *fakeSMTPSession) bool {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
reply("501 syntax")
|
||||
return false
|
||||
}
|
||||
switch strings.ToUpper(fields[1]) {
|
||||
case "PLAIN":
|
||||
raw, _ := base64.StdEncoding.DecodeString(fields[2])
|
||||
parts := strings.Split(string(raw), "\x00")
|
||||
session.username, session.password = parts[1], parts[2]
|
||||
case "LOGIN":
|
||||
reply("334 " + base64.StdEncoding.EncodeToString([]byte("Username:")))
|
||||
username, _ := reader.ReadString('\n')
|
||||
reply("334 " + base64.StdEncoding.EncodeToString([]byte("Password:")))
|
||||
password, _ := reader.ReadString('\n')
|
||||
session.username = decodeBase64Line(username)
|
||||
session.password = decodeBase64Line(password)
|
||||
default:
|
||||
reply("504 unsupported")
|
||||
return false
|
||||
}
|
||||
reply("235 ok")
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeBase64Line(line string) string {
|
||||
raw, _ := base64.StdEncoding.DecodeString(strings.TrimSpace(line))
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func smtpAngleContent(line string, open string, close string) string {
|
||||
start := strings.Index(line, open)
|
||||
end := strings.LastIndex(line, close)
|
||||
if start < 0 || end <= start {
|
||||
return ""
|
||||
}
|
||||
return line[start+1 : end]
|
||||
}
|
||||
|
||||
func readSMTPData(reader *bufio.Reader) string {
|
||||
var data strings.Builder
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil || line == ".\r\n" {
|
||||
return data.String()
|
||||
}
|
||||
data.WriteString(strings.TrimPrefix(line, "."))
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockArticleRepo, imageRepo *mockImageRepo, mailRepo *mockMailRepo, hasher *stubHasher, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockArticleRepo, imageRepo *mockImageRepo, mailRepo *mockMailRepo, hasher *stubHasher, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
@@ -50,8 +50,8 @@ func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockAr
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUserUseCase))
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
@@ -63,8 +63,8 @@ func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockAr
|
||||
r.Post("/password-reset", passwordResetHandler.RequestPasswordReset)
|
||||
r.Post("/password-reset/confirm", passwordResetHandler.ResetPassword)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUserUseCase))
|
||||
r.Post("/verify", verificationHandler.Verify)
|
||||
r.Delete("/register", verificationHandler.CancelRegistration)
|
||||
@@ -90,9 +90,9 @@ func newStaleTokenAliceRepo(isVerified bool) *mockUserRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func mintStaleTokenCookie(t *testing.T, token *jwtauth.JWTAuth, claims map[string]any) *http.Cookie {
|
||||
func mintStaleTokenCookie(t *testing.T, token *authtoken.Auth, claims map[string]any) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(claims)
|
||||
tokenString, err := token.Encode(claims)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
|
||||
t.Run("A jwt minted before a password reset is refused afterward, but a fresh login's jwt is accepted", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(true)
|
||||
articleRepo := newMockArticleRepo()
|
||||
router := newStaleTokenInvalidationRouter(userRepo, articleRepo, newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
@@ -211,7 +211,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A stale jwt is also refused on the auth group's own protected routes, not just article routes", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(false)
|
||||
router := newStaleTokenInvalidationRouter(userRepo, newMockArticleRepo(), newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
preResetCookie := mintStaleTokenCookie(t, token, map[string]any{"user_id": uint32(7), "token_version": uint32(0)})
|
||||
@@ -242,7 +242,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Resetting the password twice invalidates both the original jwt and the jwt from the interim login", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(true)
|
||||
articleRepo := newMockArticleRepo()
|
||||
router := newStaleTokenInvalidationRouter(userRepo, articleRepo, newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
@@ -298,7 +298,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Resetting one user's password never invalidates another user's jwt", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(true)
|
||||
userRepo.users[9] = user.User{ID: 9, Username: "bob", IsVerified: true, TokenVersion: 0}
|
||||
articleRepo := newMockArticleRepo()
|
||||
@@ -329,7 +329,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cancelledRegistrationCases {
|
||||
t.Run("A cancelled registration still refuses a jwt minted before the cancellation, on every protected route: "+tc.method+" "+tc.path, func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(false)
|
||||
router := newStaleTokenInvalidationRouter(userRepo, newMockArticleRepo(), newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
preCancelCookie := mintStaleTokenCookie(t, token, map[string]any{"user_id": uint32(7), "token_version": uint32(0)})
|
||||
@@ -350,7 +350,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("A jwt with no token_version claim at all is refused, treated the same as a mismatch", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(true)
|
||||
router := newStaleTokenInvalidationRouter(userRepo, newMockArticleRepo(), newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
cookie := mintStaleTokenCookie(t, token, map[string]any{"user_id": uint32(7)})
|
||||
@@ -365,7 +365,7 @@ func TestStaleTokenInvalidationHttp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("A jwt whose token_version claim is a string, not a number, is refused instead of crashing", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
userRepo := newStaleTokenAliceRepo(true)
|
||||
router := newStaleTokenInvalidationRouter(userRepo, newMockArticleRepo(), newMockImageRepo(), newMockMailRepo(), &stubHasher{}, token)
|
||||
cookie := mintStaleTokenCookie(t, token, map[string]any{"user_id": uint32(7), "token_version": "0"})
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newTrustedProxyAuthRouter(t *testing.T, userRepo user.Repository, mailRepo *mockMailRepo, trustedProxies []string) *chi.Mux {
|
||||
t.Helper()
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder),
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newMigratedUserRepository(t *testing.T) *persistence.UserRepository {
|
||||
@@ -151,7 +151,7 @@ func TestUnverifiedUsersAndReregistrationRepository(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newUnverifiedRegistrationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newUnverifiedRegistrationRouter(userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
@@ -184,7 +184,7 @@ func registerViaHttp(t *testing.T, router *chi.Mux, username, password, email st
|
||||
|
||||
func TestUnverifiedUsersAndReregistrationHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("Two unverified users can register one after another without a reset-code collision", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
)
|
||||
|
||||
const downloadPath = "/update/download"
|
||||
@@ -31,7 +31,7 @@ const downloadPath = "/update/download"
|
||||
func newDownloadHTTP(t *testing.T, rig *downloadRig) *updateHarness {
|
||||
t.Helper()
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
users := newUpdateUserRepo()
|
||||
feed := &fakeReleaseFeed{}
|
||||
now := func() time.Time { return updateTestCheckedAt }
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,7 +32,7 @@ var updateStatusKeys = []string{"available", "can_apply", "checked_at", "contain
|
||||
|
||||
type updateHarness struct {
|
||||
router http.Handler
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
feed *fakeReleaseFeed
|
||||
store *fakeUpdateStore
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func newUpdateHarness(t *testing.T, running string) *updateHarness {
|
||||
Notes: update.ReleaseNotes{Version: "v2.3.0-beta", Items: []string{"Media library"}},
|
||||
}}
|
||||
store := &fakeUpdateStore{}
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
now := func() time.Time { return updateTestCheckedAt }
|
||||
container := &api.UseCases{
|
||||
Token: token,
|
||||
@@ -74,7 +74,7 @@ func (h *updateHarness) send(t *testing.T, method, path string, userId uint32, t
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
if userId != 0 {
|
||||
_, tokenString, err := h.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := h.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
)
|
||||
|
||||
const restartPath = "/update/restart"
|
||||
|
||||
type restartHTTP struct {
|
||||
router http.Handler
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
dataDir string
|
||||
markerPath string
|
||||
resultPath string
|
||||
@@ -44,7 +44,7 @@ func newRestartHTTP(t *testing.T) *restartHTTP {
|
||||
validator.InitValidator()
|
||||
dataDir := t.TempDir()
|
||||
h := &restartHTTP{
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
dataDir: dataDir,
|
||||
markerPath: filepath.Join(dataDir, update.RestartRequestFileName),
|
||||
resultPath: filepath.Join(dataDir, update.ResultFileName),
|
||||
@@ -86,7 +86,7 @@ func (h *restartHTTP) request(t *testing.T, method string, target string, userId
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, target, strings.NewReader(body))
|
||||
if userId != 0 {
|
||||
_, tokenString, err := h.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := h.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -554,7 +554,7 @@ func TestRestartResultLivesUntilTheNextJob(t *testing.T) {
|
||||
func TestHealthzIsUnchanged(t *testing.T) {
|
||||
t.Run("Healthz is unchanged", func(t *testing.T) {
|
||||
container := &api.UseCases{
|
||||
Token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
Token: authtoken.New([]byte("test-secret")),
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(storage.NewLocalStorage(t.TempDir())),
|
||||
}
|
||||
router := api.InitRoutes(container, config.Settings{Environment: "production"})
|
||||
@@ -615,7 +615,7 @@ func TestRunningInfoFileAtStartup(t *testing.T) {
|
||||
|
||||
t.Run("the file is not served by any route", func(t *testing.T) {
|
||||
container := &api.UseCases{
|
||||
Token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
Token: authtoken.New([]byte("test-secret")),
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(storage.NewLocalStorage(t.TempDir())),
|
||||
}
|
||||
router := api.InitRoutes(container, config.Settings{Environment: "production"})
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
var displayNameResponseKeys = []string{"id", "username", "email", "is_verified", "created_at", "updated_at", "display_name", "role"}
|
||||
|
||||
func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newDisplayNameRouter(userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
getUser := useCases.NewGetUserUseCase(userRepo)
|
||||
update := useCases.NewUpdateUserProfileUseCase(userRepo, discardRecorder)
|
||||
@@ -37,8 +37,8 @@ func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.M
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1/user", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Use(middleware.RequireVerifiedUser(getUser))
|
||||
r.Patch("/me", handler.UpdateMe)
|
||||
@@ -46,8 +46,8 @@ func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.M
|
||||
r.Route("/v1/auth", func(r chi.Router) {
|
||||
r.Post("/login", loginHandler.Login)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Get("/me", meHandler.Me)
|
||||
})
|
||||
@@ -55,9 +55,9 @@ func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.M
|
||||
return r
|
||||
}
|
||||
|
||||
func displayNameCookie(t *testing.T, token *jwtauth.JWTAuth, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
func displayNameCookie(t *testing.T, token *authtoken.Auth, userId uint32, tokenVersion uint32) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func TestUpdateUserProfileUseCase(t *testing.T) {
|
||||
|
||||
func TestUserDisplayNameHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
patch := func(t *testing.T, repo *mockUserRepo, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newUserFieldHygieneRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newUserFieldHygieneRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *authtoken.Auth) *chi.Mux {
|
||||
hasher := &stubHasher{}
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
token,
|
||||
@@ -44,8 +44,8 @@ func newUserFieldHygieneRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, t
|
||||
r.Post("/password-reset", passwordResetHandler.RequestPasswordReset)
|
||||
r.Post("/password-reset/confirm", passwordResetHandler.ResetPassword)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Post("/verify", verificationHandler.Verify)
|
||||
})
|
||||
})
|
||||
@@ -94,7 +94,7 @@ func assertNoForbiddenKeys(t *testing.T, body []byte) {
|
||||
|
||||
func TestUserResponseFieldHygiene(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("A successful verification response carries only public fields", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
@@ -111,7 +111,7 @@ func TestUserResponseFieldHygiene(t *testing.T) {
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"code": "abc123"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/verify", bytes.NewReader(body))
|
||||
_, tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
@@ -164,7 +164,7 @@ func TestUserResponseFieldHygiene(t *testing.T) {
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"code": "wrong-code"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/auth/verify", bytes.NewReader(body))
|
||||
_, tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
tokenString, _ := token.Encode(map[string]any{"user_id": uint32(7)})
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
+10
-10
@@ -16,9 +16,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
@@ -33,7 +33,7 @@ type userRolesFixture struct {
|
||||
invites *inviteFakeRepo
|
||||
mail *mockMailRepo
|
||||
router *chi.Mux
|
||||
token *jwtauth.JWTAuth
|
||||
token *authtoken.Auth
|
||||
}
|
||||
|
||||
func newUserRolesFixture(t *testing.T, maxAttempts int) *userRolesFixture {
|
||||
@@ -43,7 +43,7 @@ func newUserRolesFixture(t *testing.T, maxAttempts int) *userRolesFixture {
|
||||
users: newMockUserRepo(),
|
||||
invites: newInviteFakeRepo(),
|
||||
mail: newMockMailRepo(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
token: authtoken.New([]byte("test-secret")),
|
||||
}
|
||||
f.invites.users = f.users
|
||||
hasher := &stubHasher{}
|
||||
@@ -73,15 +73,15 @@ func newUserRolesFixture(t *testing.T, maxAttempts int) *userRolesFixture {
|
||||
r.Post("/register", loginHandler.Register)
|
||||
r.Post("/invite/accept", acceptHandler.AcceptInvite)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Get("/me", meHandler.Me)
|
||||
})
|
||||
})
|
||||
f.router.Route("/v1/user", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(f.token))
|
||||
r.Use(jwtauth.Authenticator(f.token))
|
||||
r.Use(authtoken.Verifier(f.token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireCurrentToken(getUser))
|
||||
r.Use(middleware.RequireVerifiedUser(getUser))
|
||||
r.Get("/", inviteHandler.ListUsers)
|
||||
@@ -386,12 +386,12 @@ func TestUserRolesUseCases(t *testing.T) {
|
||||
|
||||
func TestRequireOwnerRepositoryFailure(t *testing.T) {
|
||||
t.Run("An unexpected repository error is a 500, not a 403", func(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1, Username: "marta", IsVerified: true, Role: user.RoleOwner}
|
||||
r := chi.NewRouter()
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireOwner(useCases.NewGetUserUseCase(repo)))
|
||||
r.Post("/", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
||||
repo.fail.failNext("Get")
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *authtoken.Auth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
@@ -32,11 +32,11 @@ func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mo
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Delete("/{id}", articleHandler.DeleteArticle)
|
||||
@@ -61,9 +61,9 @@ func seedVerifiedAccountRequiredUserRepo(isVerified bool) *mockUserRepo {
|
||||
return repo
|
||||
}
|
||||
|
||||
func mintVerifiedAccountRequiredCookie(t *testing.T, token *jwtauth.JWTAuth, claims map[string]any) *http.Cookie {
|
||||
func mintVerifiedAccountRequiredCookie(t *testing.T, token *authtoken.Auth, claims map[string]any) *http.Cookie {
|
||||
t.Helper()
|
||||
_, tokenString, err := token.Encode(claims)
|
||||
tokenString, err := token.Encode(claims)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode jwt: %v", err)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func newVerifiedAccountRequiredRequest(method, target string, body string) *http
|
||||
|
||||
func TestVerifiedAccountRequiredHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
protectedRouteCases := []struct {
|
||||
method string
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -208,7 +208,7 @@ func TestTheFakesMirrorTheRealRepositoriesOnEveryWriteNotFoundPath(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *authtoken.Auth) *chi.Mux {
|
||||
userRepo := newVerifiedUserRepo()
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
@@ -224,8 +224,8 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(authtoken.Authenticator)
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Delete("/{id}", articleHandler.DeleteArticle)
|
||||
@@ -237,7 +237,7 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
|
||||
func TestWriteNotFoundErrorsHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
|
||||
t.Run("Deleting an unknown article answers 404, not a silent success or a 500", func(t *testing.T) {
|
||||
articleRepo := newMockArticleRepo()
|
||||
|
||||
Reference in new issue
Block a user