You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
knowfoolery/docs/development-guidelines.md

39 KiB

Know Foolery - Development Guidelines & Coding Standards

Overview

This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase.

Code Organization

Project Structure Standards

knowfoolery/
├── backend/
│   ├── services/                    # Microservices
│   │   ├── {service-name}/
│   │   │   ├── cmd/
│   │   │   │   └── main.go         # Service entry point
│   │   │   ├── internal/
│   │   │   │   ├── handlers/       # Fiber HTTP handlers
│   │   │   │   ├── services/       # Business logic
│   │   │   │   ├── middleware/     # Service-specific middleware
│   │   │   │   └── models/         # Ent models and schemas
│   │   │   ├── api/                # API definitions (OpenAPI/gRPC)
│   │   │   ├── config/             # Configuration management
│   │   │   ├── tests/              # Service-specific tests
│   │   │   ├── Dockerfile          # Container definition
│   │   │   └── go.mod              # Go module
│   │   └── shared/                 # Shared packages
│   │       ├── auth/               # JWT middleware & Zitadel integration
│   │       ├── database/           # Ent database client
│   │       ├── observability/      # Metrics and tracing
│   │       ├── security/           # Security utilities
│   │       └── utils/              # Common utilities
├── frontend/
│   ├── packages/
│   │   ├── ui-components/          # Shared Gluestack UI components
│   │   ├── shared-logic/           # Business logic
│   │   ├── shared-types/           # TypeScript types
│   │   └── shared-utils/           # Utility functions
│   ├── apps/
│   │   ├── web/                    # React web app
│   │   ├── mobile/                 # React Native app
│   │   └── desktop/                # Wails desktop app
└── infrastructure/                 # DevOps and deployment

Naming Conventions

Go Code

// Files: snake_case
user_service.go
jwt_middleware.go
database_client.go

// Packages: lowercase, single word when possible
package auth
package database
package observability

// Exported types: PascalCase
type GameService struct {}
type AuthClaims struct {}
type DatabaseConfig struct {}

// Unexported types: camelCase
type userRepository struct {}
type tokenValidator struct {}

// Functions: PascalCase (exported), camelCase (unexported)
func NewGameService() *GameService {}
func (s *GameService) StartGame() error {}
func validateToken() error {}

// Constants: PascalCase or SCREAMING_SNAKE_CASE for groups
const DefaultTimeout = 30 * time.Second
const (
    StatusActive   = "active"
    StatusInactive = "inactive"
)

// Interfaces: noun or adjective + "er" suffix
type TokenValidator interface {}
type UserRepository interface {}
type ScoreCalculator interface {}

TypeScript Code

// Files: kebab-case
game-card.tsx
user-service.ts
auth-context.tsx

// Components: PascalCase
export const GameCard: React.FC<GameCardProps> = () => {}
export const LeaderboardList: React.FC = () => {}

// Hooks: camelCase with "use" prefix
export const useGameSession = () => {}
export const useAuthentication = () => {}

// Functions: camelCase
export const calculateScore = () => {}
export const validateAnswer = () => {}

// Types/Interfaces: PascalCase
interface GameSessionProps {}
type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated'

// Constants: SCREAMING_SNAKE_CASE
export const MAX_ATTEMPTS = 3
export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes

Go Development Standards

Fiber Framework Patterns

Service Structure

// Service initialization with Fiber
package main

import (
    "log"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/cors"
    "github.com/gofiber/fiber/v3/middleware/logger"
    "github.com/gofiber/fiber/v3/middleware/recover"
)

func main() {
    // Create Fiber app with configuration
    app := fiber.New(fiber.Config{
        AppName:      "Know Foolery Game Service",
        ServerHeader: "Fiber",
        ErrorHandler: customErrorHandler,
        ReadTimeout:  time.Second * 10,
        WriteTimeout: time.Second * 10,
        IdleTimeout:  time.Second * 120,
    })

    // Global middleware
    app.Use(logger.New())
    app.Use(recover.New())
    app.Use(cors.New(cors.Config{
        AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com",
        AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
        AllowHeaders: "Origin, Content-Type, Authorization",
    }))

    // Health check endpoint
    app.Get("/health", healthCheck)

    // API routes with versioning
    api := app.Group("/api/v1")
    setupGameRoutes(api)

    // Start server
    log.Fatal(app.Listen(":8080"))
}

func customErrorHandler(c *fiber.Ctx, err error) error {
    code := fiber.StatusInternalServerError
    message := "Internal Server Error"

    if e, ok := err.(*fiber.Error); ok {
        code = e.Code
        message = e.Message
    }

    return c.Status(code).JSON(fiber.Map{
        "error":   true,
        "message": message,
        "code":    code,
    })
}

Handler Patterns

// Handler structure and patterns
package handlers

import (
    "github.com/gofiber/fiber/v3"
    "knowfoolery/internal/services"
)

type GameHandler struct {
    gameService *services.GameService
    validator   *validator.Validate
}

func NewGameHandler(gameService *services.GameService) *GameHandler {
    return &GameHandler{
        gameService: gameService,
        validator:   validator.New(),
    }
}

// HTTP handler with proper error handling
func (h *GameHandler) StartGame(c *fiber.Ctx) error {
    var req StartGameRequest
    
    // Parse and validate request
    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Invalid request body",
        })
    }
    
    if err := h.validator.Struct(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Validation failed",
            "details": err.Error(),
        })
    }
    
    // Extract user context from JWT middleware
    userID := c.Locals("user_id").(string)
    userRole := c.Locals("user_role").(string)
    
    // Business logic
    gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName)
    if err != nil {
        return handleServiceError(c, err)
    }
    
    return c.Status(fiber.StatusCreated).JSON(fiber.Map{
        "success": true,
        "data":    gameSession,
    })
}

// Centralized error handling
func handleServiceError(c *fiber.Ctx, err error) error {
    switch {
    case errors.Is(err, services.ErrInvalidInput):
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   true,
            "message": "Invalid input",
            "details": err.Error(),
        })
    case errors.Is(err, services.ErrNotFound):
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error":   true,
            "message": "Resource not found",
        })
    case errors.Is(err, services.ErrUnauthorized):
        return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
            "error":   true,
            "message": "Unauthorized access",
        })
    default:
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error":   true,
            "message": "Internal server error",
        })
    }
}

// Request/Response structures with validation
type StartGameRequest struct {
    PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"`
}

type StartGameResponse struct {
    SessionID     string    `json:"session_id"`
    PlayerName    string    `json:"player_name"`
    StartTime     time.Time `json:"start_time"`
    TimeRemaining int       `json:"time_remaining_seconds"`
}

Middleware Patterns

// Custom middleware for authentication
package middleware

import (
    "github.com/gofiber/fiber/v3"
    "knowfoolery/shared/auth"
)

func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // Extract token from Authorization header
        authHeader := c.Get("Authorization")
        if authHeader == "" {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Authorization header required",
            })
        }
        
        token := strings.TrimPrefix(authHeader, "Bearer ")
        if token == authHeader {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Invalid authorization header format",
            })
        }
        
        // Validate token
        claims, err := zitadelRepo.ValidateToken(c.Context(), token)
        if err != nil {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error":   true,
                "message": "Invalid token",
            })
        }
        
        // Set user context
        c.Locals("user_id", claims.Subject)
        c.Locals("user_email", claims.Email)
        c.Locals("user_name", claims.Name)
        c.Locals("user_roles", claims.Roles)
        
        return c.Next()
    }
}

// Rate limiting middleware
func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler {
    return func(c *fiber.Ctx) error {
        userID := c.Locals("user_id")
        clientIP := c.IP()
        
        // Per-user rate limiting
        if userID != nil {
            userKey := fmt.Sprintf("rate_limit:user:%s", userID)
            if !allowRequest(redisClient, userKey, 60, time.Minute) {
                return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                    "error":   true,
                    "message": "Rate limit exceeded for user",
                })
            }
        }
        
        // Per-IP rate limiting
        ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP)
        if !allowRequest(redisClient, ipKey, 100, time.Minute) {
            return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                "error":   true,
                "message": "Rate limit exceeded for IP",
            })
        }
        
        return c.Next()
    }
}

Database Patterns with Ent

Schema Definition Standards

// Ent schema with proper field validation and indexes
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
    "entgo.io/ent/schema/index"
    "time"
)

type Question struct {
    ent.Schema
}

func (Question) Fields() []ent.Field {
    return []ent.Field{
        field.String("theme").
            NotEmpty().
            MaxLen(100).
            Comment("Question category/theme"),
        field.Text("text").
            NotEmpty().
            MaxLen(1000).
            Comment("The actual question text"),
        field.String("answer").
            NotEmpty().
            MaxLen(500).
            Comment("Correct answer"),
        field.Text("hint").
            Optional().
            MaxLen(500).
            Comment("Optional hint text"),
        field.Enum("difficulty").
            Values("easy", "medium", "hard").
            Default("medium").
            Comment("Question difficulty level"),
        field.Bool("is_active").
            Default(true).
            Comment("Whether question is active"),
        field.Time("created_at").
            Default(time.Now).
            Immutable().
            Comment("Creation timestamp"),
        field.Time("updated_at").
            Default(time.Now).
            UpdateDefault(time.Now).
            Comment("Last update timestamp"),
    }
}

func (Question) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("theme", "is_active"),
        index.Fields("difficulty", "is_active"),
        index.Fields("created_at"),
    }
}

func (Question) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("attempts", QuestionAttempt.Type),
    }
}

Service Layer Patterns

// Service layer with proper error handling
package services

import (
    "context"
    "errors"
    "knowfoolery/internal/models"
    "knowfoolery/ent"
)

var (
    ErrInvalidInput    = errors.New("invalid input")
    ErrNotFound        = errors.New("resource not found")
    ErrUnauthorized    = errors.New("unauthorized access")
    ErrGameInProgress  = errors.New("game already in progress")
    ErrSessionExpired  = errors.New("session expired")
)

type GameService struct {
    client       *ent.Client
    questionSvc  QuestionService
    sessionSvc   SessionService
    validator    *validator.Validate
}

func NewGameService(
    client *ent.Client,
    questionSvc QuestionService,
    sessionSvc SessionService,
) *GameService {
    return &GameService{
        client:      client,
        questionSvc: questionSvc,
        sessionSvc:  sessionSvc,
        validator:   validator.New(),
    }
}

func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) {
    // Validate input
    if err := s.validatePlayerName(playerName); err != nil {
        return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
    }
    
    // Check for existing active session
    activeSession, err := s.client.GameSession.
        Query().
        Where(gamesession.And(
            gamesession.PlayerNameEQ(playerName),
            gamesession.StatusEQ(gamesession.StatusActive),
        )).
        Only(ctx)
    
    if err != nil && !ent.IsNotFound(err) {
        return nil, fmt.Errorf("failed to check existing session: %w", err)
    }
    
    if activeSession != nil {
        return nil, ErrGameInProgress
    }
    
    // Start database transaction
    tx, err := s.client.Tx(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to start transaction: %w", err)
    }
    defer tx.Rollback()
    
    // Create new game session
    session, err := tx.GameSession.
        Create().
        SetPlayerName(playerName).
        SetTotalScore(0).
        SetQuestionsAsked(0).
        SetQuestionsCorrect(0).
        SetHintsUsed(0).
        SetStartTime(time.Now()).
        SetStatus(gamesession.StatusActive).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to create session: %w", err)
    }
    
    // Get first question
    question, err := s.questionSvc.GetRandomQuestion(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to get question: %w", err)
    }
    
    // Update session with first question
    session, err = session.Update().
        SetCurrentQuestionID(question.ID).
        SetCurrentAttempts(0).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to update session: %w", err)
    }
    
    // Commit transaction
    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("failed to commit transaction: %w", err)
    }
    
    return &models.GameSession{
        ID:            session.ID,
        PlayerName:    session.PlayerName,
        TotalScore:    session.TotalScore,
        StartTime:     session.StartTime,
        Status:        string(session.Status),
        CurrentQuestion: &models.Question{
            ID:     question.ID,
            Theme:  question.Theme,
            Text:   question.Text,
            Hint:   question.Hint,
        },
        TimeRemaining: s.calculateTimeRemaining(session.StartTime),
    }, nil
}

func (s *GameService) validatePlayerName(name string) error {
    type PlayerNameValidation struct {
        Name string `validate:"required,min=2,max=50,alphanum_space"`
    }
    
    return s.validator.Struct(&PlayerNameValidation{Name: name})
}

func (s *GameService) calculateTimeRemaining(startTime time.Time) int {
    const sessionDuration = 30 * time.Minute
    elapsed := time.Since(startTime)
    remaining := sessionDuration - elapsed
    
    if remaining < 0 {
        return 0
    }
    
    return int(remaining.Seconds())
}

TypeScript Development Standards

React Component Patterns

Component Structure

// Component with proper TypeScript types and patterns
import React, { useState, useEffect, useCallback } from 'react'
import {
  Card,
  VStack,
  HStack,
  Text,
  Input,
  Button,
  Badge,
  Progress
} from '@gluestack-ui/themed'
import { useGameSession } from '../hooks/useGameSession'
import { useTimer } from '../hooks/useTimer'

// Props interface with proper documentation
export interface GameCardProps {
  /** The question to display */
  question: string
  /** Theme/category of the question */
  theme: string
  /** Time remaining in seconds */
  timeRemaining: number
  /** Number of attempts left */
  attemptsLeft: number
  /** Current player score */
  currentScore: number
  /** Loading state */
  isLoading?: boolean
  /** Callback when answer is submitted */
  onSubmitAnswer: (answer: string) => Promise<void>
  /** Callback when hint is requested */
  onRequestHint: () => Promise<void>
  /** Callback when time expires */
  onTimeExpire?: () => void
}

/**
 * GameCard component displays a quiz question with input and controls
 * 
 * @param props - GameCard props
 * @returns React component
 */
export const GameCard: React.FC<GameCardProps> = ({
  question,
  theme,
  timeRemaining,
  attemptsLeft,
  currentScore,
  isLoading = false,
  onSubmitAnswer,
  onRequestHint,
  onTimeExpire
}) => {
  // Local state with proper typing
  const [answer, setAnswer] = useState<string>('')
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
  
  // Custom hooks
  const { formatTime } = useTimer()
  
  // Memoized handlers
  const handleSubmit = useCallback(async () => {
    if (!answer.trim() || isSubmitting) return
    
    setIsSubmitting(true)
    try {
      await onSubmitAnswer(answer.trim())
      setAnswer('') // Clear input on successful submission
    } catch (error) {
      console.error('Failed to submit answer:', error)
      // Error handling would be managed by parent component
    } finally {
      setIsSubmitting(false)
    }
  }, [answer, isSubmitting, onSubmitAnswer])
  
  const handleHintRequest = useCallback(async () => {
    if (isLoading) return
    
    try {
      await onRequestHint()
    } catch (error) {
      console.error('Failed to request hint:', error)
    }
  }, [isLoading, onRequestHint])
  
  // Effects
  useEffect(() => {
    if (timeRemaining === 0 && onTimeExpire) {
      onTimeExpire()
    }
  }, [timeRemaining, onTimeExpire])
  
  // Computed values
  const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100
  const isTimeWarning = timeRemaining <= 300 // 5 minutes
  const isTimeCritical = timeRemaining <= 60  // 1 minute
  
  return (
    <Card size="lg" variant="elevated" m="$4">
      <VStack space="md" p="$4">
        {/* Header with theme and timer */}
        <HStack justifyContent="space-between" alignItems="center">
          <Badge 
            size="sm" 
            variant="solid" 
            action="info"
            testID="theme-badge"
          >
            <Text color="$white" fontSize="$sm" fontWeight="$semibold">
              {theme}
            </Text>
          </Badge>
          <HStack space="sm" alignItems="center">
            <Text 
              fontSize="$sm" 
              color={isTimeCritical ? "$error500" : isTimeWarning ? "$warning500" : "$textLight600"}
              testID="timer-display"
            >
              ⏱️ {formatTime(timeRemaining)}
            </Text>
          </HStack>
        </HStack>
        
        {/* Progress indicator */}
        <Progress 
          value={progressValue} 
          size="sm"
          testID="session-progress"
        >
          <ProgressFilledTrack />
        </Progress>
        
        {/* Question */}
        <Text 
          fontSize="$lg" 
          fontWeight="$semibold" 
          lineHeight="$xl"
          testID="question-text"
        >
          {question}
        </Text>
        
        {/* Answer input */}
        <Input 
          size="lg" 
          isDisabled={isLoading || isSubmitting}
          testID="answer-input"
        >
          <InputField
            placeholder="Enter your answer..."
            value={answer}
            onChangeText={setAnswer}
            autoCapitalize="none"
            autoCorrect={false}
            onSubmitEditing={handleSubmit}
            returnKeyType="send"
          />
        </Input>
        
        {/* Game stats */}
        <HStack justifyContent="space-between" alignItems="center">
          <Text fontSize="$sm" color="$textLight600" testID="attempts-counter">
            Attempts left: {attemptsLeft}/3
          </Text>
          <Text fontSize="$sm" color="$textLight600" testID="score-display">
            Score: {currentScore} points
          </Text>
        </HStack>
        
        {/* Action buttons */}
        <HStack space="sm">
          <Button
            size="lg"
            variant="solid"
            action="primary"
            flex={1}
            onPress={handleSubmit}
            isDisabled={!answer.trim() || isLoading || isSubmitting}
            testID="submit-button"
          >
            <ButtonText>
              {isSubmitting ? 'Submitting...' : 'Submit Answer'}
            </ButtonText>
          </Button>
          <Button
            size="lg"
            variant="outline"
            action="secondary"
            onPress={handleHintRequest}
            isDisabled={isLoading || isSubmitting}
            testID="hint-button"
          >
            <ButtonText>💡 Hint</ButtonText>
          </Button>
        </HStack>
      </VStack>
    </Card>
  )
}

// Default export for dynamic imports
export default GameCard

Custom Hooks Pattern

// Custom hook with proper TypeScript types
import { useState, useEffect, useCallback, useRef } from 'react'
import { GameSessionService } from '../services/game-session-service'

interface UseGameSessionOptions {
  playerName: string
  onSessionEnd?: (finalScore: number) => void
  onError?: (error: Error) => void
}

interface GameSessionState {
  sessionId: string | null
  currentQuestion: Question | null
  score: number
  attemptsLeft: number
  timeRemaining: number
  isLoading: boolean
  error: string | null
}

interface GameSessionActions {
  startGame: () => Promise<void>
  submitAnswer: (answer: string) => Promise<boolean>
  requestHint: () => Promise<string>
  endGame: () => Promise<void>
}

export const useGameSession = (
  options: UseGameSessionOptions
): [GameSessionState, GameSessionActions] => {
  const [state, setState] = useState<GameSessionState>({
    sessionId: null,
    currentQuestion: null,
    score: 0,
    attemptsLeft: 3,
    timeRemaining: 30 * 60, // 30 minutes
    isLoading: false,
    error: null,
  })
  
  const timerRef = useRef<NodeJS.Timeout | null>(null)
  const gameService = useRef(new GameSessionService())
  
  // Timer effect
  useEffect(() => {
    if (state.sessionId && state.timeRemaining > 0) {
      timerRef.current = setInterval(() => {
        setState(prev => {
          const newTimeRemaining = prev.timeRemaining - 1
          if (newTimeRemaining <= 0 && options.onSessionEnd) {
            options.onSessionEnd(prev.score)
          }
          return { ...prev, timeRemaining: Math.max(0, newTimeRemaining) }
        })
      }, 1000)
    }
    
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current)
      }
    }
  }, [state.sessionId, state.timeRemaining, options.onSessionEnd])
  
  // Actions
  const startGame = useCallback(async () => {
    setState(prev => ({ ...prev, isLoading: true, error: null }))
    
    try {
      const session = await gameService.current.startGame(options.playerName)
      setState(prev => ({
        ...prev,
        sessionId: session.sessionId,
        currentQuestion: session.firstQuestion,
        score: 0,
        attemptsLeft: 3,
        timeRemaining: session.timeRemaining,
        isLoading: false,
      }))
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to start game'
      setState(prev => ({ ...prev, error: errorMessage, isLoading: false }))
      options.onError?.(error instanceof Error ? error : new Error(errorMessage))
    }
  }, [options.playerName, options.onError])
  
  const submitAnswer = useCallback(async (answer: string): Promise<boolean> => {
    if (!state.sessionId || !state.currentQuestion) {
      throw new Error('No active game session')
    }
    
    setState(prev => ({ ...prev, isLoading: true }))
    
    try {
      const result = await gameService.current.submitAnswer(
        state.sessionId,
        state.currentQuestion.id,
        answer
      )
      
      setState(prev => ({
        ...prev,
        score: result.newScore,
        attemptsLeft: result.attemptsLeft,
        currentQuestion: result.nextQuestion,
        isLoading: false,
      }))
      
      return result.isCorrect
    } catch (error) {
      setState(prev => ({ ...prev, isLoading: false }))
      throw error
    }
  }, [state.sessionId, state.currentQuestion])
  
  const requestHint = useCallback(async (): Promise<string> => {
    if (!state.sessionId || !state.currentQuestion) {
      throw new Error('No active game session')
    }
    
    const hint = await gameService.current.requestHint(
      state.sessionId,
      state.currentQuestion.id
    )
    
    return hint
  }, [state.sessionId, state.currentQuestion])
  
  const endGame = useCallback(async () => {
    if (!state.sessionId) return
    
    await gameService.current.endGame(state.sessionId)
    
    setState({
      sessionId: null,
      currentQuestion: null,
      score: 0,
      attemptsLeft: 3,
      timeRemaining: 30 * 60,
      isLoading: false,
      error: null,
    })
    
    if (timerRef.current) {
      clearInterval(timerRef.current)
    }
  }, [state.sessionId])
  
  return [
    state,
    {
      startGame,
      submitAnswer,
      requestHint,
      endGame,
    },
  ]
}

Testing Standards

Go Testing Patterns

// Comprehensive testing with testify and testcontainers
package services_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/suite"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    
    "knowfoolery/internal/services"
    "knowfoolery/ent/enttest"
)

type GameServiceTestSuite struct {
    suite.Suite
    ctx         context.Context
    gameService *services.GameService
    client      *ent.Client
}

func (suite *GameServiceTestSuite) SetupSuite() {
    suite.ctx = context.Background()
    
    // Start PostgreSQL container for integration tests
    postgresContainer, err := postgres.RunContainer(suite.ctx,
        testcontainers.WithImage("postgres:15-alpine"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
    )
    require.NoError(suite.T(), err)
    
    connStr, err := postgresContainer.ConnectionString(suite.ctx)
    require.NoError(suite.T(), err)
    
    // Create Ent client with test database
    suite.client = enttest.Open(suite.T(), "postgres", connStr)
    
    // Initialize service
    suite.gameService = services.NewGameService(
        suite.client,
        services.NewQuestionService(suite.client),
        services.NewSessionService(suite.client),
    )
}

func (suite *GameServiceTestSuite) TearDownSuite() {
    suite.client.Close()
}

func (suite *GameServiceTestSuite) SetupTest() {
    // Clean database before each test
    suite.client.GameSession.Delete().ExecX(suite.ctx)
    suite.client.Question.Delete().ExecX(suite.ctx)
    
    // Seed test data
    suite.seedTestQuestions()
}

func (suite *GameServiceTestSuite) TestStartGame_Success() {
    // Arrange
    playerName := "TestPlayer"
    
    // Act
    session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
    
    // Assert
    require.NoError(suite.T(), err)
    assert.NotEmpty(suite.T(), session.ID)
    assert.Equal(suite.T(), playerName, session.PlayerName)
    assert.Equal(suite.T(), 0, session.TotalScore)
    assert.NotNil(suite.T(), session.CurrentQuestion)
    assert.True(suite.T(), session.TimeRemaining > 0)
}

func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() {
    // Test cases for validation
    testCases := []struct {
        name        string
        playerName  string
        expectedErr string
    }{
        {"empty name", "", "required"},
        {"too short", "a", "min"},
        {"too long", strings.Repeat("a", 51), "max"},
        {"invalid chars", "player@123", "alphanum_space"},
    }
    
    for _, tc := range testCases {
        suite.T().Run(tc.name, func(t *testing.T) {
            _, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName)
            require.Error(t, err)
            assert.Contains(t, err.Error(), tc.expectedErr)
        })
    }
}

func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() {
    // Arrange
    playerName := "TestPlayer"
    
    // Start first game
    _, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
    require.NoError(suite.T(), err)
    
    // Act - try to start second game
    _, err = suite.gameService.StartGame(suite.ctx, "user123", playerName)
    
    // Assert
    require.Error(suite.T(), err)
    assert.Equal(suite.T(), services.ErrGameInProgress, err)
}

func (suite *GameServiceTestSuite) seedTestQuestions() {
    questions := []struct {
        theme  string
        text   string
        answer string
        hint   string
    }{
        {"Geography", "What is the capital of France?", "Paris", "City of Light"},
        {"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"},
        {"Science", "What is the largest planet?", "Jupiter", "Gas giant"},
    }
    
    for _, q := range questions {
        suite.client.Question.Create().
            SetTheme(q.theme).
            SetText(q.text).
            SetAnswer(q.answer).
            SetHint(q.hint).
            SetIsActive(true).
            ExecX(suite.ctx)
    }
}

func TestGameServiceSuite(t *testing.T) {
    suite.Run(t, new(GameServiceTestSuite))
}

// Benchmark tests
func BenchmarkGameService_StartGame(b *testing.B) {
    // Setup
    ctx := context.Background()
    client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    defer client.Close()
    
    gameService := services.NewGameService(client, nil, nil)
    
    // Benchmark
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        playerName := fmt.Sprintf("Player%d", i)
        _, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName)
    }
}

TypeScript Testing Patterns

// React component testing with Jest and Testing Library
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { GluestackUIProvider } from '@gluestack-ui/themed'
import { GameCard, GameCardProps } from '../GameCard'

// Test wrapper for Gluestack UI
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <GluestackUIProvider>{children}</GluestackUIProvider>
)

// Mock props factory
const createMockProps = (overrides: Partial<GameCardProps> = {}): GameCardProps => ({
  question: "What is the capital of France?",
  theme: "Geography",
  timeRemaining: 1800, // 30 minutes
  attemptsLeft: 3,
  currentScore: 0,
  onSubmitAnswer: jest.fn(),
  onRequestHint: jest.fn(),
  ...overrides,
})

describe('GameCard', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })
  
  describe('Rendering', () => {
    it('should render question and theme correctly', () => {
      const props = createMockProps()
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('question-text')).toHaveTextContent(props.question)
      expect(screen.getByTestId('theme-badge')).toHaveTextContent(props.theme)
    })
    
    it('should display timer in correct format', () => {
      const props = createMockProps({ timeRemaining: 125 }) // 2:05
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05')
    })
    
    it('should show attempts and score correctly', () => {
      const props = createMockProps({ attemptsLeft: 2, currentScore: 5 })
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('attempts-counter')).toHaveTextContent('Attempts left: 2/3')
      expect(screen.getByTestId('score-display')).toHaveTextContent('Score: 5 points')
    })
  })
  
  describe('User Interactions', () => {
    it('should call onSubmitAnswer when submit button is clicked', async () => {
      const props = createMockProps()
      const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined)
      props.onSubmitAnswer = mockOnSubmitAnswer
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      // Type an answer
      const input = screen.getByTestId('answer-input')
      fireEvent.changeText(input, 'Paris')
      
      // Click submit
      const submitButton = screen.getByTestId('submit-button')
      fireEvent.press(submitButton)
      
      await waitFor(() => {
        expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris')
      })
    })
    
    it('should not submit empty answer', () => {
      const props = createMockProps()
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const submitButton = screen.getByTestId('submit-button')
      expect(submitButton).toBeDisabled()
    })
    
    it('should call onRequestHint when hint button is clicked', async () => {
      const props = createMockProps()
      const mockOnRequestHint = jest.fn().mockResolvedValue('City of Light')
      props.onRequestHint = mockOnRequestHint
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const hintButton = screen.getByTestId('hint-button')
      fireEvent.press(hintButton)
      
      await waitFor(() => {
        expect(mockOnRequestHint).toHaveBeenCalled()
      })
    })
  })
  
  describe('Loading States', () => {
    it('should disable inputs when loading', () => {
      const props = createMockProps({ isLoading: true })
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('answer-input')).toBeDisabled()
      expect(screen.getByTestId('submit-button')).toBeDisabled()
      expect(screen.getByTestId('hint-button')).toBeDisabled()
    })
  })
  
  describe('Timer States', () => {
    it('should show warning color when time is low', () => {
      const props = createMockProps({ timeRemaining: 300 }) // 5 minutes
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const timer = screen.getByTestId('timer-display')
      expect(timer).toHaveStyle({ color: expect.stringContaining('warning') })
    })
    
    it('should show critical color when time is very low', () => {
      const props = createMockProps({ timeRemaining: 30 }) // 30 seconds
      
      render(<GameCard {...props} />, { wrapper: TestWrapper })
      
      const timer = screen.getByTestId('timer-display')
      expect(timer).toHaveStyle({ color: expect.stringContaining('error') })
    })
  })
})

// Hook testing
import { renderHook, act } from '@testing-library/react'
import { useGameSession } from '../hooks/useGameSession'

// Mock the service
jest.mock('../services/game-session-service')

describe('useGameSession', () => {
  const mockGameService = {
    startGame: jest.fn(),
    submitAnswer: jest.fn(),
    requestHint: jest.fn(),
    endGame: jest.fn(),
  }
  
  beforeEach(() => {
    jest.clearAllMocks()
    // Set up service mock
    ;(GameSessionService as jest.Mock).mockImplementation(() => mockGameService)
  })
  
  it('should start game successfully', async () => {
    mockGameService.startGame.mockResolvedValue({
      sessionId: 'session123',
      firstQuestion: { id: 'q1', text: 'Test question' },
      timeRemaining: 1800,
    })
    
    const { result } = renderHook(() => 
      useGameSession({ playerName: 'TestPlayer' })
    )
    
    await act(async () => {
      await result.current[1].startGame()
    })
    
    expect(result.current[0].sessionId).toBe('session123')
    expect(result.current[0].currentQuestion).toBeDefined()
  })
  
  it('should handle errors during game start', async () => {
    const mockError = new Error('Failed to start game')
    mockGameService.startGame.mockRejectedValue(mockError)
    
    const onError = jest.fn()
    const { result } = renderHook(() => 
      useGameSession({ playerName: 'TestPlayer', onError })
    )
    
    await act(async () => {
      await result.current[1].startGame()
    })
    
    expect(result.current[0].error).toBe('Failed to start game')
    expect(onError).toHaveBeenCalledWith(mockError)
  })
})

Code Quality Standards

Linting and Formatting

# .golangci.yml
run:
  timeout: 5m
  tests: true

linters:
  enable:
    - gofmt
    - goimports
    - govet
    - errcheck
    - staticcheck
    - unused
    - gosimple
    - structcheck
    - varcheck
    - ineffassign
    - deadcode
    - typecheck
    - gosec
    - misspell
    - lll
    - unconvert
    - dupl
    - goconst
    - gocyclo

linters-settings:
  lll:
    line-length: 120
  gocyclo:
    min-complexity: 15
  dupl:
    threshold: 150

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - gocyclo
        - errcheck
        - dupl
        - gosec
// .eslintrc.json
{
  "extends": [
    "@react-native",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "react-hooks"],
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/prefer-const": "error",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "prefer-const": "error",
    "no-var": "error",
    "object-shorthand": "error",
    "prefer-template": "error"
  }
}

Git Workflow

# Branch naming conventions
feature/add-game-session-management
bugfix/fix-timer-display-issue
hotfix/security-vulnerability-patch
refactor/improve-database-queries

# Commit message format
type(scope): description

# Types: feat, fix, docs, style, refactor, test, chore
# Examples:
feat(game): add session timeout handling
fix(auth): resolve JWT token validation issue
docs(api): update authentication endpoint documentation
test(services): add comprehensive game service tests

This comprehensive development guidelines document ensures consistent, maintainable, and high-quality code across the Know Foolery project.