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) } }