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