package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "fission-console/internal/fission" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func TestHandleTimeTriggersRootCreateListGetUpdateDelete(t *testing.T) { ctx := context.Background() scheme := runtime.NewScheme() client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds( scheme, map[schema.GroupVersionResource]string{ fission.TimeTrigGVR: "TimeTriggerList", }, functionObject("fission-test", "hello"), ) s := &Server{dyn: client, ns: "fission-test"} createReq := httptest.NewRequest(http.MethodPost, "/console/api/timetriggers", strings.NewReader(`{"name":"cron-job","functionName":"hello","cron":"*/5 * * * *","method":"get","subpath":"logs"}`)) createRec := httptest.NewRecorder() s.handleTimeTriggersRoot(createRec, createReq) if createRec.Code != http.StatusCreated { t.Fatalf("create status = %d, want %d body=%s", createRec.Code, http.StatusCreated, createRec.Body.String()) } created := decodeMap(t, createRec.Body.Bytes()) if created["name"] != "cron-job" { t.Fatalf("create name = %v", created["name"]) } if created["cron"] != "*/5 * * * *" { t.Fatalf("create cron = %v", created["cron"]) } if created["method"] != http.MethodGet { t.Fatalf("create method = %v", created["method"]) } if created["subpath"] != "/logs" { t.Fatalf("create subpath = %v", created["subpath"]) } createdObj, err := client.Resource(fission.TimeTrigGVR).Namespace("fission-test").Get(ctx, "cron-job", metav1.GetOptions{}) if err != nil { t.Fatalf("get created timetrigger: %v", err) } if cron, _, _ := unstructured.NestedString(createdObj.Object, "spec", "cron"); cron != "*/5 * * * *" { t.Fatalf("stored cron = %q", cron) } listReq := httptest.NewRequest(http.MethodGet, "/console/api/timetriggers", nil) listRec := httptest.NewRecorder() s.handleTimeTriggersRoot(listRec, listReq) if listRec.Code != http.StatusOK { t.Fatalf("list status = %d body=%s", listRec.Code, listRec.Body.String()) } if !strings.Contains(listRec.Body.String(), "cron-job") { t.Fatalf("list body does not contain created trigger: %s", listRec.Body.String()) } getReq := httptest.NewRequest(http.MethodGet, "/console/api/timetriggers/cron-job", nil) getRec := httptest.NewRecorder() s.handleTimeTriggersAction(getRec, getReq) if getRec.Code != http.StatusOK { t.Fatalf("get status = %d body=%s", getRec.Code, getRec.Body.String()) } got := decodeMap(t, getRec.Body.Bytes()) if got["function"] != "hello" { t.Fatalf("get function = %v", got["function"]) } updateReq := httptest.NewRequest(http.MethodPut, "/console/api/timetriggers/cron-job", strings.NewReader(`{"name":"cron-job","functionName":"hello","cron":"@hourly","method":"post","subpath":"/"}`)) updateRec := httptest.NewRecorder() s.handleTimeTriggersAction(updateRec, updateReq) if updateRec.Code != http.StatusOK { t.Fatalf("update status = %d body=%s", updateRec.Code, updateRec.Body.String()) } updated, err := client.Resource(fission.TimeTrigGVR).Namespace("fission-test").Get(ctx, "cron-job", metav1.GetOptions{}) if err != nil { t.Fatalf("get updated timetrigger: %v", err) } if cron, _, _ := unstructured.NestedString(updated.Object, "spec", "cron"); cron != "@hourly" { t.Fatalf("updated cron = %q", cron) } deleteReq := httptest.NewRequest(http.MethodDelete, "/console/api/timetriggers/cron-job", nil) deleteRec := httptest.NewRecorder() s.handleTimeTriggersAction(deleteRec, deleteReq) if deleteRec.Code != http.StatusOK { t.Fatalf("delete status = %d body=%s", deleteRec.Code, deleteRec.Body.String()) } if _, err := client.Resource(fission.TimeTrigGVR).Namespace("fission-test").Get(ctx, "cron-job", metav1.GetOptions{}); !apierrors.IsNotFound(err) { t.Fatalf("expected timetrigger to be deleted, got err=%v", err) } } func TestHandleCreateTimeTriggerRejectsMissingFunction(t *testing.T) { client := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) s := &Server{dyn: client, ns: "fission-test"} req := httptest.NewRequest(http.MethodPost, "/console/api/timetriggers", strings.NewReader(`{"name":"cron-job","functionName":"missing","cron":"*/5 * * * *"}`)) rec := httptest.NewRecorder() s.handleTimeTriggersRoot(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "not found") { t.Fatalf("expected not found error, got %s", rec.Body.String()) } } func decodeMap(t *testing.T, data []byte) map[string]any { t.Helper() var result map[string]any if err := json.Unmarshal(data, &result); err != nil { t.Fatalf("decode json: %v body=%s", err, string(data)) } return result } func functionObject(namespace, name string) *unstructured.Unstructured { return &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Function", "metadata": map[string]any{ "name": name, "namespace": namespace, }, }} }