Changeset 0.28.6 (#192)
This commit is contained in:
281
server/handlers/git_credentials_test.go
Normal file
281
server/handlers/git_credentials_test.go
Normal file
@@ -0,0 +1,281 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user