rdev/internal/handlers/credentials_test.go
jordan 56e3f83955 feat: add auth scopes, OpenAPI docs, SDLC guides, and code quality improvements
- Add auth.RequireScope() to all handler routes for proper authorization
- Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator)
- Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog)
- Add artifact_test.go for SDLC artifact coverage
- Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w
- Fix error wrapping to use %w instead of %v throughout codebase
- Improve CLI merge command with conflict detection and resolution
- Fix handler tests to include auth middleware for RequireScope
- Add cookbook tree runner scripts for automated testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:55:50 -07:00

442 lines
12 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/orchard9/rdev/internal/domain"
)
// mockCredentialStore implements port.CredentialStore for testing.
type mockCredentialStore struct {
creds map[string]domain.Credential
err error
}
func newMockCredentialStore() *mockCredentialStore {
return &mockCredentialStore{
creds: make(map[string]domain.Credential),
}
}
func (m *mockCredentialStore) Get(_ context.Context, key string) (string, error) {
if m.err != nil {
return "", m.err
}
c, ok := m.creds[key]
if !ok {
return "", nil
}
return c.Value, nil
}
func (m *mockCredentialStore) GetRequired(_ context.Context, key string) (string, error) {
if m.err != nil {
return "", m.err
}
c, ok := m.creds[key]
if !ok {
return "", fmt.Errorf("credential not found: %s", key)
}
return c.Value, nil
}
func (m *mockCredentialStore) Set(_ context.Context, cred domain.Credential) error {
if m.err != nil {
return m.err
}
cred.CreatedAt = time.Now()
cred.UpdatedAt = time.Now()
m.creds[cred.Key] = cred
return nil
}
func (m *mockCredentialStore) Delete(_ context.Context, key string) error {
if m.err != nil {
return m.err
}
if _, ok := m.creds[key]; !ok {
return domain.ErrCredentialNotFound
}
delete(m.creds, key)
return nil
}
func (m *mockCredentialStore) List(_ context.Context) ([]domain.Credential, error) {
if m.err != nil {
return nil, m.err
}
var result []domain.Credential
for _, c := range m.creds {
result = append(result, c)
}
return result, nil
}
func (m *mockCredentialStore) ListByCategory(_ context.Context, category string) ([]domain.Credential, error) {
if m.err != nil {
return nil, m.err
}
var result []domain.Credential
for _, c := range m.creds {
if c.Category == category {
result = append(result, c)
}
}
return result, nil
}
func (m *mockCredentialStore) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
if m.err != nil {
return nil, m.err
}
result := make(map[string]string)
for _, k := range keys {
if c, ok := m.creds[k]; ok {
result[k] = c.Value
}
}
return result, nil
}
func (m *mockCredentialStore) SetMultiple(_ context.Context, creds []domain.Credential) error {
if m.err != nil {
return m.err
}
for _, c := range creds {
c.CreatedAt = time.Now()
c.UpdatedAt = time.Now()
m.creds[c.Key] = c
}
return nil
}
func setupCredentialsHandler() (*CredentialsHandler, *mockCredentialStore, chi.Router) {
store := newMockCredentialStore()
h := NewCredentialsHandler(store)
r := chi.NewRouter()
r.Use(testAdminAuth) // Add auth middleware for tests
h.Mount(r)
return h, store, r
}
func TestCredentialsHandler_List(t *testing.T) {
t.Run("empty list", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
req := httptest.NewRequest("GET", "/credentials", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data, ok := resp["data"].([]any)
if !ok {
t.Fatalf("response missing data array")
}
if len(data) != 0 {
t.Errorf("data length = %d, want 0", len(data))
}
})
t.Run("with credentials", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
store.creds["MY_TOKEN"] = domain.Credential{
Key: "MY_TOKEN",
Value: "****",
Category: "gitea",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
req := httptest.NewRequest("GET", "/credentials", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data, ok := resp["data"].([]any)
if !ok {
t.Fatalf("response missing data array")
}
if len(data) != 1 {
t.Errorf("data length = %d, want 1", len(data))
}
})
t.Run("filter by category", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
store.creds["GITEA_TOKEN"] = domain.Credential{
Key: "GITEA_TOKEN", Value: "****", Category: "gitea",
CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
store.creds["CF_TOKEN"] = domain.Credential{
Key: "CF_TOKEN", Value: "****", Category: "cloudflare",
CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
req := httptest.NewRequest("GET", "/credentials?category=gitea", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data := resp["data"].([]any)
if len(data) != 1 {
t.Errorf("data length = %d, want 1", len(data))
}
})
}
func TestCredentialsHandler_Get(t *testing.T) {
t.Run("existing credential", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
store.creds["MY_TOKEN"] = domain.Credential{
Key: "MY_TOKEN", Value: "secret123",
CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
req := httptest.NewRequest("GET", "/credentials/MY_TOKEN", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data := resp["data"].(map[string]any)
if data["key"] != "MY_TOKEN" {
t.Errorf("key = %q, want %q", data["key"], "MY_TOKEN")
}
if data["value"] != "secret123" {
t.Errorf("value = %q, want %q", data["value"], "secret123")
}
})
t.Run("not found", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
req := httptest.NewRequest("GET", "/credentials/MISSING", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
})
}
func TestCredentialsHandler_Set(t *testing.T) {
t.Run("valid credential", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
body, _ := json.Marshal(SetCredentialRequest{
Key: "NEW_TOKEN",
Value: "secret",
Description: "A test token",
Category: "gitea",
})
req := httptest.NewRequest("POST", "/credentials", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if _, ok := store.creds["NEW_TOKEN"]; !ok {
t.Error("credential not stored")
}
})
t.Run("missing key", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
body, _ := json.Marshal(SetCredentialRequest{Value: "secret"})
req := httptest.NewRequest("POST", "/credentials", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
t.Run("missing value", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
body, _ := json.Marshal(SetCredentialRequest{Key: "TOKEN"})
req := httptest.NewRequest("POST", "/credentials", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
t.Run("invalid json", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
req := httptest.NewRequest("POST", "/credentials", bytes.NewReader([]byte("not json")))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
}
func TestCredentialsHandler_SetBatch(t *testing.T) {
t.Run("valid batch", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
body, _ := json.Marshal(SetBatchRequest{
Credentials: []SetCredentialRequest{
{Key: "TOKEN1", Value: "val1"},
{Key: "TOKEN2", Value: "val2"},
},
})
req := httptest.NewRequest("POST", "/credentials/batch", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if len(store.creds) != 2 {
t.Errorf("stored credentials = %d, want 2", len(store.creds))
}
})
t.Run("empty array", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
body, _ := json.Marshal(SetBatchRequest{Credentials: []SetCredentialRequest{}})
req := httptest.NewRequest("POST", "/credentials/batch", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
t.Run("missing key in batch", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
body, _ := json.Marshal(SetBatchRequest{
Credentials: []SetCredentialRequest{
{Key: "TOKEN1", Value: "val1"},
{Key: "", Value: "val2"},
},
})
req := httptest.NewRequest("POST", "/credentials/batch", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
t.Run("missing value in batch", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
body, _ := json.Marshal(SetBatchRequest{
Credentials: []SetCredentialRequest{
{Key: "TOKEN1", Value: ""},
},
})
req := httptest.NewRequest("POST", "/credentials/batch", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
}
func TestCredentialsHandler_Delete(t *testing.T) {
t.Run("existing credential", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
store.creds["TO_DELETE"] = domain.Credential{
Key: "TO_DELETE", Value: "val",
CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
req := httptest.NewRequest("DELETE", "/credentials/TO_DELETE", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
var resp map[string]any
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data := resp["data"].(map[string]any)
if data["status"] != "deleted" {
t.Errorf("status = %q, want %q", data["status"], "deleted")
}
})
t.Run("not found returns 404", func(t *testing.T) {
_, _, router := setupCredentialsHandler()
req := httptest.NewRequest("DELETE", "/credentials/NONEXISTENT", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
})
t.Run("store error returns 500", func(t *testing.T) {
_, store, router := setupCredentialsHandler()
store.err = fmt.Errorf("database connection lost")
req := httptest.NewRequest("DELETE", "/credentials/ANY_KEY", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
})
}