This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/git_credentials_test.go
2026-03-15 01:33:38 +00:00

282 lines
8.3 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"testing"
"github.com/gin-gonic/gin"
"git.gobha.me/xcaliber/chat-switchboard/config"
"git.gobha.me/xcaliber/chat-switchboard/crypto"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/middleware"
"git.gobha.me/xcaliber/chat-switchboard/store"
postgres "git.gobha.me/xcaliber/chat-switchboard/store/postgres"
sqlite "git.gobha.me/xcaliber/chat-switchboard/store/sqlite"
)
// ── Git Credentials Test Harness ──────────
type gitCredHarness struct {
*testHarness
userToken string
userID string
user2Token string
user2ID string
}
func setupGitCredHarness(t *testing.T) *gitCredHarness {
t.Helper()
database.RequireTestDB(t)
database.TruncateAll(t)
cfg := &config.Config{
JWTSecret: testJWTSecret,
BasePath: "",
}
var stores store.Stores
if database.IsSQLite() {
stores = sqlite.NewStores(database.TestDB)
} else {
stores = postgres.NewStores(database.TestDB)
}
userCache := middleware.NewUserStatusCache()
// Create a test vault with a static env key (32 bytes for AES-256)
envKey := []byte("test-encryption-key-32-bytes!!")
for len(envKey) < 32 {
envKey = append(envKey, '0')
}
envKey = envKey[:32]
vault := crypto.NewKeyResolver(envKey, nil)
r := gin.New()
api := r.Group("/api/v1")
protected := api.Group("")
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
gitCredH := NewGitCredentialHandler(stores, vault)
protected.POST("/git-credentials", gitCredH.Create)
protected.POST("/git-credentials/generate", gitCredH.Generate)
protected.GET("/git-credentials", gitCredH.List)
protected.GET("/git-credentials/:id/public-key", gitCredH.GetPublicKey)
protected.DELETE("/git-credentials/:id", gitCredH.Delete)
// Seed users
userID := database.SeedTestUser(t, "gituser", "gituser@test.com")
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
userToken := makeToken(userID, "gituser@test.com", "user")
user2ID := database.SeedTestUser(t, "gituser2", "gituser2@test.com")
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), user2ID)
user2Token := makeToken(user2ID, "gituser2@test.com", "user")
return &gitCredHarness{
testHarness: &testHarness{router: r, t: t},
userToken: userToken,
userID: userID,
user2Token: user2Token,
user2ID: user2ID,
}
}
// ── GET /git-credentials — empty state ───
func TestGitCreds_List_Empty(t *testing.T) {
h := setupGitCredHarness(t)
resp := h.request("GET", "/api/v1/git-credentials", h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
}
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
data, ok := body["data"]
if !ok {
t.Fatal("response must have 'data' key")
}
arr := data.([]interface{})
if len(arr) != 0 {
t.Fatalf("expected empty array, got %d items", len(arr))
}
}
// ── POST /git-credentials/generate ───────
func TestGitCreds_Generate(t *testing.T) {
h := setupGitCredHarness(t)
resp := h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "Test Key",
})
if resp.Code != http.StatusCreated {
t.Fatalf("generate: got %d, body: %s", resp.Code, resp.Body.String())
}
var cred map[string]interface{}
json.NewDecoder(resp.Body).Decode(&cred)
// Must have public_key and fingerprint
pubKey, _ := cred["public_key"].(string)
fp, _ := cred["fingerprint"].(string)
if pubKey == "" {
t.Error("public_key should be non-empty")
}
if fp == "" {
t.Error("fingerprint should be non-empty")
}
if cred["auth_type"] != "ssh_key" {
t.Errorf("auth_type: got %v, want ssh_key", cred["auth_type"])
}
if cred["name"] != "Test Key" {
t.Errorf("name: got %v", cred["name"])
}
// Must NOT have encrypted data
if _, has := cred["encrypted_data"]; has {
t.Error("encrypted_data must not appear in response")
}
if _, has := cred["nonce"]; has {
t.Error("nonce must not appear in response")
}
}
// ── GET /git-credentials/:id/public-key ──
func TestGitCreds_GetPublicKey(t *testing.T) {
h := setupGitCredHarness(t)
// Generate a key first
resp := h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "PK Test",
})
var cred map[string]interface{}
json.NewDecoder(resp.Body).Decode(&cred)
id := cred["id"].(string)
origPK := cred["public_key"].(string)
// Retrieve public key
resp = h.request("GET", "/api/v1/git-credentials/"+id+"/public-key", h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("get public-key: got %d, body: %s", resp.Code, resp.Body.String())
}
var pkBody map[string]interface{}
json.NewDecoder(resp.Body).Decode(&pkBody)
if pkBody["public_key"] != origPK {
t.Errorf("public key mismatch")
}
if pkBody["fingerprint"] == nil || pkBody["fingerprint"] == "" {
t.Error("fingerprint should be present")
}
}
// ── List after generate ──────────────────
func TestGitCreds_ListAfterGenerate(t *testing.T) {
h := setupGitCredHarness(t)
h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "Key A",
})
h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "Key B",
})
resp := h.request("GET", "/api/v1/git-credentials", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 2 {
t.Fatalf("expected 2 keys, got %d", len(arr))
}
// Verify summaries have public_key + fingerprint
for _, raw := range arr {
item := raw.(map[string]interface{})
if item["public_key"] == nil || item["public_key"] == "" {
t.Error("list item should have public_key")
}
if item["fingerprint"] == nil || item["fingerprint"] == "" {
t.Error("list item should have fingerprint")
}
}
}
// ── Delete ───────────────────────────────
func TestGitCreds_Delete(t *testing.T) {
h := setupGitCredHarness(t)
resp := h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "Delete Me",
})
var cred map[string]interface{}
json.NewDecoder(resp.Body).Decode(&cred)
id := cred["id"].(string)
// Delete
resp = h.request("DELETE", "/api/v1/git-credentials/"+id, h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("delete: got %d", resp.Code)
}
// Verify gone
resp = h.request("GET", "/api/v1/git-credentials", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 0 {
t.Fatalf("expected 0 keys after delete, got %d", len(arr))
}
}
// ── User isolation ───────────────────────
func TestGitCreds_UserIsolation(t *testing.T) {
h := setupGitCredHarness(t)
// User 1 generates a key
resp := h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{
"name": "User1 Key",
})
var cred map[string]interface{}
json.NewDecoder(resp.Body).Decode(&cred)
id := cred["id"].(string)
// User 2 should see empty list
resp = h.request("GET", "/api/v1/git-credentials", h.user2Token, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 0 {
t.Fatalf("user2 should see 0 keys, got %d", len(arr))
}
// User 2 should not be able to get user1's public key
resp = h.request("GET", "/api/v1/git-credentials/"+id+"/public-key", h.user2Token, nil)
if resp.Code != http.StatusNotFound {
t.Fatalf("cross-user public-key: want 404, got %d", resp.Code)
}
// User 2 should not be able to delete user1's key
resp = h.request("DELETE", "/api/v1/git-credentials/"+id, h.user2Token, nil)
if resp.Code != http.StatusNotFound {
t.Fatalf("cross-user delete: want 404, got %d", resp.Code)
}
}
// ── Validation ───────────────────────────
func TestGitCreds_Generate_MissingName(t *testing.T) {
h := setupGitCredHarness(t)
resp := h.request("POST", "/api/v1/git-credentials/generate", h.userToken, map[string]interface{}{})
if resp.Code != http.StatusBadRequest {
t.Fatalf("missing name: want 400, got %d", resp.Code)
}
}