Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,48 @@ jobs:
- name: Run E2E tests
run: make e2e

e2e-enterprise-test:
name: E2E Test (Enterprise Edition)
runs-on: ubuntu-latest
# The Enterprise Edition image exposes enterprise-only API surface without
# a license (most endpoints work unlicensed; analysis and a few
# license-gated operations do not). This job never blocks the pipeline:
# if the image can't be pulled in this environment, or the suite finds
# itself pointed at a non-Enterprise instance, it skips cleanly instead
# of failing. Set the SONAR_ENTERPRISE_LICENSE repo secret to also
# exercise license-dependent behavior.
continue-on-error: true
services:
sonarqube-enterprise:
image: docker.io/library/sonarqube:2026.3.1-enterprise
ports:
- 9001:9000
env:
SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true"
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: "1.26.5"
cache: true

- name: Cache Go tools
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
/home/runner/go/bin
key: ${{ runner.os }}-go-tools-${{ hashFiles('Makefile') }}-e2e-enterprise
restore-keys: |
${{ runner.os }}-go-tools-

- name: Run E2E tests (Enterprise Edition)
run: make e2e.enterprise
env:
SONAR_LICENSE: ${{ secrets.SONAR_ENTERPRISE_LICENSE }}

build:
runs-on: ubuntu-latest
steps:
Expand Down
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ setup.sonar:
fi

# Setup SonarQube Enterprise Edition instance for integration testing.
# Requires a valid SonarQube Enterprise Edition license.
# Runs the Enterprise Edition Docker image, which exposes enterprise-only API
# surface without a license (most endpoints work unlicensed; analysis and a
# few license-gated operations do not). If SONAR_LICENSE is set, it is
# applied on top so license-dependent tests can run too; this is optional.
# Override the endpoint with: make setup.sonar.enterprise enterprise_endpoint=http://my-sonarqube:9000
setup.sonar.enterprise:
@command -v curl >/dev/null 2>&1 || { echo "curl is required but not installed. Please install curl."; exit 1; }
Expand All @@ -163,6 +166,13 @@ setup.sonar.enterprise:
done; \
echo "\nSonarQube Enterprise is ready at ${enterprise_endpoint}."; \
fi
@if [ -n "$$SONAR_LICENSE" ]; then \
echo "SONAR_LICENSE is set; activating license on ${enterprise_endpoint}..."; \
curl -s -u ${username}:${password} -X POST --data-urlencode "license=$$SONAR_LICENSE" \
"${enterprise_endpoint}/api/editions/set_license" -o /dev/null -w "set_license responded with HTTP %{http_code}\n"; \
else \
echo "SONAR_LICENSE not set; running unlicensed. License-gated tests will be skipped."; \
fi

# Teardown SonarQube instance
teardown.sonar:
Expand Down
217 changes: 217 additions & 0 deletions integration_testing/enterprise/applications_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package enterprise_test

import (
"context"
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/boxboxjason/sonarqube-client-go/v2/integration_testing/helpers"
"github.com/boxboxjason/sonarqube-client-go/v2/sonar"
)

var _ = Describe("Applications Service", Ordered, func() {
var (
client *sonar.Client
cleanup *helpers.CleanupManager
projectKey string
)

BeforeAll(func() {
var err error
client, err = helpers.NewDefaultClient()
Expect(err).NotTo(HaveOccurred())
Expect(client).NotTo(BeNil())
cleanup = helpers.NewCleanupManager(client)

projectKey = helpers.UniqueResourceName("app-project")
_, resp, err := client.Projects.Create(context.Background(), &sonar.ProjectsCreateOptions{
Name: projectKey,
Project: projectKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
cleanup.RegisterCleanup("project", projectKey, func() error {
_, err := client.Projects.Delete(context.Background(), &sonar.ProjectsDeleteOptions{Project: projectKey})
return err
})
})

AfterAll(func() {
errors := cleanup.Cleanup()
for _, err := range errors {
GinkgoWriter.Printf("Cleanup error: %v\n", err)
}
})

// =========================================================================
// Full lifecycle: Create -> Show -> AddProject -> SearchProjects ->
// SetTags -> Update -> RemoveProject -> Delete
// =========================================================================
Describe("Lifecycle", func() {
It("should create a new application", func() {
appKey := helpers.UniqueResourceName("application")

result, resp, err := client.Applications.Create(context.Background(), &sonar.ApplicationsCreateOptions{
Name: appKey,
Key: appKey,
Description: "created by e2e enterprise suite",
Visibility: sonar.ProjectVisibilityPrivate,
})
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
Expect(result).NotTo(BeNil())
Expect(result.Application.Key).To(Equal(appKey))
Expect(result.Application.Name).To(Equal(appKey))
Expect(result.Application.Visibility).To(Equal(sonar.ProjectVisibilityPrivate))

cleanup.RegisterCleanup("application", appKey, func() error {
_, err := client.Applications.Delete(context.Background(), &sonar.ApplicationsDeleteOptions{Application: appKey})
return err
})

By("showing the created application")
showResult, showResp, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(showResp.StatusCode).To(Equal(http.StatusOK))
Expect(showResult).NotTo(BeNil())
Expect(showResult.Application.Key).To(Equal(appKey))
Expect(showResult.Application.Projects).To(BeEmpty())

By("adding the real project to the application")
addResp, err := client.Applications.AddProject(context.Background(), &sonar.ApplicationsAddProjectOptions{
Application: appKey,
Project: projectKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(addResp.StatusCode).To(BeElementOf(http.StatusOK, http.StatusNoContent))

By("finding the project via SearchProjects")
searchResult, searchResp, err := client.Applications.SearchProjects(context.Background(), &sonar.ApplicationsSearchProjectsOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(searchResp.StatusCode).To(Equal(http.StatusOK))
Expect(searchResult).NotTo(BeNil())

var found bool
for _, p := range searchResult.Projects {
if p.Key == projectKey {
found = true

break
}
}
Expect(found).To(BeTrue(), "expected project %q to be listed in application %q", projectKey, appKey)

By("confirming the project now appears on Show")
showAfterAdd, _, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())

var projectListed bool
for _, p := range showAfterAdd.Application.Projects {
if p.Key == projectKey {
projectListed = true

break
}
}
Expect(projectListed).To(BeTrue())

By("setting tags on the application")
tagsResp, err := client.Applications.SetTags(context.Background(), &sonar.ApplicationsSetTagsOptions{
Application: appKey,
Tags: []string{"e2e", "enterprise"},
})
Expect(err).NotTo(HaveOccurred())
Expect(tagsResp.StatusCode).To(BeElementOf(http.StatusOK, http.StatusNoContent))

By("verifying tags were applied")
showAfterTags, _, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(showAfterTags.Application.Tags).To(ConsistOf("e2e", "enterprise"))

By("updating the application name and description")
newName := appKey + "-updated"
updateResp, err := client.Applications.Update(context.Background(), &sonar.ApplicationsUpdateOptions{
Application: appKey,
Name: newName,
Description: "updated by e2e enterprise suite",
})
Expect(err).NotTo(HaveOccurred())
Expect(updateResp.StatusCode).To(BeElementOf(http.StatusOK, http.StatusNoContent))

showAfterUpdate, _, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(showAfterUpdate.Application.Name).To(Equal(newName))
Expect(showAfterUpdate.Application.Description).To(Equal("updated by e2e enterprise suite"))

By("removing the project from the application")
removeResp, err := client.Applications.RemoveProject(context.Background(), &sonar.ApplicationsRemoveProjectOptions{
Application: appKey,
Project: projectKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(removeResp.StatusCode).To(BeElementOf(http.StatusOK, http.StatusNoContent))

showAfterRemove, _, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(showAfterRemove.Application.Projects).To(BeEmpty())

By("deleting the application")
deleteResp, err := client.Applications.Delete(context.Background(), &sonar.ApplicationsDeleteOptions{
Application: appKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(deleteResp.StatusCode).To(BeElementOf(http.StatusOK, http.StatusNoContent))

_, showAfterDelete, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: appKey,
})
Expect(err).To(HaveOccurred())
Expect(showAfterDelete.StatusCode).To(Equal(http.StatusNotFound))
})
})

// =========================================================================
// Show / Delete: not-found behavior must be a real 404, never a
// license-gate error, once the suite is running against Enterprise.
// =========================================================================
Describe("Show", func() {
Context("Functional Tests", func() {
It("should return 404 for a nonexistent application", func() {
result, resp, err := client.Applications.Show(context.Background(), &sonar.ApplicationsShowOptions{
Application: "nonexistent-application-xyz",
})
Expect(err).To(HaveOccurred())
Expect(resp).NotTo(BeNil())
Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
Expect(result).To(BeNil())
})
})
})

Describe("Delete", func() {
Context("Functional Tests", func() {
It("should return 404 for a nonexistent application", func() {
resp, err := client.Applications.Delete(context.Background(), &sonar.ApplicationsDeleteOptions{
Application: "nonexistent-application-xyz",
})
Expect(err).To(HaveOccurred())
Expect(resp).NotTo(BeNil())
Expect(resp.StatusCode).To(Equal(http.StatusNotFound))
})
})
})
})
85 changes: 85 additions & 0 deletions integration_testing/enterprise/architecture_v2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package enterprise_test

import (
"context"
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/boxboxjason/sonarqube-client-go/v2/integration_testing/helpers"
"github.com/boxboxjason/sonarqube-client-go/v2/sonar"
)

var _ = Describe("Architecture V2 Service", Ordered, func() {
var (
client *sonar.Client
cleanup *helpers.CleanupManager
projectKey string
)

BeforeAll(func() {
var err error
client, err = helpers.NewDefaultClient()
Expect(err).NotTo(HaveOccurred())
Expect(client).NotTo(BeNil())
cleanup = helpers.NewCleanupManager(client)

projectKey = helpers.UniqueResourceName("architecture-project")
_, resp, err := client.Projects.Create(context.Background(), &sonar.ProjectsCreateOptions{
Name: projectKey,
Project: projectKey,
})
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
cleanup.RegisterCleanup("project", projectKey, func() error {
_, err := client.Projects.Delete(context.Background(), &sonar.ProjectsDeleteOptions{Project: projectKey})
return err
})
})

AfterAll(func() {
errors := cleanup.Cleanup()
for _, err := range errors {
GinkgoWriter.Printf("Cleanup error: %v\n", err)
}
})

// =========================================================================
// FileGraph
//
// Architecture graphs are only populated once a scanner analysis of the
// project has run. This suite has no scanner-analysis helper available, so
// the real project created above is guaranteed to have no analysis data.
// That means a "no graph data" style response (an empty/opaque payload, or
// a not-found-ish error) is a legitimate, expected outcome here. Per
// product decision, the Architecture service does NOT require a license to
// respond meaningfully, so the one thing this test must never accept is a
// license-gate response: not 402 Payment Required, and — since this
// suite's BeforeSuite has already confirmed the server is Enterprise+ and
// architecture analysis itself doesn't require a license — not 403
// Forbidden either.
// =========================================================================
Describe("FileGraph", func() {
Context("Functional Tests", func() {
It("should return file graph data or a data-availability error, never a license gate", func() {
result, resp, err := client.V2.Architecture.FileGraph(context.Background(), &sonar.ArchitectureFileGraphOptions{
ProjectKey: projectKey,
BranchKey: "main",
Source: "java",
})
if err != nil {
Expect(resp).NotTo(BeNil())
// Data-availability condition (no analysis has ever run against this
// project), not an enterprise/license gate: explicitly rule out the
// license-gate status codes.
Expect(resp.StatusCode).NotTo(Equal(http.StatusPaymentRequired))
Expect(resp.StatusCode).NotTo(Equal(http.StatusForbidden))
} else {
Expect(resp.StatusCode).To(BeNumerically("<", 400))
Expect(result).NotTo(BeNil())
}
})
})
})
})
Loading