package resources import ( "archive/zip" "bytes" "context" "crypto/sha256" "encoding/base64" "fmt" "os" "path/filepath" "testing" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestValidatePackageSource(t *testing.T) { var diagnostics diag.Diagnostics ok := validatePackageSource(types.StringValue("code"), types.StringNull(), &diagnostics) if !ok || diagnostics.HasError() { t.Fatalf("expected valid source_dir-only config") } diagnostics = diag.Diagnostics{} ok = validatePackageSource(types.StringNull(), types.StringValue("main.zip"), &diagnostics) if !ok || diagnostics.HasError() { t.Fatalf("expected valid code_path-only config") } diagnostics = diag.Diagnostics{} ok = validatePackageSource(types.StringNull(), types.StringNull(), &diagnostics) if ok || !diagnostics.HasError() { t.Fatalf("expected invalid config when both values are empty") } } func TestLoadPackageLiteralFromSourceDir(t *testing.T) { tempDir := t.TempDir() mainPath := filepath.Join(tempDir, "main.py") content := []byte("def main():\n return 'ok'\n") if err := os.WriteFile(mainPath, content, 0o600); err != nil { t.Fatalf("write temp main.py: %v", err) } loaded, err := loadPackageLiteral(tempDir, "") if err != nil { t.Fatalf("loadPackageLiteral returned error: %v", err) } if string(loaded) != string(content) { t.Fatalf("unexpected loaded content") } } func TestLoadPackageLiteralFromSourceDirJS(t *testing.T) { tempDir := t.TempDir() mainPath := filepath.Join(tempDir, "main.js") content := []byte("module.exports = async function() { return 'ok'; }\n") if err := os.WriteFile(mainPath, content, 0o600); err != nil { t.Fatalf("write temp main.js: %v", err) } loaded, err := loadPackageLiteral(tempDir, "") if err != nil { t.Fatalf("loadPackageLiteral returned error: %v", err) } if string(loaded) != string(content) { t.Fatalf("unexpected loaded content") } } func TestLoadPackageLiteralFromSourceDirGo(t *testing.T) { tempDir := t.TempDir() mainPath := filepath.Join(tempDir, "main.go") content := []byte("package main\nfunc main() {}\n") if err := os.WriteFile(mainPath, content, 0o600); err != nil { t.Fatalf("write temp main.go: %v", err) } loaded, err := loadPackageLiteral(tempDir, "") if err != nil { t.Fatalf("loadPackageLiteral returned error: %v", err) } if string(loaded) != string(content) { t.Fatalf("unexpected loaded content") } } func TestPackageToUnstructured(t *testing.T) { input := packageResourceModel{ Name: types.StringValue("pkg-a"), Environment: types.StringValue("env-a"), } obj := packageToUnstructured(input, "default", []byte("print('hi')")) literal, found, err := unstructured.NestedString(obj.Object, "spec", "deployment", "literal") if err != nil || !found { t.Fatalf("deployment.literal not found") } decoded, err := base64.StdEncoding.DecodeString(literal) if err != nil { t.Fatalf("decode literal: %v", err) } if string(decoded) != "print('hi')" { t.Fatalf("unexpected decoded literal: %q", string(decoded)) } // Verify no empty source map is generated _, sourceFound, _ := unstructured.NestedMap(obj.Object, "spec", "source") if sourceFound { t.Fatalf("empty source should not be present in deployment mode") } } func TestPackageToUnstructuredSourceMode(t *testing.T) { input := packageResourceModel{ Name: types.StringValue("pkg-go"), Environment: types.StringValue("go-env"), DeployType: types.StringValue("source"), BuildCmd: types.StringValue("build"), } obj := packageToUnstructured(input, "default", []byte("zip-content")) sourceLiteral, found, err := unstructured.NestedString(obj.Object, "spec", "source", "literal") if err != nil || !found { t.Fatalf("source.literal not found") } decoded, err := base64.StdEncoding.DecodeString(sourceLiteral) if err != nil { t.Fatalf("decode source literal: %v", err) } if string(decoded) != "zip-content" { t.Fatalf("unexpected source literal: %q", string(decoded)) } // Verify no deployment section in source mode _, deployFound, _ := unstructured.NestedMap(obj.Object, "spec", "deployment") if deployFound { t.Fatalf("deployment should not be present in source mode") } // Verify buildcmd is set buildCmd, _, _ := unstructured.NestedString(obj.Object, "spec", "buildcmd") if buildCmd != "build" { t.Fatalf("unexpected buildcmd: %q", buildCmd) } } func TestResolveNamespace(t *testing.T) { if got := resolveNamespace(types.StringValue("custom"), "default"); got != "custom" { t.Fatalf("unexpected namespace: %q", got) } if got := resolveNamespace(types.StringNull(), "default"); got != "default" { t.Fatalf("unexpected namespace fallback: %q", got) } } func TestCalculateCodeHash(t *testing.T) { input := []byte("def main():\n return 'ok'\n") got := calculateCodeHash(input) expected := fmt.Sprintf("%x", sha256.Sum256(input)) if got != expected { t.Fatalf("unexpected code hash: got %q want %q", got, expected) } } func TestUnstructuredToPackageModelSetsNullComputed(t *testing.T) { obj := &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": "fission.io/v1", "kind": "Package", "metadata": map[string]interface{}{ "name": "pkg-a", "namespace": "default", }, "spec": map[string]interface{}{ "environment": map[string]interface{}{"name": "env-a"}, }, }} state := unstructuredToPackageModel(obj, packageResourceModel{}) if !state.BuildStatus.IsNull() { t.Fatalf("expected build_status to be null when status is absent") } if !state.BuildLog.IsNull() { t.Fatalf("expected build_log to be null when status is absent") } } func TestHTTPTriggerRoundTrip(t *testing.T) { ctx := context.Background() methods, diagnostics := types.ListValueFrom(ctx, types.StringType, []string{"GET", "POST"}) if diagnostics.HasError() { t.Fatalf("failed to create methods list") } model := httpTriggerResourceModel{ Name: types.StringValue("tr-a"), Function: types.StringValue("fn-a"), URL: types.StringValue("/hello"), Methods: methods, CreateIngress: types.BoolValue(true), Host: types.StringValue("fission.kube5s.ru"), } obj, err := httpTriggerToUnstructured(ctx, model, "default") if err != nil { t.Fatalf("httpTriggerToUnstructured error: %v", err) } state := unstructuredToHTTPTriggerModel(ctx, obj, model) if state.Name.ValueString() != "tr-a" { t.Fatalf("unexpected trigger name: %q", state.Name.ValueString()) } if state.Function.ValueString() != "fn-a" { t.Fatalf("unexpected function name: %q", state.Function.ValueString()) } if state.URL.ValueString() != "/hello" { t.Fatalf("unexpected url: %q", state.URL.ValueString()) } } func TestLoadPackageSourceArchive(t *testing.T) { tempDir := t.TempDir() // Create multiple files to zip files := map[string]string{ "main.go": "package main\n\nimport \"net/http\"\n\nfunc Handler(w http.ResponseWriter, r *http.Request) {}\n", "go.mod": "module example.com/fn\n\ngo 1.21\n", } for name, content := range files { if err := os.WriteFile(filepath.Join(tempDir, name), []byte(content), 0o600); err != nil { t.Fatalf("write %s: %v", name, err) } } zipBytes, err := loadPackageSourceArchive(tempDir) if err != nil { t.Fatalf("loadPackageSourceArchive error: %v", err) } // Verify it's a valid zip reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) if err != nil { t.Fatalf("invalid zip: %v", err) } foundFiles := map[string]bool{} for _, f := range reader.File { foundFiles[f.Name] = true } if !foundFiles["main.go"] { t.Fatalf("main.go not found in zip") } if !foundFiles["go.mod"] { t.Fatalf("go.mod not found in zip") } }