From 6d3d111bf23f313a66ea42f7dcf0aa7012aaab55 Mon Sep 17 00:00:00 2001 From: Animesh Pathak Date: Thu, 13 Feb 2025 16:17:33 +0530 Subject: [PATCH] feat: enhanced and added testcases to increase unit test coverage Signed-off-by: Animesh Pathak --- cmd/kubeconform/main_test.go | 94 +++++++++++++++++++++ pkg/output/output.go | 45 +++++----- pkg/output/output_test.go | 84 +++++++++++++++++++ pkg/registry/local.go | 96 +++++++++++----------- pkg/registry/local_test.go | 109 ++++++++++++++++++++++++ pkg/resource/files_test.go | 106 ++++++++++++++++++++++++ pkg/validator/validator_test.go | 141 ++++++++++++++++++++++++++++++-- 7 files changed, 601 insertions(+), 74 deletions(-) create mode 100644 cmd/kubeconform/main_test.go create mode 100644 pkg/output/output_test.go create mode 100644 pkg/registry/local_test.go diff --git a/cmd/kubeconform/main_test.go b/cmd/kubeconform/main_test.go new file mode 100644 index 0000000..2ec95bb --- /dev/null +++ b/cmd/kubeconform/main_test.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "testing" + + "github.com/yannh/kubeconform/pkg/validator" +) + +type MockOutput struct{} + +func (m *MockOutput) Write(res validator.Result) error { + return nil +} + +func (m *MockOutput) Flush() error { + return nil +} + +// Test generated using Keploy +func TestProcessResults_SuccessWithValidResults(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + validationResults := make(chan validator.Result, 2) + validationResults <- validator.Result{Status: validator.Valid} + validationResults <- validator.Result{Status: validator.Valid} + close(validationResults) + + outputMock := &MockOutput{} + exitOnError := false + + resultChan := processResults(cancel, outputMock, validationResults, exitOnError) + success := <-resultChan + + if !success { + t.Errorf("Expected success to be true, got false") + } + + if ctx.Err() != nil { + t.Errorf("Context should not be canceled") + } +} + +// Test generated using Keploy +func TestProcessResults_FailureWithInvalidResults(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + validationResults := make(chan validator.Result, 2) + validationResults <- validator.Result{Status: validator.Valid} + validationResults <- validator.Result{Status: validator.Invalid} + close(validationResults) + + outputMock := &MockOutput{} + exitOnError := false + + resultChan := processResults(cancel, outputMock, validationResults, exitOnError) + success := <-resultChan + + if success { + t.Errorf("Expected success to be false, got true") + } + + if ctx.Err() != nil { + t.Errorf("Context should not be canceled when exitOnError is false") + } +} + +// Test generated using Keploy +func TestProcessResults_CancelOnInvalidWithExitOnError(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + validationResults := make(chan validator.Result, 2) + validationResults <- validator.Result{Status: validator.Valid} + validationResults <- validator.Result{Status: validator.Invalid} + close(validationResults) + + outputMock := &MockOutput{} + exitOnError := true + + resultChan := processResults(cancel, outputMock, validationResults, exitOnError) + success := <-resultChan + + if success { + t.Errorf("Expected success to be false, got true") + } + + select { + case <-ctx.Done(): + default: + t.Errorf("Expected context to be canceled when exitOnError is true") + } +} diff --git a/pkg/output/output.go b/pkg/output/output.go index 67c0262..2125246 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -1,30 +1,37 @@ package output import ( - "fmt" - "io" + "fmt" + "io" - "github.com/yannh/kubeconform/pkg/validator" + "github.com/yannh/kubeconform/pkg/validator" ) type Output interface { - Write(validator.Result) error - Flush() error + Write(validator.Result) error + Flush() error } func New(w io.Writer, outputFormat string, printSummary, isStdin, verbose bool) (Output, error) { - switch { - case outputFormat == "json": - return jsonOutput(w, printSummary, isStdin, verbose), nil - case outputFormat == "junit": - return junitOutput(w, printSummary, isStdin, verbose), nil - case outputFormat == "pretty": - return prettyOutput(w, printSummary, isStdin, verbose), nil - case outputFormat == "tap": - return tapOutput(w, printSummary, isStdin, verbose), nil - case outputFormat == "text": - return textOutput(w, printSummary, isStdin, verbose), nil - default: - return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'") - } + switch outputFormat { + case "json": + return jsonOutput(w, printSummary, isStdin, verbose), nil + case "junit": + return junitOutput(w, printSummary, isStdin, verbose), nil + case "pretty": + return prettyOutput(w, printSummary, isStdin, verbose), nil + case "tap": + return tapOutput(w, printSummary, isStdin, verbose), nil + case "text": + return textOutput(w, printSummary, isStdin, verbose), nil + default: + return nil, fmt.Errorf("'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'") + } +} + +// Mock writer for testing purposes +type mockWriter struct{} + +func (m *mockWriter) Write(p []byte) (n int, err error) { + return len(p), nil } diff --git a/pkg/output/output_test.go b/pkg/output/output_test.go new file mode 100644 index 0000000..c524919 --- /dev/null +++ b/pkg/output/output_test.go @@ -0,0 +1,84 @@ +package output + +import ( + "testing" +) + + +// Test generated using Keploy +func TestNew_JSONOutput(t *testing.T) { + writer := &mockWriter{} + output, err := New(writer, "json", false, false, false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if output == nil { + t.Fatalf("Expected a valid Output implementation, got nil") + } +} + + +// Test generated using Keploy +func TestNew_JUnitOutput(t *testing.T) { + writer := &mockWriter{} + output, err := New(writer, "junit", false, false, false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if output == nil { + t.Fatalf("Expected a valid Output implementation, got nil") + } +} + + +// Test generated using Keploy +func TestNew_PrettyOutput(t *testing.T) { + writer := &mockWriter{} + output, err := New(writer, "pretty", false, false, false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if output == nil { + t.Fatalf("Expected a valid Output implementation, got nil") + } +} + + +// Test generated using Keploy +func TestNew_UnsupportedFormat(t *testing.T) { + writer := &mockWriter{} + _, err := New(writer, "unsupported", false, false, false) + if err == nil { + t.Fatalf("Expected an error, got nil") + } + expectedError := "'outputFormat' must be 'json', 'junit', 'pretty', 'tap' or 'text'" + if err.Error() != expectedError { + t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error()) + } +} + + +// Test generated using Keploy +func TestNew_TapOutput(t *testing.T) { + writer := &mockWriter{} + output, err := New(writer, "tap", false, false, false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if output == nil { + t.Fatalf("Expected a valid Output implementation, got nil") + } +} + + +// Test generated using Keploy +func TestNew_TextOutput(t *testing.T) { + writer := &mockWriter{} + output, err := New(writer, "text", false, false, false) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if output == nil { + t.Fatalf("Expected a valid Output implementation, got nil") + } +} diff --git a/pkg/registry/local.go b/pkg/registry/local.go index 7501290..afb901a 100644 --- a/pkg/registry/local.go +++ b/pkg/registry/local.go @@ -1,63 +1,61 @@ package registry import ( - "errors" - "fmt" - "io" - "log" - "os" + "errors" + "fmt" + "io" + "log" + "os" ) type LocalRegistry struct { - pathTemplate string - strict bool - debug bool + pathTemplate string + strict bool + debug bool + schemaPathFunc func(pathTemplate, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) } -// NewLocalSchemas creates a new "registry", that will serve schemas from files, given a list of schema filenames func newLocalRegistry(pathTemplate string, strict bool, debug bool) (*LocalRegistry, error) { - return &LocalRegistry{ - pathTemplate, - strict, - debug, - }, nil + return &LocalRegistry{ + pathTemplate: pathTemplate, + strict: strict, + debug: debug, + schemaPathFunc: schemaPath, + }, nil } -// DownloadSchema retrieves the schema from a file for the resource func (r LocalRegistry) DownloadSchema(resourceKind, resourceAPIVersion, k8sVersion string) (string, []byte, error) { - schemaFile, err := schemaPath(r.pathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict) - if err != nil { - return schemaFile, []byte{}, nil - } - f, err := os.Open(schemaFile) - if err != nil { - if os.IsNotExist(err) { - msg := fmt.Sprintf("could not open file %s", schemaFile) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, newNotFoundError(errors.New(msg)) - } + schemaFile, err := r.schemaPathFunc(r.pathTemplate, resourceKind, resourceAPIVersion, k8sVersion, r.strict) + if err != nil { + return schemaFile, []byte{}, nil + } + f, err := os.Open(schemaFile) + if err != nil { + if os.IsNotExist(err) { + msg := fmt.Sprintf("could not open file %s", schemaFile) + if r.debug { + log.Print(msg) + } + return schemaFile, nil, newNotFoundError(errors.New(msg)) + } - msg := fmt.Sprintf("failed to open schema at %s: %s", schemaFile, err) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, errors.New(msg) - } - - defer f.Close() - content, err := io.ReadAll(f) - if err != nil { - msg := fmt.Sprintf("failed to read schema at %s: %s", schemaFile, err) - if r.debug { - log.Print(msg) - } - return schemaFile, nil, err - } - - if r.debug { - log.Printf("using schema found at %s", schemaFile) - } - return schemaFile, content, nil + msg := fmt.Sprintf("failed to open schema at %s: %s", schemaFile, err) + if r.debug { + log.Print(msg) + } + return schemaFile, nil, errors.New(msg) + } + defer f.Close() + content, err := io.ReadAll(f) + if err != nil { + msg := fmt.Sprintf("failed to read schema at %s: %s", schemaFile, err) + if r.debug { + log.Print(msg) + } + return schemaFile, nil, err + } + if r.debug { + log.Printf("using schema found at %s", schemaFile) + } + return schemaFile, content, nil } diff --git a/pkg/registry/local_test.go b/pkg/registry/local_test.go new file mode 100644 index 0000000..4bead2c --- /dev/null +++ b/pkg/registry/local_test.go @@ -0,0 +1,109 @@ +package registry + +import ( + "fmt" + "testing" + "os" +) + + +// Test generated using Keploy +func TestDownloadSchema_SchemaPathError(t *testing.T) { + // Arrange + pathTemplate := "./schemas/%s/%s/%s.json" + strict := false + debug := false + registry, err := newLocalRegistry(pathTemplate, strict, debug) + if err != nil { + t.Fatalf("Failed to create LocalRegistry: %v", err) + } + + // Mock schemaPathFunc to return an error + registry.schemaPathFunc = func(pathTemplate, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) { + return "", fmt.Errorf("mock schemaPath error") + } + + // Act + filePath, content, err := registry.DownloadSchema("Pod", "v1", "1.21") + + // Assert + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + if filePath != "" { + t.Errorf("Expected empty filePath, got %s", filePath) + } + if len(content) != 0 { + t.Errorf("Expected empty content, got %v", content) + } +} + + +// Test generated using Keploy +func TestDownloadSchema_Success(t *testing.T) { + // Arrange + pathTemplate := "./schemas/%s/%s/%s.json" + strict := false + debug := true + registry, err := newLocalRegistry(pathTemplate, strict, debug) + if err != nil { + t.Fatalf("Failed to create LocalRegistry: %v", err) + } + + // Mock schemaPathFunc to return a valid file path + registry.schemaPathFunc = func(pathTemplate, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) { + return "./valid_schema.json", nil + } + + // Create a valid schema file + validFile := "./valid_schema.json" + expectedContent := []byte(`{"type": "object"}`) + os.WriteFile(validFile, expectedContent, 0644) + defer os.Remove(validFile) + + // Act + filePath, content, err := registry.DownloadSchema("Pod", "v1", "1.21") + + // Assert + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + if filePath != validFile { + t.Errorf("Expected filePath '%s', got %s", validFile, filePath) + } + if string(content) != string(expectedContent) { + t.Errorf("Expected content '%s', got %s", string(expectedContent), string(content)) + } +} + + +// Test generated using Keploy +func TestDownloadSchema_FileNotFound(t *testing.T) { + // Arrange + pathTemplate := "./schemas/%s/%s/%s.json" + strict := false + debug := true + registry, err := newLocalRegistry(pathTemplate, strict, debug) + if err != nil { + t.Fatalf("Failed to create LocalRegistry: %v", err) + } + + // Mock schemaPathFunc to return a valid file path + registry.schemaPathFunc = func(pathTemplate, resourceKind, resourceAPIVersion, k8sVersion string, strict bool) (string, error) { + return "./nonexistent_file.json", nil + } + + // Act + filePath, content, err := registry.DownloadSchema("Pod", "v1", "1.21") + + // Assert + if err == nil || err.Error() != "could not open file ./nonexistent_file.json" { + t.Errorf("Expected NotFoundError, got %v", err) + } + if filePath != "./nonexistent_file.json" { + t.Errorf("Expected filePath './nonexistent_file.json', got %s", filePath) + } + if content != nil { + t.Errorf("Expected nil content, got %v", content) + } +} diff --git a/pkg/resource/files_test.go b/pkg/resource/files_test.go index fa1ca2e..ede8c9d 100644 --- a/pkg/resource/files_test.go +++ b/pkg/resource/files_test.go @@ -1,7 +1,10 @@ package resource import ( + "context" + "fmt" "os" + "path/filepath" "strings" "sync" "testing" @@ -202,3 +205,106 @@ lorem: ipsum } } } + +// Test generated using Keploy +func TestFindFilesInFolders_BasicFunctionality(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempDir := t.TempDir() + os.WriteFile(filepath.Join(tempDir, "file1.yaml"), []byte{}, 0644) + os.WriteFile(filepath.Join(tempDir, "file2.json"), []byte{}, 0644) + os.WriteFile(filepath.Join(tempDir, "ignored.txt"), []byte{}, 0644) + + ignorePatterns := []string{".*ignored.*"} + files, errors := findFilesInFolders(ctx, []string{tempDir}, ignorePatterns) + + receivedFiles := []string{} + for f := range files { + receivedFiles = append(receivedFiles, f) + } + + if len(receivedFiles) != 2 { + t.Errorf("expected 2 files, got %d: %v", len(receivedFiles), receivedFiles) + } + + select { + case err := <-errors: + t.Errorf("unexpected error: %v", err) + default: + } +} + +// Test generated using Keploy +func TestFindResourcesInFile_ErrorHandling(t *testing.T) { + resources := make(chan Resource) + errors := make(chan error) + buf := make([]byte, 4*1024*1024) + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + for err := range errors { + if err == nil { + t.Errorf("expected an error, got nil") + } + } + }() + + findResourcesInFile("nonexistent.yaml", resources, errors, buf) + close(resources) + close(errors) + wg.Wait() +} + +// Test generated using Keploy +func TestDiscoveryError_ErrorMethod(t *testing.T) { + underlyingErr := "file not found" + de := DiscoveryError{ + Path: "/path/to/file.yaml", + Err: fmt.Errorf(underlyingErr), + } + + if de.Error() != underlyingErr { + t.Errorf("expected error message '%s', got '%s'", underlyingErr, de.Error()) + } +} + +// Test generated using Keploy +func TestIsIgnored_MultiplePatterns(t *testing.T) { + for i, testCase := range []struct { + path string + ignorePatterns []string + expectedIgnored bool + expectedError bool + }{ + { + path: "/path/to/ignored/file.yaml", + ignorePatterns: []string{".*ignored.*", ".*file.*"}, + expectedIgnored: true, + expectedError: false, + }, + { + path: "/path/to/not_ignored/file.yaml", + ignorePatterns: []string{".*ignored.*", ".*not.*"}, + expectedIgnored: true, + expectedError: false, + }, + { + path: "/path/to/file.yaml", + ignorePatterns: []string{"[invalid_regex"}, + expectedIgnored: false, + expectedError: true, + }, + } { + ignored, err := isIgnored(testCase.path, testCase.ignorePatterns) + if ignored != testCase.expectedIgnored { + t.Errorf("test %d: expected ignored=%t, got %t", i+1, testCase.expectedIgnored, ignored) + } + if (err != nil) != testCase.expectedError { + t.Errorf("test %d: expected error=%t, got %v", i+1, testCase.expectedError, err) + } + } +} diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index aa86951..2ba3229 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -1,14 +1,15 @@ package validator import ( - "bytes" - "io" - "reflect" - "testing" + "bytes" + "io" + "reflect" + "testing" - "github.com/yannh/kubeconform/pkg/registry" + "github.com/yannh/kubeconform/pkg/registry" - "github.com/yannh/kubeconform/pkg/resource" + "github.com/yannh/kubeconform/pkg/resource" + "github.com/yannh/kubeconform/pkg/cache" ) type mockRegistry struct { @@ -532,3 +533,131 @@ firstName: foo t.Errorf("Expected %+v, got %+v", expectedValidationErrors, gotValidationErrors) } } + + +// Test generated using Keploy +func TestValidateResource_EmptyResource(t *testing.T) { + val := v{ + opts: Opts{ + SkipKinds: map[string]struct{}{}, + RejectKinds: map[string]struct{}{}, + }, + schemaCache: nil, + schemaDownload: downloadSchema, + regs: []registry.Registry{}, + } + + res := resource.Resource{Bytes: []byte{}} + result := val.ValidateResource(res) + + if result.Status != Empty { + t.Errorf("Expected status %v, got %v", Empty, result.Status) + } +} + + +// Test generated using Keploy +func TestValidateResource_ProhibitedKind(t *testing.T) { + val := v{ + opts: Opts{ + SkipKinds: map[string]struct{}{}, + RejectKinds: map[string]struct{}{"ProhibitedKind": {}}, + }, + schemaCache: nil, + schemaDownload: downloadSchema, + regs: []registry.Registry{}, + } + + rawResource := []byte(` + kind: ProhibitedKind + apiVersion: v1 + firstName: foo + lastName: bar + `) + res := resource.Resource{Bytes: rawResource} + result := val.ValidateResource(res) + + if result.Status != Error { + t.Errorf("Expected status %v, got %v", Error, result.Status) + } + if result.Err == nil || result.Err.Error() != "prohibited resource kind ProhibitedKind" { + t.Errorf("Expected error 'prohibited resource kind ProhibitedKind', got %v", result.Err) + } +} + + +// Test generated using Keploy +func TestValidateResource_RegistrySchema(t *testing.T) { + schema := []byte(`{ + "title": "Example Schema", + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + } + }, + "required": ["firstName", "lastName"] + }`) + + val := v{ + opts: Opts{ + SkipKinds: map[string]struct{}{}, + RejectKinds: map[string]struct{}{}, + }, + schemaCache: cache.NewInMemoryCache(), + schemaDownload: downloadSchema, + regs: []registry.Registry{ + newMockRegistry(func() (string, []byte, error) { + return "", schema, nil + }), + }, + } + + rawResource := []byte(` + kind: name + apiVersion: v1 + firstName: foo + lastName: bar + `) + res := resource.Resource{Bytes: rawResource} + result := val.ValidateResource(res) + + if result.Status != Valid { + t.Errorf("Expected status %v, got %v", Valid, result.Status) + } +} + + +// Test generated using Keploy +func TestValidateResource_SkipKinds(t *testing.T) { + val := v{ + opts: Opts{ + SkipKinds: map[string]struct{}{ + "SkippedKind": {}, + }, + RejectKinds: map[string]struct{}{}, + }, + schemaCache: nil, + schemaDownload: downloadSchema, + regs: []registry.Registry{}, + } + + rawResource := []byte(` + kind: SkippedKind + apiVersion: v1 + firstName: foo + lastName: bar + `) + res := resource.Resource{Bytes: rawResource} + result := val.ValidateResource(res) + + if result.Status != Skipped { + t.Errorf("Expected status %v, got %v", Skipped, result.Status) + } +}