282 lines
8.2 KiB
Go
282 lines
8.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"chat-switchboard/config"
|
|
"chat-switchboard/crypto"
|
|
"chat-switchboard/database"
|
|
"chat-switchboard/middleware"
|
|
"chat-switchboard/store"
|
|
postgres "chat-switchboard/store/postgres"
|
|
sqlite "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)
|
|
}
|
|
}
|