github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/integrationutils.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "github.com/jfrog/frogbot/scanpullrequest" 7 "github.com/jfrog/frogbot/scanrepository" 8 "github.com/jfrog/frogbot/utils" 9 "github.com/jfrog/frogbot/utils/outputwriter" 10 "github.com/jfrog/froggit-go/vcsclient" 11 "github.com/jfrog/froggit-go/vcsutils" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 "os" 15 "strconv" 16 "strings" 17 "testing" 18 "time" 19 ) 20 21 const ( 22 repoName = "integration" 23 issuesBranch = "issues-branch" 24 mainBranch = "main" 25 expectedNumberOfIssues = 11 26 ) 27 28 type IntegrationTestDetails struct { 29 RepoName string 30 RepoOwner string 31 GitToken string 32 GitCloneURL string 33 GitProvider string 34 GitProject string 35 GitUsername string 36 ApiEndpoint string 37 PullRequestID string 38 CustomBranchName string 39 } 40 41 func NewIntegrationTestDetails(token, gitProvider, gitCloneUrl, repoOwner string) *IntegrationTestDetails { 42 return &IntegrationTestDetails{ 43 GitProject: repoName, 44 RepoOwner: repoOwner, 45 RepoName: repoName, 46 GitToken: token, 47 GitUsername: "frogbot", 48 GitProvider: gitProvider, 49 GitCloneURL: gitCloneUrl, 50 } 51 } 52 53 func buildGitManager(t *testing.T, testDetails *IntegrationTestDetails) *utils.GitManager { 54 gitManager, err := utils.NewGitManager(). 55 SetAuth(testDetails.GitUsername, testDetails.GitToken). 56 SetEmailAuthor("frogbot-test@jfrog.com"). 57 SetRemoteGitUrl(testDetails.GitCloneURL) 58 assert.NoError(t, err) 59 return gitManager 60 } 61 62 func getIssuesBranchName() string { 63 return fmt.Sprintf("%s-%s", issuesBranch, getTimestamp()) 64 } 65 66 func getTimestamp() string { 67 return strconv.FormatInt(time.Now().Unix(), 10) 68 } 69 70 func setIntegrationTestEnvs(t *testing.T, testDetails *IntegrationTestDetails) func() { 71 // Frogbot sanitizes all the environment variables that start with 'JF', 72 // so we restore them at the end of the test to avoid collisions with other tests 73 envRestoreFunc := getJfrogEnvRestoreFunc(t) 74 unsetEnvs := utils.SetEnvsAndAssertWithCallback(t, map[string]string{ 75 utils.RequirementsFileEnv: "requirements.txt", 76 utils.GitPullRequestIDEnv: testDetails.PullRequestID, 77 utils.GitProvider: testDetails.GitProvider, 78 utils.GitTokenEnv: testDetails.GitToken, 79 utils.GitRepoEnv: testDetails.RepoName, 80 utils.GitRepoOwnerEnv: testDetails.RepoOwner, 81 utils.BranchNameTemplateEnv: testDetails.CustomBranchName, 82 utils.GitApiEndpointEnv: testDetails.ApiEndpoint, 83 utils.GitProjectEnv: testDetails.GitProject, 84 utils.GitUsernameEnv: testDetails.GitUsername, 85 utils.GitBaseBranchEnv: mainBranch, 86 }) 87 return func() { 88 envRestoreFunc() 89 unsetEnvs() 90 } 91 } 92 93 func createAndCheckoutIssueBranch(t *testing.T, testDetails *IntegrationTestDetails, tmpDir, currentIssuesBranch string) func() { 94 gitManager := buildGitManager(t, testDetails) 95 // buildGitManager in an empty directory automatically creates a default .git folder, which prevents cloning. 96 // So we remove the default .git and clone the repository with its .git content 97 err := vcsutils.RemoveTempDir(".git") 98 require.NoError(t, err) 99 100 err = gitManager.Clone(tmpDir, issuesBranch) 101 require.NoError(t, err) 102 103 err = gitManager.CreateBranchAndCheckout(currentIssuesBranch) 104 require.NoError(t, err) 105 106 // This step is necessary because GitHub limits the number of pull requests from the same commit of the source branch 107 _, err = os.Create("emptyfile.txt") 108 assert.NoError(t, err) 109 err = gitManager.AddAllAndCommit("emptyfile added") 110 assert.NoError(t, err) 111 112 err = gitManager.Push(false, currentIssuesBranch) 113 require.NoError(t, err) 114 return func() { 115 // Remove the branch from remote 116 err := gitManager.RemoveRemoteBranch(currentIssuesBranch) 117 assert.NoError(t, err) 118 } 119 } 120 121 func findRelevantPrID(pullRequests []vcsclient.PullRequestInfo, branch string) (prId int) { 122 for _, pr := range pullRequests { 123 if pr.Source.Name == branch && pr.Target.Name == mainBranch { 124 prId = int(pr.ID) 125 return 126 } 127 } 128 return 129 } 130 131 func getOpenPullRequests(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails) []vcsclient.PullRequestInfo { 132 ctx := context.Background() 133 pullRequests, err := client.ListOpenPullRequests(ctx, testDetails.RepoOwner, testDetails.RepoName) 134 require.NoError(t, err) 135 return pullRequests 136 } 137 138 func runScanPullRequestCmd(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails) { 139 // Change working dir to temp dir for cloning and branch checkout 140 tmpDir, restoreFunc := utils.ChangeToTempDirWithCallback(t) 141 defer func() { 142 assert.NoError(t, restoreFunc()) 143 }() 144 145 // Get a timestamp based issues-branch 146 currentIssuesBranch := getIssuesBranchName() 147 removeBranchFunc := createAndCheckoutIssueBranch(t, testDetails, tmpDir, currentIssuesBranch) 148 defer removeBranchFunc() 149 150 ctx := context.Background() 151 // Create a pull request from the timestamp based issue branch against the main branch 152 err := client.CreatePullRequest(ctx, testDetails.RepoOwner, testDetails.RepoName, currentIssuesBranch, mainBranch, "scan pull request integration test", "") 153 require.NoError(t, err) 154 155 // Find the relevant pull request id 156 pullRequests := getOpenPullRequests(t, client, testDetails) 157 prId := findRelevantPrID(pullRequests, currentIssuesBranch) 158 testDetails.PullRequestID = strconv.Itoa(prId) 159 require.NotZero(t, prId) 160 defer func() { 161 closePullRequest(t, client, testDetails, prId) 162 }() 163 164 // Set the required environment variables for the scan-pull-request command 165 unsetEnvs := setIntegrationTestEnvs(t, testDetails) 166 defer unsetEnvs() 167 168 err = Exec(&scanpullrequest.ScanPullRequestCmd{}, utils.ScanPullRequest) 169 // Validate that issues were found and the relevant error returned 170 require.Errorf(t, err, scanpullrequest.SecurityIssueFoundErr) 171 172 validateResults(t, ctx, client, testDetails, prId) 173 } 174 175 func runScanRepositoryCmd(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails) { 176 _, restoreFunc := utils.ChangeToTempDirWithCallback(t) 177 defer func() { 178 assert.NoError(t, restoreFunc()) 179 }() 180 181 timestamp := getTimestamp() 182 // Add a timestamp to the fixing pull requests, to identify them later 183 testDetails.CustomBranchName = "frogbot-{IMPACTED_PACKAGE}-{BRANCH_NAME_HASH}-" + timestamp 184 185 // Set the required environment variables for the scan-repository command 186 unsetEnvs := setIntegrationTestEnvs(t, testDetails) 187 defer unsetEnvs() 188 189 err := Exec(&scanrepository.ScanRepositoryCmd{}, utils.ScanRepository) 190 require.NoError(t, err) 191 192 gitManager := buildGitManager(t, testDetails) 193 194 pullRequests := getOpenPullRequests(t, client, testDetails) 195 196 expectedBranchName := "frogbot-pyjwt-45ebb5a61916a91ae7c1e3ff7ffb6112-" + timestamp 197 prId := findRelevantPrID(pullRequests, expectedBranchName) 198 assert.NotZero(t, prId) 199 closePullRequest(t, client, testDetails, prId) 200 assert.NoError(t, gitManager.RemoveRemoteBranch(expectedBranchName)) 201 202 expectedBranchName = "frogbot-pyyaml-985622f4dbf3a64873b6b8440288e005-" + timestamp 203 prId = findRelevantPrID(pullRequests, expectedBranchName) 204 assert.NotZero(t, prId) 205 closePullRequest(t, client, testDetails, prId) 206 assert.NoError(t, gitManager.RemoveRemoteBranch(expectedBranchName)) 207 } 208 209 func validateResults(t *testing.T, ctx context.Context, client vcsclient.VcsClient, testDetails *IntegrationTestDetails, prID int) { 210 comments, err := client.ListPullRequestComments(ctx, testDetails.RepoOwner, testDetails.RepoName, prID) 211 require.NoError(t, err) 212 213 switch actualClient := client.(type) { 214 case *vcsclient.GitHubClient: 215 validateGitHubComments(t, ctx, actualClient, testDetails, prID, comments) 216 case *vcsclient.AzureReposClient: 217 validateAzureComments(t, comments) 218 case *vcsclient.BitbucketServerClient: 219 validateBitbucketServerComments(t, comments) 220 case *vcsclient.GitLabClient: 221 validateGitLabComments(t, comments) 222 } 223 } 224 225 func validateGitHubComments(t *testing.T, ctx context.Context, client *vcsclient.GitHubClient, testDetails *IntegrationTestDetails, prID int, comments []vcsclient.CommentInfo) { 226 assert.Len(t, comments, 1) 227 comment := comments[0] 228 assert.Contains(t, comment.Content, string(outputwriter.VulnerabilitiesPrBannerSource)) 229 230 reviewComments, err := client.ListPullRequestReviewComments(ctx, testDetails.RepoOwner, testDetails.RepoName, prID) 231 assert.NoError(t, err) 232 assert.GreaterOrEqual(t, len(reviewComments), 10) 233 } 234 235 func validateAzureComments(t *testing.T, comments []vcsclient.CommentInfo) { 236 assert.GreaterOrEqual(t, len(comments), expectedNumberOfIssues) 237 assertBannerExists(t, comments, string(outputwriter.VulnerabilitiesPrBannerSource)) 238 } 239 240 func validateBitbucketServerComments(t *testing.T, comments []vcsclient.CommentInfo) { 241 assert.GreaterOrEqual(t, len(comments), expectedNumberOfIssues) 242 assertBannerExists(t, comments, outputwriter.GetSimplifiedTitle(outputwriter.VulnerabilitiesPrBannerSource)) 243 } 244 245 func validateGitLabComments(t *testing.T, comments []vcsclient.CommentInfo) { 246 assert.GreaterOrEqual(t, len(comments), expectedNumberOfIssues) 247 assertBannerExists(t, comments, string(outputwriter.VulnerabilitiesMrBannerSource)) 248 } 249 250 func getJfrogEnvRestoreFunc(t *testing.T) func() { 251 jfrogEnvs := make(map[string]string) 252 for _, env := range os.Environ() { 253 envSplit := strings.Split(env, "=") 254 key := envSplit[0] 255 val := envSplit[1] 256 if strings.HasPrefix(key, "JF_") { 257 jfrogEnvs[key] = val 258 } 259 } 260 261 return func() { 262 for key, val := range jfrogEnvs { 263 assert.NoError(t, os.Setenv(key, val)) 264 } 265 } 266 } 267 268 // This function retrieves the relevant VCS provider access token based on the corresponding environment variable. 269 // If the environment variable is empty, the test is skipped. 270 func getIntegrationToken(t *testing.T, tokenEnv string) string { 271 integrationRepoToken := os.Getenv(tokenEnv) 272 if integrationRepoToken == "" { 273 t.Skipf("%s is not set, skipping integration test", tokenEnv) 274 } 275 return integrationRepoToken 276 } 277 278 func assertBannerExists(t *testing.T, comments []vcsclient.CommentInfo, banner string) { 279 var isContains bool 280 var occurrences int 281 for _, c := range comments { 282 if strings.Contains(c.Content, banner) { 283 isContains = true 284 occurrences++ 285 } 286 } 287 288 assert.True(t, isContains) 289 assert.Equal(t, 1, occurrences) 290 } 291 292 func closePullRequest(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails, prID int) { 293 targetBranch := mainBranch 294 if _, isAzureClient := client.(*vcsclient.AzureReposClient); isAzureClient { 295 // The Azure API requires not adding parameters that won't be updated, so we omit the targetBranch in that case 296 targetBranch = "" 297 } 298 err := client.UpdatePullRequest(context.Background(), testDetails.RepoOwner, testDetails.RepoName, "integration test finished", "", targetBranch, prID, vcsutils.Closed) 299 assert.NoError(t, err) 300 }