Upgrade and add new testing functions

This commit is contained in:
Zeni Kim 2026-09-13 00:09:50 -05:00
parent d12679df65
commit 9330ed8e74
13 changed files with 1867 additions and 130 deletions

118
cache_test.go Normal file
View 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)
}
}

View file

@ -157,7 +157,7 @@ func TestGetPathParams(t *testing.T) {
}
a := New()
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"))
return c.Response.Text(rsp)
}), nil)
@ -178,11 +178,13 @@ func TestGetRequestParams(t *testing.T) {
app.SetBasePath(pwd)
hr := httprouter.New()
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"))
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"))
return nil
}))
@ -218,11 +220,13 @@ func TestRequestParamsExists(t *testing.T) {
app := New()
hr := httprouter.New()
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"))
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"))
return nil
}))
@ -259,11 +263,13 @@ func TestGetHeader(t *testing.T) {
app := New()
hr := httprouter.New()
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"))
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"))
return nil
}))
@ -309,8 +315,13 @@ func TestGetUploadedFile(t *testing.T) {
app := New()
hr := httprouter.New()
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)
fmt.Fprintln(c.Response.HttpResponseWriter, rs)
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 {
t.Helper()
return &Context{

294
cookies_test.go Normal file
View 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)
}

View file

@ -50,7 +50,8 @@ func TestMakeHTTPHandlerFunc(t *testing.T) {
app.SetLogsDriver(&logger.LogFileDriver{
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.WriteString("DFT2V56H")
c.Response.SetHeader("header-key", "header-val")
@ -76,7 +77,8 @@ func TestMakeHTTPHandlerFuncVerifyJson(t *testing.T) {
app.SetLogsDriver(&logger.LogFileDriver{
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.WriteString("DFT2V56H")
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)
UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") }))
if len(app.middlewares.GetMiddlewares()) != 1 {
t.Errorf("failed testing use middleware")
app.Bootstrap()
UseHook(Hook(func(c *Context) { c.GetLogger().Info("Testing!") }))
if len(ResolveHooks().GetHooks()) != 1 {
t.Errorf("failed testing use hook")
}
}
func TestChainReset(t *testing.T) {
c := &chain{}
c.nodes = append(c.nodes, Middleware(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("Testing1!") }))
c.nodes = append(c.nodes, Hook(func(c *Context) { c.GetLogger().Info("Testing2!") }))
c.reset()
if len(c.nodes) != 0 {
@ -151,11 +154,11 @@ func TestChainReset(t *testing.T) {
func TestNext(t *testing.T) {
app := createNewApp(t)
app.t = 0
app.Bootstrap()
tfPath := filepath.Join(t.TempDir(), uuid.NewString())
var hs []interface{}
hs = append(hs, Middleware(func(c *Context) { c.Next() }))
hs = append(hs, Handler(func(c *Context) *Response {
hs = append(hs, Hook(func(c *Context) { c.Next() }))
hs = append(hs, Controller(func(c *Context) *Response {
f, _ := os.Create(tfPath)
f.WriteString("DFT2V56H")
return nil
@ -165,7 +168,7 @@ func TestNext(t *testing.T) {
app.chain.execute(makeCTX(t))
cnt, _ := os.ReadFile(tfPath)
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{}
tf := filepath.Join(t.TempDir(), uuid.NewString())
var hs []interface{}
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing!") }))
hs = append(hs, Middleware(func(c *Context) {
hs = append(hs, Hook(func(c *Context) { c.GetLogger().Info("testing!") }))
hs = append(hs, Hook(func(c *Context) {
f, _ := os.Create(tf)
f.WriteString("DFT2V56H")
}))
c.nodes = hs
pf := c.getByIndex(1)
f, ok := pf.(Middleware)
f, ok := pf.(Hook)
if ok {
f(makeCTX(t))
}
@ -192,10 +195,11 @@ func TestChainGetByIndex(t *testing.T) {
func TestPrepareChain(t *testing.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{}
hs = append(hs, Middleware(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("testing1!") }))
hs = append(hs, Hook(func(c *Context) { c.GetLogger().Info("testing2!") }))
app.prepareChain(hs)
if len(app.chain.nodes) != 3 {
t.Errorf("failed preparing chain")
@ -207,7 +211,7 @@ func TestChainExecute(t *testing.T) {
f1Path := filepath.Join(tmpDir, uuid.NewString())
c := &chain{}
c.nodes = []interface{}{
Handler(func(c *Context) *Response {
Controller(func(c *Context) *Response {
tf, _ := os.Create(f1Path)
defer tf.Close()
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)
t1 := Handler(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil })
t2 := Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") })
t1 := Controller(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil })
t2 := Hook(func(c *Context) { c.GetLogger().Info("Testing2!") })
mw := []Middleware{t2}
mw := []Hook{t2}
comb := app.combHandlers(t1, mw)
if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[0]).Pointer() {
t.Errorf("failed testing reverse handlers")
// combHandlers builds the slice as [hooks..., controller]
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() {
t.Errorf("failed testing reverse handlers")
if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[1]).Pointer() {
t.Errorf("failed testing comb handlers: controller should come last")
}
}
@ -259,31 +267,31 @@ func TestRegisterGetRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Get("/", Handler(func(c *Context) *Response {
gcr.Get("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Post("/", Handler(func(c *Context) *Response {
gcr.Post("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Delete("/", Handler(func(c *Context) *Response {
gcr.Delete("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Patch("/", Handler(func(c *Context) *Response {
gcr.Patch("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Put("/", Handler(func(c *Context) *Response {
gcr.Put("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Options("/", Handler(func(c *Context) *Response {
gcr.Options("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Head("/", Handler(func(c *Context) *Response {
gcr.Head("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -313,7 +321,7 @@ func TestRegisterPostRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/", Handler(func(c *Context) *Response {
gcr.Post("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -343,7 +351,7 @@ func TestRegisterDeleteRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Delete("/", Handler(func(c *Context) *Response {
gcr.Delete("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -373,7 +381,7 @@ func TestRegisterPatchRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Patch("/", Handler(func(c *Context) *Response {
gcr.Patch("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -403,7 +411,7 @@ func TestRegisterPutRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Put("/", Handler(func(c *Context) *Response {
gcr.Put("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -433,7 +441,7 @@ func TestRegisterOptionsRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Options("/", Handler(func(c *Context) *Response {
gcr.Options("/", Controller(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
@ -464,7 +472,7 @@ func TestRegisterHeadRoute(t *testing.T) {
hr := httprouter.New()
gcr := NewRouter()
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")
p, _ := param.(string)
f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 777)

View file

@ -2,7 +2,12 @@ package core
import (
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/google/uuid"
)
func TestNewEventsManager(t *testing.T) {
@ -12,92 +17,171 @@ func TestNewEventsManager(t *testing.T) {
}
}
// func TestResolveEventsManager(t *testing.T) {
// NewEventsManager()
// m := ResolveEventsManager()
// if fmt.Sprintf("%T", m) != "*core.EventsManager" {
// t.Errorf("failed testing new events manager")
// }
// }
func TestResolveEventsManager(t *testing.T) {
NewEventsManager()
m := ResolveEventsManager()
if fmt.Sprintf("%T", m) != "*core.EventsManager" {
t.Errorf("failed testing resolve events manager")
}
}
// func TestEvents(t *testing.T) {
// pwd, _ := os.Getwd()
// const eventName1 string = "test-event-name1"
// 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()
func TestEventsFireAndProcess(t *testing.T) {
const eventName1 string = "test-event-name1"
const eventName2 string = "test-event-name2"
// ff, err := os.Open(tmpFile1)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
tmpDir := t.TempDir()
tmpFile1 := filepath.Join(tmpDir, uuid.NewString())
tmpFile2 := filepath.Join(tmpDir, uuid.NewString())
tmpFile3 := filepath.Join(tmpDir, uuid.NewString())
// d, err := io.ReadAll(ff)
// if string(d) != eventName1 {
// t.Error("faild testing events")
// }
// ff.Close()
// os.Remove(tmpFile1)
m := NewEventsManager()
// ff, err = os.Open(tmpFile3)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
// Two jobs registered on the same event must BOTH run.
m.Register(eventName1, func(event *Event, requestContext *Context) {
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 string(d) != eventName1 {
// t.Error("faild testing events")
// }
// ff.Close()
// os.Remove(tmpFile3)
if err := m.Fire(&Event{Name: eventName1}); err != nil {
t.Fatalf("failed firing event: %v", err)
}
m.processFiredEvents()
// m.Register(eventName2, func(event *Event, requestContext *Context) {
// f, err := os.Create(tmpFile2)
// if err != nil {
// t.Errorf("error testing register event: %v", err.Error())
// }
// f.WriteString(event.Name)
// f.Close()
// })
// m.Fire(&Event{Name: eventName2})
// m.processFiredEvents()
for _, fp := range []string{tmpFile1, tmpFile3} {
f, err := os.Open(fp)
if err != nil {
t.Errorf("error opening event file %v: %v", fp, err.Error())
continue
}
d, err := io.ReadAll(f)
if err != nil {
t.Errorf("error reading event file %v: %v", fp, err.Error())
}
if string(d) != eventName1 {
t.Errorf("failed testing events: expected %q, got %q", eventName1, string(d))
}
f.Close()
}
// ff, err = os.Open(tmpFile2)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
// A registered event with a distinct payload is processed independently.
m.Register(eventName2, func(event *Event, requestContext *Context) {
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)
// if string(d) != eventName2 {
// t.Error("faild testing events")
// }
// ff.Close()
// }
f, err := os.Open(tmpFile2)
if err != nil {
t.Fatalf("error opening event file: %v", err.Error())
}
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
View 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")
}
}

View file

@ -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 {
t.Helper()
j := newJWT(JWTOptions{

View file

@ -1,7 +1,10 @@
package core
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
@ -152,3 +155,169 @@ func TestCastBasicVarToString(t *testing.T) {
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())
}
}

View file

@ -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) {
r := NewRouter()
handler := Controller(func(c *Context) *Response {

469
session_test.go Normal file
View 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
View 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))
}
}

View file

@ -0,0 +1 @@
{{define "test_funcmap"}}{{capitalize .Raw}}{{end}}

View file

@ -0,0 +1 @@
{{define "test_greeting"}}Hello, {{.Name}}!{{end}}