Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# Pre-push hook to run linting before pushing

set -e

echo "🔍 Running linter before push..."

# Check if golangci-lint is installed
if ! command -v golangci-lint &> /dev/null; then
echo "❌ golangci-lint is not installed"
echo "Install it with: brew install golangci-lint"
exit 1
fi

# Run golangci-lint
if golangci-lint run ./... --timeout=5m; then
echo "✅ Linting passed"
exit 0
else
echo "❌ Linting failed. Please fix the errors before pushing."
exit 1
fi
93 changes: 93 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: CI

on:
push:
branches: [ main, develop, 'feat/**' ]

jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15

services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: chattorumu_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
cache: true

- name: Install dependencies
run: go mod download

- name: Run tests with coverage
run: |
go test -v -race -covermode=atomic -coverprofile=coverage.out ./...

- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false

lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
cache: true

- name: Install golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=5m

build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
cache: true

- name: Build chat-server
run: go build -o chat-server ./cmd/chat-server

- name: Build stock-bot
run: go build -o stock-bot ./cmd/stock-bot
66 changes: 66 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
run:
timeout: 5m
tests: true
build-tags: []
skip-dirs: []
skip-files: []

linters:
enable:
- errcheck
- govet
- revive
- goimports
- ineffassign
- gosec
- gocritic
- misspell
- stylecheck
- unconvert
- unparam
- unused

linters-settings:
errcheck:
check-type-assertions: true
check-blank: false

govet:
enable-all: true
disable:
- fieldalignment

revive:
severity: warning

gosec:
severity: medium

gocritic:
enabled-checks:
- boolExprSimplify
- commentedOutCode
- emptyFallthrough
- emptyStringTest
- equalFold
- nilValReturn
- paramTypeCombine
- sloppyReassign
- stringsCompare
- typeAssertChain
- unnecessaryBlock
- yodaStyleExpr

issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- gocritic
- unparam
- path: cmd/.*
linters:
- revive

max-issues-per-linter: 0
max-same-issues: 0
87 changes: 87 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Pre-commit hooks configuration for automated code quality checks
# Install with: pip install pre-commit && pre-commit install
# Run manually: pre-commit run --all-files

default_stages: [commit]
fail_fast: false

repos:
# Common file checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
# Detects and fixes trailing whitespace
- id: trailing-whitespace
name: Trim trailing whitespace
types: [text]
exclude: \.md$

# Ensures files end with a newline
- id: end-of-file-fixer
name: Fix end of file
types: [text]

# Validates YAML syntax
- id: check-yaml
name: Check YAML syntax
args: [--unsafe]

# Prevents committing large files
- id: check-added-large-files
name: Check for large files
args: [--maxkb=500]

# Detects merge conflict markers
- id: check-merge-conflict
name: Check for merge conflicts
types: [text]

# Detects private keys and other secrets
- id: detect-private-key
name: Detect private keys
types: [text]

# Ensures JSON files are valid
- id: check-json
name: Check JSON validity

# Go code formatting and quality
- repo: local
hooks:
# Format Go code with gofmt (simplified syntax)
- id: go-fmt
name: Format code with gofmt
entry: bash -c 'gofmt -s -w "$@"' --
language: system
types: [go]
pass_filenames: true
stages: [commit]

# Organize and format imports
- id: go-imports
name: Organize imports with goimports
entry: bash -c 'goimports -w "$@"' --
language: system
types: [go]
pass_filenames: true
stages: [commit]

# Run go vet for static analysis
- id: go-vet
name: Analyze code with go vet
entry: go vet ./...
language: system
types: [go]
pass_filenames: false
stages: [commit]
always_run: true

# Run golangci-lint for comprehensive linting
- id: golangci-lint
name: Lint with golangci-lint
entry: golangci-lint run
language: system
types: [go]
pass_filenames: false
stages: [commit]
require_serial: true
21 changes: 14 additions & 7 deletions cmd/chat-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"jobsity-chat/internal/middleware"
"jobsity-chat/internal/observability"
"jobsity-chat/internal/repository/postgres"
"jobsity-chat/internal/security"
"jobsity-chat/internal/service"
"jobsity-chat/internal/websocket"

Expand Down Expand Up @@ -46,12 +47,13 @@ func main() {
db, err := config.NewPostgresConnection(cfg.DatabaseURL)
if err != nil {
slog.Error("failed to connect to database", slog.String("error", err.Error()))
//nolint:gocritic // Intentional exit before defer
os.Exit(1)
}
defer db.Close()

if err := db.PingContext(connCtx); err != nil {
slog.Error("database ping failed", slog.String("error", err.Error()))
if pingErr := db.PingContext(connCtx); pingErr != nil {
slog.Error("database ping failed", slog.String("error", pingErr.Error()))
os.Exit(1)
}
slog.Info("connected to postgresql")
Expand Down Expand Up @@ -119,7 +121,10 @@ func main() {
go startSessionCleanup(ctx, sessionRepo)
slog.Info("session cleanup task started")

authHandler := handler.NewAuthHandler(authService)
// Initialize CSRF token manager
csrfTokenMgr := security.NewTokenManager()

authHandler := handler.NewAuthHandler(authService, sessionRepo, csrfTokenMgr)
chatroomHandler := handler.NewChatroomHandler(chatService, hub)
wsHandler := handler.NewWebSocketHandler(hub, chatService, authService, rmq, sessionRepo, cfg.AllowedOrigins)

Expand All @@ -131,7 +136,6 @@ func main() {
r.Use(chimiddleware.RealIP)
r.Use(middleware.CORS(middleware.ParseOrigins(cfg.AllowedOrigins)))
r.Use(middleware.Metrics())
// r.Use(middleware.OpenAPIValidator(middleware.DefaultOpenAPIValidatorConfig()))

r.Get("/health", handler.Health)
r.Get("/health/ready", handler.Ready(db, rmq))
Expand Down Expand Up @@ -174,6 +178,7 @@ func main() {

r.Group(func(r chi.Router) {
r.Use(middleware.Auth(sessionRepo))
r.Use(middleware.CSRF(sessionRepo))
r.Use(apiLimiter.Middleware())

r.Get("/auth/me", authHandler.Me)
Expand Down Expand Up @@ -242,10 +247,11 @@ func ensureBotUser(authService *service.AuthService) string {

case errors.Is(err, domain.ErrUsernameExists):
slog.Info("bot user already exists, fetching")
botUser, err := authService.GetUserByUsername(ctx, "StockBot")
if err != nil {
botUser, fetchErr := authService.GetUserByUsername(ctx, "StockBot")
if fetchErr != nil {
slog.Error("bot user exists but cannot fetch",
slog.String("error", err.Error()))
slog.String("error", fetchErr.Error()))
//nolint:gocritic // Intentional exit before defer
os.Exit(1)
}
slog.Info("using existing bot user",
Expand All @@ -255,6 +261,7 @@ func ensureBotUser(authService *service.AuthService) string {

default:
slog.Error("failed to ensure bot user", slog.String("error", err.Error()))
//nolint:gocritic // Intentional exit before defer
os.Exit(1)
}

Expand Down
3 changes: 2 additions & 1 deletion cmd/stock-bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func main() {
rmq, err := messaging.NewRabbitMQWithRetry(rmqCtx, cfg.RabbitMQURL)
if err != nil {
slog.Error("failed to connect to rabbitmq", slog.String("error", err.Error()))
//nolint:gocritic // Intentional exit before defer
os.Exit(1)
}
defer rmq.Close()
Expand Down Expand Up @@ -73,7 +74,7 @@ func main() {
slog.Error("error processing command", slog.String("error", err.Error()))
}
msgCancel()
msg.Ack(false)
_ = msg.Ack(false)
}
}
}()
Expand Down
8 changes: 3 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ func (c *Config) Validate() error {
if c.AllowedOrigins != "" {
log.Println("WARNING: Ensure ALLOWED_ORIGINS uses HTTPS in production")
}
} else {
} else if c.SessionSecret == "" {
// Development/staging: provide default if not set
if c.SessionSecret == "" {
c.SessionSecret = "dev-secret-not-for-production"
log.Println("Using default SESSION_SECRET for development")
}
c.SessionSecret = "dev-secret-not-for-production"
log.Println("Using default SESSION_SECRET for development")
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func TestConfig_Validate_Staging(t *testing.T) {
// Helper function
func contains(s, substr string) bool {
return len(s) >= len(substr) && s[:len(substr)] == substr ||
len(s) > len(substr) && containsHelper(s, substr)
len(s) > len(substr) && containsHelper(s, substr)
}

func containsHelper(s, substr string) bool {
Expand Down
Loading
Loading