package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "github.com/kami/hexis/internal/domain" "github.com/kami/hexis/internal/execution" "github.com/kami/hexis/internal/nexusclient" "github.com/kami/hexis/internal/provider" "github.com/kami/hexis/internal/storage" ) type targetTestProvider struct{} func (targetTestProvider) Name() string { return "fake" } func (targetTestProvider) Execute(ctx context.Context, c *domain.Capability, r *domain.ExecuteRequest) (map[string]any, error) { return map[string]any{"ok": true}, nil } // newTargetValidationServer wires a Handler against a stub Nexus that knows // exactly one container entity, plus one capability scoped to containers. func newTargetValidationServer(t *testing.T) (*http.ServeMux, *domain.Capability) { t.Helper() nexus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch strings.TrimPrefix(r.URL.Path, "/api/v1/entities/") { case "ent_container_maven": json.NewEncoder(w).Encode(map[string]any{ "id": "ent_container_maven", "type": "container", "state": "active", }) case "ent_host_homesrv": json.NewEncoder(w).Encode(map[string]any{ "id": "ent_host_homesrv", "type": "host", "state": "active", }) default: w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": "entity not found"}) } })) t.Cleanup(nexus.Close) store, err := storage.Open(filepath.Join(t.TempDir(), "hexis.db")) if err != nil { t.Fatalf("open store: %v", err) } t.Cleanup(func() { store.Close() }) reg := provider.NewRegistry() reg.Register(targetTestProvider{}) now := time.Now().UTC() cap := &domain.Capability{ ID: domain.NewCapabilityID(), Name: "workspace.docker.restart", Provider: "fake", Operation: "noop", Risk: "low", Enabled: true, TargetTypes: []string{"container"}, Attributes: map[string]any{}, CreatedAt: now, UpdatedAt: now, Version: 1, } if err := store.CreateCapability(cap); err != nil { t.Fatalf("create capability: %v", err) } engine := execution.New(store, reg, execution.WithEntityLookup(nexusclient.New(nexus.URL))) mux := http.NewServeMux() NewHandler(store, engine, testToken).Register(mux) return mux, cap } // TestExecuteHTTP_TargetValidation pins the HTTP status codes for each way a // target can be refused (ECOSYSTEM-SPEC.md ยง4.3). func TestExecuteHTTP_TargetValidation(t *testing.T) { mux, cap := newTargetValidationServer(t) cases := []struct { name string target string want int }{ {"free text", "the maven container", http.StatusBadRequest}, {"bare name", "maven", http.StatusBadRequest}, {"unknown entity", "ent_not_real", http.StatusNotFound}, {"wrong type", "ent_host_homesrv", http.StatusUnprocessableEntity}, {"valid target", "ent_container_maven", http.StatusOK}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { body := `{"capability_id":"` + cap.ID + `","target_entity_id":"` + tc.target + `"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/execute", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+testToken) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != tc.want { t.Fatalf("target %q: expected %d, got %d: %s", tc.target, tc.want, w.Code, w.Body.String()) } }) } }