Upgrade and add new testing functions
This commit is contained in:
parent
d12679df65
commit
9330ed8e74
13 changed files with 1867 additions and 130 deletions
118
cache_test.go
Normal file
118
cache_test.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
|
||||||
|
// Use of this source code is governed by MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCacheNilDoesNotPanic(t *testing.T) {
|
||||||
|
// With caching disabled, a failed Redis ping must NOT panic.
|
||||||
|
prevHost := os.Getenv("REDIS_HOST")
|
||||||
|
prevPort := os.Getenv("REDIS_PORT")
|
||||||
|
prevDB := os.Getenv("REDIS_DB")
|
||||||
|
os.Setenv("REDIS_HOST", "127.0.0.1")
|
||||||
|
os.Setenv("REDIS_PORT", "6379")
|
||||||
|
os.Setenv("REDIS_DB", "0")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Setenv("REDIS_HOST", prevHost)
|
||||||
|
os.Setenv("REDIS_PORT", prevPort)
|
||||||
|
os.Setenv("REDIS_DB", prevDB)
|
||||||
|
})
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("NewCache should not panic when cache is disabled: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c := NewCache(CacheConfig{EnableCache: false})
|
||||||
|
if c == nil {
|
||||||
|
t.Errorf("expected a non-nil Cache instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCacheInvalidDBPanics(t *testing.T) {
|
||||||
|
prevDB := os.Getenv("REDIS_DB")
|
||||||
|
os.Setenv("REDIS_DB", "not-a-number")
|
||||||
|
t.Cleanup(func() { os.Setenv("REDIS_DB", prevDB) })
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected NewCache to panic on an invalid REDIS_DB value")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = NewCache(CacheConfig{EnableCache: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheSetGetDelete(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
key := fmt.Sprintf("goffee_cache_test_%d", time.Now().UnixNano())
|
||||||
|
t.Cleanup(func() { _ = c.Delete(key) })
|
||||||
|
|
||||||
|
if err := c.Set(key, "hello"); err != nil {
|
||||||
|
t.Fatalf("failed cache set: %v", err)
|
||||||
|
}
|
||||||
|
got, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed cache get: %v", err)
|
||||||
|
}
|
||||||
|
if got != "hello" {
|
||||||
|
t.Errorf("expected 'hello', got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Delete(key); err != nil {
|
||||||
|
t.Fatalf("failed cache delete: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Get(key); err == nil {
|
||||||
|
t.Errorf("expected error getting a deleted key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheGetMissingKey(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
if _, err := c.Get(fmt.Sprintf("goffee_missing_%d", time.Now().UnixNano())); err == nil {
|
||||||
|
t.Errorf("expected error getting a missing key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheSetWithExpiration(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
key := fmt.Sprintf("goffee_cache_exp_%d", time.Now().UnixNano())
|
||||||
|
t.Cleanup(func() { _ = c.Delete(key) })
|
||||||
|
|
||||||
|
if err := c.SetWithExpiration(key, "expiring", 2*time.Second); err != nil {
|
||||||
|
t.Fatalf("failed cache set with expiration: %v", err)
|
||||||
|
}
|
||||||
|
got, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed cache get: %v", err)
|
||||||
|
}
|
||||||
|
if got != "expiring" {
|
||||||
|
t.Errorf("expected 'expiring', got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheOverwrite(t *testing.T) {
|
||||||
|
c := newTestCache(t)
|
||||||
|
key := fmt.Sprintf("goffee_cache_overwrite_%d", time.Now().UnixNano())
|
||||||
|
t.Cleanup(func() { _ = c.Delete(key) })
|
||||||
|
|
||||||
|
if err := c.Set(key, "first"); err != nil {
|
||||||
|
t.Fatalf("failed first set: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.Set(key, "second"); err != nil {
|
||||||
|
t.Fatalf("failed second set: %v", err)
|
||||||
|
}
|
||||||
|
got, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed get: %v", err)
|
||||||
|
}
|
||||||
|
if got != "second" {
|
||||||
|
t.Errorf("expected 'second', got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
112
context_test.go
112
context_test.go
|
|
@ -157,7 +157,7 @@ func TestGetPathParams(t *testing.T) {
|
||||||
}
|
}
|
||||||
a := New()
|
a := New()
|
||||||
h := a.makeHTTPRouterHandlerFunc(
|
h := a.makeHTTPRouterHandlerFunc(
|
||||||
Handler(func(c *Context) *Response {
|
Controller(func(c *Context) *Response {
|
||||||
rsp := fmt.Sprintf("param1: %v | param2: %v", c.GetPathParam("param1"), c.GetPathParam("param2"))
|
rsp := fmt.Sprintf("param1: %v | param2: %v", c.GetPathParam("param1"), c.GetPathParam("param2"))
|
||||||
return c.Response.Text(rsp)
|
return c.Response.Text(rsp)
|
||||||
}), nil)
|
}), nil)
|
||||||
|
|
@ -178,11 +178,13 @@ func TestGetRequestParams(t *testing.T) {
|
||||||
app.SetBasePath(pwd)
|
app.SetBasePath(pwd)
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Post("/pt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Post("/pt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Get("/gt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Get("/gt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -218,11 +220,13 @@ func TestRequestParamsExists(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Post("/pt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Post("/pt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Get("/gt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Get("/gt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -259,11 +263,13 @@ func TestGetHeader(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Post("/pt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Post("/pt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Get("/gt", Handler(func(c *Context) *Response {
|
|
||||||
|
gcr.Get("/gt", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -309,8 +315,13 @@ func TestGetUploadedFile(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Post("/pt", Handler(func(c *Context) *Response {
|
|
||||||
uploadedFile := c.GetUploadedFile("myfile")
|
gcr.Post("/pt", Controller(func(c *Context) *Response {
|
||||||
|
uploadedFile, err := c.GetUploadedFile("myfile")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(c.Response.HttpResponseWriter, "error: "+err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
rs := fmt.Sprintf("file name: %v | size: %v", uploadedFile.Name, uploadedFile.Size)
|
rs := fmt.Sprintf("file name: %v | size: %v", uploadedFile.Name, uploadedFile.Size)
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, rs)
|
fmt.Fprintln(c.Response.HttpResponseWriter, rs)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -534,6 +545,89 @@ func TestGetBaseDirPath(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetUserAgent(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
r.Header.Set("User-Agent", "goffee-test-agent")
|
||||||
|
c := makeCTX(t)
|
||||||
|
c.Request.httpRequest = r
|
||||||
|
if got := c.GetUserAgent(); got != "goffee-test-agent" {
|
||||||
|
t.Errorf("expected 'goffee-test-agent', got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequesBodyMap(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(POST, LOCALHOST, strings.NewReader(`{"name":"alice","age":"30"}`))
|
||||||
|
c := makeCTX(t)
|
||||||
|
c.Request.httpRequest = r
|
||||||
|
m := c.GetRequesBodyMap()
|
||||||
|
if m["name"] != "alice" {
|
||||||
|
t.Errorf("expected name 'alice', got %v", m["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequesBodyStruct(t *testing.T) {
|
||||||
|
type payload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(POST, LOCALHOST, strings.NewReader(`{"name":"bob"}`))
|
||||||
|
c := makeCTX(t)
|
||||||
|
c.Request.httpRequest = r
|
||||||
|
var p payload
|
||||||
|
if err := c.GetRequesBodyStruct(&p); err != nil {
|
||||||
|
t.Fatalf("failed binding body struct: %v", err)
|
||||||
|
}
|
||||||
|
if p.Name != "bob" {
|
||||||
|
t.Errorf("expected name 'bob', got %q", p.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequesBodyStructNonPointer(t *testing.T) {
|
||||||
|
type payload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(POST, LOCALHOST, strings.NewReader(`{"name":"bob"}`))
|
||||||
|
c := makeCTX(t)
|
||||||
|
c.Request.httpRequest = r
|
||||||
|
var p payload
|
||||||
|
err := c.GetRequesBodyStruct(p)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error when dest is not a pointer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapToJson(t *testing.T) {
|
||||||
|
c := makeCTX(t)
|
||||||
|
got := c.MapToJson(map[string]interface{}{"a": 1})
|
||||||
|
if got != `{"a":1}` {
|
||||||
|
t.Errorf("expected {\"a\":1}, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapToJsonPanicsOnNonMap(t *testing.T) {
|
||||||
|
c := makeCTX(t)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected panic for non-map input")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.MapToJson("not a map")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequesForm(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodPost, LOCALHOST, strings.NewReader("param=value"))
|
||||||
|
r.Header.Set(CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||||
|
c := makeCTX(t)
|
||||||
|
c.Request.httpRequest = r
|
||||||
|
got := c.GetRequesForm("param")
|
||||||
|
vals, ok := got.([]string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected []string form values, got %T", got)
|
||||||
|
}
|
||||||
|
if len(vals) != 1 || vals[0] != "value" {
|
||||||
|
t.Errorf("expected form value 'value', got %v", vals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeCTXLogTestCTX(t *testing.T, w http.ResponseWriter, r *http.Request, tmpFilePath string) *Context {
|
func makeCTXLogTestCTX(t *testing.T, w http.ResponseWriter, r *http.Request, tmpFilePath string) *Context {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return &Context{
|
return &Context{
|
||||||
|
|
|
||||||
294
cookies_test.go
Normal file
294
cookies_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
|
||||||
|
// Use of this source code is governed by MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testCookieSecret is a 32 byte (AES-256) key expressed as a hex string, suitable
|
||||||
|
// for the COOKIE_SECRET env var used by SetCookie/ClearCookie.
|
||||||
|
const testCookieSecret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
|
// enableTemplateCookieEnv enables the template engine (required for cookie handling)
|
||||||
|
// and sets a deterministic cookie secret, restoring both env vars on cleanup.
|
||||||
|
func enableTemplateCookieEnv(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
prevTemplate := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
prevSecret := os.Getenv("COOKIE_SECRET")
|
||||||
|
prevSecure := os.Getenv("COOKIE_SECURE")
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "true")
|
||||||
|
os.Setenv("COOKIE_SECRET", testCookieSecret)
|
||||||
|
os.Setenv("COOKIE_SECURE", "false")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", prevTemplate)
|
||||||
|
os.Setenv("COOKIE_SECRET", prevSecret)
|
||||||
|
os.Setenv("COOKIE_SECURE", prevSecure)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeSecret(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
key, err := hex.DecodeString(testCookieSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed decoding test cookie secret: %v", err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieWrite(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: "hello", Path: "/"}
|
||||||
|
if err := CookieWrite(w, cookie); err != nil {
|
||||||
|
t.Fatalf("failed testing cookie write: %v", err)
|
||||||
|
}
|
||||||
|
rsp := w.Result()
|
||||||
|
cookies := rsp.Cookies()
|
||||||
|
if len(cookies) != 1 {
|
||||||
|
t.Fatalf("expected 1 cookie, got %d", len(cookies))
|
||||||
|
}
|
||||||
|
if cookies[0].Name != "goffee" {
|
||||||
|
t.Errorf("expected cookie name 'goffee', got %q", cookies[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieWriteTooLong(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
// A value large enough that the base64-encoded cookie string exceeds 4096 bytes.
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: strings.Repeat("a", 5000), Path: "/"}
|
||||||
|
err := CookieWrite(w, cookie)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected ErrValueTooLong, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieRead(t *testing.T) {
|
||||||
|
// Build a request carrying a base64-encoded cookie value.
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: "hello-world", Path: "/"}
|
||||||
|
if err := CookieWrite(w, cookie); err != nil {
|
||||||
|
t.Fatalf("failed writing cookie: %v", err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
for _, c := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(c)
|
||||||
|
}
|
||||||
|
val, err := CookieRead(r, "goffee")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing cookie read: %v", err)
|
||||||
|
}
|
||||||
|
if val != "hello-world" {
|
||||||
|
t.Errorf("expected 'hello-world', got %q", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieReadMissing(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
_, err := CookieRead(r, "goffee")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error reading missing cookie")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieReadInvalidBase64(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
r.AddCookie(&http.Cookie{Name: "goffee", Value: "!!!not-base64!!!"})
|
||||||
|
_, err := CookieRead(r, "goffee")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error reading invalid base64 cookie")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieWriteReadEncrypted(t *testing.T) {
|
||||||
|
key := decodeSecret(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"}
|
||||||
|
if err := CookieWriteEncrypted(w, cookie, key); err != nil {
|
||||||
|
t.Fatalf("failed writing encrypted cookie: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
for _, c := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(c)
|
||||||
|
}
|
||||||
|
val, err := CookieReadEncrypted(r, "goffee", key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed reading encrypted cookie: %v", err)
|
||||||
|
}
|
||||||
|
if val != "secret-value" {
|
||||||
|
t.Errorf("expected 'secret-value', got %q", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieReadEncryptedWrongKey(t *testing.T) {
|
||||||
|
key := decodeSecret(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"}
|
||||||
|
if err := CookieWriteEncrypted(w, cookie, key); err != nil {
|
||||||
|
t.Fatalf("failed writing encrypted cookie: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A different but valid AES key must fail decryption.
|
||||||
|
wrongKey, _ := hex.DecodeString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
for _, c := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(c)
|
||||||
|
}
|
||||||
|
if _, err := CookieReadEncrypted(r, "goffee", wrongKey); err == nil {
|
||||||
|
t.Errorf("expected error decrypting with wrong key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCookieReadEncryptedWrongName(t *testing.T) {
|
||||||
|
key := decodeSecret(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"}
|
||||||
|
if err := CookieWriteEncrypted(w, cookie, key); err != nil {
|
||||||
|
t.Fatalf("failed writing encrypted cookie: %v", err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
for _, c := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(c)
|
||||||
|
}
|
||||||
|
// Reading with a different name must fail the authenticated name check.
|
||||||
|
if _, err := CookieReadEncrypted(r, "other", key); err == nil {
|
||||||
|
t.Errorf("expected error decrypting with wrong cookie name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCookie(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookie(w, "user@example.com", "token-123"); err != nil {
|
||||||
|
t.Fatalf("failed testing set cookie: %v", err)
|
||||||
|
}
|
||||||
|
cookies := w.Result().Cookies()
|
||||||
|
if len(cookies) != 1 {
|
||||||
|
t.Fatalf("expected 1 cookie, got %d", len(cookies))
|
||||||
|
}
|
||||||
|
if cookies[0].Name != "goffee" {
|
||||||
|
t.Errorf("expected cookie name 'goffee', got %q", cookies[0].Name)
|
||||||
|
}
|
||||||
|
if !cookies[0].HttpOnly {
|
||||||
|
t.Errorf("expected cookie to be HttpOnly")
|
||||||
|
}
|
||||||
|
if cookies[0].MaxAge <= 0 {
|
||||||
|
t.Errorf("expected a positive MaxAge, got %d", cookies[0].MaxAge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCookieUsesJWT_Lifetime(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
prev := os.Getenv("JWT_LIFESPAN_MINUTES")
|
||||||
|
os.Setenv("JWT_LIFESPAN_MINUTES", "5")
|
||||||
|
t.Cleanup(func() { os.Setenv("JWT_LIFESPAN_MINUTES", prev) })
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookie(w, "user@example.com", "token-123"); err != nil {
|
||||||
|
t.Fatalf("failed testing set cookie: %v", err)
|
||||||
|
}
|
||||||
|
cookies := w.Result().Cookies()
|
||||||
|
if cookies[0].MaxAge != 5*60 {
|
||||||
|
t.Errorf("expected MaxAge of %d, got %d", 5*60, cookies[0].MaxAge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCookieWithMaxAge(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookieWithMaxAge(w, "user@example.com", "token-abc", 120); err != nil {
|
||||||
|
t.Fatalf("failed testing set cookie with max age: %v", err)
|
||||||
|
}
|
||||||
|
cookies := w.Result().Cookies()
|
||||||
|
if cookies[0].MaxAge != 120 {
|
||||||
|
t.Errorf("expected MaxAge of 120, got %d", cookies[0].MaxAge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCookieTemplatesDisabledPanics(t *testing.T) {
|
||||||
|
prev := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "false")
|
||||||
|
t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) })
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected panic when templates are disabled")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
_ = SetCookieWithMaxAge(w, "user@example.com", "token", 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClearCookie(t *testing.T) {
|
||||||
|
prevSecure := os.Getenv("COOKIE_SECURE")
|
||||||
|
os.Setenv("COOKIE_SECURE", "false")
|
||||||
|
t.Cleanup(func() { os.Setenv("COOKIE_SECURE", prevSecure) })
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := ClearCookie(w); err != nil {
|
||||||
|
t.Fatalf("failed testing clear cookie: %v", err)
|
||||||
|
}
|
||||||
|
cookies := w.Result().Cookies()
|
||||||
|
if len(cookies) != 1 {
|
||||||
|
t.Fatalf("expected 1 cookie, got %d", len(cookies))
|
||||||
|
}
|
||||||
|
c := cookies[0]
|
||||||
|
if c.Name != "goffee" {
|
||||||
|
t.Errorf("expected cookie name 'goffee', got %q", c.Name)
|
||||||
|
}
|
||||||
|
if c.Value != "" {
|
||||||
|
t.Errorf("expected empty cookie value, got %q", c.Value)
|
||||||
|
}
|
||||||
|
if c.MaxAge != -1 {
|
||||||
|
t.Errorf("expected MaxAge of -1 to delete the cookie, got %d", c.MaxAge)
|
||||||
|
}
|
||||||
|
if !c.HttpOnly {
|
||||||
|
t.Errorf("expected cleared cookie to be HttpOnly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCookieRoundTrip(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
|
||||||
|
// Set a cookie and feed it back into a request, then decrypt it.
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookie(w, "user@example.com", "token-xyz"); err != nil {
|
||||||
|
t.Fatalf("failed setting cookie: %v", err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
for _, c := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := GetCookie(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing get cookie: %v", err)
|
||||||
|
}
|
||||||
|
if user.Email != "user@example.com" {
|
||||||
|
t.Errorf("expected email 'user@example.com', got %q", user.Email)
|
||||||
|
}
|
||||||
|
if user.Token != "token-xyz" {
|
||||||
|
t.Errorf("expected token 'token-xyz', got %q", user.Token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCookieTemplatesDisabledPanics(t *testing.T) {
|
||||||
|
prev := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "false")
|
||||||
|
t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) })
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected panic when templates are disabled")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
_, _ = GetCookie(r)
|
||||||
|
}
|
||||||
88
core_test.go
88
core_test.go
|
|
@ -50,7 +50,8 @@ func TestMakeHTTPHandlerFunc(t *testing.T) {
|
||||||
app.SetLogsDriver(&logger.LogFileDriver{
|
app.SetLogsDriver(&logger.LogFileDriver{
|
||||||
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
|
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
|
||||||
})
|
})
|
||||||
hdlr := Handler(func(c *Context) *Response {
|
app.Bootstrap()
|
||||||
|
hdlr := Controller(func(c *Context) *Response {
|
||||||
f, _ := os.Create(tmpFile)
|
f, _ := os.Create(tmpFile)
|
||||||
f.WriteString("DFT2V56H")
|
f.WriteString("DFT2V56H")
|
||||||
c.Response.SetHeader("header-key", "header-val")
|
c.Response.SetHeader("header-key", "header-val")
|
||||||
|
|
@ -76,7 +77,8 @@ func TestMakeHTTPHandlerFuncVerifyJson(t *testing.T) {
|
||||||
app.SetLogsDriver(&logger.LogFileDriver{
|
app.SetLogsDriver(&logger.LogFileDriver{
|
||||||
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
|
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
|
||||||
})
|
})
|
||||||
hdlr := Handler(func(c *Context) *Response {
|
app.Bootstrap()
|
||||||
|
hdlr := Controller(func(c *Context) *Response {
|
||||||
f, _ := os.Create(tmpFile)
|
f, _ := os.Create(tmpFile)
|
||||||
f.WriteString("DFT2V56H")
|
f.WriteString("DFT2V56H")
|
||||||
c.Response.SetHeader("header-key", "header-val")
|
c.Response.SetHeader("header-key", "header-val")
|
||||||
|
|
@ -130,18 +132,19 @@ func TestNotFoundHandler(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUseMiddleware(t *testing.T) {
|
func TestUseHook(t *testing.T) {
|
||||||
app := createNewApp(t)
|
app := createNewApp(t)
|
||||||
UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") }))
|
app.Bootstrap()
|
||||||
if len(app.middlewares.GetMiddlewares()) != 1 {
|
UseHook(Hook(func(c *Context) { c.GetLogger().Info("Testing!") }))
|
||||||
t.Errorf("failed testing use middleware")
|
if len(ResolveHooks().GetHooks()) != 1 {
|
||||||
|
t.Errorf("failed testing use hook")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChainReset(t *testing.T) {
|
func TestChainReset(t *testing.T) {
|
||||||
c := &chain{}
|
c := &chain{}
|
||||||
c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing1!") }))
|
c.nodes = append(c.nodes, Hook(func(c *Context) { c.GetLogger().Info("Testing1!") }))
|
||||||
c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") }))
|
c.nodes = append(c.nodes, Hook(func(c *Context) { c.GetLogger().Info("Testing2!") }))
|
||||||
|
|
||||||
c.reset()
|
c.reset()
|
||||||
if len(c.nodes) != 0 {
|
if len(c.nodes) != 0 {
|
||||||
|
|
@ -151,11 +154,11 @@ func TestChainReset(t *testing.T) {
|
||||||
|
|
||||||
func TestNext(t *testing.T) {
|
func TestNext(t *testing.T) {
|
||||||
app := createNewApp(t)
|
app := createNewApp(t)
|
||||||
app.t = 0
|
app.Bootstrap()
|
||||||
tfPath := filepath.Join(t.TempDir(), uuid.NewString())
|
tfPath := filepath.Join(t.TempDir(), uuid.NewString())
|
||||||
var hs []interface{}
|
var hs []interface{}
|
||||||
hs = append(hs, Middleware(func(c *Context) { c.Next() }))
|
hs = append(hs, Hook(func(c *Context) { c.Next() }))
|
||||||
hs = append(hs, Handler(func(c *Context) *Response {
|
hs = append(hs, Controller(func(c *Context) *Response {
|
||||||
f, _ := os.Create(tfPath)
|
f, _ := os.Create(tfPath)
|
||||||
f.WriteString("DFT2V56H")
|
f.WriteString("DFT2V56H")
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -165,7 +168,7 @@ func TestNext(t *testing.T) {
|
||||||
app.chain.execute(makeCTX(t))
|
app.chain.execute(makeCTX(t))
|
||||||
cnt, _ := os.ReadFile(tfPath)
|
cnt, _ := os.ReadFile(tfPath)
|
||||||
if string(cnt) != "DFT2V56H" {
|
if string(cnt) != "DFT2V56H" {
|
||||||
// t.Errorf("failed testing next")
|
t.Errorf("failed testing next")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,14 +176,14 @@ func TestChainGetByIndex(t *testing.T) {
|
||||||
c := &chain{}
|
c := &chain{}
|
||||||
tf := filepath.Join(t.TempDir(), uuid.NewString())
|
tf := filepath.Join(t.TempDir(), uuid.NewString())
|
||||||
var hs []interface{}
|
var hs []interface{}
|
||||||
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing!") }))
|
hs = append(hs, Hook(func(c *Context) { c.GetLogger().Info("testing!") }))
|
||||||
hs = append(hs, Middleware(func(c *Context) {
|
hs = append(hs, Hook(func(c *Context) {
|
||||||
f, _ := os.Create(tf)
|
f, _ := os.Create(tf)
|
||||||
f.WriteString("DFT2V56H")
|
f.WriteString("DFT2V56H")
|
||||||
}))
|
}))
|
||||||
c.nodes = hs
|
c.nodes = hs
|
||||||
pf := c.getByIndex(1)
|
pf := c.getByIndex(1)
|
||||||
f, ok := pf.(Middleware)
|
f, ok := pf.(Hook)
|
||||||
if ok {
|
if ok {
|
||||||
f(makeCTX(t))
|
f(makeCTX(t))
|
||||||
}
|
}
|
||||||
|
|
@ -192,10 +195,11 @@ func TestChainGetByIndex(t *testing.T) {
|
||||||
|
|
||||||
func TestPrepareChain(t *testing.T) {
|
func TestPrepareChain(t *testing.T) {
|
||||||
app := createNewApp(t)
|
app := createNewApp(t)
|
||||||
UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") }))
|
app.Bootstrap()
|
||||||
|
UseHook(Hook(func(c *Context) { c.GetLogger().Info("Testing!") }))
|
||||||
var hs []interface{}
|
var hs []interface{}
|
||||||
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing1!") }))
|
hs = append(hs, Hook(func(c *Context) { c.GetLogger().Info("testing1!") }))
|
||||||
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing2!") }))
|
hs = append(hs, Hook(func(c *Context) { c.GetLogger().Info("testing2!") }))
|
||||||
app.prepareChain(hs)
|
app.prepareChain(hs)
|
||||||
if len(app.chain.nodes) != 3 {
|
if len(app.chain.nodes) != 3 {
|
||||||
t.Errorf("failed preparing chain")
|
t.Errorf("failed preparing chain")
|
||||||
|
|
@ -207,7 +211,7 @@ func TestChainExecute(t *testing.T) {
|
||||||
f1Path := filepath.Join(tmpDir, uuid.NewString())
|
f1Path := filepath.Join(tmpDir, uuid.NewString())
|
||||||
c := &chain{}
|
c := &chain{}
|
||||||
c.nodes = []interface{}{
|
c.nodes = []interface{}{
|
||||||
Handler(func(c *Context) *Response {
|
Controller(func(c *Context) *Response {
|
||||||
tf, _ := os.Create(f1Path)
|
tf, _ := os.Create(f1Path)
|
||||||
defer tf.Close()
|
defer tf.Close()
|
||||||
tf.WriteString("DFT2V56H")
|
tf.WriteString("DFT2V56H")
|
||||||
|
|
@ -239,19 +243,23 @@ func makeCTX(t *testing.T) *Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestcombHndlers(t *testing.T) {
|
func TestCombHandlers(t *testing.T) {
|
||||||
app := createNewApp(t)
|
app := createNewApp(t)
|
||||||
t1 := Handler(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil })
|
t1 := Controller(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil })
|
||||||
t2 := Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") })
|
t2 := Hook(func(c *Context) { c.GetLogger().Info("Testing2!") })
|
||||||
|
|
||||||
mw := []Middleware{t2}
|
mw := []Hook{t2}
|
||||||
comb := app.combHandlers(t1, mw)
|
comb := app.combHandlers(t1, mw)
|
||||||
if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[0]).Pointer() {
|
// combHandlers builds the slice as [hooks..., controller]
|
||||||
t.Errorf("failed testing reverse handlers")
|
if len(comb) != 2 {
|
||||||
|
t.Errorf("failed testing comb handlers: unexpected length %d", len(comb))
|
||||||
|
}
|
||||||
|
if reflect.ValueOf(t2).Pointer() != reflect.ValueOf(comb[0]).Pointer() {
|
||||||
|
t.Errorf("failed testing comb handlers: hook should come first")
|
||||||
}
|
}
|
||||||
|
|
||||||
if reflect.ValueOf(t2).Pointer() != reflect.ValueOf(comb[1]).Pointer() {
|
if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[1]).Pointer() {
|
||||||
t.Errorf("failed testing reverse handlers")
|
t.Errorf("failed testing comb handlers: controller should come last")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,31 +267,31 @@ func TestRegisterGetRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Get("/", Handler(func(c *Context) *Response {
|
gcr.Get("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Post("/", Handler(func(c *Context) *Response {
|
gcr.Post("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Delete("/", Handler(func(c *Context) *Response {
|
gcr.Delete("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Patch("/", Handler(func(c *Context) *Response {
|
gcr.Patch("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Put("/", Handler(func(c *Context) *Response {
|
gcr.Put("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Options("/", Handler(func(c *Context) *Response {
|
gcr.Options("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
gcr.Head("/", Handler(func(c *Context) *Response {
|
gcr.Head("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -313,7 +321,7 @@ func TestRegisterPostRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Post("/", Handler(func(c *Context) *Response {
|
gcr.Post("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -343,7 +351,7 @@ func TestRegisterDeleteRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Delete("/", Handler(func(c *Context) *Response {
|
gcr.Delete("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -373,7 +381,7 @@ func TestRegisterPatchRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Patch("/", Handler(func(c *Context) *Response {
|
gcr.Patch("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -403,7 +411,7 @@ func TestRegisterPutRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Put("/", Handler(func(c *Context) *Response {
|
gcr.Put("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -433,7 +441,7 @@ func TestRegisterOptionsRoute(t *testing.T) {
|
||||||
app := New()
|
app := New()
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
gcr.Options("/", Handler(func(c *Context) *Response {
|
gcr.Options("/", Controller(func(c *Context) *Response {
|
||||||
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
@ -464,7 +472,7 @@ func TestRegisterHeadRoute(t *testing.T) {
|
||||||
hr := httprouter.New()
|
hr := httprouter.New()
|
||||||
gcr := NewRouter()
|
gcr := NewRouter()
|
||||||
tfp := filepath.Join(t.TempDir(), uuid.NewString())
|
tfp := filepath.Join(t.TempDir(), uuid.NewString())
|
||||||
gcr.Head("/", Handler(func(c *Context) *Response {
|
gcr.Head("/", Controller(func(c *Context) *Response {
|
||||||
param := c.GetRequestParam("param")
|
param := c.GetRequestParam("param")
|
||||||
p, _ := param.(string)
|
p, _ := param.(string)
|
||||||
f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 777)
|
f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 777)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewEventsManager(t *testing.T) {
|
func TestNewEventsManager(t *testing.T) {
|
||||||
|
|
@ -12,92 +17,171 @@ func TestNewEventsManager(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func TestResolveEventsManager(t *testing.T) {
|
func TestResolveEventsManager(t *testing.T) {
|
||||||
// NewEventsManager()
|
NewEventsManager()
|
||||||
// m := ResolveEventsManager()
|
m := ResolveEventsManager()
|
||||||
// if fmt.Sprintf("%T", m) != "*core.EventsManager" {
|
if fmt.Sprintf("%T", m) != "*core.EventsManager" {
|
||||||
// t.Errorf("failed testing new events manager")
|
t.Errorf("failed testing resolve events manager")
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// func TestEvents(t *testing.T) {
|
func TestEventsFireAndProcess(t *testing.T) {
|
||||||
// pwd, _ := os.Getwd()
|
const eventName1 string = "test-event-name1"
|
||||||
// const eventName1 string = "test-event-name1"
|
const eventName2 string = "test-event-name2"
|
||||||
// const eventName2 string = "test-event-name2"
|
|
||||||
// var tmpDir string
|
|
||||||
// if runtime.GOOS == "linux" {
|
|
||||||
// tmpDir = t.TempDir()
|
|
||||||
// } else {
|
|
||||||
// tmpDir = filepath.Join(pwd, "/testingdata/tmp")
|
|
||||||
// }
|
|
||||||
// tmpFile1 := filepath.Join(tmpDir, uuid.NewString())
|
|
||||||
// tmpFile2 := filepath.Join(tmpDir, uuid.NewString())
|
|
||||||
// tmpFile3 := filepath.Join(tmpDir, uuid.NewString())
|
|
||||||
// m := NewEventsManager()
|
|
||||||
// m.Register(eventName1, func(event *Event, requestContext *Context) {
|
|
||||||
// os.Create(tmpFile1)
|
|
||||||
// f, err := os.Create(tmpFile1)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Errorf("error testing register event: %v", err.Error())
|
|
||||||
// }
|
|
||||||
// f.WriteString(event.Name)
|
|
||||||
// f.Close()
|
|
||||||
// })
|
|
||||||
// m.Register(eventName1, func(event *Event, requestContext *Context) {
|
|
||||||
// os.Create(tmpFile3)
|
|
||||||
// f, err := os.Create(tmpFile3)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Errorf("error testing register event: %v", err.Error())
|
|
||||||
// }
|
|
||||||
// f.WriteString(event.Name)
|
|
||||||
// f.Close()
|
|
||||||
// })
|
|
||||||
// m.Fire(&Event{Name: eventName1})
|
|
||||||
// m.processFiredEvents()
|
|
||||||
|
|
||||||
// ff, err := os.Open(tmpFile1)
|
tmpDir := t.TempDir()
|
||||||
// if err != nil {
|
tmpFile1 := filepath.Join(tmpDir, uuid.NewString())
|
||||||
// t.Errorf("error testing register event : %v", err.Error())
|
tmpFile2 := filepath.Join(tmpDir, uuid.NewString())
|
||||||
// }
|
tmpFile3 := filepath.Join(tmpDir, uuid.NewString())
|
||||||
|
|
||||||
// d, err := io.ReadAll(ff)
|
m := NewEventsManager()
|
||||||
// if string(d) != eventName1 {
|
|
||||||
// t.Error("faild testing events")
|
|
||||||
// }
|
|
||||||
// ff.Close()
|
|
||||||
// os.Remove(tmpFile1)
|
|
||||||
|
|
||||||
// ff, err = os.Open(tmpFile3)
|
// Two jobs registered on the same event must BOTH run.
|
||||||
// if err != nil {
|
m.Register(eventName1, func(event *Event, requestContext *Context) {
|
||||||
// t.Errorf("error testing register event : %v", err.Error())
|
f, err := os.Create(tmpFile1)
|
||||||
// }
|
if err != nil {
|
||||||
|
t.Errorf("error testing register event: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.WriteString(event.Name)
|
||||||
|
f.Close()
|
||||||
|
})
|
||||||
|
m.Register(eventName1, func(event *Event, requestContext *Context) {
|
||||||
|
f, err := os.Create(tmpFile3)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error testing register event: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.WriteString(event.Name)
|
||||||
|
f.Close()
|
||||||
|
})
|
||||||
|
|
||||||
// d, err = io.ReadAll(ff)
|
if err := m.Fire(&Event{Name: eventName1}); err != nil {
|
||||||
// if string(d) != eventName1 {
|
t.Fatalf("failed firing event: %v", err)
|
||||||
// t.Error("faild testing events")
|
}
|
||||||
// }
|
m.processFiredEvents()
|
||||||
// ff.Close()
|
|
||||||
// os.Remove(tmpFile3)
|
|
||||||
|
|
||||||
// m.Register(eventName2, func(event *Event, requestContext *Context) {
|
for _, fp := range []string{tmpFile1, tmpFile3} {
|
||||||
// f, err := os.Create(tmpFile2)
|
f, err := os.Open(fp)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// t.Errorf("error testing register event: %v", err.Error())
|
t.Errorf("error opening event file %v: %v", fp, err.Error())
|
||||||
// }
|
continue
|
||||||
// f.WriteString(event.Name)
|
}
|
||||||
// f.Close()
|
d, err := io.ReadAll(f)
|
||||||
// })
|
if err != nil {
|
||||||
// m.Fire(&Event{Name: eventName2})
|
t.Errorf("error reading event file %v: %v", fp, err.Error())
|
||||||
// m.processFiredEvents()
|
}
|
||||||
|
if string(d) != eventName1 {
|
||||||
|
t.Errorf("failed testing events: expected %q, got %q", eventName1, string(d))
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// ff, err = os.Open(tmpFile2)
|
// A registered event with a distinct payload is processed independently.
|
||||||
// if err != nil {
|
m.Register(eventName2, func(event *Event, requestContext *Context) {
|
||||||
// t.Errorf("error testing register event : %v", err.Error())
|
f, err := os.Create(tmpFile2)
|
||||||
// }
|
if err != nil {
|
||||||
|
t.Errorf("error testing register event: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.WriteString(event.Name)
|
||||||
|
f.Close()
|
||||||
|
})
|
||||||
|
if err := m.Fire(&Event{Name: eventName2}); err != nil {
|
||||||
|
t.Fatalf("failed firing event: %v", err)
|
||||||
|
}
|
||||||
|
m.processFiredEvents()
|
||||||
|
|
||||||
// d, err = io.ReadAll(ff)
|
f, err := os.Open(tmpFile2)
|
||||||
// if string(d) != eventName2 {
|
if err != nil {
|
||||||
// t.Error("faild testing events")
|
t.Fatalf("error opening event file: %v", err.Error())
|
||||||
// }
|
}
|
||||||
// ff.Close()
|
d, err := io.ReadAll(f)
|
||||||
// }
|
if err != nil {
|
||||||
|
t.Errorf("error reading event file: %v", err.Error())
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
if string(d) != eventName2 {
|
||||||
|
t.Errorf("failed testing events: expected %q, got %q", eventName2, string(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventFireUnregistered(t *testing.T) {
|
||||||
|
m := NewEventsManager()
|
||||||
|
err := m.Fire(&Event{Name: "not-registered"})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error firing an unregistered event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventFireEmptyName(t *testing.T) {
|
||||||
|
m := NewEventsManager()
|
||||||
|
err := m.Fire(&Event{Name: ""})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error firing an event with an empty name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventRegisterEmptyNamePanics(t *testing.T) {
|
||||||
|
m := NewEventsManager()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected panic registering an event with an empty name")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
m.Register("", func(event *Event, requestContext *Context) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventsProcessClearsFiredList(t *testing.T) {
|
||||||
|
const eventName = "test-clear-fired"
|
||||||
|
m := NewEventsManager()
|
||||||
|
m.Register(eventName, func(event *Event, requestContext *Context) {})
|
||||||
|
if err := m.Fire(&Event{Name: eventName}); err != nil {
|
||||||
|
t.Fatalf("failed firing event: %v", err)
|
||||||
|
}
|
||||||
|
if len(m.firedEvents) != 1 {
|
||||||
|
t.Fatalf("expected 1 fired event, got %d", len(m.firedEvents))
|
||||||
|
}
|
||||||
|
m.processFiredEvents()
|
||||||
|
if len(m.firedEvents) != 0 {
|
||||||
|
t.Errorf("expected fired events to be cleared after processing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventsDisabled(t *testing.T) {
|
||||||
|
DisableEvents()
|
||||||
|
defer EnableEvents()
|
||||||
|
|
||||||
|
const eventName = "test-disabled-event"
|
||||||
|
m := NewEventsManager()
|
||||||
|
m.Register(eventName, func(event *Event, requestContext *Context) {})
|
||||||
|
// When disabled, Register is a no-op and Fire returns nil without recording.
|
||||||
|
if len(m.eventsJobsList) != 0 {
|
||||||
|
t.Errorf("expected no jobs registered while events are disabled")
|
||||||
|
}
|
||||||
|
if err := m.Fire(&Event{Name: eventName}); err != nil {
|
||||||
|
t.Errorf("expected Fire to be a no-op while events are disabled, got: %v", err)
|
||||||
|
}
|
||||||
|
if len(m.firedEvents) != 0 {
|
||||||
|
t.Errorf("expected no fired events while events are disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventsSetContextAndExecute(t *testing.T) {
|
||||||
|
const eventName = "test-context-event"
|
||||||
|
m := NewEventsManager()
|
||||||
|
var receivedCtx *Context
|
||||||
|
m.Register(eventName, func(event *Event, requestContext *Context) {
|
||||||
|
receivedCtx = requestContext
|
||||||
|
})
|
||||||
|
|
||||||
|
expected := &Context{}
|
||||||
|
m.setContext(expected)
|
||||||
|
if err := m.Fire(&Event{Name: eventName}); err != nil {
|
||||||
|
t.Fatalf("failed firing event: %v", err)
|
||||||
|
}
|
||||||
|
m.processFiredEvents()
|
||||||
|
if receivedCtx != expected {
|
||||||
|
t.Errorf("expected the event job to receive the request context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
109
hashing_test.go
Normal file
109
hashing_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
|
||||||
|
// Use of this source code is governed by MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.smarteching.com/goffee/core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHashPassword(t *testing.T) {
|
||||||
|
h := &Hashing{}
|
||||||
|
password := "super-secret-password"
|
||||||
|
hashed, err := h.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing hash password: %v", err)
|
||||||
|
}
|
||||||
|
if hashed == "" {
|
||||||
|
t.Errorf("expected a non-empty hash")
|
||||||
|
}
|
||||||
|
if hashed == password {
|
||||||
|
t.Errorf("hash must differ from the plaintext password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashPasswordIsSalted(t *testing.T) {
|
||||||
|
h := &Hashing{}
|
||||||
|
first, err := h.HashPassword("same-password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing hash password: %v", err)
|
||||||
|
}
|
||||||
|
second, err := h.HashPassword("same-password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing hash password: %v", err)
|
||||||
|
}
|
||||||
|
// bcrypt generates a random salt, so equal inputs must yield different hashes.
|
||||||
|
if first == second {
|
||||||
|
t.Errorf("expected different hashes for the same password (salting)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPasswordHash(t *testing.T) {
|
||||||
|
// The CheckPasswordHash error path logs through the global logger; make sure
|
||||||
|
// it is initialized so the test does not panic on a nil logger.
|
||||||
|
loggr = logger.NewLogger(&logger.LogNullDriver{})
|
||||||
|
|
||||||
|
h := &Hashing{}
|
||||||
|
password := "correct-horse-battery-staple"
|
||||||
|
hashed, err := h.HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := h.CheckPasswordHash(hashed, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing check password hash: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected password check to succeed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPasswordHashMismatch(t *testing.T) {
|
||||||
|
loggr = logger.NewLogger(&logger.LogNullDriver{})
|
||||||
|
|
||||||
|
h := &Hashing{}
|
||||||
|
hashed, err := h.HashPassword("the-right-password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := h.CheckPasswordHash(hashed, "the-wrong-password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mismatched password should not return an error, got: %v", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Errorf("expected password check to fail for wrong password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPasswordHashInvalidHash(t *testing.T) {
|
||||||
|
loggr = logger.NewLogger(&logger.LogNullDriver{})
|
||||||
|
|
||||||
|
h := &Hashing{}
|
||||||
|
// An invalid hash that is not a mismatched-but-valid bcrypt hash should
|
||||||
|
// surface as an error rather than a simple false.
|
||||||
|
ok, err := h.CheckPasswordHash("not-a-valid-bcrypt-hash", "whatever")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected an error for an invalid hash")
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Errorf("expected ok to be false for an invalid hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPasswordHashEmptyHash(t *testing.T) {
|
||||||
|
loggr = logger.NewLogger(&logger.LogNullDriver{})
|
||||||
|
|
||||||
|
h := &Hashing{}
|
||||||
|
ok, err := h.CheckPasswordHash("", "password")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected an error for an empty hash")
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Errorf("expected ok to be false for an empty hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
73
jwt_test.go
73
jwt_test.go
|
|
@ -104,6 +104,79 @@ func TestMapClaims(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLifetimeMinutes(t *testing.T) {
|
||||||
|
j := newJWT(JWTOptions{
|
||||||
|
SigningKey: "testsigning",
|
||||||
|
LifetimeMinutes: 42,
|
||||||
|
})
|
||||||
|
if j.LifetimeMinutes() != 42 {
|
||||||
|
t.Errorf("expected lifetime 42, got %d", j.LifetimeMinutes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpiresAtIgnoreExpiry(t *testing.T) {
|
||||||
|
j := initiateJWTHelper(t)
|
||||||
|
token, err := j.GenerateToken(map[string]interface{}{
|
||||||
|
"userID": 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed generating token: %v", err)
|
||||||
|
}
|
||||||
|
exp, err := j.ExpiresAtIgnoreExpiry(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed testing expires at ignore expiry: %v", err)
|
||||||
|
}
|
||||||
|
expected := time.Now().Add(time.Duration(j.LifetimeMinutes()) * time.Minute)
|
||||||
|
if diff := exp.Sub(expected); diff > time.Minute || diff < -time.Minute {
|
||||||
|
t.Errorf("expected expiration close to %v, got %v", expected, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An expired token must still return its expiration (no validation performed).
|
||||||
|
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag"
|
||||||
|
_, err = j.ExpiresAtIgnoreExpiry(expiredToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected to read expiration of an expired token, got error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpiresAtIgnoreExpiryInvalid(t *testing.T) {
|
||||||
|
j := initiateJWTHelper(t)
|
||||||
|
_, err := j.ExpiresAtIgnoreExpiry("not-a-token")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for an invalid token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeTokenIgnoreExpiry(t *testing.T) {
|
||||||
|
j := initiateJWTHelper(t)
|
||||||
|
|
||||||
|
// An already expired token should still be decodable.
|
||||||
|
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag"
|
||||||
|
_, err := j.DecodeTokenIgnoreExpiry(expiredToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected to decode an expired token ignoring expiry, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := j.GenerateToken(map[string]interface{}{
|
||||||
|
"userID": 99,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed generating token: %v", err)
|
||||||
|
}
|
||||||
|
payload, err := j.DecodeTokenIgnoreExpiry(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed decoding token ignoring expiry: %v", err)
|
||||||
|
}
|
||||||
|
if fmt.Sprintf("%v", payload["userID"]) != "99" {
|
||||||
|
t.Errorf("expected userID 99, got %v", payload["userID"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// An invalid token must fail.
|
||||||
|
if _, err := j.DecodeTokenIgnoreExpiry("invalid"); err == nil {
|
||||||
|
t.Errorf("expected error decoding an invalid token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initiateJWTHelper(t *testing.T) *JWT {
|
func initiateJWTHelper(t *testing.T) *JWT {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
j := newJWT(JWTOptions{
|
j := newJWT(JWTOptions{
|
||||||
|
|
|
||||||
169
response_test.go
169
response_test.go
|
|
@ -1,7 +1,10 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -152,3 +155,169 @@ func TestCastBasicVarToString(t *testing.T) {
|
||||||
t.Errorf("failed test cast basic var to string")
|
t.Errorf("failed test cast basic var to string")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCastBasicVarToStringPanicsOnUnsupported(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r == nil {
|
||||||
|
t.Errorf("expected panic for unsupported type")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
res := Response{}
|
||||||
|
res.castBasicVarsToString(struct{ Name string }{Name: "x"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestText(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.Text("plain text")
|
||||||
|
if res.contentType != CONTENT_TYPE_TEXT {
|
||||||
|
t.Errorf("expected content type TEXT, got %q", res.contentType)
|
||||||
|
}
|
||||||
|
if string(res.body) != "plain text" {
|
||||||
|
t.Errorf("unexpected body %q", string(res.body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTML(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.HTML("<h1>Hi</h1>")
|
||||||
|
if res.contentType != CONTENT_TYPE_HTML {
|
||||||
|
t.Errorf("expected content type HTML, got %q", res.contentType)
|
||||||
|
}
|
||||||
|
if string(res.body) != "<h1>Hi</h1>" {
|
||||||
|
t.Errorf("unexpected body %q", string(res.body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetStatusCode(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.SetStatusCode(http.StatusCreated)
|
||||||
|
if res.statusCode != http.StatusCreated {
|
||||||
|
t.Errorf("expected status code 201, got %d", res.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetContentType(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.SetContentType(CONTENT_TYPE_JSON)
|
||||||
|
if res.overrideContentType != CONTENT_TYPE_JSON {
|
||||||
|
t.Errorf("expected override content type JSON, got %q", res.overrideContentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForceSendResponse(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.ForceSendResponse()
|
||||||
|
if !res.isTerminated {
|
||||||
|
t.Errorf("expected response to be terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTerminatedResponseIgnoresWrites(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.ForceSendResponse()
|
||||||
|
res.Text("ignored")
|
||||||
|
res.SetHeader("x", "y")
|
||||||
|
res.SetStatusCode(500)
|
||||||
|
|
||||||
|
if res.body != nil {
|
||||||
|
t.Errorf("expected body to be untouched after termination, got %q", string(res.body))
|
||||||
|
}
|
||||||
|
if len(res.headers) != 0 {
|
||||||
|
t.Errorf("expected no headers to be added after termination")
|
||||||
|
}
|
||||||
|
if res.statusCode != 0 {
|
||||||
|
t.Errorf("expected status code to stay 0 after termination, got %d", res.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirect(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.Redirect("https://example.com")
|
||||||
|
if res.redirectTo != "https://example.com" {
|
||||||
|
t.Errorf("expected redirect to 'https://example.com', got %q", res.redirectTo)
|
||||||
|
}
|
||||||
|
if res.redirectStatusCode != http.StatusTemporaryRedirect {
|
||||||
|
t.Errorf("expected default 307 redirect, got %d", res.redirectStatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirectUse303(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.Redirect("https://example.com", true)
|
||||||
|
if res.redirectStatusCode != http.StatusSeeOther {
|
||||||
|
t.Errorf("expected 303 redirect, got %d", res.redirectStatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirectInvalidUrlGetsLeadingSlash(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
// A relative path is treated as an invalid URL by the validator and should
|
||||||
|
// be normalized to an absolute path.
|
||||||
|
res.Redirect("dashboard")
|
||||||
|
if res.redirectTo != "/dashboard" {
|
||||||
|
t.Errorf("expected '/dashboard', got %q", res.redirectTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetRestoresDefaults(t *testing.T) {
|
||||||
|
res := Response{}
|
||||||
|
res.SetStatusCode(http.StatusTeapot)
|
||||||
|
res.SetContentType(CONTENT_TYPE_JSON)
|
||||||
|
res.Redirect("https://example.com")
|
||||||
|
res.Text("body")
|
||||||
|
res.reset()
|
||||||
|
|
||||||
|
if res.body != nil {
|
||||||
|
t.Errorf("expected body to be cleared")
|
||||||
|
}
|
||||||
|
if res.statusCode != http.StatusOK {
|
||||||
|
t.Errorf("expected status code to reset to 200, got %d", res.statusCode)
|
||||||
|
}
|
||||||
|
if res.contentType != CONTENT_TYPE_HTML {
|
||||||
|
t.Errorf("expected content type to reset to HTML, got %q", res.contentType)
|
||||||
|
}
|
||||||
|
if res.overrideContentType != "" {
|
||||||
|
t.Errorf("expected override content type to be cleared")
|
||||||
|
}
|
||||||
|
if res.redirectTo != "" {
|
||||||
|
t.Errorf("expected redirect to be cleared")
|
||||||
|
}
|
||||||
|
if res.isTerminated {
|
||||||
|
t.Errorf("expected termination flag to be cleared")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferFile(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
res := Response{HttpResponseWriter: w}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("file-content")
|
||||||
|
res.BufferFile("report.csv", "text/csv", buf)
|
||||||
|
|
||||||
|
rsp := w.Result()
|
||||||
|
if ct := rsp.Header.Get(CONTENT_TYPE); ct != "text/csv" {
|
||||||
|
t.Errorf("expected content type 'text/csv', got %q", ct)
|
||||||
|
}
|
||||||
|
if cd := rsp.Header.Get("Content-Disposition"); cd != "attachment; filename=report.csv" {
|
||||||
|
t.Errorf("unexpected content disposition %q", cd)
|
||||||
|
}
|
||||||
|
if w.Body.String() != "file-content" {
|
||||||
|
t.Errorf("unexpected body %q", w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferInline(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
res := Response{HttpResponseWriter: w}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("inline-content")
|
||||||
|
res.BufferInline("image.png", "image/png", buf)
|
||||||
|
|
||||||
|
rsp := w.Result()
|
||||||
|
if ct := rsp.Header.Get(CONTENT_TYPE); ct != "image/png" {
|
||||||
|
t.Errorf("expected content type 'image/png', got %q", ct)
|
||||||
|
}
|
||||||
|
if w.Body.String() != "inline-content" {
|
||||||
|
t.Errorf("unexpected body %q", w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,20 @@ func TestDeleteRequest(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPatchRequest(t *testing.T) {
|
||||||
|
r := NewRouter()
|
||||||
|
handler := Controller(func(c *Context) *Response {
|
||||||
|
c.GetLogger().Info(TEST_STR)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
r.Patch("/", handler)
|
||||||
|
|
||||||
|
route := r.GetRoutes()[0]
|
||||||
|
if route.Method != "patch" || route.Path != "/" {
|
||||||
|
t.Errorf("failed adding route with patch http method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPutRequest(t *testing.T) {
|
func TestPutRequest(t *testing.T) {
|
||||||
r := NewRouter()
|
r := NewRouter()
|
||||||
handler := Controller(func(c *Context) *Response {
|
handler := Controller(func(c *Context) *Response {
|
||||||
|
|
|
||||||
469
session_test.go
Normal file
469
session_test.go
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
|
||||||
|
// Use of this source code is governed by MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.smarteching.com/goffee/core/logger"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Pure helpers (no external dependencies)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestCreateAuthTokenHashedCacheKey(t *testing.T) {
|
||||||
|
k1 := CreateAuthTokenHashedCacheKey(1, "Mozilla/5.0")
|
||||||
|
k2 := CreateAuthTokenHashedCacheKey(1, "Mozilla/5.0")
|
||||||
|
if k1 != k2 {
|
||||||
|
t.Errorf("expected deterministic cache keys, got %q and %q", k1, k2)
|
||||||
|
}
|
||||||
|
// MD5 hex representation is 32 characters long.
|
||||||
|
if len(k1) != 32 {
|
||||||
|
t.Errorf("expected a 32 char md5 hex key, got %d chars (%q)", len(k1), k1)
|
||||||
|
}
|
||||||
|
k3 := CreateAuthTokenHashedCacheKey(2, "Mozilla/5.0")
|
||||||
|
if k1 == k3 {
|
||||||
|
t.Errorf("expected different keys for different user IDs")
|
||||||
|
}
|
||||||
|
k4 := CreateAuthTokenHashedCacheKey(1, "curl/8.0")
|
||||||
|
if k1 == k4 {
|
||||||
|
t.Errorf("expected different keys for different user agents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateEngineEnabled(t *testing.T) {
|
||||||
|
prev := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) })
|
||||||
|
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "")
|
||||||
|
if templateEngineEnabled() {
|
||||||
|
t.Errorf("expected template engine to be disabled when unset")
|
||||||
|
}
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "false")
|
||||||
|
if templateEngineEnabled() {
|
||||||
|
t.Errorf("expected template engine to be disabled for 'false'")
|
||||||
|
}
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "true")
|
||||||
|
if !templateEngineEnabled() {
|
||||||
|
t.Errorf("expected template engine to be enabled for 'true'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlidingRenewThresholdPercent(t *testing.T) {
|
||||||
|
if slidingRenewThresholdPercent != 0.25 {
|
||||||
|
t.Errorf("expected threshold 0.25, got %v", slidingRenewThresholdPercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewedTokenForRequest(t *testing.T) {
|
||||||
|
// A context without a request should not panic and return an empty string.
|
||||||
|
empty := &Context{}
|
||||||
|
if got := renewedTokenForRequest(empty); got != "" {
|
||||||
|
t.Errorf("expected empty token for nil request, got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := &Context{Request: &Request{httpRequest: r}}
|
||||||
|
if got := renewedTokenForRequest(c); got != "" {
|
||||||
|
t.Errorf("expected empty token before marking, got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
markRenewedTokenForRequest(c, "new-token")
|
||||||
|
if got := renewedTokenForRequest(c); got != "new-token" {
|
||||||
|
t.Errorf("expected 'new-token', got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkRenewedTokenForRequestNilSafety(t *testing.T) {
|
||||||
|
// Must not panic on a context without request/writer.
|
||||||
|
markRenewedTokenForRequest(&Context{}, "x")
|
||||||
|
markRenewedTokenForRequest(&Context{Request: &Request{}}, "x")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// SessionUser value store (in-memory only)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSessionUserGetSetIsAuthenticated(t *testing.T) {
|
||||||
|
// Construct a session without a cache, exercising the value map directly.
|
||||||
|
s := &SessionUser{values: make(map[string]interface{})}
|
||||||
|
|
||||||
|
if s.IsAuthenticated() {
|
||||||
|
t.Errorf("expected a fresh session to be unauthenticated")
|
||||||
|
}
|
||||||
|
if _, ok := s.Get("missing"); ok {
|
||||||
|
t.Errorf("expected 'missing' key to be absent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserGetUserID(t *testing.T) {
|
||||||
|
s := &SessionUser{userID: 7}
|
||||||
|
if s.GetUserID() != 7 {
|
||||||
|
t.Errorf("expected user ID 7, got %d", s.GetUserID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Redis-backed integration tests (skipped when Redis is unavailable)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
// newTestCache returns a Cache connected to a local Redis, or skips the test if
|
||||||
|
// Redis is not reachable. Tests that require a live cache call this helper.
|
||||||
|
func newTestCache(t *testing.T) *Cache {
|
||||||
|
t.Helper()
|
||||||
|
if os.Getenv("REDIS_HOST") == "" {
|
||||||
|
os.Setenv("REDIS_HOST", "127.0.0.1")
|
||||||
|
}
|
||||||
|
if os.Getenv("REDIS_PORT") == "" {
|
||||||
|
os.Setenv("REDIS_PORT", "6379")
|
||||||
|
}
|
||||||
|
os.Setenv("REDIS_DB", "0")
|
||||||
|
|
||||||
|
c := NewCache(CacheConfig{EnableCache: false})
|
||||||
|
// Probe the connection; skip if Redis is not available.
|
||||||
|
probeKey := fmt.Sprintf("goffee_test_probe_%d", time.Now().UnixNano())
|
||||||
|
if err := c.Set(probeKey, "1"); err != nil {
|
||||||
|
t.Skipf("redis is not available, skipping integration test: %v", err)
|
||||||
|
}
|
||||||
|
_ = c.Delete(probeKey)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionContext(r *http.Request, cch *Cache) *Context {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
return &Context{
|
||||||
|
Request: &Request{
|
||||||
|
httpRequest: r,
|
||||||
|
},
|
||||||
|
Response: &Response{
|
||||||
|
headers: []header{},
|
||||||
|
HttpResponseWriter: w,
|
||||||
|
},
|
||||||
|
GetLogger: loggerResolverForTest,
|
||||||
|
GetCache: func() *Cache { return cch },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggerResolverForTest() *logger.Logger {
|
||||||
|
return logger.NewLogger(&logger.LogNullDriver{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserSetGetDelete(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
|
||||||
|
s := &SessionUser{
|
||||||
|
context: c,
|
||||||
|
values: make(map[string]interface{}),
|
||||||
|
hashedSessionKey: CreateAuthTokenHashedCacheKey(1, "sess_test-agent"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Set("username", "alice"); err != nil {
|
||||||
|
t.Fatalf("failed setting session value: %v", err)
|
||||||
|
}
|
||||||
|
if v, ok := s.Get("username"); !ok || v != "alice" {
|
||||||
|
t.Errorf("expected username 'alice', got %v (ok=%v)", v, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value should be persisted to the cache.
|
||||||
|
cached, err := cch.Get(s.hashedSessionKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected value in cache: %v", err)
|
||||||
|
}
|
||||||
|
if cached == "" {
|
||||||
|
t.Errorf("expected cached session to be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete returns the removed value and persists the change.
|
||||||
|
deleted := s.Delete("username")
|
||||||
|
if deleted != "alice" {
|
||||||
|
t.Errorf("expected deleted value 'alice', got %v", deleted)
|
||||||
|
}
|
||||||
|
if _, ok := s.Get("username"); ok {
|
||||||
|
t.Errorf("expected username to be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() { _ = cch.Delete(s.hashedSessionKey) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserFlush(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
|
||||||
|
key := CreateAuthTokenHashedCacheKey(2, "sess_flush-agent")
|
||||||
|
if err := cch.Set(key, `{"a":"b"}`); err != nil {
|
||||||
|
t.Fatalf("failed seeding cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &SessionUser{
|
||||||
|
context: c,
|
||||||
|
values: map[string]interface{}{"a": "b"},
|
||||||
|
hashedSessionKey: key,
|
||||||
|
authenticated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Flush(); err != nil {
|
||||||
|
t.Fatalf("failed flushing session: %v", err)
|
||||||
|
}
|
||||||
|
if s.IsAuthenticated() {
|
||||||
|
t.Errorf("expected session to be unauthenticated after flush")
|
||||||
|
}
|
||||||
|
if len(s.values) != 0 {
|
||||||
|
t.Errorf("expected session values to be cleared")
|
||||||
|
}
|
||||||
|
if _, ok := s.Get("a"); ok {
|
||||||
|
t.Errorf("expected value 'a' to be removed after flush")
|
||||||
|
}
|
||||||
|
if _, err := cch.Get(key); err == nil {
|
||||||
|
t.Errorf("expected cache key to be deleted after flush")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserSaveEmptyDeletesKey(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
|
||||||
|
key := CreateAuthTokenHashedCacheKey(3, "sess_save-agent")
|
||||||
|
if err := cch.Set(key, `{"a":"b"}`); err != nil {
|
||||||
|
t.Fatalf("failed seeding cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &SessionUser{
|
||||||
|
context: c,
|
||||||
|
values: map[string]interface{}{},
|
||||||
|
hashedSessionKey: key,
|
||||||
|
}
|
||||||
|
if err := s.Save(); err != nil {
|
||||||
|
t.Fatalf("failed saving empty session: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := cch.Get(key); err == nil {
|
||||||
|
t.Errorf("expected cache key to be deleted when saving an empty session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserInitNoCookie(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
// With no cookie present Init must fail gracefully.
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := &Context{
|
||||||
|
Request: &Request{httpRequest: r},
|
||||||
|
Response: &Response{
|
||||||
|
HttpResponseWriter: httptest.NewRecorder(),
|
||||||
|
},
|
||||||
|
GetLogger: loggerResolverForTest,
|
||||||
|
}
|
||||||
|
s := &SessionUser{}
|
||||||
|
if s.Init(c) {
|
||||||
|
t.Errorf("expected Init to return false when no cookie is present")
|
||||||
|
}
|
||||||
|
if s.IsAuthenticated() {
|
||||||
|
t.Errorf("expected session to remain unauthenticated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSessionUserInitValidToken exercises the full happy path against Redis:
|
||||||
|
// JWT generation, cookie round-trip, cache verification and session loading.
|
||||||
|
func TestSessionUserInitValidToken(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
|
||||||
|
jwtSecret := "test-session-secret"
|
||||||
|
jwtObj := newJWT(JWTOptions{SigningKey: jwtSecret, LifetimeMinutes: 60})
|
||||||
|
token, err := jwtObj.GenerateToken(map[string]interface{}{"userID": 123})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed generating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := "goffee-test-agent"
|
||||||
|
hashedCacheKey := CreateAuthTokenHashedCacheKey(123, userAgent)
|
||||||
|
if err := cch.Set(hashedCacheKey, token); err != nil {
|
||||||
|
t.Fatalf("failed seeding cached token: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cch.Delete(hashedCacheKey) })
|
||||||
|
|
||||||
|
// Build the request with a valid encrypted goffee cookie.
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookie(w, "user@example.com", token); err != nil {
|
||||||
|
t.Fatalf("failed setting cookie: %v", err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
r.Header.Set("User-Agent", userAgent)
|
||||||
|
for _, ck := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(ck)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
c.GetJWT = func() *JWT { return jwtObj }
|
||||||
|
|
||||||
|
s := &SessionUser{}
|
||||||
|
if !s.Init(c) {
|
||||||
|
t.Fatalf("expected Init to succeed with a valid token")
|
||||||
|
}
|
||||||
|
if !s.IsAuthenticated() {
|
||||||
|
t.Errorf("expected session to be authenticated")
|
||||||
|
}
|
||||||
|
if s.GetUserID() != 123 {
|
||||||
|
t.Errorf("expected user ID 123, got %d", s.GetUserID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSessionUserInitWrongToken ensures a mismatched cached token fails Init.
|
||||||
|
func TestSessionUserInitWrongToken(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
|
||||||
|
jwtSecret := "test-session-secret"
|
||||||
|
jwtObj := newJWT(JWTOptions{SigningKey: jwtSecret, LifetimeMinutes: 60})
|
||||||
|
token, err := jwtObj.GenerateToken(map[string]interface{}{"userID": 5})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed generating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := "goffee-test-agent-2"
|
||||||
|
hashedCacheKey := CreateAuthTokenHashedCacheKey(5, userAgent)
|
||||||
|
// Cache a different token so the verification fails.
|
||||||
|
if err := cch.Set(hashedCacheKey, "a-different-token"); err != nil {
|
||||||
|
t.Fatalf("failed seeding cached token: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cch.Delete(hashedCacheKey) })
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if err := SetCookie(w, "user@example.com", token); err != nil {
|
||||||
|
t.Fatalf("failed setting cookie: %v", err)
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
r.Header.Set("User-Agent", userAgent)
|
||||||
|
for _, ck := range w.Result().Cookies() {
|
||||||
|
r.AddCookie(ck)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
c.GetJWT = func() *JWT { return jwtObj }
|
||||||
|
|
||||||
|
s := &SessionUser{}
|
||||||
|
if s.Init(c) {
|
||||||
|
t.Errorf("expected Init to fail when the cached token does not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserMaybeRenewSkipsWhenTemplatesDisabled(t *testing.T) {
|
||||||
|
prev := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
os.Setenv("TEMPLATE_ENABLE", "false")
|
||||||
|
t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) })
|
||||||
|
|
||||||
|
jwtObj := newJWT(JWTOptions{SigningKey: "k", LifetimeMinutes: 60})
|
||||||
|
token, _ := jwtObj.GenerateToken(map[string]interface{}{"userID": 1})
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := &Context{
|
||||||
|
Request: &Request{httpRequest: r},
|
||||||
|
Response: &Response{HttpResponseWriter: httptest.NewRecorder()},
|
||||||
|
GetLogger: loggerResolverForTest,
|
||||||
|
GetJWT: func() *JWT { return jwtObj },
|
||||||
|
GetCache: func() *Cache { return &Cache{} },
|
||||||
|
}
|
||||||
|
// Should simply return without renewing or panicking.
|
||||||
|
s := &SessionUser{}
|
||||||
|
s.maybeRenew(c, "user@example.com", 1, "key", token)
|
||||||
|
if renewedTokenForRequest(c) != "" {
|
||||||
|
t.Errorf("expected no renewal when templates are disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildTokenWithCustomLifetime signs a token whose exp is now + expiresIn, while
|
||||||
|
// the JWT object advertises lifetimeMinutes. This lets us simulate a token that
|
||||||
|
// appears to have been issued long ago (and is therefore due for renewal) while
|
||||||
|
// still being cryptographically valid.
|
||||||
|
func buildTokenWithCustomLifetime(t *testing.T, jwtObj *JWT, expiresIn time.Duration, payload map[string]interface{}) string {
|
||||||
|
t.Helper()
|
||||||
|
claims, err := mapClaims(payload, time.Now().Add(expiresIn))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed building claims: %v", err)
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
signed, err := token.SignedString(jwtObj.signingKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed signing token: %v", err)
|
||||||
|
}
|
||||||
|
return signed
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserMaybeRenewRenews(t *testing.T) {
|
||||||
|
cch := newTestCache(t)
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
|
||||||
|
// Lifetime 60 min, token expiring in 1 min → derived issuance ~59 min ago,
|
||||||
|
// which is well past the 25% (15 min) threshold → renewal must happen.
|
||||||
|
jwtObj := newJWT(JWTOptions{SigningKey: "renew-secret", LifetimeMinutes: 60})
|
||||||
|
token := buildTokenWithCustomLifetime(t, jwtObj, time.Minute, map[string]interface{}{"userID": 77})
|
||||||
|
|
||||||
|
userAgent := "renew-agent"
|
||||||
|
hashedCacheKey := CreateAuthTokenHashedCacheKey(77, userAgent)
|
||||||
|
if err := cch.Set(hashedCacheKey, token); err != nil {
|
||||||
|
t.Fatalf("failed seeding token: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cch.Delete(hashedCacheKey) })
|
||||||
|
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
r.Header.Set("User-Agent", userAgent)
|
||||||
|
c := newSessionContext(r, cch)
|
||||||
|
c.GetJWT = func() *JWT { return jwtObj }
|
||||||
|
|
||||||
|
s := &SessionUser{}
|
||||||
|
s.maybeRenew(c, "user@example.com", 77, hashedCacheKey, token)
|
||||||
|
|
||||||
|
renewed := renewedTokenForRequest(c)
|
||||||
|
if renewed == "" {
|
||||||
|
t.Fatalf("expected a renewed token to be issued")
|
||||||
|
}
|
||||||
|
if renewed == token {
|
||||||
|
t.Errorf("expected a brand new token different from the previous one")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cache must now hold the renewed token.
|
||||||
|
cached, err := cch.Get(hashedCacheKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed reading renewed token from cache: %v", err)
|
||||||
|
}
|
||||||
|
if cached != renewed {
|
||||||
|
t.Errorf("expected cache to store the renewed token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A renewed cookie must be written to the response.
|
||||||
|
if len(c.Response.HttpResponseWriter.(*httptest.ResponseRecorder).Result().Cookies()) == 0 {
|
||||||
|
t.Errorf("expected a renewed cookie to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionUserMaybeRenewSkipsWhenTooEarly(t *testing.T) {
|
||||||
|
enableTemplateCookieEnv(t)
|
||||||
|
|
||||||
|
// Lifetime 1000 minutes, token just issued → elapsed ~0 < threshold → no renewal.
|
||||||
|
jwtObj := newJWT(JWTOptions{SigningKey: "k", LifetimeMinutes: 1000})
|
||||||
|
token, _ := jwtObj.GenerateToken(map[string]interface{}{"userID": 1})
|
||||||
|
|
||||||
|
r := httptest.NewRequest(GET, LOCALHOST, nil)
|
||||||
|
c := &Context{
|
||||||
|
Request: &Request{httpRequest: r},
|
||||||
|
Response: &Response{HttpResponseWriter: httptest.NewRecorder()},
|
||||||
|
GetLogger: loggerResolverForTest,
|
||||||
|
GetJWT: func() *JWT { return jwtObj },
|
||||||
|
GetCache: func() *Cache { return &Cache{} },
|
||||||
|
}
|
||||||
|
s := &SessionUser{}
|
||||||
|
s.maybeRenew(c, "user@example.com", 1, "key", token)
|
||||||
|
if renewedTokenForRequest(c) != "" {
|
||||||
|
t.Errorf("expected no renewal before the threshold elapses")
|
||||||
|
}
|
||||||
|
}
|
||||||
303
templates_test.go
Normal file
303
templates_test.go
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
|
||||||
|
// Use of this source code is governed by MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type templateFieldSample struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasField(t *testing.T) {
|
||||||
|
s := templateFieldSample{Name: "test", Age: 3}
|
||||||
|
if !hasField(s, "Name") {
|
||||||
|
t.Errorf("expected struct to have field 'Name'")
|
||||||
|
}
|
||||||
|
if !hasField(&s, "Age") {
|
||||||
|
t.Errorf("expected pointer to struct to have field 'Age'")
|
||||||
|
}
|
||||||
|
if hasField(s, "Missing") {
|
||||||
|
t.Errorf("expected struct NOT to have field 'Missing'")
|
||||||
|
}
|
||||||
|
if hasField(42, "Name") {
|
||||||
|
t.Errorf("expected non-struct to have no fields")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapitalize(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"hello world", "Hello world"},
|
||||||
|
{"HELLO", "Hello"},
|
||||||
|
{"", ""},
|
||||||
|
{"a", "A"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := capitalize(tt.in); got != tt.want {
|
||||||
|
t.Errorf("capitalize(%q) = %q, want %q", tt.in, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepend(t *testing.T) {
|
||||||
|
if got := prepend("World", "Hello, "); got != "Hello, World" {
|
||||||
|
t.Errorf("prepend returned %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrAppend(t *testing.T) {
|
||||||
|
if got := strAppend("Hello", "!"); got != "Hello!" {
|
||||||
|
t.Errorf("strAppend returned %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitHelper(t *testing.T) {
|
||||||
|
got := split("a,b,c", ",")
|
||||||
|
if len(got) != 3 {
|
||||||
|
t.Fatalf("expected 3 parts, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0] != "a" || got[2] != "c" {
|
||||||
|
t.Errorf("unexpected split result: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncate(t *testing.T) {
|
||||||
|
if got := truncate(5, "hello world"); got != "hello…" {
|
||||||
|
t.Errorf("truncate returned %q", got)
|
||||||
|
}
|
||||||
|
if got := truncate(20, "short"); got != "short" {
|
||||||
|
t.Errorf("truncate should not modify short strings, got %q", got)
|
||||||
|
}
|
||||||
|
// Multibyte safety.
|
||||||
|
if got := truncate(3, "héllo"); got != "hél…" {
|
||||||
|
t.Errorf("truncate multibyte returned %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFmtNumber(t *testing.T) {
|
||||||
|
if got := fmtNumber(1000000); got != "1,000,000" {
|
||||||
|
t.Errorf("fmtNumber(int) = %q, want %q", got, "1,000,000")
|
||||||
|
}
|
||||||
|
if got := fmtNumber(int64(1500)); got != "1,500" {
|
||||||
|
t.Errorf("fmtNumber(int64) = %q, want %q", got, "1,500")
|
||||||
|
}
|
||||||
|
if got := fmtNumber(12.5); got != "12.50" {
|
||||||
|
t.Errorf("fmtNumber(float64) = %q, want %q", got, "12.50")
|
||||||
|
}
|
||||||
|
// Fallback for unsupported types.
|
||||||
|
if got := fmtNumber("abc"); got != "abc" {
|
||||||
|
t.Errorf("fmtNumber(string) = %q, want %q", got, "abc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFmtDate(t *testing.T) {
|
||||||
|
d := time.Date(2023, time.May, 2, 15, 4, 0, 0, time.UTC)
|
||||||
|
if got := fmtDate(d, "short"); got != "02 May 2023" {
|
||||||
|
t.Errorf("fmtDate(short) = %q", got)
|
||||||
|
}
|
||||||
|
if got := fmtDate(d, "long"); got != "02 May 2023" {
|
||||||
|
t.Errorf("fmtDate(long) = %q", got)
|
||||||
|
}
|
||||||
|
if got := fmtDate(d, "iso"); got != "2023-05-02" {
|
||||||
|
t.Errorf("fmtDate(iso) = %q", got)
|
||||||
|
}
|
||||||
|
if got := fmtDate(d, "datetime"); got != "02 May 2023 15:04" {
|
||||||
|
t.Errorf("fmtDate(datetime) = %q", got)
|
||||||
|
}
|
||||||
|
if got := fmtDate(d, "02/01/2006"); got != "02/05/2023" {
|
||||||
|
t.Errorf("fmtDate(custom) = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeAgo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
at time.Time
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"just now", time.Now().Add(-10 * time.Second), "just now"},
|
||||||
|
{"minutes", time.Now().Add(-5 * time.Minute), "5 minutes ago"},
|
||||||
|
{"one minute", time.Now().Add(-1 * time.Minute), "1 minute ago"},
|
||||||
|
{"hours", time.Now().Add(-3 * time.Hour), "3 hours ago"},
|
||||||
|
{"one hour", time.Now().Add(-1 * time.Hour), "1 hour ago"},
|
||||||
|
{"days", time.Now().Add(-48 * time.Hour), "2 days ago"},
|
||||||
|
{"one day", time.Now().Add(-24 * time.Hour), "1 day ago"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := timeAgo(tt.at); got != tt.want {
|
||||||
|
t.Errorf("timeAgo(%s) = %q, want %q", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlural(t *testing.T) {
|
||||||
|
if got := plural(1, "item"); got != "1 item" {
|
||||||
|
t.Errorf("plural(1) = %q", got)
|
||||||
|
}
|
||||||
|
if got := plural(3, "item"); got != "3 items" {
|
||||||
|
t.Errorf("plural(3) = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstAndLast(t *testing.T) {
|
||||||
|
items := []int{10, 20, 30}
|
||||||
|
if got := first(items); got != 10 {
|
||||||
|
t.Errorf("first = %v, want 10", got)
|
||||||
|
}
|
||||||
|
if got := last(items); got != 30 {
|
||||||
|
t.Errorf("last = %v, want 30", got)
|
||||||
|
}
|
||||||
|
if got := first([]int{}); got != nil {
|
||||||
|
t.Errorf("first of empty slice should be nil, got %v", got)
|
||||||
|
}
|
||||||
|
if got := last([]int{}); got != nil {
|
||||||
|
t.Errorf("last of empty slice should be nil, got %v", got)
|
||||||
|
}
|
||||||
|
if got := first("not a slice"); got != nil {
|
||||||
|
t.Errorf("first of non-slice should be nil, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSliceOf(t *testing.T) {
|
||||||
|
items := []int{1, 2, 3, 4, 5}
|
||||||
|
got, ok := sliceOf(items, 1, 4).([]int)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("sliceOf should return a slice")
|
||||||
|
}
|
||||||
|
if len(got) != 3 || got[0] != 2 || got[2] != 4 {
|
||||||
|
t.Errorf("sliceOf returned %v", got)
|
||||||
|
}
|
||||||
|
// Negative start should be clamped to 0.
|
||||||
|
got, _ = sliceOf(items, -5, 2).([]int)
|
||||||
|
if len(got) != 2 || got[0] != 1 {
|
||||||
|
t.Errorf("sliceOf with negative start returned %v", got)
|
||||||
|
}
|
||||||
|
// End beyond length should be clamped.
|
||||||
|
got, _ = sliceOf(items, 3, 100).([]int)
|
||||||
|
if len(got) != 2 || got[1] != 5 {
|
||||||
|
t.Errorf("sliceOf with large end returned %v", got)
|
||||||
|
}
|
||||||
|
if got := sliceOf("not slice", 0, 1); got != nil {
|
||||||
|
t.Errorf("sliceOf of non-slice should be nil, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsHelper(t *testing.T) {
|
||||||
|
if !contains("hello world", "world") {
|
||||||
|
t.Errorf("expected substring match")
|
||||||
|
}
|
||||||
|
if contains("hello", "xyz") {
|
||||||
|
t.Errorf("expected no substring match")
|
||||||
|
}
|
||||||
|
items := []string{"a", "b", "c"}
|
||||||
|
if !contains(items, "b") {
|
||||||
|
t.Errorf("expected element in slice")
|
||||||
|
}
|
||||||
|
if contains(items, "z") {
|
||||||
|
t.Errorf("expected element not in slice")
|
||||||
|
}
|
||||||
|
if contains(42, "x") {
|
||||||
|
t.Errorf("expected no match for non-string/non-slice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinHelper(t *testing.T) {
|
||||||
|
if got := join([]string{"a", "b", "c"}, ", "); got != "a, b, c" {
|
||||||
|
t.Errorf("join returned %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultVal(t *testing.T) {
|
||||||
|
if got := defaultVal("N/A", ""); got != "N/A" {
|
||||||
|
t.Errorf("defaultVal for empty string = %v, want N/A", got)
|
||||||
|
}
|
||||||
|
if got := defaultVal("N/A", nil); got != "N/A" {
|
||||||
|
t.Errorf("defaultVal for nil = %v, want N/A", got)
|
||||||
|
}
|
||||||
|
if got := defaultVal("N/A", 0); got != "N/A" {
|
||||||
|
t.Errorf("defaultVal for zero = %v, want N/A", got)
|
||||||
|
}
|
||||||
|
if got := defaultVal("N/A", "value"); got != "value" {
|
||||||
|
t.Errorf("defaultVal for present value = %v, want value", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTernary(t *testing.T) {
|
||||||
|
if got := ternary("yes", "no", true); got != "yes" {
|
||||||
|
t.Errorf("ternary(true) = %v", got)
|
||||||
|
}
|
||||||
|
if got := ternary("yes", "no", false); got != "no" {
|
||||||
|
t.Errorf("ternary(false) = %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoalesce(t *testing.T) {
|
||||||
|
if got := coalesce(nil, "", "first", "second"); got != "first" {
|
||||||
|
t.Errorf("coalesce = %v, want first", got)
|
||||||
|
}
|
||||||
|
if got := coalesce(nil, ""); got != nil {
|
||||||
|
t.Errorf("coalesce of all empty should be nil, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncMap(t *testing.T) {
|
||||||
|
fm := funcMap()
|
||||||
|
expected := []string{
|
||||||
|
"hasField", "capitalize", "prepend", "strAppend", "split", "truncate",
|
||||||
|
"fmtNumber", "fmtDate", "timeAgo", "first", "last", "sliceOf",
|
||||||
|
"contains", "join", "defaultVal", "ternary", "coalesce",
|
||||||
|
}
|
||||||
|
for _, name := range expected {
|
||||||
|
if _, ok := fm[name]; !ok {
|
||||||
|
t.Errorf("expected funcMap to contain %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed all:testingdata/templates
|
||||||
|
var testTemplatesFS embed.FS
|
||||||
|
|
||||||
|
func TestNewTemplatesAndRenderNamed(t *testing.T) {
|
||||||
|
// Register the built-in components plus our test templates.
|
||||||
|
NewTemplates(components_resources, testTemplatesFS)
|
||||||
|
if tmpl == nil {
|
||||||
|
t.Fatalf("expected templates to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := RenderNamedTemplate("test_greeting", map[string]interface{}{"Name": "Goffee"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed rendering named template: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "Hello, Goffee!") {
|
||||||
|
t.Errorf("unexpected rendered output: %q", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderNamedTemplateMissing(t *testing.T) {
|
||||||
|
NewTemplates(components_resources, testTemplatesFS)
|
||||||
|
_, err := RenderNamedTemplate("does_not_exist", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error rendering a missing template")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderNamedTemplateUsesFuncMap(t *testing.T) {
|
||||||
|
NewTemplates(components_resources, testTemplatesFS)
|
||||||
|
out, err := RenderNamedTemplate("test_funcmap", map[string]interface{}{"Raw": "hello world"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed rendering named template: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "Hello world") {
|
||||||
|
t.Errorf("expected capitalize helper to be applied, got %q", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
1
testingdata/templates/funcmap.html
Normal file
1
testingdata/templates/funcmap.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{{define "test_funcmap"}}{{capitalize .Raw}}{{end}}
|
||||||
1
testingdata/templates/greeting.html
Normal file
1
testingdata/templates/greeting.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{{define "test_greeting"}}Hello, {{.Name}}!{{end}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue