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" ) // mockGitRepository implements port.GitRepository for testing. type mockGitRepository struct { repos map[string]*domain.Repo err error } func newMockGitRepository() *mockGitRepository { return &mockGitRepository{repos: make(map[string]*domain.Repo)} } func (m *mockGitRepository) CreateRepo(_ context.Context, name, description string, private bool) (*domain.Repo, error) { if m.err != nil { return nil, m.err } repo := &domain.Repo{ ID: 1, Owner: "threesix", Name: name, FullName: "threesix/" + name, Description: description, Private: private, CloneSSH: fmt.Sprintf("git@git.threesix.ai:threesix/%s.git", name), CloneHTTP: fmt.Sprintf("https://git.threesix.ai/threesix/%s.git", name), HTMLURL: fmt.Sprintf("https://git.threesix.ai/threesix/%s", name), CreatedAt: time.Now(), UpdatedAt: time.Now(), } m.repos[name] = repo return repo, nil } func (m *mockGitRepository) DeleteRepo(_ context.Context, _, name string) error { if m.err != nil { return m.err } delete(m.repos, name) return nil } func (m *mockGitRepository) ListRepos(_ context.Context, _ string) ([]*domain.Repo, error) { if m.err != nil { return nil, m.err } var repos []*domain.Repo for _, r := range m.repos { repos = append(repos, r) } return repos, nil } func (m *mockGitRepository) GetRepo(_ context.Context, _, name string) (*domain.Repo, error) { if m.err != nil { return nil, m.err } r, ok := m.repos[name] if !ok { return nil, fmt.Errorf("repo not found: %s", name) } return r, nil } func (m *mockGitRepository) AddCollaborator(context.Context, string, string, string, string) error { return m.err } func (m *mockGitRepository) RemoveCollaborator(context.Context, string, string, string) error { return m.err } func (m *mockGitRepository) AddDeployKey(context.Context, string, string, string, string, bool) (*domain.DeployKey, error) { return nil, m.err } func (m *mockGitRepository) DeleteDeployKey(context.Context, string, string, int64) error { return m.err } func (m *mockGitRepository) CreateWebhook(context.Context, string, string, string, string, []string) (*domain.RepoWebhook, error) { return nil, m.err } func (m *mockGitRepository) DeleteWebhook(context.Context, string, string, int64) error { return m.err } // mockDNSProvider implements port.DNSProvider for testing. type mockDNSProvider struct { records map[string]*domain.DNSRecord err error } func newMockDNSProvider() *mockDNSProvider { return &mockDNSProvider{records: make(map[string]*domain.DNSRecord)} } func (m *mockDNSProvider) CreateRecord(_ context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) { if m.err != nil { return nil, m.err } record.ID = "rec-" + record.Name m.records[record.Name] = &record return &record, nil } func (m *mockDNSProvider) UpdateRecord(_ context.Context, recordID string, record domain.DNSRecord) (*domain.DNSRecord, error) { if m.err != nil { return nil, m.err } record.ID = recordID m.records[recordID] = &record return &record, nil } func (m *mockDNSProvider) DeleteRecord(_ context.Context, recordID string) error { if m.err != nil { return m.err } delete(m.records, recordID) return nil } func (m *mockDNSProvider) DeleteRecordByName(_ context.Context, _, name string) error { if m.err != nil { return m.err } delete(m.records, name) return nil } func (m *mockDNSProvider) GetRecord(_ context.Context, recordID string) (*domain.DNSRecord, error) { if m.err != nil { return nil, m.err } r, ok := m.records[recordID] if !ok { return nil, fmt.Errorf("record not found") } return r, nil } func (m *mockDNSProvider) ListRecords(_ context.Context, recordType string) ([]*domain.DNSRecord, error) { if m.err != nil { return nil, m.err } var result []*domain.DNSRecord for _, r := range m.records { if recordType == "" || r.Type == recordType { result = append(result, r) } } return result, nil } func (m *mockDNSProvider) FindRecord(_ context.Context, _, name string) (*domain.DNSRecord, error) { if m.err != nil { return nil, m.err } r, ok := m.records[name] if !ok { return nil, nil } return r, nil } // mockDeployer implements port.Deployer for testing. type mockDeployer struct { deployments map[string]*domain.DeployStatus logs string err error } func newMockDeployer() *mockDeployer { return &mockDeployer{deployments: make(map[string]*domain.DeployStatus)} } func (m *mockDeployer) Deploy(_ context.Context, spec domain.DeploySpec) error { if m.err != nil { return m.err } m.deployments[spec.ProjectName] = &domain.DeployStatus{ ProjectName: spec.ProjectName, Image: spec.Image, Replicas: spec.Replicas, ReadyReplicas: 0, URL: "https://" + spec.Domain, Status: domain.DeploymentStatusPending, CreatedAt: time.Now(), UpdatedAt: time.Now(), } return nil } func (m *mockDeployer) Undeploy(_ context.Context, projectName string) error { if m.err != nil { return m.err } delete(m.deployments, projectName) return nil } func (m *mockDeployer) GetStatus(_ context.Context, projectName string) (*domain.DeployStatus, error) { if m.err != nil { return nil, m.err } s, ok := m.deployments[projectName] if !ok { return nil, nil } return s, nil } func (m *mockDeployer) Restart(_ context.Context, _ string) error { return m.err } func (m *mockDeployer) Scale(_ context.Context, projectName string, replicas int) error { if m.err != nil { return m.err } if s, ok := m.deployments[projectName]; ok { s.Replicas = replicas } return nil } func (m *mockDeployer) GetLogs(_ context.Context, _ string, _ int) (string, error) { if m.err != nil { return "", m.err } return m.logs, nil } func setupInfraHandler() (*InfrastructureHandler, *mockGitRepository, *mockDNSProvider, *mockDeployer, chi.Router) { git := newMockGitRepository() dns := newMockDNSProvider() deployer := newMockDeployer() h := NewInfrastructureHandler(git, dns, deployer, nil, nil, nil, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() h.Mount(r) return h, git, dns, deployer, r } func TestInfrastructureHandler_CreateRepo(t *testing.T) { t.Run("success", func(t *testing.T) { _, git, _, _, router := setupInfraHandler() body, _ := json.Marshal(CreateRepoRequest{Description: "Test repo", Private: true}) req := httptest.NewRequest("POST", "/projects/myapp/repo", 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 := git.repos["myapp"]; !ok { t.Error("repo not created") } }) t.Run("invalid project id", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() req := httptest.NewRequest("POST", "/projects/INVALID_NAME!/repo", bytes.NewReader([]byte("{}"))) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("empty body allowed", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() req := httptest.NewRequest("POST", "/projects/myapp/repo", bytes.NewReader([]byte(""))) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) // Should succeed with empty body (EOF is allowed) if rec.Code != http.StatusCreated { t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated) } }) t.Run("git not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{}) r := chi.NewRouter() h.Mount(r) req := httptest.NewRequest("POST", "/projects/myapp/repo", bytes.NewReader([]byte("{}"))) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError) } }) } func TestInfrastructureHandler_GetRepo(t *testing.T) { t.Run("found", func(t *testing.T) { _, git, _, _, router := setupInfraHandler() git.repos["myapp"] = &domain.Repo{ ID: 1, Owner: "threesix", Name: "myapp", FullName: "threesix/myapp", CloneSSH: "git@git.threesix.ai:threesix/myapp.git", } req := httptest.NewRequest("GET", "/projects/myapp/repo", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } }) t.Run("not found", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() req := httptest.NewRequest("GET", "/projects/missing/repo", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound) } }) } func TestInfrastructureHandler_DeleteRepo(t *testing.T) { t.Run("success", func(t *testing.T) { _, git, _, _, router := setupInfraHandler() git.repos["myapp"] = &domain.Repo{ID: 1, Name: "myapp"} req := httptest.NewRequest("DELETE", "/projects/myapp/repo", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } }) } func TestInfrastructureHandler_Deploy(t *testing.T) { t.Run("success", func(t *testing.T) { _, _, _, deployer, router := setupInfraHandler() body, _ := json.Marshal(DeployRequest{ Image: "registry.threesix.ai/myapp:latest", Port: 8080, Replicas: 2, }) req := httptest.NewRequest("POST", "/projects/myapp/deploy", 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 := deployer.deployments["myapp"]; !ok { t.Error("deployment not created") } }) t.Run("missing image", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() body, _ := json.Marshal(DeployRequest{Port: 8080}) req := httptest.NewRequest("POST", "/projects/myapp/deploy", 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("deployer not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{}) r := chi.NewRouter() h.Mount(r) body, _ := json.Marshal(DeployRequest{Image: "myimage:latest"}) req := httptest.NewRequest("POST", "/projects/myapp/deploy", bytes.NewReader(body)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError) } }) } func TestInfrastructureHandler_GetDeployStatus(t *testing.T) { t.Run("found", func(t *testing.T) { _, _, _, deployer, router := setupInfraHandler() deployer.deployments["myapp"] = &domain.DeployStatus{ ProjectName: "myapp", Image: "myimage:latest", Status: domain.DeploymentStatusRunning, Replicas: 2, } req := httptest.NewRequest("GET", "/projects/myapp/deploy/status", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } }) t.Run("not found", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() req := httptest.NewRequest("GET", "/projects/missing/deploy/status", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound) } }) } func TestInfrastructureHandler_Undeploy(t *testing.T) { t.Run("success", func(t *testing.T) { _, _, _, deployer, router := setupInfraHandler() deployer.deployments["myapp"] = &domain.DeployStatus{ProjectName: "myapp"} req := httptest.NewRequest("DELETE", "/projects/myapp/deploy", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } }) }