Added unit tests for steps up to 1.3.2
parent
b7d3ed051c
commit
995c452408
@ -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))
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
@ -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 <b>World</b>", 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…
Reference in New Issue