package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "switchboard-core/database" ) const testJWTSecret = "test-secret-key-for-handler-tests" type testHarness struct { router *gin.Engine t *testing.T } func (h *testHarness) request(method, path, token string, body interface{}) *httptest.ResponseRecorder { var bodyReader *bytes.Reader if body != nil { b, _ := json.Marshal(body) bodyReader = bytes.NewReader(b) } else { bodyReader = bytes.NewReader(nil) } req := httptest.NewRequest(method, path, bodyReader) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() h.router.ServeHTTP(w, req) return w } func makeToken(userID, email, role string) string { claims := Claims{ UserID: userID, Email: email, Role: role, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), Issuer: "switchboard-core", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) s, _ := token.SignedString([]byte(testJWTSecret)) return s } // seedInsertReturningID inserts a row, pre-generating an id for SQLite. // Expects query like: INSERT INTO users (col1, col2, ...) VALUES ($1, $2, ...) RETURNING id func seedInsertReturningID(t *testing.T, query string, args ...interface{}) string { t.Helper() id := uuid.New().String() // Inject id column and value into the query // "INSERT INTO users (username, ..." → "INSERT INTO users (id, username, ..." q := strings.Replace(query, "(username,", "(id, username,", 1) // Strip RETURNING clause if idx := strings.Index(q, " RETURNING "); idx > 0 { q = q[:idx] } if database.IsSQLite() { // Convert $N → ? for i := len(args) + 1; i >= 1; i-- { q = strings.ReplaceAll(q, "$"+itoa(i), "?") } // Add ? for the injected id column q = strings.Replace(q, "VALUES (", "VALUES (?, ", 1) newArgs := append([]interface{}{id}, args...) _, err := database.TestDB.Exec(q, newArgs...) if err != nil { t.Fatalf("seedInsertReturningID (sqlite): %v\nquery: %s", err, q) } return id } // Postgres: renumber $N → $N+1, insert $1 for id for i := len(args); i >= 1; i-- { q = strings.ReplaceAll(q, "$"+itoa(i), "$"+itoa(i+1)) } q = strings.Replace(q, "(id,", "(id,", 1) // Add id as first value placeholder q = strings.Replace(q, "VALUES (", "VALUES ($1, ", 1) newArgs := append([]interface{}{id}, args...) _, err := database.TestDB.Exec(q, newArgs...) if err != nil { t.Fatalf("seedInsertReturningID (pg): %v\nquery: %s", err, q) } return id } func itoa(i int) string { if i < 10 { return string(rune('0' + i)) } return string(rune('0'+i/10)) + string(rune('0'+i%10)) } func decode(args ...interface{}) { switch len(args) { case 2: w := args[0].(*httptest.ResponseRecorder) json.NewDecoder(w.Body).Decode(args[1]) case 3: t := args[0].(*testing.T) t.Helper() w := args[1].(*httptest.ResponseRecorder) if err := json.NewDecoder(w.Body).Decode(args[2]); err != nil { t.Fatalf("decode response: %v", err) } } } // dialectSQL returns query as-is. Placeholder conversion happens in seedInsertReturningID. func dialectSQL(query string) string { if database.IsSQLite() { q := query for i := 20; i >= 1; i-- { q = strings.ReplaceAll(q, "$"+itoa(i), "?") } return q } return query } func init() { _ = http.StatusOK }