diff --git a/cmd/pint/ci.go b/cmd/pint/ci.go
index f6c3ab09..fee8d2c6 100644
--- a/cmd/pint/ci.go
+++ b/cmd/pint/ci.go
@@ -165,17 +165,15 @@ func actionCI(ctx context.Context, c *cli.Command) error {
timeout, _ := time.ParseDuration(meta.cfg.Repository.BitBucket.Timeout)
br := reporter.NewBitBucketReporter(
- version,
meta.cfg.Repository.BitBucket.URI,
timeout,
token,
meta.cfg.Repository.BitBucket.Project,
meta.cfg.Repository.BitBucket.Repository,
meta.cfg.Repository.BitBucket.MaxComments,
- c.Bool(showDupsFlag),
git.RunGit,
)
- reps = append(reps, br)
+ reps = append(reps, reporter.NewCommentReporter(br, c.Bool(showDupsFlag)))
}
if meta.cfg.Repository != nil && meta.cfg.Repository.GitLab != nil {
diff --git a/cmd/pint/tests/0031_ci_bitbucket.txt b/cmd/pint/tests/0031_ci_bitbucket.txt
index 850e77e8..1e6ad985 100644
--- a/cmd/pint/tests/0031_ci_bitbucket.txt
+++ b/cmd/pint/tests/0031_ci_bitbucket.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6031
@@ -55,72 +54,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 2
- title: Number of rules parsed
- type: NUMBER
- - value: 2
- title: Number of rules checked
- type: NUMBER
- - value: 2
- title: Number of problems found
- type: NUMBER
- - value: 9
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem reported on unmodified line 2, annotation moved here: alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 3
- - path: rules.yml
- message: "alerts/for: redundant field with default value"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/for.html
- line: 3
---- END ---
-
diff --git a/cmd/pint/tests/0068_skip_ci.txt b/cmd/pint/tests/0068_skip_ci.txt
index 1c292a96..190071ca 100644
--- a/cmd/pint/tests/0068_skip_ci.txt
+++ b/cmd/pint/tests/0068_skip_ci.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6068
@@ -54,52 +53,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 0
- title: Number of rules parsed
- type: NUMBER
- - value: 0
- title: Number of rules checked
- type: NUMBER
- - value: 0
- title: Number of problems found
- type: NUMBER
- - value: 0
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
diff --git a/cmd/pint/tests/0069_bitbucket_unmodified.txt b/cmd/pint/tests/0069_bitbucket_unmodified.txt
index d5e1d507..639d6814 100644
--- a/cmd/pint/tests/0069_bitbucket_unmodified.txt
+++ b/cmd/pint/tests/0069_bitbucket_unmodified.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6069
@@ -73,96 +72,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 4
- title: Number of rules parsed
- type: NUMBER
- - value: 4
- title: Number of rules checked
- type: NUMBER
- - value: 6
- title: Number of problems found
- type: NUMBER
- - value: 20
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem reported on unmodified line 2, annotation moved here: alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 1
- - path: rules.yml
- message: "Problem reported on unmodified line 2, annotation moved here: promql/regexp: redundant regexp"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/promql/regexp.html
- line: 1
- - path: rules.yml
- message: "alerts/for: redundant field with default value"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/for.html
- line: 3
- - path: rules.yml
- message: "Problem reported on unmodified line 5, annotation moved here: alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 4
- - path: rules.yml
- message: "Problem reported on unmodified line 5, annotation moved here: promql/regexp: redundant regexp"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/promql/regexp.html
- line: 4
- - path: rules.yml
- message: "Problem reported on unmodified line 6, annotation moved here: alerts/for: redundant field with default value"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/for.html
- line: 4
---- END ---
-
diff --git a/cmd/pint/tests/0070_bitbucket_strict.txt b/cmd/pint/tests/0070_bitbucket_strict.txt
index af2c3496..4cc36c69 100644
--- a/cmd/pint/tests/0070_bitbucket_strict.txt
+++ b/cmd/pint/tests/0070_bitbucket_strict.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6070
@@ -53,66 +52,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 1
- title: Number of problems found
- type: NUMBER
- - value: 1
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem reported on unmodified line 1, annotation moved here: yaml/parse: top level field must be a groups key, got list"
- severity: HIGH
- type: BUG
- link: https://cloudflare.github.io/pint/checks/yaml/parse.html
- line: 3
---- END ---
-
diff --git a/cmd/pint/tests/0071_ci_owner.txt b/cmd/pint/tests/0071_ci_owner.txt
index 6d04620a..10960209 100644
--- a/cmd/pint/tests/0071_ci_owner.txt
+++ b/cmd/pint/tests/0071_ci_owner.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6071
@@ -71,78 +70,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 3
- title: Number of rules parsed
- type: NUMBER
- - value: 2
- title: Number of rules checked
- type: NUMBER
- - value: 3
- title: Number of problems found
- type: NUMBER
- - value: 18
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 5
- - path: rules.yml
- message: "alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 5
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 7
---- END ---
-
diff --git a/cmd/pint/tests/0072_bitbucket_move_bug_to_modified.txt b/cmd/pint/tests/0072_bitbucket_move_bug_to_modified.txt
index 79fb335b..28d98188 100644
--- a/cmd/pint/tests/0072_bitbucket_move_bug_to_modified.txt
+++ b/cmd/pint/tests/0072_bitbucket_move_bug_to_modified.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6072
@@ -54,66 +53,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 1
- title: Number of problems found
- type: NUMBER
- - value: 1
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem reported on unmodified line 2, annotation moved here: yaml/parse: did not find expected key"
- severity: HIGH
- type: BUG
- link: https://cloudflare.github.io/pint/checks/yaml/parse.html
- line: 3
---- END ---
-
diff --git a/cmd/pint/tests/0075_ci_strict.txt b/cmd/pint/tests/0075_ci_strict.txt
index 6661191f..c7474818 100644
--- a/cmd/pint/tests/0075_ci_strict.txt
+++ b/cmd/pint/tests/0075_ci_strict.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6075
@@ -51,72 +50,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 2
- title: Number of problems found
- type: NUMBER
- - value: 9
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 5
- - path: rules.yml
- message: "promql/syntax: PromQL syntax error"
- severity: HIGH
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/syntax.html
- line: 5
---- END ---
-
diff --git a/cmd/pint/tests/0076_ci_group_errors.txt b/cmd/pint/tests/0076_ci_group_errors.txt
index 55ccb8f7..412ac0fa 100644
--- a/cmd/pint/tests/0076_ci_group_errors.txt
+++ b/cmd/pint/tests/0076_ci_group_errors.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6076
@@ -210,338 +209,10 @@ Bug: required label is being removed via aggregation (promql/aggregate)
^^^ Query is using aggregation that removes all labels.
`job` label is required and should be preserved when aggregating all rules.
-level=ERROR msg="Execution completed with error(s)" err="submitting reports: fatal error(s) reported"
+level=ERROR msg="Execution completed with error(s)" err="problems found"
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 10
- title: Number of rules parsed
- type: NUMBER
- - value: 10
- title: Number of rules checked
- type: NUMBER
- - value: 46
- title: Number of problems found
- type: NUMBER
- - value: 116
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 5
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 5
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 5
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 5
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 5
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 5
- - path: rules.yml
- message: "promql/syntax: PromQL syntax error"
- severity: HIGH
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/syntax.html
- line: 5
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 8
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 8
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 8
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 8
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 8
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 8
- - path: rules.yml
- message: "alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 8
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 11
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 17
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 17
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 23
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 23
- - path: rules.yml
- message: "alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 20
- - path: rules.yml
- message: "alerts/for: redundant field with default value"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/for.html
- line: 21
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 30
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 30
- - path: rules.yml
- message: "alerts/template: template uses non-existent label"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/template.html
- line: 30
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 30
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 30
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 30
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 30
- - path: rules.yml
- message: "alerts/template: value used in labels"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/template.html
- line: 28
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 33
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 33
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 33
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 33
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 33
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 33
- - path: rules.yml
- message: "alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 33
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 36
- - path: rules.yml
- message: "promql/regexp: redundant regexp"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/promql/regexp.html
- line: 36
- - path: rules.yml
- message: "promql/aggregate: required label is being removed via aggregation"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/aggregate.html
- line: 36
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 39
- - path: rules.yml
- message: "alerts/annotation: required annotation not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/alerts/annotation.html
- line: 39
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 39
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 39
- - path: rules.yml
- message: "rule/label: required label not set"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/label.html
- line: 39
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 39
- - path: rules.yml
- message: "alerts/comparison: always firing alert"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/comparison.html
- line: 39
---- END ---
-
diff --git a/cmd/pint/tests/0093_ci_bitbucket_ignore_file.txt b/cmd/pint/tests/0093_ci_bitbucket_ignore_file.txt
index 8c92644f..a19f5611 100644
--- a/cmd/pint/tests/0093_ci_bitbucket_ignore_file.txt
+++ b/cmd/pint/tests/0093_ci_bitbucket_ignore_file.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6093
@@ -56,66 +55,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 1
- title: Number of problems found
- type: NUMBER
- - value: 1
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem reported on unmodified line 1, annotation moved here: ignore/file: This file was excluded from pint checks."
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/ignore/file.html
- line: 3
---- END ---
-
diff --git a/cmd/pint/tests/0094_rule_file_symlink_bb.txt b/cmd/pint/tests/0094_rule_file_symlink_bb.txt
index 4fcf5775..0627a73e 100644
--- a/cmd/pint/tests/0094_rule_file_symlink_bb.txt
+++ b/cmd/pint/tests/0094_rule_file_symlink_bb.txt
@@ -12,7 +12,6 @@ http response prometheus5m /api/v1/query_range 200 {"status":"success","data":{"
http response prometheus5m /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[]}}
http start prometheus5m 127.0.0.1:2094
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6094
@@ -49,7 +48,6 @@ stderr 'path=double.yml rule=rule2'
! stderr 'path=ignored.yml'
stderr 'level=INFO msg="Problems found" Bug=4'
-stderr 'result":"FAIL"'
! stderr '---> rules.yml:5 Bug: Duration for `rate'
! stderr '---> rules.yml:7 Bug: Duration for `rate'
@@ -58,16 +56,6 @@ stderr '---> symlink.yml ~> rules.yml:7'
stderr '---> double.yml ~> rules.yml:5'
stderr '---> double.yml ~> rules.yml:7'
-stderr '{"path":"rules.yml","message":"Problem detected on symlinked file symlink.yml: .*","line":5}'
-stderr '{"path":"rules.yml","message":"Problem detected on symlinked file double.yml: .*","line":5}'
-stderr '{"path":"rules.yml","message":"Problem detected on symlinked file symlink.yml: .*","line":7}'
-stderr '{"path":"rules.yml","message":"Problem detected on symlinked file double.yml: .*","line":7}'
-! stderr '{"path":"symlink.yml"'
-! stderr '{"path":"double.yml"'
-
-stderr 'Problem detected on symlinked file symlink.yml:'
-stderr 'Problem detected on symlinked file double.yml:'
-
cmp bitbucket.got ../bitbucket.expected
-- src/v1.yml --
@@ -114,84 +102,8 @@ prometheus "5m" {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 6
- title: Number of rules parsed
- type: NUMBER
- - value: 6
- title: Number of rules checked
- type: NUMBER
- - value: 4
- title: Number of problems found
- type: NUMBER
- - value: 60
- title: Number of offline checks
- type: NUMBER
- - value: 48
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "Problem detected on symlinked file double.yml: promql/rate: duration too small"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/rate.html
- line: 5
- - path: rules.yml
- message: "Problem detected on symlinked file double.yml: promql/rate: duration too small"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/rate.html
- line: 7
- - path: rules.yml
- message: "Problem detected on symlinked file symlink.yml: promql/rate: duration too small"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/rate.html
- line: 5
- - path: rules.yml
- message: "Problem detected on symlinked file symlink.yml: promql/rate: duration too small"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/promql/rate.html
- line: 7
---- END ---
-
diff --git a/cmd/pint/tests/0100_ci_alerts_count.txt b/cmd/pint/tests/0100_ci_alerts_count.txt
index e0c93b7a..ae12a0d6 100644
--- a/cmd/pint/tests/0100_ci_alerts_count.txt
+++ b/cmd/pint/tests/0100_ci_alerts_count.txt
@@ -5,7 +5,6 @@ http response prometheus /api/v1/query_range 200 {"status":"success","data":{"re
http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[]}}
http start prometheus 127.0.0.1:2100
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6100
@@ -82,72 +81,8 @@ rule {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 2
- title: Number of rules parsed
- type: NUMBER
- - value: 2
- title: Number of rules checked
- type: NUMBER
- - value: 2
- title: Number of problems found
- type: NUMBER
- - value: 20
- title: Number of offline checks
- type: NUMBER
- - value: 18
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "alerts/count: alert count estimate"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/count.html
- line: 4
- - path: rules.yml
- message: "alerts/count: alert count estimate"
- severity: LOW
- type: CODE_SMELL
- link: https://cloudflare.github.io/pint/checks/alerts/count.html
- line: 11
---- END ---
-
diff --git a/cmd/pint/tests/0123_ci_owner_allowed.txt b/cmd/pint/tests/0123_ci_owner_allowed.txt
index 9b259f21..f65c9f3d 100644
--- a/cmd/pint/tests/0123_ci_owner_allowed.txt
+++ b/cmd/pint/tests/0123_ci_owner_allowed.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6123
@@ -82,72 +81,8 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 6
- title: Number of rules parsed
- type: NUMBER
- - value: 6
- title: Number of rules checked
- type: NUMBER
- - value: 2
- title: Number of problems found
- type: NUMBER
- - value: 30
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-POST /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-annotations:
- - path: rules.yml
- message: "rule/owner: missing owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 4
- - path: rules.yml
- message: "rule/owner: invalid owner"
- severity: MEDIUM
- type: BUG
- link: https://cloudflare.github.io/pint/checks/rule/owner.html
- line: 7
---- END ---
-
diff --git a/cmd/pint/tests/0163_ci_comment_resolve.txt b/cmd/pint/tests/0163_ci_comment_resolve.txt
index 3514fa77..1200f7a2 100644
--- a/cmd/pint/tests/0163_ci_comment_resolve.txt
+++ b/cmd/pint/tests/0163_ci_comment_resolve.txt
@@ -1,5 +1,4 @@
http response bitbucket /plugins/servlet/applinks/whoami 200 pint
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {"size":1,"isLastPage":true,"values":[{"id":123,"open":true,"fromRef":{"id":"refs/heads/modify","latestCommit":"fake-commit-id"},"toRef":{"id":"refs/heads/main","latestCommit":"fake-commit-id"}}]}
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/changes 200 {"values":[{"path":{"toString":"rules.yml"}}],"size":1,"isLastPage":true}
http response bitbucket /rest/api/latest/projects/prometheus/repos/rules/commits/fake-commit-id/diff/rules.yml 200 {"diffs":[{"hunks":[{"segments":[{"type":"ADDED", "lines":[{"source":5,"destination":5}]}]}]}]}
@@ -29,8 +28,7 @@ exec git commit -am 'v1'
stderr 'msg="Problems found" Fatal=1'
stderr 'msg="Found open pull request, reporting problems using comments" id=123 srcBranch=modify srcCommit=fake-commit-id dstBranch=main dstCommit=fake-commit-id'
stderr 'msg="Got existing pull request comments from BitBucket" count=1'
-stderr 'msg="Generated comments to add to BitBucket" count=1'
-stderr 'msg="Added pull request comments to BitBucket" count=1'
+stderr 'msg="Creating a new comment" reporter=BitBucket path=rules.yml line=5'
cp ../src/v2.yml rules.yml
exec git commit -am 'v2'
@@ -38,10 +36,7 @@ exec pint --no-color ci
! stdout .
! stderr 'msg="Problems found"'
stderr 'msg="Found open pull request, reporting problems using comments" id=123 srcBranch=modify srcCommit=fake-commit-id dstBranch=main dstCommit=fake-commit-id'
-stderr 'msg="Getting pull request changes from BitBucket"'
stderr 'msg="Got existing pull request comments from BitBucket" count=1'
-stderr 'msg="Generated comments to add to BitBucket" count=0'
-stderr 'msg="Added pull request comments to BitBucket" count=0'
cmp bitbucket.got ../bitbucket.expected
-- src/v0.yml --
@@ -75,60 +70,11 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 1
- title: Number of problems found
- type: NUMBER
- - value: 9
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-GET /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/changes
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-GET /rest/api/latest/projects/prometheus/repos/rules/commits/fake-commit-id/diff/rules.yml
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
GET /plugins/servlet/applinks/whoami
Accept-Encoding: gzip
Authorization: Bearer "12345"
@@ -139,15 +85,6 @@ GET /rest/api/latest/projects/prometheus/repos/rules/pull-requests/123/activitie
Authorization: Bearer "12345"
Content-Type: application/json
-PUT /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-state: RESOLVED
-version: 0
---- END ---
-
POST /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
Accept-Encoding: gzip
Authorization: Bearer "12345"
@@ -156,9 +93,8 @@ POST /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
text: |
:stop_sign: **Fatal** reported by [pint](https://cloudflare.github.io/pint/) **promql/syntax** check.
- ------
-
- PromQL syntax error
+
+ PromQL syntax error
```yaml
5 | expr: count(up == 1) bie(job)
@@ -169,6 +105,8 @@ text: |
[Click here](https://prometheus.io/docs/prometheus/latest/querying/basics/) for PromQL documentation.
+
+
------
:information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/promql/syntax.html).
@@ -181,60 +119,16 @@ anchor:
line: 5
--- END ---
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
+DELETE /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 2
- title: Number of rules parsed
- type: NUMBER
- - value: 2
- title: Number of rules checked
- type: NUMBER
- - value: 0
- title: Number of problems found
- type: NUMBER
- - value: 9
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-GET /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/changes
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-GET /rest/api/latest/projects/prometheus/repos/rules/commits/fake-commit-id/diff/rules.yml
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
GET /plugins/servlet/applinks/whoami
Accept-Encoding: gzip
Authorization: Bearer "12345"
@@ -245,12 +139,8 @@ GET /rest/api/latest/projects/prometheus/repos/rules/pull-requests/123/activitie
Authorization: Bearer "12345"
Content-Type: application/json
-PUT /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
+DELETE /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
---- BODY ---
-state: RESOLVED
-version: 0
---- END ---
diff --git a/cmd/pint/tests/0164_ci_comment_resolve_delete.txt b/cmd/pint/tests/0164_ci_comment_resolve_delete.txt
index 610efd5a..b1237c5b 100644
--- a/cmd/pint/tests/0164_ci_comment_resolve_delete.txt
+++ b/cmd/pint/tests/0164_ci_comment_resolve_delete.txt
@@ -1,5 +1,4 @@
http response bitbucket /plugins/servlet/applinks/whoami 200 pint
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {"size":1,"isLastPage":true,"values":[{"id":123,"open":true,"fromRef":{"id":"refs/heads/modify","latestCommit":"fake-commit-id"},"toRef":{"id":"refs/heads/main","latestCommit":"fake-commit-id"}}]}
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/changes 200 {"values":[{"path":{"toString":"rules.yml"}}],"size":1,"isLastPage":true}
http response bitbucket /rest/api/latest/projects/prometheus/repos/rules/commits/fake-commit-id/diff/rules.yml 200 {"diffs":[{"hunks":[{"segments":[{"type":"ADDED", "lines":[{"source":5,"destination":5}]}]}]}]}
@@ -29,8 +28,7 @@ exec git commit -am 'v1'
stderr 'msg="Problems found" Fatal=1'
stderr 'msg="Found open pull request, reporting problems using comments" id=123 srcBranch=modify srcCommit=fake-commit-id dstBranch=main dstCommit=fake-commit-id'
stderr 'msg="Got existing pull request comments from BitBucket" count=1'
-stderr 'msg="Generated comments to add to BitBucket" count=1'
-stderr 'msg="Added pull request comments to BitBucket" count=1'
+stderr 'msg="Creating a new comment" reporter=BitBucket path=rules.yml line=5'
cmp bitbucket.got ../bitbucket.expected
cp ../src/v2.yml rules.yml
@@ -39,10 +37,7 @@ exec pint --no-color ci
! stdout .
! stderr 'msg="Problems found"'
stderr 'msg="Found open pull request, reporting problems using comments" id=123 srcBranch=modify srcCommit=fake-commit-id dstBranch=main dstCommit=fake-commit-id'
-stderr 'msg="Getting pull request changes from BitBucket"'
stderr 'msg="Got existing pull request comments from BitBucket" count=1'
-stderr 'msg="Generated comments to add to BitBucket" count=0'
-stderr 'msg="Added pull request comments to BitBucket" count=0'
-- src/v0.yml --
groups:
@@ -75,60 +70,11 @@ repository {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: FAIL
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 1
- title: Number of rules parsed
- type: NUMBER
- - value: 1
- title: Number of rules checked
- type: NUMBER
- - value: 1
- title: Number of problems found
- type: NUMBER
- - value: 9
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-GET /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/changes
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-GET /rest/api/latest/projects/prometheus/repos/rules/commits/fake-commit-id/diff/rules.yml
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
GET /plugins/servlet/applinks/whoami
Accept-Encoding: gzip
Authorization: Bearer "12345"
@@ -139,24 +85,6 @@ GET /rest/api/latest/projects/prometheus/repos/rules/pull-requests/123/activitie
Authorization: Bearer "12345"
Content-Type: application/json
-PUT /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-severity: BLOCKER
-version: 0
---- END ---
-
-PUT /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-state: RESOLVED
-version: 0
---- END ---
-
POST /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
Accept-Encoding: gzip
Authorization: Bearer "12345"
@@ -165,9 +93,8 @@ POST /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
text: |
:stop_sign: **Fatal** reported by [pint](https://cloudflare.github.io/pint/) **promql/syntax** check.
- ------
-
- PromQL syntax error
+
+ PromQL syntax error
```yaml
5 | expr: count(up == 1) bie(job)
@@ -178,6 +105,8 @@ text: |
[Click here](https://prometheus.io/docs/prometheus/latest/querying/basics/) for PromQL documentation.
+
+
------
:information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/promql/syntax.html).
@@ -190,3 +119,8 @@ anchor:
line: 5
--- END ---
+DELETE /rest/api/1.0/projects/prometheus/repos/rules/pull-requests/123/comments
+ Accept-Encoding: gzip
+ Authorization: Bearer "12345"
+ Content-Type: application/json
+
diff --git a/cmd/pint/tests/0188_ci_noop.txt b/cmd/pint/tests/0188_ci_noop.txt
index a76583ec..b540a816 100644
--- a/cmd/pint/tests/0188_ci_noop.txt
+++ b/cmd/pint/tests/0188_ci_noop.txt
@@ -1,4 +1,3 @@
-http response bitbucket /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint 200 OK
http response bitbucket /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests 200 {}
http start bitbucket 127.0.0.1:6188
@@ -64,52 +63,8 @@ rule {
}
-- bitbucket.expected --
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
-PUT /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
---- BODY ---
-reporter: Prometheus rule linter
-title: pint unknown
-result: PASS
-details: |-
- pint is a Prometheus rule linter/validator.
- It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.
- Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server).
-link: https://cloudflare.github.io/pint/
-data:
- - value: 2
- title: Number of rules parsed
- type: NUMBER
- - value: 2
- title: Number of rules checked
- type: NUMBER
- - value: 0
- title: Number of problems found
- type: NUMBER
- - value: 0
- title: Number of offline checks
- type: NUMBER
- - value: 0
- title: Number of online checks
- type: NUMBER
- - value: 0
- title: Checks duration
- type: DURATION
---- END ---
-
GET /rest/api/1.0/projects/prometheus/repos/rules/commits/.*/pull-requests
Accept-Encoding: gzip
Authorization: Bearer "12345"
Content-Type: application/json
-DELETE /rest/insights/1.0/projects/prometheus/repos/rules/commits/.*/reports/pint
- Accept-Encoding: gzip
- Authorization: Bearer "12345"
- Content-Type: application/json
-
diff --git a/docs/changelog.md b/docs/changelog.md
index ea467c87..5b33420e 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,8 @@
- When running `pint ci` pint will now use `git diff` to calculate modified lines
instead of `git blame`.
+- BitBucket reporter no longer creates Code Insight reports and annotations.
+ All problems are now reported exclusively via pull request comments.
## v0.80.0
diff --git a/internal/reporter/bitbucket.go b/internal/reporter/bitbucket.go
index 2d4dec41..f747d91f 100644
--- a/internal/reporter/bitbucket.go
+++ b/internal/reporter/bitbucket.go
@@ -1,164 +1,478 @@
package reporter
import (
+ "bytes"
"context"
- "errors"
+ "encoding/json"
"fmt"
+ "io"
"log/slog"
+ "net/http"
+ "strings"
"time"
+ "github.com/cloudflare/pint/internal/checks"
"github.com/cloudflare/pint/internal/git"
"github.com/cloudflare/pint/internal/output"
)
-const (
- BitBucketDescription = "pint is a Prometheus rule linter/validator.\n" +
- "It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.\n" +
- "Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server)."
-)
+type BitBucketRef struct {
+ ID string `json:"id"`
+ Commit string `json:"latestCommit"`
+}
-func NewBitBucketReporter(version, uri string, timeout time.Duration, token, project, repo string, maxComments int, showDuplicates bool, gitCmd git.CommandRunner) BitBucketReporter {
- slog.LogAttrs(context.Background(), slog.LevelInfo,
- "Will report problems to BitBucket",
- slog.String("uri", uri),
- slog.String("timeout", output.HumanizeDuration(timeout)),
- slog.String("project", project),
- slog.String("repo", repo),
- slog.Int("maxComments", maxComments),
- )
- return BitBucketReporter{
- api: newBitBucketAPI(version, uri, timeout, token, project, repo, maxComments, showDuplicates),
- gitCmd: gitCmd,
+type BitBucketPullRequest struct {
+ FromRef BitBucketRef `json:"fromRef"`
+ ToRef BitBucketRef `json:"toRef"`
+ ID int `json:"id"`
+ Open bool `json:"open"`
+}
+
+type BitBucketPullRequests struct {
+ Values []BitBucketPullRequest `json:"values"`
+ Start int `json:"start"`
+ NextPageStart int `json:"nextPageStart"`
+ IsLastPage bool `json:"isLastPage"`
+}
+
+type bitBucketPR struct {
+ srcBranch string
+ srcHead string
+ dstBranch string
+ dstHead string
+ ID int
+}
+
+type bitBucketCommentMeta struct {
+ id int
+ version int
+}
+
+type bitBucketComment struct {
+ text string
+ anchor BitBucketCommentAnchor
+ id int
+ version int
+}
+
+type BitBucketCommentAuthor struct {
+ Name string `json:"name"`
+}
+
+type BitBucketPullRequestComment struct {
+ State string `json:"state"`
+ Author BitBucketCommentAuthor `json:"author"`
+ Text string `json:"text"`
+ Severity string `json:"severity"`
+ Comments []BitBucketPullRequestComment `json:"comments"`
+ ID int `json:"id"`
+ Version int `json:"version"`
+ Resolved bool `json:"threadResolved"`
+}
+
+type BitBucketCommentAnchor struct {
+ LineType string `json:"lineType"`
+ DiffType string `json:"diffType"`
+ Path string `json:"path"`
+ Line int `json:"line"`
+ Orphaned bool `json:"orphaned"`
+}
+
+type BitBucketPullRequestActivity struct {
+ Action string `json:"action"`
+ CommentAction string `json:"commentAction"`
+ CommentAnchor BitBucketCommentAnchor `json:"commentAnchor"`
+ Comment BitBucketPullRequestComment `json:"comment"`
+}
+
+type BitBucketPullRequestActivities struct {
+ Values []BitBucketPullRequestActivity `json:"values"`
+ Start int `json:"start"`
+ NextPageStart int `json:"nextPageStart"`
+ IsLastPage bool `json:"isLastPage"`
+}
+
+type bitBucketPendingCommentAnchor struct {
+ Path string `json:"path,omitempty"`
+ LineType string `json:"lineType,omitempty"`
+ FileType string `json:"fileType,omitempty"`
+ DiffType string `json:"diffType"`
+ Line int `json:"line,omitempty"`
+}
+
+type BitBucketPendingComment struct {
+ Text string `json:"text"`
+ Severity string `json:"severity"`
+ Anchor bitBucketPendingCommentAnchor `json:"anchor"`
+}
+
+func newBitBucketAPI(uri string, timeout time.Duration, token, project, repo string) *bitBucketAPI {
+ return &bitBucketAPI{
+ uri: uri,
+ timeout: timeout,
+ authToken: token,
+ project: project,
+ repo: repo,
}
}
-// BitBucketReporter send linter results to BitBucket using
-// https://docs.atlassian.com/bitbucket-server/rest/7.8.0/bitbucket-code-insights-rest.html
-type BitBucketReporter struct {
- api *bitBucketAPI
- gitCmd git.CommandRunner
+type bitBucketAPI struct {
+ uri string
+ authToken string
+ project string
+ repo string
+ timeout time.Duration
}
-func (bb BitBucketReporter) Submit(ctx context.Context, summary Summary) (err error) {
- var headCommit string
- if headCommit, err = git.HeadCommit(bb.gitCmd); err != nil {
- return fmt.Errorf("failed to get HEAD commit: %w", err)
+func (bb bitBucketAPI) request(method, path string, body io.Reader) ([]byte, error) {
+ slog.LogAttrs(context.Background(), slog.LevelInfo, "Sending a request to BitBucket", slog.String("method", method), slog.String("path", path))
+
+ if body != nil {
+ payload, _ := io.ReadAll(body)
+ slog.LogAttrs(context.Background(), slog.LevelDebug, "Request payload", slog.String("body", string(payload)))
+ body = bytes.NewReader(payload)
}
- slog.LogAttrs(ctx, slog.LevelInfo, "Got HEAD commit from git", slog.String("commit", headCommit))
- if err = bb.api.deleteReport(headCommit); err != nil {
- slog.LogAttrs(ctx, slog.LevelError, "Failed to delete old BitBucket report", slog.Any("err", err))
+ req, err := http.NewRequest(method, bb.uri+path, body)
+ if err != nil {
+ return nil, err
}
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+bb.authToken)
- if err = bb.api.createReport(summary, headCommit); err != nil {
- return fmt.Errorf("failed to create BitBucket report: %w", err)
+ netClient := &http.Client{
+ Timeout: bb.timeout,
}
- var headBranch string
- if headBranch, err = git.CurrentBranch(bb.gitCmd); err != nil {
- return fmt.Errorf("failed to get current branch: %w", err)
+ resp, err := netClient.Do(req)
+ if err != nil {
+ return nil, err
}
+ defer resp.Body.Close()
- var pr *bitBucketPR
- if pr, err = bb.api.findPullRequestForBranch(headBranch, headCommit); err != nil {
- return fmt.Errorf("failed to get open pull requests from BitBucket: %w", err)
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return data, err
}
- if pr != nil {
- slog.LogAttrs(ctx, slog.LevelInfo,
- "Found open pull request, reporting problems using comments",
- slog.Int("id", pr.ID),
- slog.String("srcBranch", pr.srcBranch),
- slog.String("srcCommit", pr.srcHead),
- slog.String("dstBranch", pr.dstBranch),
- slog.String("dstCommit", pr.dstHead),
+ slog.LogAttrs(context.Background(), slog.LevelInfo, "BitBucket request completed", slog.Int("status", resp.StatusCode))
+ slog.LogAttrs(context.Background(), slog.LevelDebug, "BitBucket response body", slog.Int("code", resp.StatusCode), slog.String("body", string(data)))
+ if resp.StatusCode >= 300 {
+ slog.LogAttrs(context.Background(), slog.LevelError,
+ "Got a non 2xx response",
+ slog.String("body", string(data)),
+ slog.String("path", path),
+ slog.Int("code", resp.StatusCode),
)
+ return data, fmt.Errorf("%s request failed", method)
+ }
+
+ return data, err
+}
+
+func (bb bitBucketAPI) whoami() (string, error) {
+ resp, err := bb.request(http.MethodGet, "/plugins/servlet/applinks/whoami", nil)
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSuffix(string(resp), "\n"), nil
+}
- slog.LogAttrs(ctx, slog.LevelInfo, "Getting pull request changes from BitBucket")
- var changes *bitBucketPRChanges
- if changes, err = bb.api.getPullRequestChanges(pr); err != nil {
- return fmt.Errorf("failed to get pull request changes from BitBucket: %w", err)
+func (bb bitBucketAPI) findPullRequestForBranch(branch, commit string) (*bitBucketPR, error) {
+ var start int
+ for {
+ resp, err := bb.request(
+ http.MethodGet,
+ fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/commits/%s/pull-requests?start=%d", bb.project, bb.repo, commit, start),
+ nil,
+ )
+ if err != nil {
+ return nil, err
}
- slog.LogAttrs(ctx, slog.LevelDebug, "Got modified files from BitBucket", slog.Any("files", changes.pathModifiedLines))
- var existingComments []bitBucketComment
- if existingComments, err = bb.api.getPullRequestComments(pr); err != nil {
- return fmt.Errorf("failed to get pull request comments from BitBucket: %w", err)
+ var prs BitBucketPullRequests
+ if err = json.Unmarshal(resp, &prs); err != nil {
+ return nil, err
}
- slog.LogAttrs(ctx, slog.LevelInfo, "Got existing pull request comments from BitBucket", slog.Int("count", len(existingComments)))
- pendingComments := bb.api.makeComments(summary, changes)
- slog.LogAttrs(ctx, slog.LevelInfo, "Generated comments to add to BitBucket", slog.Int("count", len(pendingComments)))
+ for _, pr := range prs.Values {
+ if !pr.Open {
+ continue
+ }
+ srcBranch := strings.TrimPrefix(pr.FromRef.ID, "refs/heads/")
+ dstBranch := strings.TrimPrefix(pr.ToRef.ID, "refs/heads/")
+ if srcBranch == branch {
+ return &bitBucketPR{
+ ID: pr.ID,
+ srcBranch: srcBranch,
+ srcHead: pr.FromRef.Commit,
+ dstBranch: dstBranch,
+ dstHead: pr.ToRef.Commit,
+ }, nil
+ }
+ }
- pendingComments = bb.api.limitComments(pendingComments)
- slog.LogAttrs(ctx, slog.LevelInfo, "Will add comments to BitBucket",
- slog.Int("count", len(pendingComments)),
- slog.Int("limit", bb.api.maxComments),
+ if prs.IsLastPage || prs.NextPageStart == start {
+ break
+ }
+ start = prs.NextPageStart
+ }
+
+ return nil, nil
+}
+
+func (bb bitBucketAPI) getPullRequestComments(pr *bitBucketPR) ([]bitBucketComment, error) {
+ username, err := bb.whoami()
+ if err != nil {
+ return nil, err
+ }
+
+ comments := []bitBucketComment{}
+
+ var start int
+ for {
+ resp, err := bb.request(
+ http.MethodGet,
+ fmt.Sprintf(
+ "/rest/api/latest/projects/%s/repos/%s/pull-requests/%d/activities?start=%d",
+ bb.project, bb.repo,
+ pr.ID,
+ start,
+ ),
+ nil,
)
+ if err != nil {
+ return nil, err
+ }
- slog.LogAttrs(ctx, slog.LevelInfo, "Deleting stale comments from BitBucket")
- bb.api.pruneComments(pr, existingComments, pendingComments)
+ var acts BitBucketPullRequestActivities
+ if err = json.Unmarshal(resp, &acts); err != nil {
+ return nil, err
+ }
- slog.LogAttrs(ctx, slog.LevelInfo, "Adding missing comments to BitBucket")
- if err = bb.api.addComments(pr, existingComments, pendingComments); err != nil {
- return fmt.Errorf("failed to create BitBucket pull request comments: %w", err)
+ for _, act := range acts.Values {
+ if act.Action != "COMMENTED" {
+ continue
+ }
+ if act.CommentAction != "ADDED" {
+ continue
+ }
+ if act.Comment.State != "OPEN" {
+ continue
+ }
+ if act.Comment.Author.Name != username {
+ continue
+ }
+ if act.Comment.Severity == "BLOCKER" && act.Comment.Resolved {
+ continue
+ }
+ if act.Comment.Severity == "NORMAL" && act.CommentAnchor.Orphaned {
+ continue
+ }
+ comments = append(comments, bitBucketComment{
+ id: act.Comment.ID,
+ version: act.Comment.Version,
+ text: act.Comment.Text,
+ anchor: act.CommentAnchor,
+ })
}
- } else {
+ if acts.IsLastPage || acts.NextPageStart == start {
+ break
+ }
+ start = acts.NextPageStart
+ }
+
+ return comments, nil
+}
+
+func (bb bitBucketAPI) createComment(pr *bitBucketPR, comment BitBucketPendingComment) error {
+ payload, _ := json.Marshal(comment)
+ _, err := bb.request(
+ http.MethodPost,
+ fmt.Sprintf(
+ "/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments",
+ bb.project, bb.repo, pr.ID,
+ ),
+ bytes.NewReader(payload),
+ )
+ return err
+}
+
+func (bb bitBucketAPI) deleteComment(pr *bitBucketPR, commentID, version int) error {
+ _, err := bb.request(
+ http.MethodDelete,
+ fmt.Sprintf(
+ "/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments/%d?version=%d",
+ bb.project, bb.repo, pr.ID, commentID, version,
+ ),
+ nil,
+ )
+ return err
+}
+
+func NewBitBucketReporter(uri string, timeout time.Duration, token, project, repo string, maxComments int, gitCmd git.CommandRunner) BitBucketReporter {
+ slog.LogAttrs(context.Background(), slog.LevelInfo,
+ "Will report problems to BitBucket",
+ slog.String("uri", uri),
+ slog.String("timeout", output.HumanizeDuration(timeout)),
+ slog.String("project", project),
+ slog.String("repo", repo),
+ slog.Int("maxComments", maxComments),
+ )
+ return BitBucketReporter{
+ api: newBitBucketAPI(uri, timeout, token, project, repo),
+ gitCmd: gitCmd,
+ maxComments: maxComments,
+ }
+}
+
+type BitBucketReporter struct {
+ api *bitBucketAPI
+ gitCmd git.CommandRunner
+ maxComments int
+}
+
+func (bb BitBucketReporter) Describe() string {
+ return "BitBucket"
+}
+
+func (bb BitBucketReporter) Destinations(ctx context.Context) ([]any, error) {
+ headCommit, err := git.HeadCommit(bb.gitCmd)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get HEAD commit: %w", err)
+ }
+ slog.LogAttrs(ctx, slog.LevelInfo, "Got HEAD commit from git", slog.String("commit", headCommit))
+
+ headBranch, err := git.CurrentBranch(bb.gitCmd)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get current branch: %w", err)
+ }
+
+ pr, err := bb.api.findPullRequestForBranch(headBranch, headCommit)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get open pull requests from BitBucket: %w", err)
+ }
+
+ if pr == nil {
slog.LogAttrs(ctx, slog.LevelInfo,
- "No open pull request found, reporting problems using code insight annotations",
+ "No open pull request found",
slog.String("branch", headBranch),
slog.String("commit", headCommit),
)
+ return nil, nil
+ }
- if err = bb.api.deleteAnnotations(headCommit); err != nil {
- return fmt.Errorf("failed to delete existing BitBucket code insight annotations: %w", err)
- }
+ slog.LogAttrs(ctx, slog.LevelInfo,
+ "Found open pull request, reporting problems using comments",
+ slog.Int("id", pr.ID),
+ slog.String("srcBranch", pr.srcBranch),
+ slog.String("srcCommit", pr.srcHead),
+ slog.String("dstBranch", pr.dstBranch),
+ slog.String("dstCommit", pr.dstHead),
+ )
- if err = bb.api.createAnnotations(summary, headCommit); err != nil {
- return fmt.Errorf("failed to create BitBucket code insight annotations: %w", err)
- }
- }
+ return []any{pr}, nil
+}
+
+func (bb BitBucketReporter) Summary(_ context.Context, _ any, _ Summary, _ []PendingComment, _ []error) error {
+ return nil
+}
- if summary.HasFatalProblems() {
- return errors.New("fatal error(s) reported")
+func (bb BitBucketReporter) List(ctx context.Context, dst any) ([]ExistingComment, error) {
+ pr := dst.(*bitBucketPR)
+ comments, err := bb.api.getPullRequestComments(pr)
+ if err != nil {
+ return nil, err
}
+ slog.LogAttrs(ctx, slog.LevelInfo, "Got existing pull request comments from BitBucket", slog.Int("count", len(comments)))
- return nil
+ existing := make([]ExistingComment, 0, len(comments))
+ for _, c := range comments {
+ existing = append(existing, ExistingComment{
+ path: c.anchor.Path,
+ line: c.anchor.Line,
+ text: c.text,
+ meta: bitBucketCommentMeta{
+ id: c.id,
+ version: c.version,
+ },
+ })
+ }
+ return existing, nil
}
-// BitBucket only allows us to report annotations for modified lines.
-// If a high severity problem is detected on a non-modified line we move that annotation
-// to the first modified line.
-// Without this we could have a report that is marked as failed, but with no annotations
-// at all, which would make it more difficult to fix.
-// If we can't find any modified line to match with our report then we return 0,
-// which will create a file level annotation.
-func moveReportedLine(report Report) (reported, original int) {
- reported = -1
- original = -1
- // No changes data, nothing to move.
- if report.Changes == nil {
- return report.Problem.Lines.Last, report.Problem.Lines.Last
- }
- for pl := report.Problem.Lines.First; pl <= report.Problem.Lines.Last; pl++ {
- if original < 0 {
- original = pl
- }
- if report.Changes.Lines.HasAfter(pl) {
- original = pl
- reported = pl
- }
+func (bb BitBucketReporter) Create(ctx context.Context, dst any, p PendingComment) error {
+ pr := dst.(*bitBucketPR)
+
+ anchor := bitBucketPendingCommentAnchor{
+ Path: p.path,
+ Line: p.line,
+ DiffType: "EFFECTIVE",
+ LineType: "CONTEXT",
+ FileType: "FROM",
+ }
+
+ if p.anchor == checks.AnchorBefore {
+ anchor.LineType = "REMOVED"
+ } else if p.changedLines.HasAfter(p.line) {
+ anchor.LineType = "ADDED"
+ anchor.FileType = "TO"
}
- if reported < 0 {
- if bestLine := report.Changes.Lines.NearestAfter(report.Problem.Lines.First); bestLine > 0 {
- return bestLine, original
+ if anchor.FileType == "FROM" && p.anchor != checks.AnchorBefore {
+ if before := p.changedLines.BeforeForAfter(p.line); before != p.line {
+ anchor.Line = before
}
}
- if reported < 0 {
- reported = 0
+ var severity string
+ if strings.HasPrefix(p.text, ":stop_sign:") {
+ severity = "BLOCKER"
+ } else {
+ severity = "NORMAL"
+ }
+
+ slog.LogAttrs(ctx, slog.LevelDebug, "Creating BitBucket comment",
+ slog.String("path", anchor.Path),
+ slog.Int("line", anchor.Line),
+ slog.String("lineType", anchor.LineType),
+ slog.String("fileType", anchor.FileType),
+ slog.String("severity", severity),
+ )
+
+ return bb.api.createComment(pr, BitBucketPendingComment{
+ Text: p.text,
+ Severity: severity,
+ Anchor: anchor,
+ })
+}
+
+func (bb BitBucketReporter) Delete(ctx context.Context, dst any, e ExistingComment) error {
+ pr := dst.(*bitBucketPR)
+ meta := e.meta.(bitBucketCommentMeta)
+ slog.LogAttrs(ctx, slog.LevelDebug, "Deleting BitBucket comment",
+ slog.Int("id", meta.id),
+ slog.String("path", e.path),
+ slog.Int("line", e.line),
+ )
+ return bb.api.deleteComment(pr, meta.id, meta.version)
+}
+
+func (bb BitBucketReporter) CanCreate(done int) bool {
+ return done < bb.maxComments
+}
+
+func (bb BitBucketReporter) CanDelete(ExistingComment) bool {
+ return true
+}
+
+func (bb BitBucketReporter) IsEqual(_ any, existing ExistingComment, pending PendingComment) bool {
+ if existing.path != pending.path {
+ return false
+ }
+ if existing.line != pending.line {
+ return false
}
- return reported, original
+ return strings.Trim(existing.text, "\n") == strings.Trim(pending.text, "\n")
}
diff --git a/internal/reporter/bitbucket_api.go b/internal/reporter/bitbucket_api.go
deleted file mode 100644
index dc35b62d..00000000
--- a/internal/reporter/bitbucket_api.go
+++ /dev/null
@@ -1,917 +0,0 @@
-package reporter
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "slices"
- "strings"
- "time"
-
- "github.com/cloudflare/pint/internal/checks"
- "github.com/cloudflare/pint/internal/diags"
- "github.com/cloudflare/pint/internal/output"
-)
-
-type BitBucketReport struct {
- Reporter string `json:"reporter"`
- Title string `json:"title"`
- Result string `json:"result"`
- Details string `json:"details"`
- Link string `json:"link"`
- Data []BitBucketReportData `json:"data"`
-}
-
-type DataType string
-
-const (
- BooleanType DataType = "BOOLEAN"
- DateType DataType = "DATA"
- DurationType DataType = "DURATION"
- LinkType DataType = "LINK"
- NumberType DataType = "NUMBER"
- PercentageType DataType = "PERCENTAGE"
- TextType DataType = "TEXT"
-
- maxCommentLength = 32768
-)
-
-type BitBucketReportData struct {
- Value any `json:"value"`
- Title string `json:"title"`
- Type DataType `json:"type"`
-}
-
-type BitBucketAnnotation struct {
- Path string `json:"path"`
- Message string `json:"message"`
- Severity string `json:"severity"`
- Type string `json:"type"`
- Link string `json:"link"`
- Line int `json:"line"`
-}
-
-type BitBucketAnnotations struct {
- Annotations []BitBucketAnnotation `json:"annotations"`
-}
-
-type BitBucketRef struct {
- ID string `json:"id"`
- Commit string `json:"latestCommit"`
-}
-
-type BitBucketPullRequest struct {
- FromRef BitBucketRef `json:"fromRef"`
- ToRef BitBucketRef `json:"toRef"`
- ID int `json:"id"`
- Open bool `json:"open"`
-}
-
-type BitBucketPullRequests struct {
- Values []BitBucketPullRequest `json:"values"`
- Start int `json:"start"`
- NextPageStart int `json:"nextPageStart"`
- IsLastPage bool `json:"isLastPage"`
-}
-
-type bitBucketPR struct {
- srcBranch string
- srcHead string
- dstBranch string
- dstHead string
- ID int
-}
-
-type bitBucketPRChanges struct {
- pathModifiedLines map[string][]int
- pathLineMapping map[string]map[int]int
-}
-
-type BitBucketPath struct {
- ToString string `json:"toString"`
-}
-
-type BitBucketPullRequestChange struct {
- Path BitBucketPath `json:"path"`
-}
-
-type BitBucketPullRequestChanges struct {
- Values []BitBucketPullRequestChange `json:"values"`
- Start int `json:"start"`
- NextPageStart int `json:"nextPageStart"`
- IsLastPage bool `json:"isLastPage"`
-}
-
-type BitBucketDiffLine struct {
- Source int `json:"source"`
- Destination int `json:"destination"`
-}
-
-type BitBucketDiffSegment struct {
- Type string `json:"type"`
- Lines []BitBucketDiffLine `json:"lines"`
-}
-
-type BitBucketDiffHunk struct {
- Segments []BitBucketDiffSegment `json:"segments"`
-}
-
-type BitBucketFileDiff struct {
- Hunks []BitBucketDiffHunk `json:"hunks"`
-}
-
-type BitBucketFileDiffs struct {
- Diffs []BitBucketFileDiff `json:"diffs"`
-}
-
-type bitBucketComment struct {
- text string
- severity string
- anchor BitBucketCommentAnchor
- id int
- version int
- replies int
-}
-
-type BitBucketCommentAuthor struct {
- Name string `json:"name"`
-}
-
-type BitBucketPullRequestComment struct {
- State string `json:"state"`
- Author BitBucketCommentAuthor `json:"author"`
- Text string `json:"text"`
- Severity string `json:"severity"`
- Comments []BitBucketPullRequestComment `json:"comments"`
- ID int `json:"id"`
- Version int `json:"version"`
- Resolved bool `json:"threadResolved"`
-}
-
-type BitBucketCommentAnchor struct {
- LineType string `json:"lineType"`
- DiffType string `json:"diffType"`
- Path string `json:"path"`
- Line int `json:"line"`
- Orphaned bool `json:"orphaned"`
-}
-
-func (ba BitBucketCommentAnchor) isEqual(pa BitBucketPendingCommentAnchor) bool {
- if ba.Path != pa.Path {
- return false
- }
- if ba.Line != pa.Line {
- return false
- }
- if ba.LineType != pa.LineType {
- return false
- }
- if ba.DiffType != pa.DiffType {
- return false
- }
- return true
-}
-
-type BitBucketPullRequestActivity struct {
- Action string `json:"action"`
- CommentAction string `json:"commentAction"`
- CommentAnchor BitBucketCommentAnchor `json:"commentAnchor"`
- Comment BitBucketPullRequestComment `json:"comment"`
-}
-
-type BitBucketPullRequestActivities struct {
- Values []BitBucketPullRequestActivity `json:"values"`
- Start int `json:"start"`
- NextPageStart int `json:"nextPageStart"`
- IsLastPage bool `json:"isLastPage"`
-}
-
-type pendingComment struct {
- severity string
- text string
- path string
- line int
- anchor checks.Anchor
-}
-
-func (pc pendingComment) toBitBucketComment(changes *bitBucketPRChanges) BitBucketPendingComment {
- c := BitBucketPendingComment{
- Anchor: BitBucketPendingCommentAnchor{
- Path: pc.path,
- Line: pc.line,
- DiffType: "EFFECTIVE",
- LineType: "CONTEXT",
- FileType: "FROM",
- },
- Text: pc.text,
- Severity: pc.severity,
- }
-
- if pc.anchor == checks.AnchorBefore {
- c.Anchor.LineType = "REMOVED"
- } else if changes != nil {
- if lines, ok := changes.pathModifiedLines[pc.path]; ok && slices.Contains(lines, pc.line) {
- c.Anchor.LineType = "ADDED"
- c.Anchor.FileType = "TO"
- }
- if c.Anchor.FileType == "FROM" {
- if m, ok := changes.pathLineMapping[pc.path]; ok {
- if v, found := m[pc.line]; found {
- c.Anchor.Line = v
- }
- }
- }
- }
-
- return c
-}
-
-type BitBucketPendingCommentAnchor struct {
- Path string `json:"path,omitempty"`
- LineType string `json:"lineType,omitempty"`
- FileType string `json:"fileType,omitempty"`
- DiffType string `json:"diffType"`
- Line int `json:"line,omitempty"`
-}
-
-type BitBucketPendingComment struct {
- Text string `json:"text"`
- Severity string `json:"severity"`
- Anchor BitBucketPendingCommentAnchor `json:"anchor"`
-}
-
-type BitBucketCommentStateUpdate struct {
- State string `json:"state"`
- Version int `json:"version"`
-}
-
-type BitBucketCommentSeverityUpdate struct {
- Severity string `json:"severity"`
- Version int `json:"version"`
-}
-
-func newBitBucketAPI(pintVersion, uri string, timeout time.Duration, token, project, repo string, maxComments int, showDuplicates bool) *bitBucketAPI {
- return &bitBucketAPI{
- pintVersion: pintVersion,
- uri: uri,
- timeout: timeout,
- authToken: token,
- project: project,
- repo: repo,
- maxComments: maxComments,
- showDuplicates: showDuplicates,
- }
-}
-
-type bitBucketAPI struct {
- pintVersion string
- uri string
- authToken string
- project string
- repo string
- timeout time.Duration
- maxComments int
- showDuplicates bool
-}
-
-func (bb bitBucketAPI) request(method, path string, body io.Reader) ([]byte, error) {
- slog.LogAttrs(context.Background(), slog.LevelInfo, "Sending a request to BitBucket", slog.String("method", method), slog.String("path", path))
-
- if body != nil {
- payload, _ := io.ReadAll(body)
- slog.LogAttrs(context.Background(), slog.LevelDebug, "Request payload", slog.String("body", string(payload)))
- body = bytes.NewReader(payload)
- }
-
- req, err := http.NewRequest(method, bb.uri+path, body)
- if err != nil {
- return nil, err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+bb.authToken)
-
- netClient := &http.Client{
- Timeout: bb.timeout,
- }
-
- resp, err := netClient.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- data, err := io.ReadAll(resp.Body)
- if err != nil {
- return data, err
- }
-
- slog.LogAttrs(context.Background(), slog.LevelInfo, "BitBucket request completed", slog.Int("status", resp.StatusCode))
- slog.LogAttrs(context.Background(), slog.LevelDebug, "BitBucket response body", slog.Int("code", resp.StatusCode), slog.String("body", string(data)))
- if resp.StatusCode >= 300 {
- slog.LogAttrs(context.Background(), slog.LevelError,
- "Got a non 2xx response",
- slog.String("body", string(data)),
- slog.String("path", path),
- slog.Int("code", resp.StatusCode),
- )
- return data, fmt.Errorf("%s request failed", method)
- }
-
- return data, err
-}
-
-func (bb bitBucketAPI) whoami() (string, error) {
- resp, err := bb.request(http.MethodGet, "/plugins/servlet/applinks/whoami", nil)
- if err != nil {
- return "", err
- }
- return strings.TrimSuffix(string(resp), "\n"), nil
-}
-
-func (bb bitBucketAPI) deleteReport(commit string) error {
- _, err := bb.request(
- http.MethodDelete,
- fmt.Sprintf("/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/pint", bb.project, bb.repo, commit),
- nil,
- )
- return err
-}
-
-func (bb bitBucketAPI) createReport(summary Summary, commit string) error {
- result := "PASS"
- var reportedProblems int
- for _, report := range summary.reports {
- reportedProblems++
- if report.Problem.Severity >= checks.Bug {
- result = "FAIL"
- }
- }
-
- payload, _ := json.Marshal(BitBucketReport{
- Title: "pint " + bb.pintVersion,
- Result: result,
- Reporter: "Prometheus rule linter",
- Details: BitBucketDescription,
- Link: "https://cloudflare.github.io/pint/",
- Data: []BitBucketReportData{
- {Title: "Number of rules parsed", Type: NumberType, Value: summary.TotalEntries},
- {Title: "Number of rules checked", Type: NumberType, Value: summary.CheckedEntries},
- {Title: "Number of problems found", Type: NumberType, Value: reportedProblems},
- {Title: "Number of offline checks", Type: NumberType, Value: summary.OfflineChecks},
- {Title: "Number of online checks", Type: NumberType, Value: summary.OnlineChecks},
- {Title: "Checks duration", Type: DurationType, Value: summary.Duration.Milliseconds()},
- },
- })
-
- _, err := bb.request(
- http.MethodPut,
- fmt.Sprintf("/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/pint", bb.project, bb.repo, commit),
- bytes.NewReader(payload),
- )
- return err
-}
-
-func (bb bitBucketAPI) createAnnotations(summary Summary, commit string) error {
- annotations := make([]BitBucketAnnotation, 0, len(summary.reports))
- for _, report := range summary.reports {
- ann := reportToAnnotation(report)
- if !report.Changes.Lines.HasAfter(ann.Line) {
- slog.LogAttrs(context.Background(), slog.LevelWarn, "Annotation for unmodified line, skipping", slog.String("path", ann.Path), slog.Int("line", ann.Line))
- continue
- }
- annotations = append(annotations, ann)
- }
-
- if len(annotations) == 0 {
- return nil
- }
-
- payload, _ := json.Marshal(BitBucketAnnotations{Annotations: annotations})
- _, err := bb.request(
- http.MethodPost,
- fmt.Sprintf("/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/pint/annotations", bb.project, bb.repo, commit),
- bytes.NewReader(payload),
- )
- return err
-}
-
-func (bb bitBucketAPI) deleteAnnotations(commit string) error {
- _, err := bb.request(
- http.MethodDelete,
- fmt.Sprintf("/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/pint/annotations", bb.project, bb.repo, commit),
- nil,
- )
- return err
-}
-
-func (bb bitBucketAPI) findPullRequestForBranch(branch, commit string) (*bitBucketPR, error) {
- var start int
- for {
- resp, err := bb.request(
- http.MethodGet,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/commits/%s/pull-requests?start=%d", bb.project, bb.repo, commit, start),
- nil,
- )
- if err != nil {
- return nil, err
- }
-
- var prs BitBucketPullRequests
- if err = json.Unmarshal(resp, &prs); err != nil {
- return nil, err
- }
-
- for _, pr := range prs.Values {
- if !pr.Open {
- continue
- }
- srcBranch := strings.TrimPrefix(pr.FromRef.ID, "refs/heads/")
- dstBranch := strings.TrimPrefix(pr.ToRef.ID, "refs/heads/")
- if srcBranch == branch {
- return &bitBucketPR{
- ID: pr.ID,
- srcBranch: srcBranch,
- srcHead: pr.FromRef.Commit,
- dstBranch: dstBranch,
- dstHead: pr.ToRef.Commit,
- }, nil
- }
- }
-
- if prs.IsLastPage || prs.NextPageStart == start {
- break
- }
- start = prs.NextPageStart
- }
-
- return nil, nil
-}
-
-func (bb bitBucketAPI) getPullRequestChanges(pr *bitBucketPR) (*bitBucketPRChanges, error) {
- prChanges := bitBucketPRChanges{
- pathModifiedLines: map[string][]int{},
- pathLineMapping: map[string]map[int]int{},
- }
-
- var start int
- for {
- resp, err := bb.request(
- http.MethodGet,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/changes?start=%d", bb.project, bb.repo, pr.ID, start),
- nil,
- )
- if err != nil {
- return nil, err
- }
-
- var changes BitBucketPullRequestChanges
- if err = json.Unmarshal(resp, &changes); err != nil {
- return nil, err
- }
-
- for _, ch := range changes.Values {
- modifiedLines, lineMap, err := bb.getFileDiff(pr, ch.Path.ToString)
- if err != nil {
- return nil, err
- }
- prChanges.pathModifiedLines[ch.Path.ToString] = modifiedLines
- prChanges.pathLineMapping[ch.Path.ToString] = lineMap
- }
-
- if changes.IsLastPage || changes.NextPageStart == start {
- break
- }
- start = changes.NextPageStart
- }
-
- return &prChanges, nil
-}
-
-func (bb bitBucketAPI) getFileDiff(pr *bitBucketPR, path string) ([]int, map[int]int, error) {
- resp, err := bb.request(
- http.MethodGet,
- fmt.Sprintf(
- "/rest/api/latest/projects/%s/repos/%s/commits/%s/diff/%s?contextLines=10000&since=%s&whitespace=show&withComments=false",
- bb.project, bb.repo,
- pr.srcHead,
- path,
- pr.dstHead,
- ),
- nil,
- )
- if err != nil {
- return nil, nil, err
- }
-
- var fileDiffs BitBucketFileDiffs
- if err = json.Unmarshal(resp, &fileDiffs); err != nil {
- return nil, nil, err
- }
-
- modifiedLines := []int{}
- lineMap := map[int]int{}
- for _, diff := range fileDiffs.Diffs {
- for _, hunk := range diff.Hunks {
- for _, seg := range hunk.Segments {
- for _, line := range seg.Lines {
- if seg.Type == "ADDED" {
- modifiedLines = append(modifiedLines, line.Destination)
- }
- if seg.Type == "CONTEXT" || seg.Type == "ADDED" {
- lineMap[line.Destination] = line.Source
- }
- }
- }
- }
- }
-
- return modifiedLines, lineMap, nil
-}
-
-func (bb bitBucketAPI) getPullRequestComments(pr *bitBucketPR) ([]bitBucketComment, error) {
- username, err := bb.whoami()
- if err != nil {
- return nil, err
- }
-
- comments := []bitBucketComment{}
-
- var start int
- for {
- resp, err := bb.request(
- http.MethodGet,
- fmt.Sprintf(
- "/rest/api/latest/projects/%s/repos/%s/pull-requests/%d/activities?start=%d",
- bb.project, bb.repo,
- pr.ID,
- start,
- ),
- nil,
- )
- if err != nil {
- return nil, err
- }
-
- var acts BitBucketPullRequestActivities
- if err = json.Unmarshal(resp, &acts); err != nil {
- return nil, err
- }
-
- for _, act := range acts.Values {
- if act.Action != "COMMENTED" {
- continue
- }
- if act.CommentAction != "ADDED" {
- continue
- }
- if act.Comment.State != "OPEN" {
- continue
- }
- if act.Comment.Author.Name != username {
- continue
- }
- if act.Comment.Severity == "BLOCKER" && act.Comment.Resolved {
- continue
- }
- if act.Comment.Severity == "NORMAL" && act.CommentAnchor.Orphaned {
- continue
- }
- comments = append(comments, bitBucketComment{
- id: act.Comment.ID,
- version: act.Comment.Version,
- text: act.Comment.Text,
- anchor: act.CommentAnchor,
- severity: act.Comment.Severity,
- replies: len(act.Comment.Comments),
- })
- }
-
- if acts.IsLastPage || acts.NextPageStart == start {
- break
- }
- start = acts.NextPageStart
- }
-
- return comments, nil
-}
-
-func (bb bitBucketAPI) makeComments(summary Summary, changes *bitBucketPRChanges) []BitBucketPendingComment {
- var buf strings.Builder
- var content string
- var err error
- comments := []BitBucketPendingComment{}
- for _, reports := range dedupReports(summary.reports, bb.showDuplicates) {
- if _, ok := changes.pathModifiedLines[reports[0].Path.SymlinkTarget]; !ok {
- continue
- }
-
- if reports[0].Problem.Anchor == checks.AnchorAfter {
- content, err = readFile(reports[0].Path.Name)
- if err != nil {
- content = ""
- }
- }
-
- mergeDetails := identicalDetails(reports)
-
- buf.Reset()
-
- buf.WriteString(problemIcon(reports[0].Problem.Severity))
- buf.WriteString(" **")
- buf.WriteString(reports[0].Problem.Severity.String())
- buf.WriteString("** reported by [pint](https://cloudflare.github.io/pint/) **")
- buf.WriteString(reports[0].Problem.Reporter)
- buf.WriteString("** check.\n\n")
- for _, report := range reports {
- buf.WriteString("------\n\n")
- buf.WriteString(report.Problem.Summary)
- buf.WriteString("\n\n")
- if len(report.Problem.Diagnostics) > 0 && content != "" {
- for _, diag := range report.Problem.Diagnostics {
- buf.WriteString("```yaml\n")
- buf.WriteString(diags.InjectDiagnostics(
- content,
- []diags.Diagnostic{
- {
- Message: "",
- Pos: diag.Pos,
- FirstColumn: diag.FirstColumn,
- LastColumn: diag.LastColumn,
- Kind: diag.Kind,
- },
- },
- output.None,
- ))
- buf.WriteString("```\n\n")
- buf.WriteString(diag.Message)
- buf.WriteString("\n\n")
- }
- }
- if !mergeDetails && report.Problem.Details != "" {
- buf.WriteString(report.Problem.Details)
- buf.WriteString("\n\n")
- }
- if report.Path.SymlinkTarget != report.Path.Name {
- buf.WriteString(":leftwards_arrow_with_hook: This problem was detected on a symlinked file ")
- buf.WriteRune('`')
- buf.WriteString(report.Path.Name)
- buf.WriteString("`.\n\n")
- }
- }
- if mergeDetails && reports[0].Problem.Details != "" {
- buf.WriteString("------\n\n")
- buf.WriteString(reports[0].Problem.Details)
- buf.WriteString("\n\n")
- }
- buf.WriteString("------\n\n")
- buf.WriteString(":information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/")
- buf.WriteString(reports[0].Problem.Reporter)
- buf.WriteString(".html).\n")
-
- var severity string
- // nolint:exhaustive
- switch reports[0].Problem.Severity {
- case checks.Bug, checks.Fatal:
- severity = "BLOCKER"
- default:
- severity = "NORMAL"
- }
-
- var text string
- // BitBucket has a max comment length limit. If we hit it then truncate the comment.
- if buf.Len() > maxCommentLength {
- text = buf.String()[:maxCommentLength-4] + " ..."
- } else {
- text = buf.String()
- }
-
- pending := pendingComment{
- severity: severity,
- path: reports[0].Path.SymlinkTarget,
- line: reports[0].Problem.Lines.Last,
- text: text,
- anchor: reports[0].Problem.Anchor,
- }
- comments = append(comments, pending.toBitBucketComment(changes))
- }
- return comments
-}
-
-func (bb bitBucketAPI) limitComments(src []BitBucketPendingComment) []BitBucketPendingComment {
- if len(src) <= bb.maxComments {
- return src
- }
- comments := src[:bb.maxComments]
- comments = append(comments, BitBucketPendingComment{
- Text: fmt.Sprintf(`This pint run would create %d comment(s), which is more than %d limit configured for pint.
-%d comments were skipped and won't be visible on this PR.`, len(src), bb.maxComments, len(src)-bb.maxComments),
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{ // nolint: exhaustruct
- DiffType: "EFFECTIVE",
- },
- })
- return comments
-}
-
-func (bb bitBucketAPI) pruneComments(pr *bitBucketPR, currentComments []bitBucketComment, pendingComments []BitBucketPendingComment) {
- for _, cur := range currentComments {
- slog.LogAttrs(context.Background(), slog.LevelDebug,
- "Existing comment",
- slog.Int("id", cur.id),
- slog.Int("version", cur.version),
- slog.String("severity", cur.severity),
- slog.String("path", cur.anchor.Path),
- slog.Int("line", cur.anchor.Line),
- slog.String("diffType", cur.anchor.DiffType),
- slog.String("lineType", cur.anchor.LineType),
- slog.Bool("orphaned", cur.anchor.Orphaned),
- slog.Int("replies", cur.replies),
- )
- var keep bool
- for _, pend := range pendingComments {
- if cur.anchor.isEqual(pend.Anchor) && cur.text == pend.Text {
- keep = true
- break
- }
- if cur.anchor.DiffType == "COMMIT" {
- keep = false
- }
- }
- if !keep {
- switch {
- case cur.replies == 0:
- bb.deleteComment(pr, cur)
- case cur.severity == "BLOCKER":
- bb.resolveTask(pr, cur)
- default:
- bb.updateSeverity(pr, cur, "BLOCKER")
- bb.resolveTask(pr, cur)
- }
- }
- }
-}
-
-func (bb bitBucketAPI) deleteComment(pr *bitBucketPR, cur bitBucketComment) {
- slog.LogAttrs(context.Background(), slog.LevelDebug,
- "Deleting stale comment",
- slog.Int("id", cur.id),
- slog.String("path", cur.anchor.Path),
- slog.Int("line", cur.anchor.Line),
- )
- _, err := bb.request(
- http.MethodDelete,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments/%d?version=%d",
- bb.project, bb.repo,
- pr.ID,
- cur.id, cur.version,
- ),
- nil,
- )
- if err != nil {
- slog.LogAttrs(context.Background(), slog.LevelError,
- "Failed to delete stale BitBucket pull request comment",
- slog.Int("id", cur.id),
- slog.Any("err", err),
- )
- }
-}
-
-func (bb bitBucketAPI) resolveTask(pr *bitBucketPR, cur bitBucketComment) {
- slog.LogAttrs(context.Background(), slog.LevelDebug,
- "Resolving stale blocker comment",
- slog.Int("id", cur.id),
- slog.String("path", cur.anchor.Path),
- slog.Int("line", cur.anchor.Line),
- )
- payload, _ := json.Marshal(BitBucketCommentStateUpdate{
- State: "RESOLVED",
- Version: cur.version,
- })
- _, err := bb.request(
- http.MethodPut,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments/%d",
- bb.project, bb.repo,
- pr.ID,
- cur.id,
- ),
- bytes.NewReader(payload),
- )
- if err != nil {
- slog.LogAttrs(context.Background(), slog.LevelError,
- "Failed to resolve stale blocker BitBucket pull request comment",
- slog.Int("id", cur.id),
- slog.Any("err", err),
- )
- }
-}
-
-func (bb bitBucketAPI) updateSeverity(pr *bitBucketPR, cur bitBucketComment, severity string) {
- slog.LogAttrs(context.Background(), slog.LevelDebug,
- "Updating comment severity",
- slog.Int("id", cur.id),
- slog.String("path", cur.anchor.Path),
- slog.Int("line", cur.anchor.Line),
- slog.String("from", cur.severity),
- slog.String("to", severity),
- )
- payload, _ := json.Marshal(BitBucketCommentSeverityUpdate{
- Severity: severity,
- Version: cur.version,
- })
- _, err := bb.request(
- http.MethodPut,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments/%d",
- bb.project, bb.repo,
- pr.ID,
- cur.id,
- ),
- bytes.NewReader(payload),
- )
- if err != nil {
- slog.LogAttrs(context.Background(), slog.LevelError,
- "Failed to update BitBucket pull request comment severity",
- slog.Int("id", cur.id),
- slog.Any("err", err),
- )
- }
-}
-
-func (bb bitBucketAPI) addComments(pr *bitBucketPR, currentComments []bitBucketComment, pendingComments []BitBucketPendingComment) error {
- var added int
- for _, pend := range pendingComments {
- add := true
- for _, cur := range currentComments {
- if cur.anchor.isEqual(pend.Anchor) && cur.text == pend.Text {
- add = false
- }
- if cur.anchor.DiffType == "COMMIT" {
- add = true
- }
- }
- if add {
- slog.LogAttrs(context.Background(), slog.LevelDebug,
- "Adding missing comment",
- slog.String("path", pend.Anchor.Path),
- slog.Int("line", pend.Anchor.Line),
- slog.String("diffType", pend.Anchor.DiffType),
- slog.String("lineType", pend.Anchor.LineType),
- slog.String("fileType", pend.Anchor.FileType),
- )
- payload, _ := json.Marshal(pend)
- _, err := bb.request(
- http.MethodPost,
- fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments",
- bb.project, bb.repo,
- pr.ID,
- ),
- bytes.NewReader(payload),
- )
- if err != nil {
- return err
- }
- added++
- }
- }
- slog.LogAttrs(context.Background(), slog.LevelInfo, "Added pull request comments to BitBucket", slog.Int("count", added))
- return nil
-}
-
-func reportToAnnotation(report Report) BitBucketAnnotation {
- var msgPrefix, severity, atype string
- reportLine, srcLine := moveReportedLine(report)
- if reportLine != srcLine {
- msgPrefix = fmt.Sprintf("Problem reported on unmodified line %d, annotation moved here: ", srcLine)
- }
- if report.Path.SymlinkTarget != report.Path.Name {
- if msgPrefix == "" {
- msgPrefix = fmt.Sprintf("Problem detected on symlinked file %s: ", report.Path.Name)
- } else {
- msgPrefix = fmt.Sprintf("Problem detected on symlinked file %s. %s", report.Path.Name, msgPrefix)
- }
- }
-
- switch report.Problem.Severity {
- case checks.Fatal:
- severity = "HIGH"
- atype = "BUG"
- case checks.Bug:
- severity = "MEDIUM"
- atype = "BUG"
- case checks.Warning, checks.Information:
- severity = "LOW"
- atype = "CODE_SMELL"
- }
-
- return BitBucketAnnotation{
- Path: report.Path.SymlinkTarget,
- Line: reportLine,
- Message: fmt.Sprintf("%s%s: %s", msgPrefix, report.Problem.Reporter, report.Problem.Summary),
- Severity: severity,
- Type: atype,
- Link: fmt.Sprintf("https://cloudflare.github.io/pint/checks/%s.html", report.Problem.Reporter),
- }
-}
diff --git a/internal/reporter/bitbucket_api_test.go b/internal/reporter/bitbucket_api_test.go
deleted file mode 100644
index bb6d4ab6..00000000
--- a/internal/reporter/bitbucket_api_test.go
+++ /dev/null
@@ -1,1300 +0,0 @@
-package reporter
-
-import (
- "bytes"
- "encoding/json"
- "log/slog"
- "net/http"
- "testing"
- "time"
-
- "github.com/neilotoole/slogt"
- "github.com/stretchr/testify/require"
- "go.nhat.io/httpmock"
-
- "github.com/cloudflare/pint/internal/checks"
- "github.com/cloudflare/pint/internal/diags"
- "github.com/cloudflare/pint/internal/discovery"
- "github.com/cloudflare/pint/internal/git"
-)
-
-func TestBitBucketCommentAnchorIsEqual(t *testing.T) {
- type testCaseT struct {
- description string
- pending BitBucketPendingCommentAnchor
- anchor BitBucketCommentAnchor
- expected bool
- }
-
- testCases := []testCaseT{
- {
- description: "all fields match",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- pending: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- expected: true,
- },
- {
- description: "path mismatch",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- pending: BitBucketPendingCommentAnchor{
- Path: "bar.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- expected: false,
- },
- {
- description: "line mismatch",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- pending: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 20,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- expected: false,
- },
- {
- description: "lineType mismatch",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- pending: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "CONTEXT",
- DiffType: "EFFECTIVE",
- },
- expected: false,
- },
- {
- description: "diffType mismatch",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- pending: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "RANGE",
- },
- expected: false,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.description, func(t *testing.T) {
- result := tc.anchor.isEqual(tc.pending)
- require.Equal(t, tc.expected, result)
- })
- }
-}
-
-func TestPendingCommentToBitBucketComment(t *testing.T) {
- type testCaseT struct {
- changes *bitBucketPRChanges
- description string
- output BitBucketPendingComment
- input pendingComment
- }
-
- testCases := []testCaseT{
- {
- description: "nil changes",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 5,
- DiffType: "EFFECTIVE",
- LineType: "CONTEXT",
- FileType: "FROM",
- },
- },
- changes: nil,
- },
- {
- description: "path not found in changes",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 5,
- DiffType: "EFFECTIVE",
- LineType: "CONTEXT",
- FileType: "FROM",
- },
- },
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{"bar.yaml": {1, 2, 3}},
- pathLineMapping: map[string]map[int]int{"bar.yaml": {1: 1, 2: 5, 3: 3}},
- },
- },
- {
- description: "path found in changes",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 5,
- DiffType: "EFFECTIVE",
- LineType: "ADDED",
- FileType: "TO",
- },
- },
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{"foo.yaml": {1, 3, 5}},
- pathLineMapping: map[string]map[int]int{"foo.yaml": {1: 1, 3: 3, 5: 4}},
- },
- },
- {
- description: "anchor before sets REMOVED lineType",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- anchor: checks.AnchorBefore,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 5,
- DiffType: "EFFECTIVE",
- LineType: "REMOVED",
- FileType: "FROM",
- },
- },
- changes: nil,
- },
- {
- description: "line not modified uses line mapping",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- DiffType: "EFFECTIVE",
- LineType: "CONTEXT",
- FileType: "FROM",
- },
- },
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{"foo.yaml": {1, 3}},
- pathLineMapping: map[string]map[int]int{"foo.yaml": {5: 10}},
- },
- },
- {
- description: "line not in mapping keeps original line",
- input: pendingComment{
- severity: "NORMAL",
- text: "this is text",
- path: "foo.yaml",
- line: 5,
- },
- output: BitBucketPendingComment{
- Text: "this is text",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 5,
- DiffType: "EFFECTIVE",
- LineType: "CONTEXT",
- FileType: "FROM",
- },
- },
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{"foo.yaml": {1, 3}},
- pathLineMapping: map[string]map[int]int{"foo.yaml": {1: 1, 3: 3}},
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.description, func(t *testing.T) {
- slog.SetDefault(slogt.New(t))
- out := tc.input.toBitBucketComment(tc.changes)
- require.Equal(t, tc.output, out, "pendingComment.toBitBucketComment() returned wrong BitBucketPendingComment")
- })
- }
-}
-
-func TestReportToAnnotation(t *testing.T) {
- type testCaseT struct {
- description string
- output BitBucketAnnotation
- input Report
- }
-
- testCases := []testCaseT{
- {
- description: "fatal report on modified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Details: "mock details",
- Severity: checks.Fatal,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "mock: report text",
- Severity: "HIGH",
- Type: "BUG",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "bug report on modified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Bug,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "mock: report text",
- Severity: "MEDIUM",
- Type: "BUG",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "warning report on modified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Warning,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "mock: report text",
- Severity: "LOW",
- Type: "CODE_SMELL",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "information report on modified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Information,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "mock: report text",
- Severity: "LOW",
- Type: "CODE_SMELL",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "fatal report on symlinked file",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "bar.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Fatal,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "Problem detected on symlinked file bar.yaml: mock: report text",
- Severity: "HIGH",
- Type: "BUG",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "fatal report on symlinked file on unmodified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "bar.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 7,
- Last: 7,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Fatal,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 6,
- Message: "Problem detected on symlinked file bar.yaml. Problem reported on unmodified line 7, annotation moved here: mock: report text",
- Severity: "HIGH",
- Type: "BUG",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- description: "information report on unmodified line",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 4, Modified: true},
- {Before: 0, After: 5, Modified: true},
- {Before: 0, After: 6, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Information,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 4,
- Message: "Problem reported on unmodified line 1, annotation moved here: mock: report text",
- Severity: "LOW",
- Type: "CODE_SMELL",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- {
- // Verify that nil Changes doesn't panic and returns
- // the problem line as-is.
- description: "nil changes",
- input: Report{
- Path: discovery.Path{
- SymlinkTarget: "foo.yaml",
- Name: "foo.yaml",
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 5,
- Last: 5,
- },
- Reporter: "mock",
- Summary: "report text",
- Severity: checks.Warning,
- },
- },
- output: BitBucketAnnotation{
- Path: "foo.yaml",
- Line: 5,
- Message: "mock: report text",
- Severity: "LOW",
- Type: "CODE_SMELL",
- Link: "https://cloudflare.github.io/pint/checks/mock.html",
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.description, func(t *testing.T) {
- slog.SetDefault(slogt.New(t))
- out := reportToAnnotation(tc.input)
- require.Equal(t, tc.output, out, "reportToAnnotation() returned wrong BitBucketAnnotation")
- })
- }
-}
-
-func TestBitBucketAPIRequest(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- t.Run("successful request with body", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectPost("/test").
- WithHeader("Content-Type", "application/json").
- WithHeader("Authorization", "Bearer test-token").
- Return(`{"result": "ok"}`).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- }
-
- body := bytes.NewReader([]byte(`{"key": "value"}`))
- resp, err := bb.request(http.MethodPost, "/test", body)
- require.NoError(t, err)
- require.JSONEq(t, `{"result": "ok"}`, string(resp))
- })
-
- t.Run("request with invalid URL", func(t *testing.T) {
- bb := bitBucketAPI{
- uri: "://invalid-url",
- authToken: "test-token",
- timeout: time.Second,
- }
-
- _, err := bb.request(http.MethodGet, "/test", nil)
- require.Error(t, err)
- })
-
- t.Run("non-2xx response returns error", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/test").
- ReturnCode(http.StatusBadRequest).
- Return("Bad Request").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- }
-
- _, err := bb.request(http.MethodGet, "/test", nil)
- require.Error(t, err)
- require.Equal(t, "GET request failed", err.Error())
- })
-}
-
-func TestBitBucketAPIPruneComments(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- t.Run("keeps matching comment", func(t *testing.T) {
- // No requests should be made when comment matches.
- srv := httpmock.New(func(_ *httpmock.Server) {})(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- version: 1,
- text: "test comment",
- severity: "NORMAL",
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{
- {
- Text: "test comment",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
-
- bb.pruneComments(pr, currentComments, pendingComments)
- require.Empty(t, srv.Requests, "no requests should be made when comment matches")
- })
-
- t.Run("deletes comment with no replies", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1?version=1").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- version: 1,
- text: "old comment",
- severity: "NORMAL",
- replies: 0,
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{}
-
- bb.pruneComments(pr, currentComments, pendingComments)
- require.Len(t, srv.Requests, 1, "expected delete to be called")
- require.Equal(t, http.MethodDelete, srv.Requests[0].Method())
- })
-
- t.Run("resolves blocker comment with replies", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectPut("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- version: 1,
- text: "old comment",
- severity: "BLOCKER",
- replies: 1,
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{}
-
- bb.pruneComments(pr, currentComments, pendingComments)
- require.Len(t, srv.Requests, 1, "expected resolve to be called")
- require.Equal(t, http.MethodPut, srv.Requests[0].Method())
- })
-
- t.Run("updates severity and resolves normal comment with replies", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- // First PUT: severity update, second PUT: resolve.
- s.ExpectPut("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1").
- Once()
- s.ExpectPut("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- version: 1,
- text: "old comment",
- severity: "NORMAL",
- replies: 1,
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{}
-
- bb.pruneComments(pr, currentComments, pendingComments)
- require.Len(t, srv.Requests, 2, "expected severity update and resolve to be called")
- require.Equal(t, http.MethodPut, srv.Requests[0].Method())
- require.Equal(t, http.MethodPut, srv.Requests[1].Method())
- })
-
- t.Run("handles COMMIT diffType", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1?version=1").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- version: 1,
- text: "commit comment",
- severity: "NORMAL",
- replies: 0,
- anchor: BitBucketCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "COMMIT",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{
- {
- Text: "different comment",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "foo.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
-
- bb.pruneComments(pr, currentComments, pendingComments)
- require.Len(t, srv.Requests, 1, "expected delete to be called for COMMIT diffType")
- require.Equal(t, http.MethodDelete, srv.Requests[0].Method())
- })
-}
-
-func TestBitBucketAPIGetPullRequestComments(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- t.Run("filters comments correctly", func(t *testing.T) {
- activities := BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []BitBucketPullRequestActivity{
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 1,
- Version: 1,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- Text: "valid comment",
- },
- CommentAnchor: BitBucketCommentAnchor{Path: "foo.yaml", Line: 10},
- },
- {
- Action: "APPROVED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 2,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "EDITED",
- Comment: BitBucketPullRequestComment{
- ID: 3,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 4,
- State: "RESOLVED",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 5,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "otheruser"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 6,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- Severity: "BLOCKER",
- Resolved: true,
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 7,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- Severity: "NORMAL",
- },
- CommentAnchor: BitBucketCommentAnchor{Orphaned: true},
- },
- },
- }
- activitiesJSON, _ := json.Marshal(activities)
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("testuser").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
- ReturnHeader("Content-Type", "application/json").
- Return(string(activitiesJSON)).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- comments, err := bb.getPullRequestComments(pr)
- require.NoError(t, err)
- require.Len(t, comments, 1)
- require.Equal(t, 1, comments[0].id)
- require.Equal(t, "valid comment", comments[0].text)
- })
-
- t.Run("handles pagination", func(t *testing.T) {
- page1, _ := json.Marshal(BitBucketPullRequestActivities{
- IsLastPage: false,
- NextPageStart: 1,
- Values: []BitBucketPullRequestActivity{
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 1,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- Text: "comment 1",
- },
- CommentAnchor: BitBucketCommentAnchor{Path: "foo.yaml"},
- },
- },
- })
- page2, _ := json.Marshal(BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []BitBucketPullRequestActivity{
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- Comment: BitBucketPullRequestComment{
- ID: 2,
- State: "OPEN",
- Author: BitBucketCommentAuthor{Name: "testuser"},
- Text: "comment 2",
- },
- CommentAnchor: BitBucketCommentAnchor{Path: "bar.yaml"},
- },
- },
- })
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("testuser").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page1)).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=1").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page2)).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- comments, err := bb.getPullRequestComments(pr)
- require.NoError(t, err)
- require.Len(t, comments, 2)
- })
-
- t.Run("returns error on invalid JSON", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("testuser").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
- Return("invalid json").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- _, err := bb.getPullRequestComments(pr)
- require.Error(t, err)
- })
-}
-
-func TestFindPullRequestForBranchErrors(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- t.Run("returns error on invalid JSON", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=0").
- Return("invalid json").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- _, err := bb.findPullRequestForBranch("feature", "commit123")
- require.Error(t, err)
- })
-
- t.Run("paginates through results", func(t *testing.T) {
- page1, _ := json.Marshal(BitBucketPullRequests{
- IsLastPage: false,
- NextPageStart: 1,
- Values: []BitBucketPullRequest{
- {ID: 1, Open: true, FromRef: BitBucketRef{ID: "refs/heads/other"}, ToRef: BitBucketRef{ID: "refs/heads/main"}},
- },
- })
- page2, _ := json.Marshal(BitBucketPullRequests{
- IsLastPage: true,
- Values: []BitBucketPullRequest{
- {ID: 2, Open: true, FromRef: BitBucketRef{ID: "refs/heads/feature", Commit: "abc123"}, ToRef: BitBucketRef{ID: "refs/heads/main", Commit: "def456"}},
- },
- })
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=0").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page1)).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=1").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page2)).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr, err := bb.findPullRequestForBranch("feature", "commit123")
- require.NoError(t, err)
- require.NotNil(t, pr)
- require.Equal(t, 2, pr.ID)
- })
-}
-
-func TestGetPullRequestChangesErrors(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- t.Run("returns error on invalid JSON", func(t *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/changes?start=0").
- Return("invalid json").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- _, err := bb.getPullRequestChanges(pr)
- require.Error(t, err)
- })
-
- t.Run("returns error on getFileDiff failure", func(t *testing.T) {
- changesJSON, _ := json.Marshal(BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []BitBucketPullRequestChange{{Path: BitBucketPath{ToString: "file.yaml"}}},
- })
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/changes?start=0").
- ReturnHeader("Content-Type", "application/json").
- Return(string(changesJSON)).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/abc/diff/file.yaml?contextLines=10000&since=def&whitespace=show&withComments=false").
- ReturnCode(http.StatusInternalServerError).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1, srcHead: "abc", dstHead: "def"}
- _, err := bb.getPullRequestChanges(pr)
- require.Error(t, err)
- })
-
- t.Run("paginates through results", func(t *testing.T) {
- page1, _ := json.Marshal(BitBucketPullRequestChanges{
- IsLastPage: false,
- NextPageStart: 1,
- Values: []BitBucketPullRequestChange{},
- })
- page2, _ := json.Marshal(BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []BitBucketPullRequestChange{},
- })
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/changes?start=0").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page1)).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/changes?start=1").
- ReturnHeader("Content-Type", "application/json").
- Return(string(page2)).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1, srcHead: "abc", dstHead: "def"}
- _, err := bb.getPullRequestChanges(pr)
- require.NoError(t, err)
- })
-}
-
-func TestGetFileDiffErrors(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/abc/diff/file.yaml?contextLines=10000&since=def&whitespace=show&withComments=false").
- Return("invalid json").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1, srcHead: "abc", dstHead: "def"}
- _, _, err := bb.getFileDiff(pr, "file.yaml")
- require.Error(t, err)
-}
-
-func TestBitBucketAPIErrorHandling(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- type testCaseT struct {
- run func(bb bitBucketAPI, pr *bitBucketPR)
- name string
- }
-
- testCases := []testCaseT{
- {
- name: "updateSeverity logs error on failure",
- run: func(bb bitBucketAPI, pr *bitBucketPR) {
- cur := bitBucketComment{id: 1, version: 1, anchor: BitBucketCommentAnchor{Path: "file.yaml", Line: 10}}
- bb.updateSeverity(pr, cur, "BLOCKER")
- },
- },
- {
- name: "resolveTask logs error on failure",
- run: func(bb bitBucketAPI, pr *bitBucketPR) {
- cur := bitBucketComment{id: 1, version: 1}
- bb.resolveTask(pr, cur)
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(_ *testing.T) {
- srv := httpmock.New(func(s *httpmock.Server) {
- s.ExpectPut("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/1").
- ReturnCode(http.StatusInternalServerError).
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- tc.run(bb, pr)
- })
- }
-}
-
-func TestAddCommentsSkipsDuplicates(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- srv := httpmock.New(func(s *httpmock.Server) {
- // Only the new comment should be posted, duplicate skipped.
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments").
- Return("{}").
- Once()
- })(t)
-
- bb := bitBucketAPI{
- uri: srv.URL(),
- authToken: "test-token",
- timeout: time.Second * 5,
- project: "proj",
- repo: "repo",
- }
-
- pr := &bitBucketPR{ID: 1}
- currentComments := []bitBucketComment{
- {
- id: 1,
- text: "existing comment",
- anchor: BitBucketCommentAnchor{
- Path: "file.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
- pendingComments := []BitBucketPendingComment{
- {
- Text: "existing comment",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "file.yaml",
- Line: 10,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: "new comment",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "file.yaml",
- Line: 20,
- LineType: "ADDED",
- DiffType: "EFFECTIVE",
- },
- },
- }
-
- err := bb.addComments(pr, currentComments, pendingComments)
- require.NoError(t, err)
- require.Len(t, srv.Requests, 1, "only the new comment should be added, duplicate skipped")
- require.Equal(t, http.MethodPost, srv.Requests[0].Method())
-}
diff --git a/internal/reporter/bitbucket_comments_test.go b/internal/reporter/bitbucket_comments_test.go
deleted file mode 100644
index 05d49bf6..00000000
--- a/internal/reporter/bitbucket_comments_test.go
+++ /dev/null
@@ -1,759 +0,0 @@
-package reporter
-
-import (
- "fmt"
- "log/slog"
- "strings"
- "testing"
- "time"
-
- "github.com/google/go-cmp/cmp"
- "github.com/neilotoole/slogt"
-
- "github.com/cloudflare/pint/internal/checks"
- "github.com/cloudflare/pint/internal/diags"
- "github.com/cloudflare/pint/internal/discovery"
- "github.com/cloudflare/pint/internal/git"
-)
-
-func TestBitBucketMakeComments(t *testing.T) {
- type testCaseT struct {
- changes *bitBucketPRChanges
- description string
- comments []BitBucketPendingComment
- summary Summary
- maxComments int
- showDuplicates bool
- }
-
- commentBody := func(icon, severity, reporter, text string) string {
- return fmt.Sprintf(
- ":%s: **%s** reported by [pint](https://cloudflare.github.io/pint/) **%s** check.\n\n------\n\n%s\n\n------\n\n:information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/%s.html).\n",
- icon, severity, reporter, text, reporter,
- )
- }
-
- testCases := []testCaseT{
- {
- description: "empty summary",
- maxComments: 50,
- comments: []BitBucketPendingComment{},
- },
- {
- description: "report not included in changes",
- maxComments: 50,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- },
- },
- }},
- changes: &bitBucketPRChanges{},
- comments: []BitBucketPendingComment{},
- },
- {
- description: "reports included in changes",
- maxComments: 50,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "first error",
- Details: "first details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Warning,
- Lines: diags.LineRange{
- First: 3,
- Last: 3,
- },
- Summary: "second error",
- Details: "second details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "third error",
- Details: "third details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "symlink.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "fourth error",
- Details: "fourth details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "second.yaml",
- Name: "second.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 1, Modified: true},
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Anchor: checks.AnchorBefore,
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "fifth error",
- Details: "fifth details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "second.yaml",
- Name: "second.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 1, Modified: true},
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "sixth error",
- Details: "sixth details",
- Reporter: "r2",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- "second.yaml": {1, 2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- "second.yaml": {1: 5, 2: 6, 3: 7},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: commentBody("stop_sign", "Bug", "r1", "first error\n\nfirst details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: commentBody("warning", "Warning", "r1", "second error\n\nsecond details"),
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 3,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: commentBody("stop_sign", "Bug", "r2", "third error\n\nthird details\n\n------\n\nfourth error\n\nfourth details\n\n:leftwards_arrow_with_hook: This problem was detected on a symlinked file `symlink.yaml`."),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: commentBody("stop_sign", "Bug", "r2", "fifth error\n\nfifth details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "second.yaml",
- Line: 2,
- LineType: "REMOVED",
- FileType: "FROM",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: commentBody("stop_sign", "Bug", "r2", "sixth error\n\nsixth details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "second.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- {
- description: "dedup reporter",
- maxComments: 50,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "first error",
- Details: "first details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "second error",
- Details: "second details",
- Reporter: "r1",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: commentBody("stop_sign", "Bug", "r1", "first error\n\nfirst details\n\n------\n\nsecond error\n\nsecond details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- {
- description: "dedup identical reports",
- maxComments: 50,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "my error",
- Details: "my details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "my error",
- Details: "my details",
- Reporter: "r1",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: commentBody("stop_sign", "Bug", "r1", "my error\n\nmy details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- {
- description: "dedup details",
- maxComments: 50,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "first error",
- Details: "shared details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "second error",
- Details: "shared details",
- Reporter: "r1",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: commentBody("stop_sign", "Bug", "r1", "first error\n\n------\n\nsecond error\n\n------\n\nshared details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- {
- description: "maxComments 2",
- maxComments: 2,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "first error",
- Details: "first details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Warning,
- Lines: diags.LineRange{
- First: 3,
- Last: 3,
- },
- Summary: "second error",
- Details: "second details",
- Reporter: "r1",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "third error",
- Details: "third details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "symlink.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "fourth error",
- Details: "fourth details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "second.yaml",
- Name: "second.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 1, Modified: true},
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Anchor: checks.AnchorBefore,
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "fifth error",
- Details: "fifth details",
- Reporter: "r2",
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "second.yaml",
- Name: "second.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 1, Modified: true},
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: "sixth error",
- Details: "sixth details",
- Reporter: "r2",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- "second.yaml": {1, 2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- "second.yaml": {1: 5, 2: 6, 3: 7},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: commentBody("stop_sign", "Bug", "r1", "first error\n\nfirst details"),
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: commentBody("warning", "Warning", "r1", "second error\n\nsecond details"),
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 3,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- {
- Text: "This pint run would create 5 comment(s), which is more than 2 limit configured for pint.\n3 comments were skipped and won't be visible on this PR.",
- Severity: "NORMAL",
- Anchor: BitBucketPendingCommentAnchor{
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- {
- description: "truncate long comments",
- maxComments: 2,
- summary: Summary{reports: []Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "rule.yaml",
- Name: "rule.yaml",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 3, Modified: true},
- },
- },
- Problem: checks.Problem{
- Severity: checks.Bug,
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Summary: strings.Repeat("X", maxCommentLength+1),
- Reporter: "r1",
- },
- },
- }},
- changes: &bitBucketPRChanges{
- pathModifiedLines: map[string][]int{
- "rule.yaml": {2, 3},
- },
- pathLineMapping: map[string]map[int]int{
- "rule.yaml": {2: 2, 3: 3},
- },
- },
- comments: []BitBucketPendingComment{
- {
- Text: ":stop_sign: **Bug** reported by [pint](https://cloudflare.github.io/pint/) **r1** check.\n\n------\n\n" + strings.Repeat("X", maxCommentLength-98-4) + " ...",
- Severity: "BLOCKER",
- Anchor: BitBucketPendingCommentAnchor{
- Path: "rule.yaml",
- Line: 2,
- LineType: "ADDED",
- FileType: "TO",
- DiffType: "EFFECTIVE",
- },
- },
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.description, func(t *testing.T) {
- slog.SetDefault(slogt.New(t))
- r := NewBitBucketReporter(
- "v0.0.0",
- "http://bitbucket.example.com",
- time.Second,
- "token",
- "proj",
- "repo",
- tc.maxComments,
- tc.showDuplicates,
- nil,
- )
- comments := r.api.limitComments(r.api.makeComments(tc.summary, tc.changes))
- if diff := cmp.Diff(tc.comments, comments); diff != "" {
- t.Errorf("api.makeComments() returned wrong output (-want +got):\n%s", diff)
- return
- }
- })
- }
-}
diff --git a/internal/reporter/bitbucket_test.go b/internal/reporter/bitbucket_test.go
index e6f421b6..fd6c3c11 100644
--- a/internal/reporter/bitbucket_test.go
+++ b/internal/reporter/bitbucket_test.go
@@ -1,2046 +1,1181 @@
-package reporter_test
+package reporter
import (
+ "bytes"
+ "context"
+ "encoding/json"
"errors"
- "fmt"
+ "io"
"log/slog"
- "net"
"net/http"
- "os"
- "path/filepath"
- "strings"
+ "net/http/httptest"
"testing"
"time"
"github.com/neilotoole/slogt"
+ "github.com/stretchr/testify/require"
"go.nhat.io/httpmock"
"github.com/cloudflare/pint/internal/checks"
- "github.com/cloudflare/pint/internal/diags"
- "github.com/cloudflare/pint/internal/discovery"
"github.com/cloudflare/pint/internal/git"
- "github.com/cloudflare/pint/internal/parser"
- "github.com/cloudflare/pint/internal/reporter"
)
-func TestBitBucketReporter(t *testing.T) {
- type errorCheck func(err error) error
-
- type testCaseT struct {
- mock httpmock.Mocker
- gitCmd git.CommandRunner
- errorHandler errorCheck
- description string
- reports []reporter.Report
- showDuplicates bool
- }
-
- p := parser.NewParser(parser.DefaultOptions)
- mockFile := p.Parse(strings.NewReader(`
-- record: target is down
- expr: up == 0
-- record: sum errors
- expr: sum(errors) by (job)
-`))
-
- fakeGit := func(args ...string) ([]byte, error) {
- if args[0] == "rev-parse" && args[1] == "--verify" && args[2] == "HEAD" {
- return []byte("fake-commit-id"), nil
- }
- if args[0] == "rev-parse" && args[1] == "--abbrev-ref" && args[2] == "HEAD" {
- return []byte("fake-branch"), nil
- }
- return nil, nil
- }
+func TestBitBucketReporterDescribe(t *testing.T) {
+ // Verifies that Describe returns the reporter name.
+ bb := BitBucketReporter{}
+ require.Equal(t, "BitBucket", bb.Describe())
+}
- diagFile := filepath.Join(t.TempDir(), "diag.txt")
- if err := os.WriteFile(diagFile, []byte("- record: target is down\n expr: up == 0\n"), 0o644); err != nil {
- t.Fatal(err)
- }
+func TestBitBucketReporterDestinations(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
- testCases := []testCaseT{
- {
- description: "returns an error on git head failure",
+ // Verifies that Destinations returns an error when git HEAD fails.
+ t.Run("git HEAD failure", func(t *testing.T) {
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ "http://localhost", time.Second,
+ "token", "proj", "repo",
+ ),
gitCmd: func(args ...string) ([]byte, error) {
- if args[0] == "rev-parse" && args[1] == "--verify" && args[2] == "HEAD" {
+ if args[0] == "rev-parse" && args[1] == "--verify" {
return nil, errors.New("git head error")
}
return nil, nil
},
- mock: httpmock.New(func(_ *httpmock.Server) {}),
- errorHandler: func(err error) error {
- if err != nil && err.Error() == "failed to get HEAD commit: git head error" {
- return nil
- }
- return fmt.Errorf("Expected git head error, got %w", err)
- },
- },
- {
- description: "returns an error on git branch failure",
+ }
+ _, err := bb.Destinations(t.Context())
+ require.EqualError(t, err, "failed to get HEAD commit: git head error")
+ })
+
+ // Verifies that Destinations returns an error when git branch fails.
+ t.Run("git branch failure", func(t *testing.T) {
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ "http://localhost", time.Second,
+ "token", "proj", "repo",
+ ),
gitCmd: func(args ...string) ([]byte, error) {
- if args[0] == "rev-parse" && args[1] == "--verify" && args[2] == "HEAD" {
- return []byte("fake-commit-id"), nil
+ if args[0] == "rev-parse" && args[1] == "--verify" {
+ return []byte("abc123"), nil
}
- if args[0] == "rev-parse" && args[1] == "--abbrev-ref" && args[2] == "HEAD" {
+ if args[0] == "rev-parse" && args[1] == "--abbrev-ref" {
return nil, errors.New("git branch error")
}
return nil, nil
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil && err.Error() == "failed to get current branch: git branch error" {
- return nil
+ }
+ _, err := bb.Destinations(t.Context())
+ require.EqualError(t, err, "failed to get current branch: git branch error")
+ })
+
+ // Verifies that Destinations returns nil when no PR matches.
+ t.Run("no matching PR", func(t *testing.T) {
+ prsJSON, _ := json.Marshal(BitBucketPullRequests{
+ IsLastPage: true,
+ Values: []BitBucketPullRequest{},
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/abc123/pull-requests?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(prsJSON)).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ gitCmd: func(args ...string) ([]byte, error) {
+ if args[0] == "rev-parse" && args[1] == "--verify" {
+ return []byte("abc123"), nil
+ }
+ if args[0] == "rev-parse" && args[1] == "--abbrev-ref" {
+ return []byte("feature"), nil
}
- return fmt.Errorf("Expected git branch error, got %w", err)
+ return nil, nil
},
- },
- {
- description: "returns an error on non-200 HTTP response",
- gitCmd: fakeGit,
- reports: []reporter.Report{
+ }
+ dsts, err := bb.Destinations(t.Context())
+ require.NoError(t, err)
+ require.Nil(t, dsts)
+ })
+
+ // Verifies that Destinations returns the matching PR destination.
+ t.Run("matching PR found", func(t *testing.T) {
+ prsJSON, _ := json.Marshal(BitBucketPullRequests{
+ IsLastPage: true,
+ Values: []BitBucketPullRequest{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
+ ID: 42,
+ Open: true,
+ FromRef: BitBucketRef{
+ ID: "refs/heads/feature",
+ Commit: "abc123",
},
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{{Before: 0, After: 2, Modified: true}},
+ ToRef: BitBucketRef{
+ ID: "refs/heads/main",
+ Commit: "def456",
},
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{},
},
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- ReturnCode(http.StatusBadRequest).
- Return("Bad Request").
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil && err.Error() == "failed to create BitBucket report: PUT request failed" {
- return nil
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/abc123/pull-requests?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(prsJSON)).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ gitCmd: func(args ...string) ([]byte, error) {
+ if args[0] == "rev-parse" && args[1] == "--verify" {
+ return []byte("abc123"), nil
+ }
+ if args[0] == "rev-parse" && args[1] == "--abbrev-ref" {
+ return []byte("feature"), nil
}
- return fmt.Errorf("Expected 'failed to create BitBucket report: PUT request failed', got %w", err)
+ return nil, nil
},
- },
- {
- description: "returns an error on HTTP response headers timeout",
- gitCmd: fakeGit,
- reports: []reporter.Report{
+ }
+ dsts, err := bb.Destinations(t.Context())
+ require.NoError(t, err)
+ require.Len(t, dsts, 1)
+ pr := dsts[0].(*bitBucketPR)
+ require.Equal(t, 42, pr.ID)
+ require.Equal(t, "feature", pr.srcBranch)
+ require.Equal(t, "abc123", pr.srcHead)
+ require.Equal(t, "main", pr.dstBranch)
+ require.Equal(t, "def456", pr.dstHead)
+ })
+}
+
+func TestBitBucketReporterList(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that List maps BitBucket comments to ExistingComment structs.
+ t.Run("maps comments correctly", func(t *testing.T) {
+ activitiesJSON, _ := json.Marshal(BitBucketPullRequestActivities{
+ IsLastPage: true,
+ Values: []BitBucketPullRequestActivity{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{{Before: 0, After: 2, Modified: true}},
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 10,
+ Version: 2,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Text: "some comment",
+ },
+ CommentAnchor: BitBucketCommentAnchor{
+ Path: "foo.yaml",
+ Line: 5,
},
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{},
},
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Run(func(_ *http.Request) ([]byte, error) {
- time.Sleep(time.Second * 2)
- return []byte("Bad Request"), nil
- }).
- ReturnCode(http.StatusBadRequest).
- Once()
- }),
- errorHandler: func(err error) error {
- if neterr, ok := errors.AsType[net.Error](errors.Unwrap(err)); ok && neterr.Timeout() {
- return nil
- }
- return fmt.Errorf("Expected a timeout error, got %w", err)
- },
- },
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ Return("testuser").
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(activitiesJSON)).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ }
+ pr := &bitBucketPR{ID: 1}
+ existing, err := bb.List(t.Context(), pr)
+ require.NoError(t, err)
+ require.Len(t, existing, 1)
+ require.Equal(t, "foo.yaml", existing[0].path)
+ require.Equal(t, 5, existing[0].line)
+ require.Equal(t, "some comment", existing[0].text)
+ meta := existing[0].meta.(bitBucketCommentMeta)
+ require.Equal(t, 10, meta.id)
+ require.Equal(t, 2, meta.version)
+ })
+}
+
+func TestBitBucketReporterCreate(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ type testCaseT struct {
+ description string
+ wantSev string
+ wantAnchor bitBucketPendingCommentAnchor
+ pending PendingComment
+ }
+
+ testCases := []testCaseT{
{
- description: "returns an error on HTTP response body timeout",
- gitCmd: fakeGit,
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{{Before: 0, After: 2, Modified: true}},
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{},
+ // Line is in changedLines (ADDED) so anchor should be ADDED/TO.
+ description: "modified line uses ADDED/TO anchor",
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 5,
+ text: ":stop_sign: Bug found",
+ changedLines: git.LineNumbers{
+ {Before: 0, After: 5, Modified: true},
},
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Run(func(_ *http.Request) ([]byte, error) {
- time.Sleep(time.Second * 2)
- return []byte("Bad Request"), nil
- }).
- ReturnCode(http.StatusBadRequest).
- Once()
- }),
- errorHandler: func(err error) error {
- if neterr, ok := errors.AsType[net.Error](errors.Unwrap(err)); ok && neterr.Timeout() {
- return nil
- }
- return fmt.Errorf("Expected a timeout error, got %w", err)
+ wantAnchor: bitBucketPendingCommentAnchor{
+ Path: "foo.yaml",
+ Line: 5,
+ DiffType: "EFFECTIVE",
+ LineType: "ADDED",
+ FileType: "TO",
},
+ wantSev: "BLOCKER",
},
{
- description: "sends a correct report that fails",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to create BitBucket report: PUT request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ // Line is NOT in changedLines so anchor should be CONTEXT/FROM.
+ description: "unmodified line uses CONTEXT/FROM anchor",
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 5,
+ text: ":warning: Warning found",
+ changedLines: git.LineNumbers{
+ {Before: 0, After: 3, Modified: true},
+ },
},
+ wantAnchor: bitBucketPendingCommentAnchor{
+ Path: "foo.yaml",
+ Line: 5,
+ DiffType: "EFFECTIVE",
+ LineType: "CONTEXT",
+ FileType: "FROM",
+ },
+ wantSev: "NORMAL",
},
{
- description: "sends a correct report but fails to delete annotations",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{},
- }).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to delete existing BitBucket code insight annotations: DELETE request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ // AnchorBefore forces REMOVED lineType.
+ description: "AnchorBefore uses REMOVED/FROM anchor",
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 5,
+ text: ":stop_sign: Bug found",
+ anchor: checks.AnchorBefore,
+ changedLines: git.LineNumbers{
+ {Before: 0, After: 5, Modified: true},
+ },
+ },
+ wantAnchor: bitBucketPendingCommentAnchor{
+ Path: "foo.yaml",
+ Line: 5,
+ DiffType: "EFFECTIVE",
+ LineType: "REMOVED",
+ FileType: "FROM",
},
+ wantSev: "BLOCKER",
},
{
- description: "sends a correct report but fails to create annotations",
- gitCmd: fakeGit,
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, line is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "bar.txt",
- Name: "bar.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{},
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, file is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "bad name",
- Severity: checks.Fatal,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 4,
- Last: 4,
- },
- Reporter: "mock",
- Summary: "mock text 2",
- Severity: checks.Warning,
- },
+ // Unmodified line with line mapping remaps the line number.
+ description: "unmodified line uses BeforeForAfter mapping",
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 5,
+ text: ":warning: Warning found",
+ changedLines: git.LineNumbers{
+ {Before: 3, After: 5, Modified: false},
},
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{},
- }).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- s.ExpectPost("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to create BitBucket code insight annotations: POST request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ wantAnchor: bitBucketPendingCommentAnchor{
+ Path: "foo.yaml",
+ Line: 3,
+ DiffType: "EFFECTIVE",
+ LineType: "CONTEXT",
+ FileType: "FROM",
},
+ wantSev: "NORMAL",
},
- {
- description: "pull requests get fails",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectPost(
+ "/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments",
+ ).
+ Return("{}").
Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to get open pull requests from BitBucket: GET request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ }
+ pr := &bitBucketPR{ID: 1}
+ err := bb.Create(t.Context(), pr, tc.pending)
+ require.NoError(t, err)
+ require.Len(t, srv.Requests, 1)
+ require.Equal(t, http.MethodPost, srv.Requests[0].Method())
+ })
+ }
+}
+
+func TestBitBucketReporterDelete(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that Delete sends a DELETE request with correct comment ID and version.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectDelete(
+ "/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/10?version=2",
+ ).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ }
+ pr := &bitBucketPR{ID: 1}
+ err := bb.Delete(t.Context(), pr, ExistingComment{
+ path: "foo.yaml",
+ line: 5,
+ text: "old comment",
+ meta: bitBucketCommentMeta{id: 10, version: 2},
+ })
+ require.NoError(t, err)
+ require.Len(t, srv.Requests, 1)
+ require.Equal(t, http.MethodDelete, srv.Requests[0].Method())
+}
+
+func TestBitBucketReporterCanCreate(t *testing.T) {
+ type testCaseT struct {
+ description string
+ maxComments int
+ done int
+ expected bool
+ }
+
+ testCases := []testCaseT{
+ {
+ // Under the limit should allow creation.
+ description: "under limit",
+ maxComments: 10,
+ done: 5,
+ expected: true,
},
{
- description: "pull request changes get fails",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to get pull request changes from BitBucket: GET request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
+ // At the limit should not allow creation.
+ description: "at limit",
+ maxComments: 10,
+ done: 10,
+ expected: false,
},
{
- description: "pull request comments get fails",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{},
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("testuser").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnCode(http.StatusInternalServerError).
- Return("Internal error").
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to get pull request comments from BitBucket: GET request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
+ // Over the limit should not allow creation.
+ description: "over limit",
+ maxComments: 10,
+ done: 15,
+ expected: false,
},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ bb := BitBucketReporter{maxComments: tc.maxComments}
+ require.Equal(t, tc.expected, bb.CanCreate(tc.done))
+ })
+ }
+}
+
+func TestBitBucketReporterCanDelete(t *testing.T) {
+ // Verifies that CanDelete always returns true.
+ bb := BitBucketReporter{}
+ require.True(t, bb.CanDelete(ExistingComment{}))
+}
+
+func TestBitBucketReporterIsEqual(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ type testCaseT struct {
+ description string
+ existing ExistingComment
+ pending PendingComment
+ expected bool
+ }
+
+ testCases := []testCaseT{
{
- description: "sends a correct report",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- s.ExpectPost("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "line is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "bad name",
- Severity: checks.Fatal,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 4,
- Last: 4,
- },
- Reporter: "mock",
- Summary: "mock text 2",
- Severity: checks.Warning,
- },
- },
+ // Same path, line, and text should be equal.
+ description: "all fields match",
+ existing: ExistingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text",
},
- errorHandler: func(err error) error {
- if err.Error() != "fatal error(s) reported" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text",
},
+ expected: true,
},
{
- description: "FATAL errors are always reported, regardless of line number",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- s.ExpectPost("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{
- {Before: 0, After: 3, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "test/mock",
- Summary: "syntax error",
- Severity: checks.Fatal,
- },
- },
+ // Different path should not be equal.
+ description: "different path",
+ existing: ExistingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text",
},
- errorHandler: func(err error) error {
- if err.Error() != "fatal error(s) reported" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ pending: PendingComment{
+ path: "bar.yaml",
+ line: 10,
+ text: "comment text",
},
+ expected: false,
},
{
- // Covers bitbucket.go:49-51 — deleteReport error is logged but does not stop the flow.
- description: "deleteReport fails but flow continues",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- ReturnCode(http.StatusInternalServerError).
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ // Different line should not be equal.
+ description: "different line",
+ existing: ExistingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text",
},
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 20,
+ text: "comment text",
+ },
+ expected: false,
},
{
- description: "sends a correct empty report",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ // Different text should not be equal.
+ description: "different text",
+ existing: ExistingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment A",
},
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment B",
+ },
+ expected: false,
},
{
- description: "reports failures from unmodified lines",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- s.ExpectPost("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- reports: []reporter.Report{
+ // Trailing newline should be stripped before comparison.
+ description: "trailing newline is ignored",
+ existing: ExistingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text\n",
+ },
+ pending: PendingComment{
+ path: "foo.yaml",
+ line: 10,
+ text: "comment text",
+ },
+ expected: true,
+ },
+ }
+
+ bb := BitBucketReporter{}
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ result := bb.IsEqual(nil, tc.existing, tc.pending)
+ require.Equal(t, tc.expected, result)
+ })
+ }
+}
+
+func TestBitBucketAPIRequest(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ t.Run("successful request with body", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectPost("/test").
+ WithHeader("Content-Type", "application/json").
+ WithHeader("Authorization", "Bearer test-token").
+ Return(`{"result": "ok"}`).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ }
+
+ body := bytes.NewReader([]byte(`{"key": "value"}`))
+ resp, err := bb.request(http.MethodPost, "/test", body)
+ require.NoError(t, err)
+ require.JSONEq(t, `{"result": "ok"}`, string(resp))
+ })
+
+ t.Run("request with invalid URL", func(t *testing.T) {
+ bb := bitBucketAPI{
+ uri: "://invalid-url",
+ authToken: "test-token",
+ timeout: time.Second,
+ }
+
+ _, err := bb.request(http.MethodGet, "/test", nil)
+ require.Error(t, err)
+ })
+
+ t.Run("non-2xx response returns error", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/test").
+ ReturnCode(http.StatusBadRequest).
+ Return("Bad Request").
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ }
+
+ _, err := bb.request(http.MethodGet, "/test", nil)
+ require.Error(t, err)
+ require.Equal(t, "GET request failed", err.Error())
+ })
+}
+
+func TestBitBucketAPIGetPullRequestComments(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ t.Run("filters comments correctly", func(t *testing.T) {
+ activities := BitBucketPullRequestActivities{
+ IsLastPage: true,
+ Values: []BitBucketPullRequestActivity{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Rule: mockFile.Groups[0].Rules[1],
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this line is not part of the diff",
- Severity: checks.Bug,
- },
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 1,
+ Version: 1,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Text: "valid comment",
+ },
+ CommentAnchor: BitBucketCommentAnchor{Path: "foo.yaml", Line: 10},
},
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Rule: mockFile.Groups[0].Rules[1],
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "bad name",
- Severity: checks.Bug,
+ Action: "APPROVED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 2,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
},
},
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Rule: mockFile.Groups[0].Rules[0],
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text",
- Severity: checks.Bug,
+ Action: "COMMENTED",
+ CommentAction: "EDITED",
+ Comment: BitBucketPullRequestComment{
+ ID: 3,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
},
},
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 4,
+ State: "RESOLVED",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
},
- Rule: mockFile.Groups[0].Rules[1],
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
+ },
+ {
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 5,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "otheruser"},
},
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 4,
- Last: 4,
- },
- Reporter: "mock",
- Summary: "mock text 2",
- Severity: checks.Warning,
+ },
+ {
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 6,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Severity: "BLOCKER",
+ Resolved: true,
},
},
+ {
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 7,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Severity: "NORMAL",
+ },
+ CommentAnchor: BitBucketCommentAnchor{Orphaned: true},
+ },
},
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "sends a correct report with pull request open",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 101,
- Open: false,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/feature",
- Commit: "pr-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{IsLastPage: true}).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{IsLastPage: true}).
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "sends a correct report using comments, deleting stale ones",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {Path: reporter.BitBucketPath{ToString: "index.txt"}},
- {Path: reporter.BitBucketPath{ToString: "foo.txt"}},
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- {Source: 5, Destination: 5},
- },
- },
- {
- Type: "CONTEXT",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 10, Destination: 6},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/foo.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 2, Destination: 2},
- },
- },
- },
- },
- },
- },
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "MODIFIED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 3, Destination: 4},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestActivity{
- {Action: "APPROVED"},
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: true,
- LineType: "CONTEXT",
- DiffType: "EFFECTIVE",
- Path: "foo.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: true,
- DiffType: "COMMIT",
- Path: "foo.txt",
- Line: 10,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: true,
- LineType: "REMOVED",
- DiffType: "COMMIT",
- Path: "foo.txt",
- Line: 14,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1003,
- Version: 1,
- State: "OPEN",
- Severity: "BLOCKER",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "EFFECTIVE",
- Path: "foo.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 2001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "COMMIT",
- Path: "foo.txt",
- Line: 4,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 2002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- },
- }).
- Once()
- // pruneComments deletes stale comments.
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1001?version=0").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1002?version=1").
- Once()
- // 1003 has 0 replies -> deleteComment.
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1003?version=1").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/2001?version=0").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/2002?version=1").
- Once()
- // addComments posts 4 new comments.
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- }),
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, line is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "bad name",
- Severity: checks.Fatal,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text",
- Details: "mock details",
- Severity: checks.Bug,
- },
- },
+ }
+ activitiesJSON, _ := json.Marshal(activities)
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ Return("testuser").
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(activitiesJSON)).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ comments, err := bb.getPullRequestComments(pr)
+ require.NoError(t, err)
+ require.Len(t, comments, 1)
+ require.Equal(t, 1, comments[0].id)
+ require.Equal(t, "valid comment", comments[0].text)
+ })
+
+ t.Run("handles pagination", func(t *testing.T) {
+ page1, _ := json.Marshal(BitBucketPullRequestActivities{
+ IsLastPage: false,
+ NextPageStart: 1,
+ Values: []BitBucketPullRequestActivity{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "symlink.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 4,
- Last: 4,
- },
- Reporter: "mock",
- Summary: "mock text 2",
- Severity: checks.Warning,
- },
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 1,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Text: "comment 1",
+ },
+ CommentAnchor: BitBucketCommentAnchor{Path: "foo.yaml"},
},
},
- errorHandler: func(err error) error {
- if err.Error() != "fatal error(s) reported" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "sends a correct report using comments, fails to delete stale comments",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {
- Path: reporter.BitBucketPath{
- ToString: "index.txt",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- {Source: 5, Destination: 5},
- },
- },
- {
- Type: "CONTEXT",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 10, Destination: 6},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestActivity{
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "EFFECTIVE",
- Path: "index.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{
- Name: "pint_user",
- },
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "COMMIT",
- Path: "index.txt",
- Line: 10,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{
- Name: "pint_user",
- },
- },
- },
- },
- }).
- Once()
- // pruneComments will try to delete both comments, which fails (500), but errors are only logged.
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1001?version=0").
- ReturnCode(http.StatusInternalServerError).
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1002?version=1").
- ReturnCode(http.StatusInternalServerError).
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "sends a correct report using comments, fails to get username",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {
- Path: reporter.BitBucketPath{
- ToString: "index.txt",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- {Source: 5, Destination: 5},
- },
- },
- {
- Type: "CONTEXT",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 10, Destination: 6},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- ReturnCode(http.StatusInternalServerError).
- Once()
- }),
- errorHandler: func(err error) error {
- if err.Error() != "failed to get pull request comments from BitBucket: GET request failed" {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "sends a correct report using comments, fails to create new comments",
- gitCmd: fakeGit,
- reports: []reporter.Report{
+ })
+ page2, _ := json.Marshal(BitBucketPullRequestActivities{
+ IsLastPage: true,
+ Values: []BitBucketPullRequestActivity{
{
- Path: discovery.Path{
- SymlinkTarget: "index.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, line is not part of the diff",
- Severity: checks.Bug,
- },
+ Action: "COMMENTED",
+ CommentAction: "ADDED",
+ Comment: BitBucketPullRequestComment{
+ ID: 2,
+ State: "OPEN",
+ Author: BitBucketCommentAuthor{Name: "testuser"},
+ Text: "comment 2",
+ },
+ CommentAnchor: BitBucketCommentAnchor{Path: "bar.yaml"},
},
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {
- Path: reporter.BitBucketPath{
- ToString: "index.txt",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- {Source: 5, Destination: 5},
- },
- },
- {
- Type: "CONTEXT",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 10, Destination: 6},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestActivity{
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "EFFECTIVE",
- Path: "index.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{
- Name: "pint_user",
- },
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "COMMIT",
- Path: "index.txt",
- Line: 10,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{
- Name: "pint_user",
- },
- },
- },
- },
- }).
- Once()
- // pruneComments deletes both stale comments.
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1001?version=0").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1002?version=1").
- Once()
- // addComments tries to POST new comment, which fails.
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- ReturnCode(http.StatusInternalServerError).
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil && err.Error() == "failed to create BitBucket pull request comments: POST request failed" {
- return nil
- }
- return fmt.Errorf("Expected failed to create BitBucket pull request comments: POST request failed, got %w", err)
- },
- },
- {
- description: "sends a correct report with deduped comments",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {Path: reporter.BitBucketPath{ToString: "index.txt"}},
- {Path: reporter.BitBucketPath{ToString: "foo.txt"}},
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- {Source: 5, Destination: 5},
- },
- },
- {
- Type: "CONTEXT",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 10, Destination: 6},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/foo.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 2, Destination: 2},
- },
- },
- },
- },
- },
- },
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "MODIFIED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 3, Destination: 4},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestActivity{
- {Action: "APPROVED"},
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: true,
- DiffType: "EFFECTIVE",
- Path: "foo.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: true,
- DiffType: "COMMIT",
- Path: "foo.txt",
- Line: 10,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 1002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "EFFECTIVE",
- Path: "foo.txt",
- Line: 3,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 2001,
- Version: 0,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- {
- Action: "COMMENTED",
- CommentAction: "ADDED",
- CommentAnchor: reporter.BitBucketCommentAnchor{
- Orphaned: false,
- DiffType: "COMMIT",
- Path: "foo.txt",
- Line: 4,
- },
- Comment: reporter.BitBucketPullRequestComment{
- ID: 2002,
- Version: 1,
- State: "OPEN",
- Author: reporter.BitBucketCommentAuthor{Name: "pint_user"},
- },
- },
- },
- }).
- Once()
- // pruneComments deletes all stale comments.
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1001?version=0").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/1002?version=1").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/2001?version=0").
- Once()
- s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments/2002?version=1").
- Once()
- // addComments posts 2 deduped comments.
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- }),
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, line is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "this should be ignored, line is not part of the diff",
- Severity: checks.Bug,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "bad name",
- Details: "bad name details",
- Severity: checks.Warning,
- },
- },
- {
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text 1",
- Details: "mock details",
- Severity: checks.Warning,
- },
- },
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ Return("testuser").
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(page1)).
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=1").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(page2)).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ comments, err := bb.getPullRequestComments(pr)
+ require.NoError(t, err)
+ require.Len(t, comments, 2)
+ })
+
+ t.Run("returns error on invalid JSON", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ Return("testuser").
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
+ Return("invalid json").
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ _, err := bb.getPullRequestComments(pr)
+ require.Error(t, err)
+ })
+}
+
+func TestFindPullRequestForBranchErrors(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ t.Run("returns error on invalid JSON", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=0").
+ Return("invalid json").
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ _, err := bb.findPullRequestForBranch("feature", "commit123")
+ require.Error(t, err)
+ })
+
+ t.Run("paginates through results", func(t *testing.T) {
+ page1, _ := json.Marshal(BitBucketPullRequests{
+ IsLastPage: false,
+ NextPageStart: 1,
+ Values: []BitBucketPullRequest{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "symlink.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: []git.LineNumber{
- {Before: 0, After: 2, Modified: true},
- {Before: 0, After: 4, Modified: true},
- },
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 2,
- Last: 2,
- },
- Reporter: "mock",
- Summary: "mock text 2",
- Details: "mock details",
- Severity: checks.Warning,
- },
+ ID: 1,
+ Open: true,
+ FromRef: BitBucketRef{ID: "refs/heads/other"},
+ ToRef: BitBucketRef{ID: "refs/heads/main"},
},
},
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- description: "annotation on unmodified lines",
- gitCmd: fakeGit,
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{IsLastPage: true}).
- Once()
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint/annotations").
- Once()
- }),
- reports: []reporter.Report{
+ })
+ page2, _ := json.Marshal(BitBucketPullRequests{
+ IsLastPage: true,
+ Values: []BitBucketPullRequest{
{
- Path: discovery.Path{
- SymlinkTarget: "foo.txt",
- Name: "foo.txt",
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{},
- },
- Rule: mockFile.Groups[0].Rules[1],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "line is not part of the diff",
- Severity: checks.Bug,
- },
+ ID: 2,
+ Open: true,
+ FromRef: BitBucketRef{ID: "refs/heads/feature", Commit: "abc123"},
+ ToRef: BitBucketRef{ID: "refs/heads/main", Commit: "def456"},
},
},
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
- },
- },
- {
- // Covers bitbucket_api.go:633-652 — diagnostics rendering in makeComments.
- description: "comment includes diagnostics when file is readable",
- gitCmd: fakeGit,
- reports: []reporter.Report{
- {
- Path: discovery.Path{
- SymlinkTarget: "index.txt",
- Name: diagFile,
- },
- Changes: &discovery.Changes{
- OldPath: "",
- Lines: git.LineNumbers{{Before: 0, After: 1, Modified: true}},
- },
- Rule: mockFile.Groups[0].Rules[0],
- Problem: checks.Problem{
- Lines: diags.LineRange{
- First: 1,
- Last: 1,
- },
- Reporter: "mock",
- Summary: "problem with diagnostics",
- Severity: checks.Bug,
- Anchor: checks.AnchorAfter,
- Diagnostics: []diags.Diagnostic{
- {
- Message: "this is wrong",
- Pos: diags.PositionRanges{
- {Line: 1, FirstColumn: 3, LastColumn: 8},
- },
- FirstColumn: 3,
- LastColumn: 8,
- },
- },
- },
- },
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(page1)).
+ Once()
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/commit123/pull-requests?start=1").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(page2)).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr, err := bb.findPullRequestForBranch("feature", "commit123")
+ require.NoError(t, err)
+ require.NotNil(t, pr)
+ require.Equal(t, 2, pr.ID)
+ })
+}
+
+func TestBitBucketAPICreateComment(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that createComment sends a POST with serialized comment body.
+ t.Run("sends POST request with comment payload", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments").
+ Return("{}").
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ comment := BitBucketPendingComment{
+ Text: "test comment",
+ Severity: "NORMAL",
+ Anchor: bitBucketPendingCommentAnchor{
+ Path: "foo.yaml",
+ Line: 10,
+ DiffType: "EFFECTIVE",
+ LineType: "ADDED",
+ FileType: "TO",
},
- mock: httpmock.New(func(s *httpmock.Server) {
- s.ExpectDelete("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectPut("/rest/insights/1.0/projects/proj/repos/repo/commits/fake-commit-id/reports/pint").
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/fake-commit-id/pull-requests?start=0").
- ReturnJSON(reporter.BitBucketPullRequests{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequest{
- {
- ID: 102,
- Open: true,
- FromRef: reporter.BitBucketRef{
- ID: "refs/heads/fake-branch",
- Commit: "fake-commit-id",
- },
- ToRef: reporter.BitBucketRef{
- ID: "refs/heads/main",
- Commit: "main-commit-id",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/changes?start=0").
- ReturnJSON(reporter.BitBucketPullRequestChanges{
- IsLastPage: true,
- Values: []reporter.BitBucketPullRequestChange{
- {
- Path: reporter.BitBucketPath{
- ToString: "index.txt",
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/commits/fake-commit-id/diff/index.txt?contextLines=10000&since=main-commit-id&whitespace=show&withComments=false").
- ReturnJSON(reporter.BitBucketFileDiffs{
- Diffs: []reporter.BitBucketFileDiff{
- {
- Hunks: []reporter.BitBucketDiffHunk{
- {
- Segments: []reporter.BitBucketDiffSegment{
- {
- Type: "ADDED",
- Lines: []reporter.BitBucketDiffLine{
- {Source: 1, Destination: 1},
- },
- },
- },
- },
- },
- },
- },
- }).
- Once()
- s.ExpectGet("/plugins/servlet/applinks/whoami").
- Return("pint_user").
- Once()
- s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/102/activities?start=0").
- ReturnJSON(reporter.BitBucketPullRequestActivities{IsLastPage: true}).
- Once()
- // addComments posts 1 new comment with diagnostics content.
- s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/102/comments").
- Once()
- }),
- errorHandler: func(err error) error {
- if err != nil {
- return fmt.Errorf("Unpexpected error: %w", err)
- }
- return nil
+ }
+
+ err := bb.createComment(pr, comment)
+ require.NoError(t, err)
+ require.Len(t, srv.Requests, 1)
+ require.Equal(t, http.MethodPost, srv.Requests[0].Method())
+ })
+
+ // Verifies that createComment returns an error on server failure.
+ t.Run("returns error on server failure", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectPost("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ comment := BitBucketPendingComment{
+ Text: "test comment",
+ Severity: "NORMAL",
+ }
+
+ err := bb.createComment(pr, comment)
+ require.Error(t, err)
+ })
+}
+
+func TestBitBucketAPIDeleteComment(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that deleteComment sends a DELETE request with correct comment ID and version.
+ t.Run("sends DELETE request with comment ID and version", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/42?version=3").
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ err := bb.deleteComment(pr, 42, 3)
+ require.NoError(t, err)
+ require.Len(t, srv.Requests, 1)
+ require.Equal(t, http.MethodDelete, srv.Requests[0].Method())
+ })
+
+ // Verifies that deleteComment returns an error on server failure.
+ t.Run("returns error on server failure", func(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectDelete("/rest/api/1.0/projects/proj/repos/repo/pull-requests/1/comments/42?version=3").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second * 5,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ err := bb.deleteComment(pr, 42, 3)
+ require.Error(t, err)
+ })
+}
+
+func TestBitBucketAPIRequestDoError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that request returns error when HTTP client cannot connect.
+ bb := bitBucketAPI{
+ uri: "http://127.0.0.1:0",
+ authToken: "test-token",
+ timeout: time.Millisecond * 100,
+ }
+
+ _, err := bb.request(http.MethodGet, "/test", nil)
+ require.Error(t, err)
+}
+
+func TestBitBucketAPIRequestReadBodyError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that request returns error when reading the response body fails.
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Length", "100")
+ w.WriteHeader(http.StatusOK)
+ // Write fewer bytes than Content-Length then close, causing io.ReadAll to fail.
+ _, _ = io.WriteString(w, "partial")
+ }))
+ t.Cleanup(srv.Close)
+
+ bb := bitBucketAPI{
+ uri: srv.URL,
+ authToken: "test-token",
+ timeout: time.Second,
+ }
+
+ _, err := bb.request(http.MethodGet, "/test", nil)
+ require.Error(t, err)
+}
+
+func TestBitBucketAPIWhoamiError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that whoami returns error when the request fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second,
+ }
+
+ _, err := bb.whoami()
+ require.EqualError(t, err, "GET request failed")
+}
+
+func TestFindPullRequestForBranchRequestError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that findPullRequestForBranch returns error when the HTTP request fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/abc/pull-requests?start=0").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second,
+ project: "proj",
+ repo: "repo",
+ }
+
+ _, err := bb.findPullRequestForBranch("feature", "abc")
+ require.EqualError(t, err, "GET request failed")
+}
+
+func TestFindPullRequestForBranchSkipsClosedPR(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that closed pull requests are skipped and nil is returned.
+ prsJSON, _ := json.Marshal(BitBucketPullRequests{
+ IsLastPage: true,
+ Values: []BitBucketPullRequest{
+ {
+ ID: 1,
+ Open: false,
+ FromRef: BitBucketRef{ID: "refs/heads/feature"},
+ ToRef: BitBucketRef{ID: "refs/heads/main"},
},
},
+ })
+
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/abc/pull-requests?start=0").
+ ReturnHeader("Content-Type", "application/json").
+ Return(string(prsJSON)).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second,
+ project: "proj",
+ repo: "repo",
}
- for _, tc := range testCases {
- t.Run(tc.description, func(t *testing.T) {
- slog.SetDefault(slogt.New(t))
-
- srv := tc.mock(t)
-
- r := reporter.NewBitBucketReporter(
- "v0.0.0",
- srv.URL(),
- time.Second,
- "token",
- "proj",
- "repo",
- 50,
- tc.showDuplicates,
- tc.gitCmd)
- summary := reporter.NewSummary(tc.reports)
- err := r.Submit(t.Context(), summary)
-
- if e := tc.errorHandler(err); e != nil {
- t.Errorf("error check failure: %s", e)
- return
+ pr, err := bb.findPullRequestForBranch("feature", "abc")
+ require.NoError(t, err)
+ require.Nil(t, pr)
+}
+
+func TestGetPullRequestCommentsWhoamiError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that getPullRequestComments returns error when whoami fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ _, err := bb.getPullRequestComments(pr)
+ require.EqualError(t, err, "GET request failed")
+}
+
+func TestGetPullRequestCommentsActivitiesRequestError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that getPullRequestComments returns error when the activities request fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ Return("testuser").
+ Once()
+ s.ExpectGet("/rest/api/latest/projects/proj/repos/repo/pull-requests/1/activities?start=0").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := bitBucketAPI{
+ uri: srv.URL(),
+ authToken: "test-token",
+ timeout: time.Second,
+ project: "proj",
+ repo: "repo",
+ }
+
+ pr := &bitBucketPR{ID: 1}
+ _, err := bb.getPullRequestComments(pr)
+ require.EqualError(t, err, "GET request failed")
+}
+
+func TestNewBitBucketReporter(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that the constructor initializes all fields correctly.
+ gitCmd := func(_ ...string) ([]byte, error) { return nil, nil }
+ bb := NewBitBucketReporter(
+ "http://localhost",
+ time.Minute,
+ "token",
+ "proj",
+ "repo",
+ 50,
+ gitCmd,
+ )
+ require.Equal(t, "BitBucket", bb.Describe())
+ require.Equal(t, 50, bb.maxComments)
+ require.NotNil(t, bb.api)
+ require.Equal(t, "http://localhost", bb.api.uri)
+ require.Equal(t, "token", bb.api.authToken)
+ require.Equal(t, "proj", bb.api.project)
+ require.Equal(t, "repo", bb.api.repo)
+ require.Equal(t, time.Minute, bb.api.timeout)
+}
+
+func TestBitBucketReporterDestinationsAPIError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that Destinations returns error when findPullRequestForBranch fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/rest/api/1.0/projects/proj/repos/repo/commits/abc123/pull-requests?start=0").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
+ gitCmd: func(args ...string) ([]byte, error) {
+ if args[0] == "rev-parse" && args[1] == "--verify" {
+ return []byte("abc123"), nil
}
- })
+ if args[0] == "rev-parse" && args[1] == "--abbrev-ref" {
+ return []byte("feature"), nil
+ }
+ return nil, nil
+ },
+ }
+ _, err := bb.Destinations(t.Context())
+ require.ErrorContains(t, err, "failed to get open pull requests from BitBucket")
+}
+
+func TestBitBucketReporterSummary(t *testing.T) {
+ // Verifies that Summary always returns nil.
+ bb := BitBucketReporter{}
+ err := bb.Summary(context.Background(), nil, Summary{}, nil, nil)
+ require.NoError(t, err)
+}
+
+func TestBitBucketReporterListError(t *testing.T) {
+ slog.SetDefault(slogt.New(t))
+
+ // Verifies that List returns error when getPullRequestComments fails.
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectGet("/plugins/servlet/applinks/whoami").
+ ReturnCode(http.StatusInternalServerError).
+ Once()
+ })(t)
+
+ bb := BitBucketReporter{
+ api: newBitBucketAPI(
+ srv.URL(), time.Second,
+ "token", "proj", "repo",
+ ),
}
+ pr := &bitBucketPR{ID: 1}
+ _, err := bb.List(t.Context(), pr)
+ require.EqualError(t, err, "GET request failed")
}
diff --git a/internal/reporter/comments.go b/internal/reporter/comments.go
index 14f7ee8e..10ddf2a5 100644
--- a/internal/reporter/comments.go
+++ b/internal/reporter/comments.go
@@ -216,23 +216,6 @@ func dedupReports(src []Report, showDuplicates bool) (dst [][]Report) {
return dst
}
-func identicalDetails(src []Report) bool {
- if len(src) <= 1 {
- return false
- }
- var details string
- for _, report := range src {
- if details == "" {
- details = report.Problem.Details
- continue
- }
- if details != report.Problem.Details {
- return false
- }
- }
- return true
-}
-
func problemIcon(s checks.Severity) string {
// nolint:exhaustive
switch s {