diff --git a/go/go.mod b/go/go.mod index a56d8a8b..a8d7bdf5 100644 --- a/go/go.mod +++ b/go/go.mod @@ -9,6 +9,7 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.1 golang.org/x/mod v0.10.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -19,6 +20,7 @@ require ( github.com/acomagu/bufpipe v1.0.4 // indirect github.com/cloudflare/circl v1.3.2 // indirect github.com/containerd/typeurl v1.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect @@ -30,10 +32,10 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/pjbgf/sha1cd v0.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.1.0 // indirect - github.com/stretchr/testify v1.8.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.8.0 // indirect golang.org/x/net v0.9.0 // indirect diff --git a/go/pkg/utils/cache_test.go b/go/pkg/utils/cache_test.go new file mode 100644 index 00000000..fa5971d8 --- /dev/null +++ b/go/pkg/utils/cache_test.go @@ -0,0 +1,48 @@ +package utils + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetCachedFilePathsFromRoot(t *testing.T) { + missingPathErr := "no such file or directory" + + tests := []struct { + name string + root string + ctx context.Context + expectedFilePaths []string + expectedError *string + }{ + { + name: "Case 1: Cached file paths exist", + root: "path/to/root", + ctx: context.WithValue(context.Background(), key("mapFilePathsFromRoot"), map[string][]string{ + "path/to/root": {"f1.txt", "f2.txt"}, + }), + expectedFilePaths: []string{"f1.txt", "f2.txt"}, + expectedError: nil, + }, + { + name: "Case 2: Invalid file path", + root: "invalid/path", + ctx: context.Background(), + expectedFilePaths: []string{}, + expectedError: &missingPathErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePaths, err := GetCachedFilePathsFromRoot(tt.root, &tt.ctx) + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + + assert.EqualValues(t, tt.expectedFilePaths, filePaths) + }) + } +} diff --git a/go/pkg/utils/detector.go b/go/pkg/utils/detector.go index 3f6c98c9..5cfdf77c 100644 --- a/go/pkg/utils/detector.go +++ b/go/pkg/utils/detector.go @@ -494,7 +494,8 @@ func GetStringValueFromEnvFile(root string, regex string) string { return "" } -func getEnvFileContent(root string) (string, error) { +// getEnvFileContent is exposed as a global variable for the purpose of running mock tests +var getEnvFileContent = func(root string) (string, error) { envPath := filepath.Join(root, ".env") bytes, err := os.ReadFile(envPath) if err != nil { diff --git a/go/pkg/utils/detector_test.go b/go/pkg/utils/detector_test.go new file mode 100644 index 00000000..f2e1a568 --- /dev/null +++ b/go/pkg/utils/detector_test.go @@ -0,0 +1,1186 @@ +package utils + +import ( + "context" + "github.com/redhat-developer/alizer/go/pkg/apis/model" + "github.com/redhat-developer/alizer/go/pkg/schema" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "regexp" + "testing" +) + +func TestGetFilesByRegex(t *testing.T) { + tests := []struct { + name string + filePaths []string + regexFile string + expectedPaths []string + }{ + { + name: "Case 1: Matching file paths", + filePaths: []string{"f1.csproj", "f2.fsproj", "f3.txt"}, + regexFile: ".*\\.\\w+proj", + expectedPaths: []string{"f1.csproj", "f2.fsproj"}, + }, + { + name: "Case 2: No matching file paths", + filePaths: []string{"f1.csproj", "f2.fsproj", "f3.txt"}, + regexFile: "pattern", + expectedPaths: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matchedPaths := GetFilesByRegex(&tt.filePaths, tt.regexFile) + + if len(matchedPaths) != len(tt.expectedPaths) { + t.Errorf("Expected %d matching paths, got %d", len(tt.expectedPaths), len(matchedPaths)) + } + assert.ElementsMatch(t, tt.expectedPaths, matchedPaths) + }) + } +} + +func TestGetFile(t *testing.T) { + tests := []struct { + name string + filePaths []string + wantedFile string + expectedPath string + }{ + { + name: "Case 1: Matching file path", + filePaths: []string{"manage.py", "app.py", "requirements.txt"}, + wantedFile: "app.py", + expectedPath: "app.py", + }, + { + name: "Case 2: No matching file path", + filePaths: []string{"manage.py", "app.py", "requirements.txt"}, + wantedFile: "go.mod", + expectedPath: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetFile(&tt.filePaths, tt.wantedFile) + + if result != tt.expectedPath { + t.Errorf("Expected path %q, got %q", tt.expectedPath, result) + } + }) + } +} + +func TestHasFile(t *testing.T) { + tests := []struct { + name string + files []string + wantedFile string + expected bool + }{ + { + name: "Case 1: Matching file path", + files: []string{"f1.txt", "f2.txt", "f3.txt"}, + wantedFile: "f2.txt", + expected: true, + }, + { + name: "Case 2: No matching file path", + files: []string{"f1.txt", "f2.txt", "f3.txt"}, + wantedFile: "f4.txt", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := HasFile(&tt.files, tt.wantedFile) + + if result != tt.expected { + t.Errorf("Expected value %v, got %v", tt.expected, result) + } + }) + } +} + +func TestIsPathOfWantedFile(t *testing.T) { + tests := []struct { + name string + path string + wantedFile string + expected bool + }{ + { + name: "Case 1: Matching file name", + path: "path/to/build.gradle", + wantedFile: "build.gradle", + expected: true, + }, + { + name: "Case 2: Mismatched file name", + path: "path/to/f1.txt", + wantedFile: "build.gradle", + expected: false, + }, + { + name: "Case 3: Capital case file name", + path: "path/to/File.txt", + wantedFile: "file.txt", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsPathOfWantedFile(tt.path, tt.wantedFile) + + if result != tt.expected { + t.Errorf("Expected value %v, got %v", tt.expected, result) + } + }) + } +} + +func TestIsTagInFile(t *testing.T) { + tests := []struct { + name string + fileContent string + tag string + expectedResult bool + expectedError *string + }{ + { + name: "Case 1: Tag exists in file", + fileContent: "File with tag flask", + tag: "flask", + expectedResult: true, + expectedError: nil, + }, + { + name: "Case 2: Tag does not exist in file", + fileContent: "File without tag", + tag: "django", + expectedResult: false, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + tempFile, err := os.Create(filepath.Join(tempDir, "testfile")) + if err != nil { + t.Errorf("Failed to create test file: %v", err) + } + + _, err = tempFile.WriteString(tt.fileContent) + if err != nil { + t.Errorf("failed to write to temp file. err: %v", err) + } + + result, err := IsTagInFile(tempFile.Name(), tt.tag) + + if result != tt.expectedResult { + t.Errorf("Expected result %v, got %v", tt.expectedResult, result) + } + + if !tt.expectedResult && err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + }) + } +} + +func TestIsTagInPomXMLFileArtifactId(t *testing.T) { + missingFileErr := "no such file or directory" + + tests := []struct { + name string + pomFilePath string + groupID string + artifactID string + expectedResult bool + expectedError *string + }{ + { + name: "Case 1: Matching dependency artifactId and groupId", + pomFilePath: "testdata/pom-dependency.xml", + groupID: "org.acme", + artifactID: "dependency", + expectedResult: true, + }, + { + name: "Case 2: Matching plugin artifactId and groupId", + pomFilePath: "testdata/pom-plugin.xml", + groupID: "com.example", + artifactID: "plugin", + expectedResult: true, + }, + { + name: "Case 3: Matching plugin artifactId and groupId", + pomFilePath: "testdata/pom-profile.xml", + groupID: "com.example", + artifactID: "plugin", + expectedResult: true, + }, + { + name: "Case 4: No matching artifactId and groupId", + pomFilePath: "testdata/pom-dependency.xml", + groupID: "com.example", + artifactID: "nonexistent", + expectedResult: false, + }, + { + name: "Case 5: Error reading pom file", + pomFilePath: "nonexistent/pom-dependency.xml", + groupID: "com.example", + artifactID: "dependency", + expectedResult: false, + expectedError: &missingFileErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := IsTagInPomXMLFileArtifactId(tt.pomFilePath, tt.groupID, tt.artifactID) + + if result != tt.expectedResult { + t.Errorf("Expected result %v, got %v", tt.expectedResult, result) + } + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + }) + } +} + +func TestIsTagInPomXMLFile(t *testing.T) { + missingFileErr := "no such file or directory" + + tests := []struct { + name string + pomFilePath string + tag string + expectedResult bool + expectedError *string + }{ + { + name: "Case 1: Matching dependency tag", + pomFilePath: "testdata/pom-dependency.xml", + tag: "org.acme", + expectedResult: true, + }, + { + name: "Case 2: Matching plugin tag", + pomFilePath: "testdata/pom-plugin.xml", + tag: "com.example", + expectedResult: true, + }, + { + name: "Case 4: No matching tag", + pomFilePath: "testdata/pom-dependency.xml", + tag: "nonexistent", + expectedResult: false, + }, + { + name: "Case 5: Error reading pom file", + pomFilePath: "nonexistent/pom-dependency.xml", + tag: "dependency", + expectedResult: false, + expectedError: &missingFileErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := IsTagInPomXMLFile(tt.pomFilePath, tt.tag) + + if result != tt.expectedResult { + t.Errorf("Expected result %v, got %v", tt.expectedResult, result) + } + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + }) + } +} + +func TestGetPomFileContent(t *testing.T) { + missingFileErr := "no such file or directory" + + testCases := []struct { + name string + filePath string + expectedResult schema.Pom + expectedError *string + }{ + { + name: "Case 1: Valid file", + filePath: "testdata/pom-dependency.xml", + expectedResult: schema.Pom{ + Dependencies: struct { + Text string `xml:",chardata"` + Dependency []struct { + Text string `xml:",chardata"` + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + Scope string `xml:"scope"` + } `xml:"dependency"` + }{ + Text: "\n \n ", + Dependency: []struct { + Text string `xml:",chardata"` + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + Scope string `xml:"scope"` + }{ + { + Text: "\n \n \n ", + GroupId: "org.acme", + ArtifactId: "dependency", + Version: "", + Scope: "", + }, + }, + }, + }, + expectedError: nil, + }, + { + name: "Case 2: File does not exist", + filePath: "path/to/nonexistent/file.xml", + expectedResult: schema.Pom{}, + expectedError: &missingFileErr, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + result, err := GetPomFileContent(tt.filePath) + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + + assert.EqualValues(t, tt.expectedResult, result) + }) + } +} + +func TestIsTagInPackageJsonFile(t *testing.T) { + tests := []struct { + name string + file string + tag string + expectedResult bool + }{ + { + name: "Case 1: Tag exists in Dependencies", + file: "testdata/package.json", + tag: "dep-tag", + expectedResult: true, + }, + { + name: "Case 2: Tag exists in DevDependencies", + file: "testdata/package.json", + tag: "dev-tag", + expectedResult: true, + }, + { + name: "Case 3: Tag exists in PeerDependencies", + file: "testdata/package.json", + tag: "peer-tag", + expectedResult: true, + }, + { + name: "Case 4: Tag does not exist", + file: "testdata/package.json", + tag: "nonexistent", + expectedResult: false, + }, + { + name: "Case 5: Error reading package.json file", + file: "nonexistent/package.json", + tag: "react", + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsTagInPackageJsonFile(tt.file, tt.tag) + + if result != tt.expectedResult { + t.Errorf("Expected result %v, got %v", tt.expectedResult, result) + } + }) + } +} + +func TestGetPackageJsonSchemaFromFile(t *testing.T) { + missingFileErr := "no such file or directory" + + tests := []struct { + name string + filePath string + expectedResult schema.PackageJson + expectedError *string + }{ + { + name: "Case 1: Valid package.json", + filePath: "testdata/package.json", + expectedResult: schema.PackageJson{ + Name: "app", + Dependencies: map[string]string{ + "dep-tag": "1.0.0", + }, + DevDependencies: map[string]string{ + "@dev-tag": "1.1.0", + }, + PeerDependencies: map[string]string{ + "peer-tag": "1.x", + }, + }, + expectedError: nil, + }, + { + name: "Case 2: Nonexistent package.json", + filePath: "nonexistent/package.json", + expectedResult: schema.PackageJson{}, + expectedError: &missingFileErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := GetPackageJsonSchemaFromFile(tt.filePath) + + assert.EqualValues(t, tt.expectedResult, result) + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + }) + } +} + +func TestIsTagInComposerJsonFile(t *testing.T) { + tests := []struct { + name string + file string + tag string + expectedResult bool + }{ + { + name: "Case 1: Tag exists in require", + file: "testdata/composer.json", + tag: "php", + expectedResult: true, + }, + { + name: "Case 2: Tag exists in require-dev", + file: "testdata/composer.json", + tag: "dev", + expectedResult: true, + }, + { + name: "Case 3: Tag does not exist", + file: "testdata/composer.json", + tag: "nonexistent", + expectedResult: false, + }, + { + name: "Case 4: Error reading composer.json file", + file: "nonexistent/composer.json", + tag: "php", + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsTagInComposerJsonFile(tt.file, tt.tag) + + if result != tt.expectedResult { + t.Errorf("Expected result %v, got %v", tt.expectedResult, result) + } + }) + } +} + +func TestGetComposerJsonSchemaFromFile(t *testing.T) { + missingFileErr := "no such file or directory" + + tests := []struct { + name string + filePath string + expectedResult schema.ComposerJson + expectedError *string + }{ + { + name: "Case 1: Valid composer.json", + filePath: "testdata/composer.json", + expectedResult: schema.ComposerJson{ + Name: "laravel/laravel", + Require: map[string]string{ + "php": "^8.0.2", + }, + RequireDev: map[string]string{ + "dev": "^9.5.10", + }, + }, + expectedError: nil, + }, + { + name: "Case 2: Nonexistent package.json", + filePath: "nonexistent/package.json", + expectedResult: schema.ComposerJson{}, + expectedError: &missingFileErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := GetComposerJsonSchemaFromFile(tt.filePath) + + assert.EqualValues(t, tt.expectedResult, result) + + if err != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + }) + } +} + +func TestGetFilePathsFromRoot(t *testing.T) { + tempDir := t.TempDir() + + testFiles := []string{ + ".gitignore", + "f1.txt", + "f2.txt", + "ignored_file.txt", + "ignoredDir/f1.ignore", + "ignoredDir/f2.ignore", + "subdir/f3.txt", + "vendor/f4.txt", + "node_modules/f5.txt", + "subdir/vendor/f6.txt", + "subdir/node_modules/f6.txt", + } + + expectedFiles := []string{ + filepath.Join(tempDir), + filepath.Join(tempDir, ".gitignore"), + // will return the root of ignored dir, but nothing inside + filepath.Join(tempDir, "ignoredDir"), + filepath.Join(tempDir, "f1.txt"), + filepath.Join(tempDir, "f2.txt"), + filepath.Join(tempDir, "subdir"), + filepath.Join(tempDir, "subdir/f3.txt"), + } + + gitIgnoreContent := `# .gitignore contents +ignored_file.txt +ignoredDir/ + ` + + for _, path := range testFiles { + err := os.MkdirAll(filepath.Join(tempDir, filepath.Dir(path)), 0755) + if err != nil { + t.Errorf("Failed to create test dir: %v", err) + } + _, err = os.Create(filepath.Join(tempDir, path)) + if err != nil { + t.Errorf("Failed to create test file: %v", err) + } + + gitIgnore, err := os.OpenFile(filepath.Clean(tempDir)+"/.gitignore", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + t.Errorf("Failed to open file: %v", err) + } + _, err = gitIgnore.WriteString(gitIgnoreContent) + if err != nil { + t.Errorf("Failed to write to file: %v", err) + } + } + + filePaths, err := GetFilePathsFromRoot(tempDir) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + assert.ElementsMatch(t, expectedFiles, filePaths) +} + +func TestGetFilePathsInRoot(t *testing.T) { + tempDir := t.TempDir() + + testFiles := []string{ + ".hiddenfile", + "f1.txt", + "f2.txt", + "subdir/f3.txt", + "subdir/nested/f6.txt", + } + + expectedFiles := []string{ + filepath.Join(tempDir, ".hiddenfile"), + filepath.Join(tempDir, "f1.txt"), + filepath.Join(tempDir, "f2.txt"), + filepath.Join(tempDir, "subdir"), + } + + for _, path := range testFiles { + err := os.MkdirAll(filepath.Join(tempDir, filepath.Dir(path)), 0755) + if err != nil { + t.Errorf("Failed to create test dir: %v", err) + } + _, err = os.Create(filepath.Join(tempDir, path)) + if err != nil { + t.Errorf("Failed to create test file: %v", err) + } + } + + filePaths, err := GetFilePathsInRoot(tempDir) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + assert.ElementsMatch(t, expectedFiles, filePaths) +} + +func TestConvertPropertiesFileToMap(t *testing.T) { + testCases := []struct { + name string + fileContent []byte + expectedResult map[string]string + expectedError error + }{ + { + name: "Case 1: Empty file", + fileContent: []byte(""), + expectedResult: map[string]string{}, + expectedError: nil, + }, + { + name: "Case 2: Valid properties file", + fileContent: []byte("key1=value1\nkey2=value2\nkey3=value3"), + expectedResult: map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}, + expectedError: nil, + }, + { + name: "Case 3: File with empty lines and comments", + fileContent: []byte("\n# Comment\nkey1=value1\n\nkey2=value2\n"), + expectedResult: map[string]string{"key1": "value1", "key2": "value2"}, + expectedError: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + + result, err := ConvertPropertiesFileToMap(tt.fileContent) + + if err != tt.expectedError { + t.Errorf("Expected error %v, got %v", tt.expectedError, err) + } + + assert.EqualValues(t, tt.expectedResult, result) + }) + } +} + +func TestGetValidPortsFromEnvs(t *testing.T) { + tests := []struct { + name string + envs []string + mockEnvValues map[string]string + expectedResult []int + }{ + { + name: "Case 1: Valid environment variables", + envs: []string{"PORT1", "PORT2"}, + mockEnvValues: map[string]string{"PORT1": "8080", "PORT2": "9000"}, + expectedResult: []int{8080, 9000}, + }, + { + name: "Case 2: Invalid environment variable", + envs: []string{"PORT3"}, + mockEnvValues: map[string]string{"PORT3": "invalid"}, + expectedResult: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for env, value := range tt.mockEnvValues { + t.Setenv(env, value) + } + + result := GetValidPortsFromEnvs(tt.envs) + + assert.EqualValues(t, tt.expectedResult, result) + }) + } +} + +func TestGetValidPorts(t *testing.T) { + tests := []struct { + name string + ports []string + expectedResult []int + }{ + { + name: "Case 1: Valid ports", + ports: []string{"8080", "9000", "3030", "invalid"}, + expectedResult: []int{8080, 9000, 3030}, + }, + { + name: "Case 2: Invalid ports", + ports: []string{"invalid", "f3030"}, + expectedResult: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetValidPorts(tt.ports) + + assert.EqualValues(t, tt.expectedResult, result) + }) + } +} + +func TestGetAnyApplicationFilePath(t *testing.T) { + tempDir := t.TempDir() + ctx := context.Background() + + testFiles := []string{ + "f1.file", + "f2.txt", + } + + for _, path := range testFiles { + err := os.MkdirAll(filepath.Join(tempDir, filepath.Dir(path)), 0755) + if err != nil { + t.Errorf("Failed to create test dir: %v", err) + } + _, err = os.Create(filepath.Join(tempDir, path)) + if err != nil { + t.Errorf("Failed to create test file: %v", err) + } + } + + tests := []struct { + name string + root string + propsFiles []model.ApplicationFileInfo + expectedResult string + }{ + { + name: "Case 1: Matching file found with regex", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: ".*.txt", Dir: ""}, + }, + expectedResult: filepath.Join(tempDir, "f2.txt"), + }, + { + name: "Case 2: Matching file found exact", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: "f1.file", Dir: ""}, + }, + expectedResult: filepath.Join(tempDir, "f1.file"), + }, + { + name: "Case 3: No matching file found", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: "missing.file", Dir: "."}, + }, + expectedResult: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetAnyApplicationFilePath(tt.root, tt.propsFiles, &ctx) + if result != tt.expectedResult { + t.Errorf("Expected result %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetAnyApplicationFilePathExactMatch(t *testing.T) { + tempDir := t.TempDir() + + testFiles := []string{ + "f1.txt", + "subdir/f1.txt", + } + + for _, path := range testFiles { + err := os.MkdirAll(filepath.Join(tempDir, filepath.Dir(path)), 0755) + if err != nil { + t.Errorf("Failed to create test dir: %v", err) + } + _, err = os.Create(filepath.Join(tempDir, path)) + if err != nil { + t.Errorf("Failed to create test file: %v", err) + } + } + + tests := []struct { + name string + root string + propsFiles []model.ApplicationFileInfo + expectedResult string + }{ + { + name: "Case 1: Matching file found in nested dir", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: "f1.txt", Dir: "subdir"}, + {File: "f1.txt", Dir: ""}, + }, + expectedResult: filepath.Join(tempDir, "subdir/f1.txt"), + }, + { + name: "Case 2: Matching file found in root", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: "f1.txt", Dir: ""}, + {File: "f1.txt", Dir: "subdir"}, + }, + expectedResult: filepath.Join(tempDir, "f1.txt"), + }, + { + name: "Case 3: No matching file found", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{ + {File: ".*.txt", Dir: ""}, + }, + expectedResult: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetAnyApplicationFilePathExactMatch(tt.root, tt.propsFiles) + if result != tt.expectedResult { + t.Errorf("Expected result %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func Test_readAnyApplicationFile(t *testing.T) { + tempDir := t.TempDir() + + file1Content := []byte("file1 content") + file2Content := []byte("file2 content") + file1Path := filepath.Join(tempDir, "f1.txt") + file2Path := filepath.Join(tempDir, "f2.txt") + err := os.WriteFile(file1Path, file1Content, 0644) + if err != nil { + t.Errorf("failed to write to temp file. err: %v", err) + } + err = os.WriteFile(file2Path, file2Content, 0644) + if err != nil { + t.Errorf("failed to write to temp file. err: %v", err) + } + + noFileFoundErr := "no file found" + + tests := []struct { + name string + root string + propsFiles []model.ApplicationFileInfo + exactMatch bool + ctx context.Context + expectedBytes []byte + expectedError *string + }{ + { + name: "Case 1: Exact match, file exists", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{{Dir: "", File: "f1.txt"}}, + exactMatch: true, + ctx: context.Background(), + expectedBytes: file1Content, + expectedError: nil, + }, + { + name: "Case 2: Exact match, file does not exist", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{{Dir: "", File: "f3.txt"}}, + exactMatch: true, + ctx: context.Background(), + expectedBytes: nil, + expectedError: &noFileFoundErr, + }, + { + name: "Case 3: Non-exact match, file exists", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{{Dir: "", File: "f2.txt"}}, + exactMatch: false, + ctx: context.Background(), + expectedBytes: file2Content, + expectedError: nil, + }, + { + name: "Case 4: Non-exact match, file does not exist", + root: tempDir, + propsFiles: []model.ApplicationFileInfo{{Dir: "", File: "f3.txt"}}, + exactMatch: false, + ctx: context.Background(), + expectedBytes: nil, + expectedError: &noFileFoundErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bytes, err := readAnyApplicationFile(tt.root, tt.propsFiles, tt.exactMatch, &tt.ctx) + + if err != nil && tt.expectedError != nil { + assert.Regexp(t, *tt.expectedError, err.Error(), "Error message should match") + } + + assert.EqualValues(t, tt.expectedBytes, bytes) + }) + } +} + +func TestFindPortSubmatch(t *testing.T) { + testCases := []struct { + name string + re *regexp.Regexp + text string + group int + expectedPort int + }{ + { + name: "Case 1: Valid port in text", + re: regexp.MustCompile(`port:\s*(\d+)*`), + text: "port: 1234", + group: 1, + expectedPort: 1234, + }, + { + name: "Case 2: No valid port in text", + re: regexp.MustCompile(`(\d{4})`), + text: "Text without any valid ports", + group: 1, + expectedPort: -1, + }, + { + name: "Case 3: Invalid port in text", + re: regexp.MustCompile(`port:\s*(\d+)*`), + text: "port: 65535", + group: 1, + expectedPort: -1, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + port := FindPortSubmatch(tt.re, tt.text, tt.group) + assert.EqualValues(t, tt.expectedPort, port) + }) + } +} + +func TestFindPotentialPortGroup(t *testing.T) { + testCases := []struct { + name string + re *regexp.Regexp + text string + group int + expectedPort string + }{ + { + name: "Case 1: First occurrence of valid port in text", + re: regexp.MustCompile(`port:\s*(\d+)*`), + text: "port: 1234, port: 5678", + group: 1, + expectedPort: "1234", + }, + { + name: "Case 2: No valid port in text", + re: regexp.MustCompile(`(\d{4})`), + text: "Text without any valid ports", + group: 1, + expectedPort: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + port := FindPotentialPortGroup(tt.re, tt.text, tt.group) + assert.EqualValues(t, tt.expectedPort, port) + }) + } +} + +func TestFindAllPortsSubmatch(t *testing.T) { + testCases := []struct { + name string + re *regexp.Regexp + text string + group int + expectedPorts []int + }{ + { + name: "Case 1: Valid ports in text", + re: regexp.MustCompile(`port:\s*(\d+)*`), + text: "port: 1234, port: 5678, invalid: 3444", + group: 1, + expectedPorts: []int{1234, 5678}, + }, + { + name: "Case 2: No valid ports in text", + re: regexp.MustCompile(`(\d{4})`), + text: "Text without any valid ports", + group: 1, + expectedPorts: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + ports := FindAllPortsSubmatch(tt.re, tt.text, tt.group) + assert.EqualValues(t, tt.expectedPorts, ports) + }) + } +} + +func TestGetPortValuesFromEnvFile(t *testing.T) { + testCases := []struct { + name string + root string + regexes []string + envFileContent string + expectedPorts []int + }{ + { + name: "Case 1: Valid port values", + root: "/path/to/root", + regexes: []string{"PORT=(\\d*)", "ANOTHER_PORT=(\\d*)"}, + envFileContent: "PORT=3030\nANOTHER_PORT=8080\n", + expectedPorts: []int{3030, 8080}, + }, + { + name: "Case 2: No valid port values", + root: "/path/to/root", + regexes: []string{"PORT=(\\d*)", "ANOTHER_PORT=(\\d*)"}, + envFileContent: "SOME_VARIABLE=abc\nANOTHER_VARIABLE=123\n", + expectedPorts: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // mock getEnvFileContent + getEnvFileContent = func(root string) (string, error) { + return tt.envFileContent, nil + } + + ports := GetPortValuesFromEnvFile(tt.root, tt.regexes) + assert.EqualValues(t, tt.expectedPorts, ports) + }) + } +} + +func TestGetStringValueFromEnvFile(t *testing.T) { + testCases := []struct { + name string + root string + regex string + envFileContent string + expectedValue string + }{ + { + name: "Case 1: Valid value", + root: "/path/to/root", + regex: "SOME_VARIABLE=(\\w+)", + envFileContent: "SOME_VARIABLE=value123\n", + expectedValue: "value123", + }, + { + name: "Case 2: No valid value", + root: "/path/to/root", + regex: "SOME_VARIABLE=(\\w+)", + envFileContent: "ANOTHER_VARIABLE=123\n", + expectedValue: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + // mock getEnvFileContent + getEnvFileContent = func(root string) (string, error) { + return tt.envFileContent, nil + } + + value := GetStringValueFromEnvFile(tt.root, tt.regex) + + if value != tt.expectedValue { + t.Errorf("Expected value %q, got %q", tt.expectedValue, value) + } + }) + } +} + +func TestNormalizeSplit(t *testing.T) { + tests := []struct { + name string + file string + expectedDir string + expectedFile string + }{ + { + name: "Case 1: File with directory", + file: "path/to/f1.txt", + expectedDir: "path/to/", + expectedFile: "f1.txt", + }, + { + name: "Case 2: File in root directory", + file: "f1.txt", + expectedDir: "./", + expectedFile: "f1.txt", + }, + { + name: "Case 3: Empty file", + file: "", + expectedDir: "./", + expectedFile: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir, file := NormalizeSplit(tt.file) + + if dir != tt.expectedDir { + t.Errorf("Expected dir %q, got %q", tt.expectedDir, dir) + } + if file != tt.expectedFile { + t.Errorf("Expected file %q, got %q", tt.expectedFile, file) + } + }) + } +} diff --git a/go/pkg/utils/langfiles/languages_file_handler_test.go b/go/pkg/utils/langfiles/languages_file_handler_test.go new file mode 100644 index 00000000..a34bcbcb --- /dev/null +++ b/go/pkg/utils/langfiles/languages_file_handler_test.go @@ -0,0 +1,234 @@ +package langfiles + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetLanguagesByExtension(t *testing.T) { + tests := []struct { + name string + extension string + expectedSize int + }{ + { + name: "Not a language extension", + extension: ".notaextension", + expectedSize: 0, + }, + { + name: "Go", + extension: ".go", + expectedSize: 1, + }, + { + name: "Java", + extension: ".java", + expectedSize: 1, + }, + { + name: "C#", + extension: ".cs", + // Smalltalk, C# + expectedSize: 2, + }, + { + name: "F#", + extension: ".fs", + // F#, GLSL, Forth, Filterscript + expectedSize: 4, + }, + { + name: "Visual Basic .NET", + extension: ".vb", + expectedSize: 1, + }, + { + name: "JavaScript", + extension: ".js", + expectedSize: 1, + }, + { + name: "Python", + extension: ".py", + expectedSize: 1, + }, + { + name: "Rust", + extension: ".rs", + // RenderScript, Rust + expectedSize: 2, + }, + { + name: "PHP", + extension: ".php", + // Hack, PHP + expectedSize: 2, + }, + } + + languageFile := Get() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + languages := languageFile.GetLanguagesByExtension(tt.extension) + if len(languages) != tt.expectedSize { + t.Errorf("For extension %s, expected %d languages, but got %d", tt.extension, tt.expectedSize, len(languages)) + } + }) + } +} + +func TestGetLanguageByName(t *testing.T) { + noLangErr := "no language found with this name" + tests := []struct { + name string + expectedItem LanguageItem + expectedErr *string + }{ + { + name: "Not a language", + expectedItem: LanguageItem{}, + expectedErr: &noLangErr, + }, + { + name: "Go", + expectedItem: LanguageItem{Name: "Go", Aliases: []string{"golang"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"go.mod"}, ExcludeFolders: []string{"vendor"}, Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Java", + expectedItem: LanguageItem{Name: "Java", Aliases: []string(nil), Kind: "programming", Group: "", ConfigurationFiles: []string{"pom.xml", "build.gradle"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "C#", + expectedItem: LanguageItem{Name: "C#", Aliases: []string{"csharp", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "F#", + expectedItem: LanguageItem{Name: "F#", Aliases: []string{"fsharp", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Visual Basic .NET", + expectedItem: LanguageItem{Name: "Visual Basic .NET", Aliases: []string{"visual basic", "vbnet", "vb .net", "vb.net", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "JavaScript", + expectedItem: LanguageItem{Name: "JavaScript", Aliases: []string{"js", "node", "nodejs", "TypeScript"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"[^-]package.json"}, ExcludeFolders: []string{"node_modules"}, Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Python", + expectedItem: LanguageItem{Name: "Python", Aliases: []string{"python3", "rusthon"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"requirements.txt", "pyproject.toml"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Rust", + expectedItem: LanguageItem{Name: "Rust", Aliases: []string(nil), Kind: "programming", Group: "", ConfigurationFiles: []string{"Cargo.toml"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "PHP", + expectedItem: LanguageItem{Name: "PHP", Aliases: []string{"inc"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"composer.json", "[^-]package.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + } + + languageFile := Get() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + langItem, err := languageFile.GetLanguageByName(tt.name) + + if err != nil { + assert.Regexp(t, *tt.expectedErr, err.Error(), "Error message should match") + } + + assert.EqualValues(t, tt.expectedItem, langItem) + }) + } +} + +func TestGetLanguageByAlias(t *testing.T) { + noLangErr := "no language found with this alias" + tests := []struct { + name string + alias string + expectedItem LanguageItem + expectedErr *string + }{ + { + name: "Not a language", + alias: "not a language", + expectedItem: LanguageItem{}, + expectedErr: &noLangErr, + }, + { + name: "Go", + alias: "golang", + expectedItem: LanguageItem{Name: "Go", Aliases: []string{"golang"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"go.mod"}, ExcludeFolders: []string{"vendor"}, Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "C#", + alias: "csharp", + expectedItem: LanguageItem{Name: "C#", Aliases: []string{"csharp", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "F#", + alias: "fsharp", + expectedItem: LanguageItem{Name: "F#", Aliases: []string{"fsharp", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Visual Basic .NET", + alias: "visual basic", + expectedItem: LanguageItem{Name: "Visual Basic .NET", Aliases: []string{"visual basic", "vbnet", "vb .net", "vb.net", "dotnet", ".NET"}, Kind: "programming", Group: "", ConfigurationFiles: []string{".*\\.\\w+proj", "appsettings.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "JavaScript", + alias: "TypeScript", + expectedItem: LanguageItem{Name: "JavaScript", Aliases: []string{"js", "node", "nodejs", "TypeScript"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"[^-]package.json"}, ExcludeFolders: []string{"node_modules"}, Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "Python", + alias: "python3", + expectedItem: LanguageItem{Name: "Python", Aliases: []string{"python3", "rusthon"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"requirements.txt", "pyproject.toml"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + { + name: "PHP", + alias: "inc", + expectedItem: LanguageItem{Name: "PHP", Aliases: []string{"inc"}, Kind: "programming", Group: "", ConfigurationFiles: []string{"composer.json", "[^-]package.json"}, ExcludeFolders: []string(nil), Component: true, disabled: false}, + expectedErr: nil, + }, + } + + languageFile := Get() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + langItem, err := languageFile.GetLanguageByAlias(tt.alias) + + if err != nil { + assert.Regexp(t, *tt.expectedErr, err.Error(), "Error message should match") + } + + assert.EqualValues(t, tt.expectedItem, langItem) + }) + } +} + +func TestGetExcludedFolders(t *testing.T) { + languageFile := Get() + expectedFolders := []string{"node_modules", "vendor"} + excludedFolders := languageFile.GetExcludedFolders() + assert.ElementsMatch(t, excludedFolders, expectedFolders) +} diff --git a/go/pkg/utils/testdata/composer.json b/go/pkg/utils/testdata/composer.json new file mode 100644 index 00000000..3c7e3827 --- /dev/null +++ b/go/pkg/utils/testdata/composer.json @@ -0,0 +1,13 @@ +{ + "name": "laravel/laravel", + "type": "project", + "description": "The Laravel Framework.", + "keywords": ["framework", "laravel"], + "license": "MIT", + "require": { + "php": "^8.0.2" + }, + "require-dev": { + "dev": "^9.5.10" + } +} \ No newline at end of file diff --git a/go/pkg/utils/testdata/package.json b/go/pkg/utils/testdata/package.json new file mode 100644 index 00000000..9e6c43b2 --- /dev/null +++ b/go/pkg/utils/testdata/package.json @@ -0,0 +1,13 @@ +{ + "name": "app", + "version": "0.0.1", + "dependencies": { + "dep-tag": "1.0.0" + }, + "devDependencies": { + "@dev-tag": "1.1.0" + }, + "peerDependencies": { + "peer-tag": "1.x" + } +} diff --git a/go/pkg/utils/testdata/pom-dependency.xml b/go/pkg/utils/testdata/pom-dependency.xml new file mode 100644 index 00000000..e8cd4fd7 --- /dev/null +++ b/go/pkg/utils/testdata/pom-dependency.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + 1.0.0 + + + org.acme + dependency + + + \ No newline at end of file diff --git a/go/pkg/utils/testdata/pom-plugin.xml b/go/pkg/utils/testdata/pom-plugin.xml new file mode 100644 index 00000000..fbefd416 --- /dev/null +++ b/go/pkg/utils/testdata/pom-plugin.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + 1.0.0 + + + + com.example + plugin + + + + diff --git a/go/pkg/utils/testdata/pom-profile.xml b/go/pkg/utils/testdata/pom-profile.xml new file mode 100644 index 00000000..71c055e7 --- /dev/null +++ b/go/pkg/utils/testdata/pom-profile.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + 1.0.0 + + + native + + + + com.example + plugin + + + + + + +