Stripped lots of boilerplate prepare for minimal test branch
This commit is contained in:
@@ -1,22 +0,0 @@
|
|||||||
# You can override the included template(s) by including variable overrides
|
|
||||||
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
|
|
||||||
# Secret Detection customization: https://docs.gitlab.com/user/application_security/secret_detection/pipeline/configure
|
|
||||||
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
|
|
||||||
# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
|
|
||||||
# Note that environment variables can be set in several places
|
|
||||||
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
|
|
||||||
:stages:
|
|
||||||
- test
|
|
||||||
:sast:
|
|
||||||
:stage: test
|
|
||||||
:include:
|
|
||||||
- :template: Security/SAST.gitlab-ci.yml
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- secret-detection
|
|
||||||
variables:
|
|
||||||
SECRET_DETECTION_ENABLED: 'true'
|
|
||||||
secret_detection:
|
|
||||||
stage: secret-detection
|
|
||||||
include:
|
|
||||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
|
||||||
18
Makefile
18
Makefile
@@ -4,24 +4,8 @@
|
|||||||
|
|
||||||
# Main target
|
# Main target
|
||||||
run:
|
run:
|
||||||
go run ./cmd
|
go run ./src
|
||||||
|
|
||||||
# Formatting the whole codebase
|
|
||||||
format:
|
|
||||||
gofmt -w ./src
|
|
||||||
gofmt -w ./cmd
|
|
||||||
|
|
||||||
# Test in verbose mode
|
|
||||||
test:
|
|
||||||
go test ./src -v
|
|
||||||
go test ./cmd -v
|
|
||||||
|
|
||||||
# Nix stuff
|
# Nix stuff
|
||||||
nix-run:
|
nix-run:
|
||||||
nix develop --command make run
|
nix develop --command make run
|
||||||
|
|
||||||
nix-test:
|
|
||||||
nix develop --command make test
|
|
||||||
|
|
||||||
nix-format:
|
|
||||||
nix develop --command make format
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
app "gitlab.gwdg.de/qrank/qrank/src"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app.App()
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dummy main function for testing
|
|
||||||
func TestMain(t *testing.T) {}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package src
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|||||||
118
src/auth_test.go
118
src/auth_test.go
@@ -1,118 +0,0 @@
|
|||||||
package src
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setupTestDB initializes an in-memory SQLite DB for tests
|
|
||||||
func setupTestDB(t *testing.T) *gorm.DB {
|
|
||||||
t.Helper()
|
|
||||||
d, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open test db: %v", err)
|
|
||||||
}
|
|
||||||
if err := d.AutoMigrate(&User{}, &Session{}); err != nil {
|
|
||||||
t.Fatalf("failed to migrate test db: %v", err)
|
|
||||||
}
|
|
||||||
db = d // override global
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFindOrCreateUserByHandle_Email
|
|
||||||
func TestFindOrCreateUserByHandle_Email(t *testing.T) {
|
|
||||||
setupTestDB(t)
|
|
||||||
|
|
||||||
u, err := findOrCreateUserByHandle("test@example.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if u.Email != "test@example.com" {
|
|
||||||
t.Errorf("expected email test@example.com, got %s", u.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should return same user if called again
|
|
||||||
u2, err := findOrCreateUserByHandle("test@example.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if u.ID != u2.ID {
|
|
||||||
t.Errorf("expected same user ID, got %d and %d", u.ID, u2.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFindOrCreateUserByHandle_Username
|
|
||||||
func TestFindOrCreateUserByHandle_Username(t *testing.T) {
|
|
||||||
setupTestDB(t)
|
|
||||||
|
|
||||||
u, err := findOrCreateUserByHandle("PlayerOne")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(u.Email, "@local") {
|
|
||||||
t.Errorf("expected placeholder email, got %s", u.Email)
|
|
||||||
}
|
|
||||||
if u.Username != "PlayerOne" {
|
|
||||||
t.Errorf("expected username PlayerOne, got %s", u.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSessionUserAndRequireAuth
|
|
||||||
func TestGetSessionUserAndRequireAuth(t *testing.T) {
|
|
||||||
setupTestDB(t)
|
|
||||||
|
|
||||||
// Create a user + session
|
|
||||||
user := &User{Email: "auth@example.com", Username: "authuser"}
|
|
||||||
if err := db.Create(user).Error; err != nil {
|
|
||||||
t.Fatalf("failed to create user: %v", err)
|
|
||||||
}
|
|
||||||
session := &Session{Token: "testtoken", UserID: user.ID}
|
|
||||||
if err := db.Create(session).Error; err != nil {
|
|
||||||
t.Fatalf("failed to create session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a Gin context with session cookie
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
c, _ := gin.CreateTestContext(w)
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session", Value: "testtoken"})
|
|
||||||
c.Request = req
|
|
||||||
|
|
||||||
// Should find user
|
|
||||||
u := getSessionUser(c)
|
|
||||||
if u == nil || u.Email != "auth@example.com" {
|
|
||||||
t.Errorf("expected user auth@example.com, got %+v", u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test requireAuth middleware with valid session
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
c, _ = gin.CreateTestContext(w)
|
|
||||||
req, _ = http.NewRequest("GET", "/", nil)
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session", Value: "testtoken"})
|
|
||||||
c.Request = req
|
|
||||||
c.Next() // prepare pipeline
|
|
||||||
mw := requireAuth()
|
|
||||||
mw(c)
|
|
||||||
|
|
||||||
if c.IsAborted() {
|
|
||||||
t.Errorf("expected request to pass middleware, but it aborted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test requireAuth middleware with no cookie
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
c, _ = gin.CreateTestContext(w)
|
|
||||||
req, _ = http.NewRequest("GET", "/", nil)
|
|
||||||
c.Request = req
|
|
||||||
mw = requireAuth()
|
|
||||||
mw(c)
|
|
||||||
|
|
||||||
if !c.IsAborted() {
|
|
||||||
t.Errorf("expected request to abort when no session, but it passed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package src
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|||||||
@@ -1,30 +1,9 @@
|
|||||||
package src
|
package main
|
||||||
|
|
||||||
// socqr-mvp: single-binary Go + Gin app with email-magic-link auth (debug links in logs),
|
|
||||||
// SQLite (switchable to Postgres), and four views: user view, enter scores, history, leaderboard.
|
|
||||||
//
|
|
||||||
// Quick start:
|
|
||||||
// go mod init socqr
|
|
||||||
// go get github.com/gin-gonic/gin gorm.io/gorm gorm.io/driver/sqlite gorm.io/driver/postgres
|
|
||||||
// go run .
|
|
||||||
//
|
|
||||||
// Env vars (optional):
|
|
||||||
// DATABASE_URL="postgres://user:pass@host:5432/dbname?sslmode=disable" // if set, uses Postgres; else SQLite file socqr.db
|
|
||||||
// APP_BASE_URL="http://localhost:8080" // used for magic-link generation (defaults to http://localhost:8080)
|
|
||||||
// APP_BIND=":8080" // server bind address (default :8080)
|
|
||||||
// APP_COOKIE_DOMAIN="" // cookie domain (default empty)
|
|
||||||
//
|
|
||||||
// Notes:
|
|
||||||
// - Magic link tokens are valid for 30 days and can be used multiple times during that window (for multi-device sign-in).
|
|
||||||
// - Session cookie is long-lived (~10 years). Delete cookie to sign out.
|
|
||||||
// - Minimal inline-styled HTML (no external CSS or JS) for fast load and pure-Go rendering.
|
|
||||||
// - QR prep: includes a Table model and routes under /t/:slug/enter to prefill table context (no scanner yet).
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -47,27 +26,13 @@ const (
|
|||||||
|
|
||||||
// ===================== Templates =====================
|
// ===================== Templates =====================
|
||||||
|
|
||||||
var tpl *template.Template
|
var tpl = template.Must(template.New("").Funcs(template.FuncMap{
|
||||||
|
"fmtTime": func(t time.Time) string { return t.Local().Format("1000-01-01 10:10") },
|
||||||
func init() {
|
}).ParseGlob("templates/*.html"))
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filepath.Base(wd) == "src" {
|
|
||||||
wd = filepath.Join(wd, "..") // adjust if running from src dir
|
|
||||||
}
|
|
||||||
path := filepath.Join(wd, "templates", "*.html")
|
|
||||||
|
|
||||||
tpl = template.Must(template.New("").Funcs(template.FuncMap{
|
|
||||||
"fmtTime": func(t time.Time) string { return t.Local().Format("1000-01-01 10:10") },
|
|
||||||
}).ParseGlob(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== Main =====================
|
// ===================== Main =====================
|
||||||
|
|
||||||
func App() {
|
func main() {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
baseURL = os.Getenv("APP_BASE_URL")
|
baseURL = os.Getenv("APP_BASE_URL")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package src
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package src
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -104,7 +104,7 @@ func atoiSafe(s string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// minimal sscanf to avoid fmt import just for one call
|
// minimal sscanf to avoid fmt import just for one call
|
||||||
func fmtSscanf(s, format string, a *int) (int, error) {
|
func fmtSscanf(s, _ string, a *int) (int, error) {
|
||||||
// Only supports "%d"
|
// Only supports "%d"
|
||||||
n := 0
|
n := 0
|
||||||
for i := 0; i < len(s); i++ {
|
for i := 0; i < len(s); i++ {
|
||||||
|
|||||||
Reference in New Issue
Block a user