Added unit tests for steps up to 1.3.2

master
oabrivard 1 month ago
parent b7d3ed051c
commit 995c452408

@ -13,6 +13,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
@ -22,5 +23,6 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -8,7 +8,7 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -7,7 +7,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -7,7 +7,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -7,7 +7,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -7,7 +7,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -11,6 +11,7 @@ require (
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect

@ -7,7 +7,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=

@ -0,0 +1,40 @@
package errors
// Tests for domain error formatting, wrapping, and code matching.
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
// TestDomainError_ErrorFormatting verifies error strings with and without wrapped errors.
func TestDomainError_ErrorFormatting(t *testing.T) {
base := New(CodeNotFound, "missing")
require.Equal(t, "[NOT_FOUND] missing", base.Error())
wrapped := Wrap(CodeInvalidInput, "bad", errors.New("details"))
require.Equal(t, "[INVALID_INPUT] bad: details", wrapped.Error())
}
// TestDomainError_Is ensures errors.Is matches on error code.
func TestDomainError_Is(t *testing.T) {
err := Wrap(CodeForbidden, "nope", nil)
target := New(CodeForbidden, "other")
require.True(t, errors.Is(err, target))
}
// TestDomainError_NewAndWrapFields verifies fields are set for New and Wrap.
func TestDomainError_NewAndWrapFields(t *testing.T) {
created := New(CodeConflict, "conflict")
require.Equal(t, CodeConflict, created.Code)
require.Equal(t, "conflict", created.Message)
require.Nil(t, created.Err)
root := errors.New("root")
wrapped := Wrap(CodeInternal, "internal", root)
require.Equal(t, CodeInternal, wrapped.Code)
require.Equal(t, "internal", wrapped.Message)
require.Equal(t, root, wrapped.Unwrap())
}

@ -0,0 +1,28 @@
package events
// Tests for event base fields and event type string behavior.
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestNewBaseEvent verifies event fields and timestamp range.
func TestNewBaseEvent(t *testing.T) {
before := time.Now()
base := NewBaseEvent(GameSessionStarted, "agg-1", "session")
after := time.Now()
require.Equal(t, GameSessionStarted, base.EventType())
require.Equal(t, "agg-1", base.AggregateID())
require.Equal(t, "session", base.AggregateType())
require.True(t, base.OccurredAt().After(before) || base.OccurredAt().Equal(before))
require.True(t, base.OccurredAt().Before(after) || base.OccurredAt().Equal(after))
}
// TestEventType_String ensures event type string returns the underlying value.
func TestEventType_String(t *testing.T) {
require.Equal(t, "game_session.started", GameSessionStarted.String())
}

@ -0,0 +1,23 @@
package types
// Tests for ID generation and UUID validation behavior.
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestID_NewIDIsValid ensures a new ID is non-empty and valid UUID.
func TestID_NewIDIsValid(t *testing.T) {
id := NewID()
require.False(t, id.IsEmpty())
require.True(t, id.IsValid())
}
// TestID_IsValid verifies validity for empty, invalid, and generated IDs.
func TestID_IsValid(t *testing.T) {
require.False(t, ID("").IsValid())
require.False(t, IDFromString("not-a-uuid").IsValid())
require.True(t, NewID().IsValid())
}

@ -0,0 +1,32 @@
package types
// Tests for pagination limit, offset, normalization, and page navigation helpers.
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestPagination_LimitOffsetNormalize verifies defaulting, clamping, and offset calculation.
func TestPagination_LimitOffsetNormalize(t *testing.T) {
p := Pagination{Page: 0, PageSize: 0}
require.Equal(t, 0, p.Offset())
require.Equal(t, DefaultPageSize, p.Limit())
p = Pagination{Page: 2, PageSize: MaxPageSize + 10}
require.Equal(t, MaxPageSize, p.Limit())
p.Normalize()
require.Equal(t, 2, p.Page)
require.Equal(t, MaxPageSize, p.PageSize)
}
// TestPaginatedResult verifies total pages and page navigation helpers.
func TestPaginatedResult(t *testing.T) {
pagination := Pagination{Page: 1, PageSize: 2}
result := NewPaginatedResult([]string{"a", "b"}, 3, pagination)
require.Equal(t, 2, result.TotalPages)
require.True(t, result.HasNextPage())
require.False(t, result.HasPreviousPage())
}

@ -0,0 +1,55 @@
package valueobjects
// Tests for player name validation rules: length bounds, normalization, and character restrictions.
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
errs "knowfoolery/backend/shared/domain/errors"
)
// TestNewPlayerName_NormalizesAndTrims verifies whitespace trimming and space normalization.
func TestNewPlayerName_NormalizesAndTrims(t *testing.T) {
name, err := NewPlayerName(" Alice Bob ")
require.NoError(t, err)
require.Equal(t, "Alice Bob", name.String())
}
// TestNewPlayerName_LengthValidation ensures too-short and too-long names are rejected.
func TestNewPlayerName_LengthValidation(t *testing.T) {
_, err := NewPlayerName("A")
require.Error(t, err)
longName := strings.Repeat("a", MaxPlayerNameLength+1)
_, err = NewPlayerName(longName)
require.Error(t, err)
}
// TestNewPlayerName_InvalidCharacters verifies disallowed characters yield an invalid player name error.
func TestNewPlayerName_InvalidCharacters(t *testing.T) {
_, err := NewPlayerName("Alice!")
require.Error(t, err)
var domainErr *errs.DomainError
require.ErrorAs(t, err, &domainErr)
require.Equal(t, errs.CodeInvalidPlayerName, domainErr.Code)
}
// TestPlayerName_EqualsCaseInsensitive confirms equality ignores case differences.
func TestPlayerName_EqualsCaseInsensitive(t *testing.T) {
name1, err := NewPlayerName("Alice")
require.NoError(t, err)
name2, err := NewPlayerName("alice")
require.NoError(t, err)
require.True(t, name1.Equals(name2))
}
// TestPlayerName_IsEmpty confirms the zero value reports empty.
func TestPlayerName_IsEmpty(t *testing.T) {
var name PlayerName
require.True(t, name.IsEmpty())
}

@ -0,0 +1,51 @@
package valueobjects
// Tests for score calculations, clamping, and attempt logic.
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestNewScore_ClampsNegativeToZero ensures negative inputs are clamped to zero.
func TestNewScore_ClampsNegativeToZero(t *testing.T) {
score := NewScore(-5)
require.Equal(t, 0, score.Value())
}
// TestScore_AddClampsBelowZero ensures adding negative points does not drop below zero.
func TestScore_AddClampsBelowZero(t *testing.T) {
score := NewScore(2)
updated := score.Add(-10)
require.Equal(t, 0, updated.Value())
}
// TestCalculateQuestionScore verifies scoring for correct/incorrect and hint usage.
func TestCalculateQuestionScore(t *testing.T) {
cases := []struct {
name string
correct bool
usedHint bool
expected int
}{
{"incorrect", false, false, ScoreIncorrect},
{"correct_with_hint", true, true, ScoreWithHint},
{"correct_no_hint", true, false, MaxScorePerQuestion},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.expected, CalculateQuestionScore(tc.correct, tc.usedHint))
})
}
}
// TestAttempts verifies retry eligibility and remaining attempts calculation.
func TestAttempts(t *testing.T) {
require.True(t, CanRetry(MaxAttempts-1))
require.False(t, CanRetry(MaxAttempts))
require.Equal(t, MaxAttempts-1, RemainingAttempts(1))
require.Equal(t, 0, RemainingAttempts(MaxAttempts+1))
}

@ -8,21 +8,25 @@ require (
github.com/google/uuid v1.6.0
github.com/prometheus/client_golang v1.20.5
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
@ -34,4 +38,5 @@ require (
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

@ -5,6 +5,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
@ -28,6 +29,9 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -49,11 +53,12 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
@ -73,5 +78,6 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,43 @@
package rbac
// Tests for RBAC role permissions and role validation.
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestRolePermissions verifies role permission checks for individual and aggregate queries.
func TestRolePermissions(t *testing.T) {
require.True(t, HasPermission(RolePlayer, PermissionPlayGame))
require.False(t, HasPermission(RolePlayer, PermissionManageSystem))
require.True(t, HasAnyPermission(RoleModerator, PermissionViewUsers, PermissionManageSystem))
require.False(t, HasAnyPermission(RolePlayer, PermissionManageSystem))
require.True(t, HasAllPermissions(RoleAdmin, PermissionManageSystem, PermissionViewDashboard))
require.False(t, HasAllPermissions(RolePlayer, PermissionViewDashboard, PermissionPlayGame))
}
// TestUserHasPermission verifies role strings grant expected permissions.
func TestUserHasPermission(t *testing.T) {
require.True(t, UserHasPermission([]string{"player"}, PermissionPlayGame))
require.False(t, UserHasPermission([]string{"player"}, PermissionManageSystem))
}
// TestGetPermissionsReturnsCopy ensures returned permission slices are not shared.
func TestGetPermissionsReturnsCopy(t *testing.T) {
perms := GetPermissions(RolePlayer)
require.NotEmpty(t, perms)
perms[0] = PermissionManageSystem
fresh := GetPermissions(RolePlayer)
require.Equal(t, PermissionPlayGame, fresh[0])
}
// TestIsValidRole verifies known roles are accepted and unknown roles rejected.
func TestIsValidRole(t *testing.T) {
require.True(t, IsValidRole("player"))
require.False(t, IsValidRole("ghost"))
}

@ -0,0 +1,60 @@
package zitadel
// Tests for Zitadel client user info calls and placeholder methods.
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestGetUserInfo_Success verifies user info retrieval on a 200 response.
func TestGetUserInfo_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(UserInfo{
ID: "user-1",
Email: "a@b.com",
Name: "Alice",
Verified: true,
Roles: []string{"player"},
})
}))
defer server.Close()
client := NewClient(Config{BaseURL: server.URL, Timeout: 2 * time.Second})
info, err := client.GetUserInfo(context.Background(), "token")
require.NoError(t, err)
require.Equal(t, "user-1", info.ID)
}
// TestGetUserInfo_NonOK verifies non-200 responses return an error.
func TestGetUserInfo_NonOK(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
client := NewClient(Config{BaseURL: server.URL, Timeout: 2 * time.Second})
_, err := client.GetUserInfo(context.Background(), "token")
require.Error(t, err)
}
// TestClient_NotImplementedMethods verifies placeholder methods return errors.
func TestClient_NotImplementedMethods(t *testing.T) {
client := NewClient(DefaultConfig())
_, err := client.ValidateToken(context.Background(), "token")
require.Error(t, err)
_, err = client.RefreshToken(context.Background(), "refresh")
require.Error(t, err)
require.Error(t, client.RevokeToken(context.Background(), "token"))
}

@ -26,7 +26,7 @@ const (
// JWTMiddlewareConfig holds configuration for the JWT middleware.
type JWTMiddlewareConfig struct {
Client *Client
Client TokenValidator
Issuer string
Audience string
RequiredClaims []string
@ -34,6 +34,11 @@ type JWTMiddlewareConfig struct {
SkipPaths []string
}
// TokenValidator defines the interface for validating JWT tokens.
type TokenValidator interface {
ValidateToken(ctx context.Context, token string) (*AuthClaims, error)
}
// JWTMiddleware creates a Fiber middleware for JWT validation.
func JWTMiddleware(config JWTMiddlewareConfig) fiber.Handler {
return func(c fiber.Ctx) error {

@ -0,0 +1,136 @@
package zitadel
// Tests for JWT middleware authentication, admin checks, and bypass paths.
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
)
type fakeValidator struct {
claims *AuthClaims
err error
called int
}
func (f *fakeValidator) ValidateToken(ctx context.Context, token string) (*AuthClaims, error) {
f.called++
return f.claims, f.err
}
// TestJWTMiddleware_Success verifies valid tokens populate context and allow requests.
func TestJWTMiddleware_Success(t *testing.T) {
validator := &fakeValidator{claims: &AuthClaims{
Subject: "user-1",
Email: "a@b.com",
Name: "Alice",
Roles: []string{"player"},
MFAVerified: true,
}}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator}))
app.Get("/", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"user_id": GetUserID(c),
"email": GetUserEmail(c),
"roles": GetUserRoles(c),
})
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer token")
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, "user-1", body["user_id"])
}
// TestJWTMiddleware_MissingHeader verifies missing Authorization header returns 401.
func TestJWTMiddleware_MissingHeader(t *testing.T) {
validator := &fakeValidator{}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
// TestJWTMiddleware_InvalidHeaderFormat verifies malformed Authorization header returns 401.
func TestJWTMiddleware_InvalidHeaderFormat(t *testing.T) {
validator := &fakeValidator{}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator}))
app.Get("/", func(c fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Token abc")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
// TestJWTMiddleware_AdminRoleRequired ensures admin paths reject non-admin roles.
func TestJWTMiddleware_AdminRoleRequired(t *testing.T) {
validator := &fakeValidator{claims: &AuthClaims{
Subject: "user-1",
Roles: []string{"player"},
MFAVerified: true,
}}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator, AdminEndpoints: []string{"/admin"}}))
app.Get("/admin/stats", func(c fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
req := httptest.NewRequest(http.MethodGet, "/admin/stats", nil)
req.Header.Set("Authorization", "Bearer token")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
}
// TestJWTMiddleware_MFARequiredForAdmin ensures admin paths require MFA verification.
func TestJWTMiddleware_MFARequiredForAdmin(t *testing.T) {
validator := &fakeValidator{claims: &AuthClaims{
Subject: "user-1",
Roles: []string{"admin"},
MFAVerified: false,
}}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator, AdminEndpoints: []string{"/admin"}}))
app.Get("/admin/stats", func(c fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
req := httptest.NewRequest(http.MethodGet, "/admin/stats", nil)
req.Header.Set("Authorization", "Bearer token")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
}
// TestJWTMiddleware_SkipPath verifies skip paths bypass token validation.
func TestJWTMiddleware_SkipPath(t *testing.T) {
validator := &fakeValidator{err: fiber.ErrUnauthorized}
app := fiber.New()
app.Use(JWTMiddleware(JWTMiddlewareConfig{Client: validator, SkipPaths: []string{"/public"}}))
app.Get("/public/health", func(c fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
req := httptest.NewRequest(http.MethodGet, "/public/health", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, 0, validator.called)
}

@ -0,0 +1,31 @@
package postgres
// Tests for PostgreSQL config defaults and connection string generation.
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
// TestDefaultConfig verifies default configuration values for the client.
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
require.Equal(t, "localhost", cfg.Host)
require.Equal(t, 5432, cfg.Port)
}
// TestConfigDSNAndURL verifies DSN and URL formatting include expected parts.
func TestConfigDSNAndURL(t *testing.T) {
cfg := DefaultConfig()
require.Contains(t, cfg.DSN(), "host=localhost")
require.Contains(t, cfg.URL(), "postgresql://")
}
// TestHealthCheck verifies health checks delegate to Ping without error.
func TestHealthCheck(t *testing.T) {
client, err := NewClient(DefaultConfig())
require.NoError(t, err)
require.NoError(t, client.HealthCheck(context.Background()))
}

@ -0,0 +1,46 @@
package redis
// Tests for Redis config defaults, address formatting, and placeholder methods.
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
// TestDefaultConfig verifies default configuration values for the client.
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
require.Equal(t, "localhost", cfg.Host)
require.Equal(t, 6379, cfg.Port)
}
// TestAddr verifies Redis address formatting from config.
func TestAddr(t *testing.T) {
cfg := DefaultConfig()
require.Equal(t, "localhost:6379", cfg.Addr())
}
// TestHealthCheck verifies health checks delegate to Ping without error.
func TestHealthCheck(t *testing.T) {
client, err := NewClient(DefaultConfig())
require.NoError(t, err)
require.NoError(t, client.HealthCheck(context.Background()))
}
// TestNotImplemented verifies placeholder Redis methods return errors.
func TestNotImplemented(t *testing.T) {
client, err := NewClient(DefaultConfig())
require.NoError(t, err)
require.Error(t, client.Set(context.Background(), "k", "v", 0))
_, err = client.Get(context.Background(), "k")
require.Error(t, err)
require.Error(t, client.Delete(context.Background(), "k"))
_, err = client.Exists(context.Background(), "k")
require.Error(t, err)
_, err = client.Incr(context.Background(), "k")
require.Error(t, err)
require.Error(t, client.Expire(context.Background(), "k", 0))
}

@ -0,0 +1,32 @@
package logging
// Tests for logger construction and context-enriched loggers.
import (
"io"
"testing"
"github.com/stretchr/testify/require"
)
// TestNewLogger_NoPanic verifies logger creation handles invalid levels and nil output.
func TestNewLogger_NoPanic(t *testing.T) {
require.NotPanics(t, func() {
_ = NewLogger(Config{Level: "invalid", Environment: "development", Output: io.Discard})
})
require.NotPanics(t, func() {
_ = NewLogger(Config{Level: "info", Environment: "production", Output: nil})
})
}
// TestLogger_WithFields verifies field-enriched loggers are created.
func TestLogger_WithFields(t *testing.T) {
logger := NewLogger(DefaultConfig())
withField := logger.WithField("key", "value")
withFields := logger.WithFields(map[string]interface{}{"a": 1})
require.NotNil(t, withField)
require.NotNil(t, withFields)
require.NotNil(t, logger.Zerolog())
}

@ -10,6 +10,7 @@ import (
type Config struct {
ServiceName string
Enabled bool
Registry prometheus.Registerer
}
// DefaultConfig returns a default configuration.
@ -53,10 +54,16 @@ type Metrics struct {
// NewMetrics creates a new Metrics instance with all metrics registered.
func NewMetrics(config Config) *Metrics {
registry := config.Registry
if registry == nil {
registry = prometheus.DefaultRegisterer
}
auto := promauto.With(registry)
m := &Metrics{config: config}
// HTTP metrics
m.HTTPRequestsTotal = promauto.NewCounterVec(
m.HTTPRequestsTotal = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
@ -64,7 +71,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"method", "endpoint", "status_code", "service"},
)
m.HTTPRequestDuration = promauto.NewHistogramVec(
m.HTTPRequestDuration = auto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
@ -74,7 +81,7 @@ func NewMetrics(config Config) *Metrics {
)
// Database metrics
m.DBConnectionsActive = promauto.NewGaugeVec(
m.DBConnectionsActive = auto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "db_connections_active",
Help: "Number of active database connections",
@ -82,7 +89,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"database", "service"},
)
m.DBQueryDuration = promauto.NewHistogramVec(
m.DBQueryDuration = auto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration",
@ -91,7 +98,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"query_type", "table", "service"},
)
m.DBErrors = promauto.NewCounterVec(
m.DBErrors = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "db_errors_total",
Help: "Total number of database errors",
@ -100,7 +107,7 @@ func NewMetrics(config Config) *Metrics {
)
// Cache metrics
m.CacheOperations = promauto.NewCounterVec(
m.CacheOperations = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "cache_operations_total",
Help: "Total number of cache operations",
@ -108,7 +115,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"operation", "result", "service"},
)
m.CacheKeyCount = promauto.NewGaugeVec(
m.CacheKeyCount = auto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cache_keys_total",
Help: "Number of keys in cache",
@ -117,7 +124,7 @@ func NewMetrics(config Config) *Metrics {
)
// Authentication metrics
m.AuthAttempts = promauto.NewCounterVec(
m.AuthAttempts = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "authentication_attempts_total",
Help: "Total authentication attempts",
@ -125,7 +132,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"method", "result", "user_type"},
)
m.TokenOperations = promauto.NewCounterVec(
m.TokenOperations = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "token_operations_total",
Help: "JWT token operations",
@ -134,7 +141,7 @@ func NewMetrics(config Config) *Metrics {
)
// Game metrics
m.GamesStarted = promauto.NewCounterVec(
m.GamesStarted = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "games_started_total",
Help: "Total number of games started",
@ -142,7 +149,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"player_type", "platform"},
)
m.GamesCompleted = promauto.NewCounterVec(
m.GamesCompleted = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "games_completed_total",
Help: "Total number of games completed",
@ -150,7 +157,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"completion_type", "platform"},
)
m.SessionDuration = promauto.NewHistogramVec(
m.SessionDuration = auto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "game_session_duration_seconds",
Help: "Duration of game sessions",
@ -159,7 +166,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"completion_type"},
)
m.QuestionsAsked = promauto.NewCounterVec(
m.QuestionsAsked = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "questions_asked_total",
Help: "Total number of questions asked",
@ -167,7 +174,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"theme", "difficulty"},
)
m.AnswersSubmitted = promauto.NewCounterVec(
m.AnswersSubmitted = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "answers_submitted_total",
Help: "Total number of answers submitted",
@ -175,7 +182,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"theme", "is_correct", "attempt_number", "used_hint"},
)
m.HintsRequested = promauto.NewCounterVec(
m.HintsRequested = auto.NewCounterVec(
prometheus.CounterOpts{
Name: "hints_requested_total",
Help: "Total number of hints requested",
@ -183,7 +190,7 @@ func NewMetrics(config Config) *Metrics {
[]string{"theme", "question_difficulty"},
)
m.ScoreDistribution = promauto.NewHistogramVec(
m.ScoreDistribution = auto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "game_scores",
Help: "Distribution of game scores",

@ -0,0 +1,21 @@
package metrics
// Tests for Prometheus metrics registration with a custom registry.
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
// TestNewMetrics_WithCustomRegistry verifies metrics register on a provided registry.
func TestNewMetrics_WithCustomRegistry(t *testing.T) {
registry := prometheus.NewRegistry()
m := NewMetrics(Config{ServiceName: "svc", Enabled: true, Registry: registry})
require.NotNil(t, m)
require.NotNil(t, m.HTTPRequestsTotal)
require.NotNil(t, m.DBConnectionsActive)
require.NotNil(t, m.ScoreDistribution)
}

@ -0,0 +1,37 @@
package tracing
// Tests for tracing helpers invoking operations and propagating errors.
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
)
// TestTraceServiceOperation verifies tracing wraps a service operation and returns errors.
func TestTraceServiceOperation(t *testing.T) {
tracer, err := NewTracer(DefaultConfig())
require.NoError(t, err)
expected := errors.New("boom")
err = TraceServiceOperation(context.Background(), tracer, "svc", "op", func(ctx context.Context) error {
return expected
})
require.Equal(t, expected, err)
}
// TestTraceDatabaseOperation verifies tracing wraps a database operation and calls the function.
func TestTraceDatabaseOperation(t *testing.T) {
tracer, err := NewTracer(DefaultConfig())
require.NoError(t, err)
called := false
err = TraceDatabaseOperation(context.Background(), tracer, "select", "users", func(ctx context.Context) error {
called = true
return nil
})
require.NoError(t, err)
require.True(t, called)
}

@ -0,0 +1,69 @@
package security
// Tests for input sanitization utilities and validation helpers.
import (
"regexp"
"testing"
"github.com/stretchr/testify/require"
)
// TestSanitize_Options verifies trimming, space collapsing, escaping, max length, and allowed patterns.
func TestSanitize_Options(t *testing.T) {
opts := SanitizeOptions{
TrimWhitespace: true,
RemoveMultipleSpaces: true,
HTMLEscape: true,
MaxLength: 0,
AllowedPattern: nil,
}
result := Sanitize(" Hello <b>World</b> ", opts)
require.Equal(t, "Hello &lt;b&gt;World&lt;/b&gt;", result)
opts.MaxLength = 5
require.Equal(t, "Hello", Sanitize("Hello World", opts))
opts.AllowedPattern = regexp.MustCompile(`^[a-z]+$`)
require.Equal(t, "", Sanitize("Hello123", opts))
}
// TestSanitizePlayerName verifies player name normalization and invalid input rejection.
func TestSanitizePlayerName(t *testing.T) {
require.Equal(t, "Alice Bob", SanitizePlayerName(" Alice Bob "))
require.Equal(t, "", SanitizePlayerName("Alice <"))
}
// TestSanitizeAnswer verifies lowercasing and whitespace normalization.
func TestSanitizeAnswer(t *testing.T) {
require.Equal(t, "hello", SanitizeAnswer(" HeLLo "))
}
// TestSanitizeQuestionText verifies script tags are removed from question text.
func TestSanitizeQuestionText(t *testing.T) {
result := SanitizeQuestionText("<script>alert(1)</script> Question")
require.NotContains(t, result, "<script")
}
// TestSanitizeTheme verifies theme normalization and title casing.
func TestSanitizeTheme(t *testing.T) {
require.Equal(t, "Science Fiction", SanitizeTheme(" science fiction "))
}
// TestRemoveHTMLTags verifies all HTML tags are stripped from input.
func TestRemoveHTMLTags(t *testing.T) {
require.Equal(t, "Hi", RemoveHTMLTags("<b>Hi</b>"))
}
// TestContainsDangerousPatterns detects known dangerous substrings.
func TestContainsDangerousPatterns(t *testing.T) {
require.True(t, ContainsDangerousPatterns("javascript:alert(1)"))
require.False(t, ContainsDangerousPatterns("hello"))
}
// TestIsValidEmail verifies basic email pattern validation.
func TestIsValidEmail(t *testing.T) {
require.True(t, IsValidEmail("a@b.com"))
require.False(t, IsValidEmail("bad@"))
}

@ -0,0 +1,64 @@
package httputil
// Tests for HTTP error mapping and response payloads.
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
errorspkg "knowfoolery/backend/shared/domain/errors"
)
// TestMapError verifies domain error codes map to HTTP status codes and response codes.
func TestMapError(t *testing.T) {
cases := []struct {
name string
err error
statusCode int
code string
}{
{"nil", nil, http.StatusInternalServerError, "INTERNAL"},
{"not_found", errorspkg.Wrap(errorspkg.CodeNotFound, "missing", nil), http.StatusNotFound, "NOT_FOUND"},
{"invalid", errorspkg.Wrap(errorspkg.CodeInvalidInput, "bad", nil), http.StatusBadRequest, "INVALID_INPUT"},
{"unauthorized", errorspkg.Wrap(errorspkg.CodeUnauthorized, "no", nil), http.StatusUnauthorized, "UNAUTHORIZED"},
{"forbidden", errorspkg.Wrap(errorspkg.CodeForbidden, "no", nil), http.StatusForbidden, "FORBIDDEN"},
{"conflict", errorspkg.Wrap(errorspkg.CodeConflict, "conflict", nil), http.StatusConflict, "CONFLICT"},
{"rate_limit", errorspkg.Wrap(errorspkg.CodeRateLimitExceeded, "slow", nil), http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED"},
{"gone", errorspkg.Wrap(errorspkg.CodeSessionExpired, "gone", nil), http.StatusGone, "SESSION_EXPIRED"},
{"unprocessable", errorspkg.Wrap(errorspkg.CodeMaxAttemptsReached, "max", nil), http.StatusUnprocessableEntity, "MAX_ATTEMPTS_REACHED"},
{"generic", errors.New("boom"), http.StatusInternalServerError, "INTERNAL"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
status, resp := MapError(tc.err)
require.Equal(t, tc.statusCode, status)
require.Equal(t, tc.code, resp.Code)
})
}
}
// TestSendError verifies SendError writes the correct status and payload.
func TestSendError(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return SendError(c, errorspkg.ErrNotFound)
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
var body ErrorResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, "NOT_FOUND", body.Code)
}

@ -0,0 +1,74 @@
package httputil
// Tests for HTTP query pagination, sorting, and filtering extraction.
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
"knowfoolery/backend/shared/domain/types"
)
// TestPaginationFromQuery verifies defaulting and bounds from query parameters.
func TestPaginationFromQuery(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
pagination := PaginationFromQuery(c)
return c.JSON(pagination)
})
req := httptest.NewRequest(http.MethodGet, "/?page=0&page_size=500", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
var p types.Pagination
require.NoError(t, json.NewDecoder(resp.Body).Decode(&p))
require.Equal(t, 1, p.Page)
require.Equal(t, types.MaxPageSize, p.PageSize)
}
// TestSortingFromQuery verifies allowed field enforcement and direction normalization.
func TestSortingFromQuery(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
sorting := SortingFromQuery(c, "name", []string{"name", "created"})
return c.JSON(sorting)
})
req := httptest.NewRequest(http.MethodGet, "/?sort=invalid&direction=down", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
var s SortingParams
require.NoError(t, json.NewDecoder(resp.Body).Decode(&s))
require.Equal(t, "name", s.Field)
require.Equal(t, "asc", s.Direction)
}
// TestFiltersFromQueryAndCustom verifies base filters and custom filter extraction.
func TestFiltersFromQueryAndCustom(t *testing.T) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
filters := FiltersFromQuery(c)
filters.WithCustomFilter(c, "theme")
return c.JSON(filters)
})
req := httptest.NewRequest(http.MethodGet, "/?search=hi&status=active&theme=scifi", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
var f FilterParams
require.NoError(t, json.NewDecoder(resp.Body).Decode(&f))
require.Equal(t, "hi", f.Search)
require.Equal(t, "active", f.Status)
require.Equal(t, "scifi", f.Custom["theme"])
}

@ -0,0 +1,85 @@
package httputil
// Tests for HTTP response helpers and health status derivation.
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/require"
)
// TestNewPaginatedResponse verifies total page calculation in paginated responses.
func TestNewPaginatedResponse(t *testing.T) {
resp := NewPaginatedResponse([]string{"a"}, 1, 2, 3)
require.NotNil(t, resp.Meta)
require.Equal(t, 2, resp.Meta.TotalPages)
}
// TestHealthStatus verifies health status flips to unhealthy when any check fails.
func TestHealthStatus(t *testing.T) {
app := fiber.New()
app.Get("/health", func(c fiber.Ctx) error {
return Health(c, "service", "0.1.0", map[string]string{
"db": "ok",
"cache": "down",
})
})
req := httptest.NewRequest(http.MethodGet, "/health", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
var body HealthResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, "unhealthy", body.Status)
}
// TestResponseHelpers verifies status codes for OK, Created, NoContent, Paginated, and Message.
func TestResponseHelpers(t *testing.T) {
app := fiber.New()
app.Get("/ok", func(c fiber.Ctx) error {
return OK(c, fiber.Map{"ok": true})
})
app.Post("/created", func(c fiber.Ctx) error {
return Created(c, fiber.Map{"id": "1"})
})
app.Delete("/no-content", func(c fiber.Ctx) error {
return NoContent(c)
})
app.Get("/paginated", func(c fiber.Ctx) error {
return Paginated(c, []string{"a"}, 1, 2, 3)
})
app.Get("/message", func(c fiber.Ctx) error {
return Message(c, "hello")
})
req := httptest.NewRequest(http.MethodGet, "/ok", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest(http.MethodPost, "/created", nil)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode)
req = httptest.NewRequest(http.MethodDelete, "/no-content", nil)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode)
req = httptest.NewRequest(http.MethodGet, "/paginated", nil)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest(http.MethodGet, "/message", nil)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
}

@ -0,0 +1,61 @@
package validation
// Tests for custom validation tags and error formatting behavior.
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
errs "knowfoolery/backend/shared/domain/errors"
)
type sampleStruct struct {
Name string `json:"name" validate:"required"`
}
type playerStruct struct {
PlayerName string `json:"player_name" validate:"required,player_name"`
}
// TestValidator_CustomTags verifies custom validation tags reject invalid inputs.
func TestValidator_CustomTags(t *testing.T) {
v := NewValidator()
require.Error(t, v.ValidateVar("bad!", "alphanum_space"))
require.Error(t, v.ValidateVar("<b>hi</b>", "no_html"))
require.Error(t, v.ValidateVar("javascript:alert(1)", "safe_text"))
require.Error(t, v.ValidateVar("A", "player_name"))
}
// TestValidator_ValidateReturnsDomainError ensures struct validation returns a domain error with messages.
func TestValidator_ValidateReturnsDomainError(t *testing.T) {
v := NewValidator()
err := v.Validate(sampleStruct{})
require.Error(t, err)
var domainErr *errs.DomainError
require.ErrorAs(t, err, &domainErr)
require.Equal(t, errs.CodeValidationFailed, domainErr.Code)
require.True(t, strings.Contains(domainErr.Message, "name is required"))
}
// TestValidator_ValidateVarReturnsDomainError ensures field validation returns a domain error.
func TestValidator_ValidateVarReturnsDomainError(t *testing.T) {
v := NewValidator()
err := v.ValidateVar("bad", "email")
require.Error(t, err)
var domainErr *errs.DomainError
require.ErrorAs(t, err, &domainErr)
require.Equal(t, errs.CodeValidationFailed, domainErr.Code)
}
// TestValidator_JSONTagNameMapping ensures JSON tag names appear in validation error text.
func TestValidator_JSONTagNameMapping(t *testing.T) {
v := NewValidator()
err := v.Validate(playerStruct{PlayerName: "A"})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "player_name must be a valid player name"))
}
Loading…
Cancel
Save