package main import ( "bytes" "context" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" ) func newTestServer(objs ...runtime.Object) *server { listKinds := map[schema.GroupVersionResource]string{ environmentGVR: "EnvironmentList", packageGVR: "PackageList", functionGVR: "FunctionList", httpTrigGVR: "HTTPTriggerList", timeTrigGVR: "TimeTriggerList", } dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, objs...) return &server{dyn: dyn, ns: "default", routerURL: "http://example.invalid", http: &http.Client{}, saTokenPath: ""} } func TestNormalizeMethods(t *testing.T) { got := normalizeMethods([]string{"get", " POST ", "get", ""}) if len(got) != 2 || got[0] != "GET" || got[1] != "POST" { t.Fatalf("unexpected methods: %#v", got) } fallback := normalizeMethods([]string{"", " "}) if len(fallback) != 1 || fallback[0] != "GET" { t.Fatalf("expected default GET, got %#v", fallback) } } func TestCreateFunctionValidation(t *testing.T) { s := newTestServer() req := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(`{"name":""}`)) rec := httptest.NewRecorder() s.handleCreateFunction(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String()) } } func TestCreateFunctionSuccessAndGetDetails(t *testing.T) { env := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Environment", "metadata": map[string]any{ "name": "python", "namespace": "default", }, }} s := newTestServer(env) body := `{"name":"t-fn","environment":"python","code":"def main(ctx):\n return {\"ok\": True}","entrypoint":"main.main","route":"/t-fn","methods":["get"]}` req := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(body)) rec := httptest.NewRecorder() s.handleCreateFunction(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String()) } ctx := context.Background() pkg, err := s.dyn.Resource(packageGVR).Namespace("default").Get(ctx, "t-fn-pkg", metav1.GetOptions{}) if err != nil { t.Fatalf("package not created: %v", err) } literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal") decoded, err := base64.StdEncoding.DecodeString(literal) if err != nil { t.Fatalf("decode literal: %v", err) } if !strings.Contains(string(decoded), "def main") { t.Fatalf("unexpected package code: %q", string(decoded)) } getReq := httptest.NewRequest(http.MethodGet, "/api/functions/t-fn", nil) getRec := httptest.NewRecorder() s.handleGetFunction(getRec, getReq, "t-fn") if getRec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", getRec.Code, getRec.Body.String()) } var out map[string]any if err := json.Unmarshal(getRec.Body.Bytes(), &out); err != nil { t.Fatalf("decode get response: %v", err) } if out["name"] != "t-fn" { t.Fatalf("unexpected name: %#v", out["name"]) } if out["environment"] != "python" { t.Fatalf("unexpected environment: %#v", out["environment"]) } } func TestUpdateFunctionCode(t *testing.T) { env := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Environment", "metadata": map[string]any{"name": "python", "namespace": "default"}, }} s := newTestServer(env) createReq := httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(`{"name":"upd-fn","environment":"python","code":"old","entrypoint":"main.main"}`)) createRec := httptest.NewRecorder() s.handleCreateFunction(createRec, createReq) if createRec.Code != http.StatusCreated { t.Fatalf("create failed: %d %s", createRec.Code, createRec.Body.String()) } updReq := httptest.NewRequest(http.MethodPut, "/api/functions/upd-fn/code", bytes.NewBufferString(`{"code":"new-code"}`)) updRec := httptest.NewRecorder() s.handleUpdateFunctionCode(updRec, updReq, "upd-fn") if updRec.Code != http.StatusOK { t.Fatalf("update failed: %d %s", updRec.Code, updRec.Body.String()) } ctx := context.Background() pkg, err := s.dyn.Resource(packageGVR).Namespace("default").Get(ctx, "upd-fn-pkg", metav1.GetOptions{}) if err != nil { t.Fatalf("get package: %v", err) } literal, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal") decoded, err := base64.StdEncoding.DecodeString(literal) if err != nil { t.Fatalf("decode literal: %v", err) } if string(decoded) != "new-code" { t.Fatalf("expected new-code, got %q", string(decoded)) } } func TestInvokeFunctionWithJWTAuth(t *testing.T) { // Mock router: /auth/login returns JWT, /inv-fn returns hello var gotAuth string mockRouter := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/auth/login" && r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"accesstoken":"fake-jwt-token-xyz","tokentype":"Bearer"}`)) return } gotAuth = r.Header.Get("Authorization") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"hello":"world"}`)) })) defer mockRouter.Close() env := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Environment", "metadata": map[string]any{"name": "python", "namespace": "default"}, }} listKinds := map[schema.GroupVersionResource]string{ environmentGVR: "EnvironmentList", packageGVR: "PackageList", functionGVR: "FunctionList", httpTrigGVR: "HTTPTriggerList", timeTrigGVR: "TimeTriggerList", } dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, env) s := &server{ dyn: dyn, ns: "default", routerURL: mockRouter.URL, http: mockRouter.Client(), authUser: "admin", authPass: "pass", } // Create function first createBody := `{"name":"inv-fn","environment":"python","code":"print(1)","route":"/inv-fn","methods":["GET"]}` createRec := httptest.NewRecorder() s.handleCreateFunction(createRec, httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(createBody))) if createRec.Code != http.StatusCreated { t.Fatalf("create: %d %s", createRec.Code, createRec.Body.String()) } // Invoke invokeRec := httptest.NewRecorder() s.handleInvokeFunction(invokeRec, httptest.NewRequest(http.MethodPost, "/api/functions/inv-fn/invoke", strings.NewReader(`{}`)), "inv-fn") if invokeRec.Code != http.StatusOK { t.Fatalf("invoke: %d %s", invokeRec.Code, invokeRec.Body.String()) } // Verify JWT was obtained via login and sent if gotAuth != "Bearer fake-jwt-token-xyz" { t.Fatalf("expected 'Bearer fake-jwt-token-xyz', got %q", gotAuth) } var out map[string]any if err := json.Unmarshal(invokeRec.Body.Bytes(), &out); err != nil { t.Fatalf("decode: %v", err) } if out["status"] != float64(200) { t.Fatalf("expected status 200, got %v", out["status"]) } if !strings.Contains(out["response_raw"].(string), "hello") { t.Fatalf("unexpected response: %v", out["response_raw"]) } }