Files
fission-console/console/main_test.go
T

285 lines
9.4 KiB
Go

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 TestGetFunctionUsesSourceLiteralWhenDeploymentLiteralMissing(t *testing.T) {
env := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Environment",
"metadata": map[string]any{"name": "go-acc", "namespace": "default"},
}}
s := newTestServer(env)
pkg := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Package",
"metadata": map[string]any{
"name": "fn-go-acc-pkg",
"namespace": "default",
},
"spec": map[string]any{
"source": map[string]any{
"literal": base64.StdEncoding.EncodeToString([]byte("package main\n\nfunc Handler() {}\n")),
},
"deployment": map[string]any{
"type": "url",
"url": "http://storagesvc.fission/v1/archive?id=dummy",
},
},
}}
fn := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Function",
"metadata": map[string]any{
"name": "fn-go-acc",
"namespace": "default",
},
"spec": map[string]any{
"environment": map[string]any{"name": "go-acc", "namespace": "default"},
"package": map[string]any{
"functionName": "Handler",
"packageref": map[string]any{
"name": "fn-go-acc-pkg",
"namespace": "default",
},
},
},
}}
if _, err := s.dyn.Resource(packageGVR).Namespace("default").Create(context.Background(), pkg, metav1.CreateOptions{}); err != nil {
t.Fatalf("create package: %v", err)
}
if _, err := s.dyn.Resource(functionGVR).Namespace("default").Create(context.Background(), fn, metav1.CreateOptions{}); err != nil {
t.Fatalf("create function: %v", err)
}
getReq := httptest.NewRequest(http.MethodGet, "/api/functions/fn-go-acc", nil)
getRec := httptest.NewRecorder()
s.handleGetFunction(getRec, getReq, "fn-go-acc")
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)
}
code, _ := out["code"].(string)
if !strings.Contains(code, "func Handler") {
t.Fatalf("expected source code from spec.source.literal, got %q", code)
}
}
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"])
}
}