Skip to content

Commit aab7029

Browse files
committed
Support sonar on prem evidence endpoint in jfrog integration
1 parent 5c5c21a commit aab7029

File tree

2 files changed

+182
-6
lines changed

2 files changed

+182
-6
lines changed

evidence/sonar/client.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package sonar
33
import (
44
"encoding/json"
55
"fmt"
6-
"net"
76
"net/url"
87
"strings"
98

@@ -81,12 +80,30 @@ func (c *httpClient) GetSonarIntotoStatement(ceTaskID string) ([]byte, error) {
8180
baseURL := c.baseURL
8281
u, _ := url.Parse(baseURL)
8382
hostname := u.Hostname()
84-
// Get sonar intoto statement has api prefix before the hostname
85-
// if hostname is localhost or an ip address or has api prefix, then don't add the api.
86-
if hostname != "localhost" && net.ParseIP(hostname) == nil && !strings.HasPrefix(hostname, "api.") {
87-
baseURL = strings.Replace(baseURL, "://", "://api.", 1)
83+
84+
var enterpriseURL string
85+
86+
// Determine URL format based on SonarQube type
87+
// SonarCloud uses subdomain (api.), SonarQube Server uses path (/api/)
88+
isSonarCloud := strings.Contains(hostname, "sonarcloud.io") || strings.Contains(hostname, "sonarqube.us")
89+
90+
if isSonarCloud {
91+
// SonarQube Cloud: subdomain approach (no /v2/ prefix)
92+
// https://api.sonarcloud.io/dop-translation/jfrog-evidence/{ceTaskID}
93+
// https://api.sonarqube.us/dop-translation/jfrog-evidence/{ceTaskID}
94+
cloudBaseURL := baseURL
95+
if !strings.HasPrefix(hostname, "api.") {
96+
cloudBaseURL = strings.Replace(baseURL, "://", "://api.", 1)
97+
}
98+
enterpriseURL = fmt.Sprintf("%s/dop-translation/jfrog-evidence/%s", cloudBaseURL, url.QueryEscape(ceTaskID))
99+
log.Debug("Using SonarCloud URL format:", enterpriseURL)
100+
} else {
101+
// SonarQube Server: path approach (with /api/v2/ prefix)
102+
// https://{customer-domain}/api/v2/dop-translation/jfrog-evidence/{ceTaskID}
103+
enterpriseURL = fmt.Sprintf("%s/api/v2/dop-translation/jfrog-evidence/%s", baseURL, url.QueryEscape(ceTaskID))
104+
log.Debug("Using SonarQube Server URL format:", enterpriseURL)
88105
}
89-
enterpriseURL := fmt.Sprintf("%s/dop-translation/jfrog-evidence/%s", baseURL, url.QueryEscape(ceTaskID))
106+
90107
body, statusCode, err := c.doGET(enterpriseURL)
91108
if err != nil {
92109
return nil, errorutils.CheckErrorf("enterprise endpoint failed with status %d and response %s %v", statusCode, string(body), err)

evidence/sonar/client_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package sonar
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGetSonarIntotoStatement_SonarQubeServerV2Success(t *testing.T) {
13+
// Mock SonarQube Server that responds to v2 API endpoint (path-based)
14+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
// Verify SonarQube Server v2 API path is called (path approach)
16+
assert.Equal(t, "/api/v2/dop-translation/jfrog-evidence/task-123", r.URL.Path)
17+
18+
w.WriteHeader(http.StatusOK)
19+
_, _ = w.Write([]byte(`{"_type":"https://in-toto.io/Statement/v1","predicateType":"https://sonar.com/evidence/sonarqube/v1"}`))
20+
}))
21+
defer server.Close()
22+
23+
client, err := NewClient(server.URL, "test-token")
24+
assert.NoError(t, err)
25+
26+
result, err := client.GetSonarIntotoStatement("task-123")
27+
assert.NoError(t, err)
28+
assert.Contains(t, string(result), "in-toto.io/Statement/v1")
29+
}
30+
31+
func TestGetSonarIntotoStatement_SonarQubeServer_404Error(t *testing.T) {
32+
// Mock SonarQube Server that returns 404
33+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34+
// Verify SonarQube Server v2 API path is called (path approach)
35+
assert.Equal(t, "/api/v2/dop-translation/jfrog-evidence/task-123", r.URL.Path)
36+
w.WriteHeader(http.StatusNotFound)
37+
_, _ = w.Write([]byte(`{"error":"not found"}`))
38+
}))
39+
defer server.Close()
40+
41+
client, err := NewClient(server.URL, "test-token")
42+
assert.NoError(t, err)
43+
44+
_, err = client.GetSonarIntotoStatement("task-123")
45+
assert.Error(t, err)
46+
assert.Contains(t, err.Error(), "status 404")
47+
}
48+
49+
func TestGetSonarIntotoStatement_ServerError(t *testing.T) {
50+
// Mock server that returns 500 error
51+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52+
w.WriteHeader(http.StatusInternalServerError)
53+
_, _ = w.Write([]byte(`{"error":"server error"}`))
54+
}))
55+
defer server.Close()
56+
57+
client, err := NewClient(server.URL, "test-token")
58+
assert.NoError(t, err)
59+
60+
_, err = client.GetSonarIntotoStatement("task-123")
61+
assert.Error(t, err)
62+
// Error may contain timeout or status 500 depending on retry mechanism
63+
assert.True(t, strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "status 500"))
64+
}
65+
66+
func TestGetSonarIntotoStatement_EmptyTaskID(t *testing.T) {
67+
client, err := NewClient("https://test.example.com", "test-token")
68+
assert.NoError(t, err)
69+
70+
result, err := client.GetSonarIntotoStatement("")
71+
assert.Error(t, err)
72+
assert.Nil(t, result)
73+
assert.Contains(t, err.Error(), "missing ce task id")
74+
}
75+
76+
func TestGetSonarIntotoStatement_AuthorizationHeader(t *testing.T) {
77+
// Mock server that verifies authorization header
78+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
79+
authHeader := r.Header.Get("Authorization")
80+
assert.Equal(t, "Bearer test-token", authHeader, "Should send Bearer token")
81+
82+
w.WriteHeader(http.StatusOK)
83+
_, _ = w.Write([]byte(`{"_type":"https://in-toto.io/Statement/v1"}`))
84+
}))
85+
defer server.Close()
86+
87+
client, err := NewClient(server.URL, "test-token")
88+
assert.NoError(t, err)
89+
90+
result, err := client.GetSonarIntotoStatement("task-123")
91+
assert.NoError(t, err)
92+
assert.NotNil(t, result)
93+
}
94+
95+
func TestGetTaskDetails_Success(t *testing.T) {
96+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
97+
assert.Equal(t, "/api/ce/task", r.URL.Path)
98+
assert.Equal(t, "task-123", r.URL.Query().Get("id"))
99+
100+
w.WriteHeader(http.StatusOK)
101+
_, _ = w.Write([]byte(`{"task":{"id":"task-123","status":"SUCCESS"}}`))
102+
}))
103+
defer server.Close()
104+
105+
client, err := NewClient(server.URL, "test-token")
106+
assert.NoError(t, err)
107+
108+
details, err := client.GetTaskDetails("task-123")
109+
assert.NoError(t, err)
110+
assert.NotNil(t, details)
111+
assert.Equal(t, "task-123", details.Task.ID)
112+
assert.Equal(t, "SUCCESS", details.Task.Status)
113+
}
114+
115+
func TestGetTaskDetails_EmptyTaskID(t *testing.T) {
116+
client, err := NewClient("https://test.example.com", "test-token")
117+
assert.NoError(t, err)
118+
119+
details, err := client.GetTaskDetails("")
120+
assert.NoError(t, err)
121+
assert.Nil(t, details)
122+
}
123+
124+
func TestGetTaskDetails_Error(t *testing.T) {
125+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126+
w.WriteHeader(http.StatusInternalServerError)
127+
_, _ = w.Write([]byte(`{"error":"server error"}`))
128+
}))
129+
defer server.Close()
130+
131+
client, err := NewClient(server.URL, "test-token")
132+
assert.NoError(t, err)
133+
134+
details, err := client.GetTaskDetails("task-123")
135+
assert.Error(t, err)
136+
assert.Nil(t, details)
137+
}
138+
139+
func TestGetSonarIntotoStatement_URLEncoding(t *testing.T) {
140+
// Test that task IDs with special characters are properly URL encoded
141+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
142+
// When Go's http server receives the request, the path is already URL-decoded
143+
// So we just verify the request succeeds with special characters
144+
assert.Contains(t, r.URL.Path, "task")
145+
146+
w.WriteHeader(http.StatusOK)
147+
_, _ = w.Write([]byte(`{"_type":"https://in-toto.io/Statement/v1"}`))
148+
}))
149+
defer server.Close()
150+
151+
client, err := NewClient(server.URL, "test-token")
152+
assert.NoError(t, err)
153+
154+
// Verify that special characters in task ID don't cause errors
155+
result, err := client.GetSonarIntotoStatement("task/123+special")
156+
assert.NoError(t, err)
157+
assert.NotNil(t, result)
158+
assert.Contains(t, string(result), "in-toto.io/Statement/v1")
159+
}

0 commit comments

Comments
 (0)