Skip to content
Open
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
35 changes: 35 additions & 0 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"strconv"
"strings"
"time"


"github.com/microcks/microcks-cli/pkg/config"
"github.com/microcks/microcks-cli/pkg/connectors"
Expand All @@ -39,6 +40,9 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
filteredOperations string
operationsHeaders string
oAuth2Context string
autoFileIssue bool
githubToken string
githubRepo string
)
var testCmd = &cobra.Command{

Expand Down Expand Up @@ -205,6 +209,34 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {

fmt.Printf("Full TestResult details are available here: %s/#/tests/%s \n", serverAddr, testResultID)

// Auto-file GitHub Issue if test failed and flag is set
if !success && autoFileIssue {
if githubToken == "" || githubRepo == "" {
fmt.Println("Warning: --auto-file-issue requires --github-token and --github-repo flags")
} else {
// Collect failed operations
var failedOps []string
testResultDetails, err := mc.GetTestResultDetails(testResultID)
if err == nil {
for _, tc := range testResultDetails.TestCaseResults {
if !tc.Success {
failedOps = append(failedOps, tc.OperationName)
}
}
}

title := fmt.Sprintf("[Microcks] Test Failed: %s", serviceRef)
body := connectors.BuildIssueBody(serviceRef, testEndpoint, runnerType, serverAddr, testResultID, failedOps)

err = connectors.CreateGitHubIssue(githubToken, githubRepo, title, body)
if err != nil {
fmt.Printf("Warning: Failed to create GitHub Issue: %s\n", err)
} else {
fmt.Printf("GitHub Issue created successfully in %s\n", githubRepo)
}
}
}

if !success {
os.Exit(1)
}
Expand All @@ -216,6 +248,9 @@ func NewTestCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
testCmd.Flags().StringVar(&filteredOperations, "filteredOperations", "", "List of operations to launch a test for")
testCmd.Flags().StringVar(&operationsHeaders, "operationsHeaders", "", "Override of operations headers as JSON string")
testCmd.Flags().StringVar(&oAuth2Context, "oAuth2Context", "", "Spec of an OAuth2 client context as JSON string")
testCmd.Flags().BoolVar(&autoFileIssue, "auto-file-issue", false, "Automatically create a GitHub Issue when test fails")
testCmd.Flags().StringVar(&githubToken, "github-token", "", "GitHub personal access token for creating issues")
testCmd.Flags().StringVar(&githubRepo, "github-repo", "", "GitHub repository in format 'owner/repo' for filing issues")

return testCmd
}
Expand Down
111 changes: 111 additions & 0 deletions pkg/connectors/github_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright The Microcks Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package connectors

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)

// GitHubIssueRequest represents the payload for creating a GitHub Issue
type GitHubIssueRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Labels []string `json:"labels"`
}

// CreateGitHubIssue creates a GitHub Issue using the GitHub REST API
func CreateGitHubIssue(token, repo, title, body string) error {
url := fmt.Sprintf("https://api.github.com/repos/%s/issues", repo)

issueReq := GitHubIssueRequest{
Title: title,
Body: body,
Labels: []string{"bug", "microcks-test-failure"},
}

payload, err := json.Marshal(issueReq)
if err != nil {
return fmt.Errorf("failed to marshal issue request: %w", err)
}

req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}

req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to call GitHub API: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}

return nil
}

// BuildIssueBody builds a structured markdown body for the GitHub Issue
func BuildIssueBody(serviceRef, testEndpoint, runnerType, serverAddr, testResultID string, failedOps []string) string {
ops := ""
for _, op := range failedOps {
ops += fmt.Sprintf("- ❌ `%s`\n", op)
}
if ops == "" {
ops = "- ❌ Test failed (no operation details available)\n"
}

return fmt.Sprintf(`## 🔴 Microcks Contract Test Failed

### Failed Operations
%s

### Test Details
| Field | Value |
|-------|-------|
| **Service** | %s |
| **Endpoint** | %s |
| **Runner** | %s |

### Reproduction Command
`+"```"+`bash
microcks test '%s' %s %s \
--microcksURL=<your-microcks-url> \
--keycloakClientId=<client-id> \
--keycloakClientSecret=<client-secret>
`+"```"+`

### Full Test Results
[View on Microcks UI](%s/#/tests/%s)

---
*This issue was automatically created by [microcks-cli](https://github.com/microcks/microcks-cli)*`,
ops,
serviceRef, testEndpoint, runnerType,
serviceRef, testEndpoint, runnerType,
serverAddr, testResultID,
)
}
52 changes: 52 additions & 0 deletions pkg/connectors/microcks_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"crypto/tls"
"encoding/json"

errs "errors"
"fmt"
"io"
Expand Down Expand Up @@ -50,6 +51,7 @@ type MicrocksClient interface {
SetOAuthToken(oauthToken string)
CreateTestResult(serviceID string, testEndpoint string, runnerType string, secretName string, timeout int64, filteredOperations string, operationsHeaders string, oAuth2Context string) (string, error)
GetTestResult(testResultID string) (*TestResultSummary, error)
GetTestResultDetails(testResultID string) (*TestResult, error)
UploadArtifact(specificationFilePath string, mainArtifact bool) (string, error)
DownloadArtifact(artifactURL string, mainArtifact bool, secret string) (string, error)
}
Expand All @@ -66,7 +68,28 @@ type TestResultSummary struct {
Success bool `json:"success"`
InProgress bool `json:"inProgress"`
}
// TestStepResult represents a single step result in a test case
type TestStepResult struct {
RequestName string `json:"requestName"`
Success bool `json:"success"`
Message string `json:"message"`
}

// TestCaseResult represents results for one operation
type TestCaseResult struct {
OperationName string `json:"operationName"`
Success bool `json:"success"`
TestStepResults []TestStepResult `json:"testStepResults"`
}

// TestResult represents the full detailed test result
type TestResult struct {
ID string `json:"id"`
Success bool `json:"success"`
InProgress bool `json:"inProgress"`
TestedEndpoint string `json:"testedEndpoint"`
TestCaseResults []TestCaseResult `json:"testCaseResults"`
}
// HeaderDTO represents an operation header passed for Test
type HeaderDTO struct {
Name string `json:"name"`
Expand Down Expand Up @@ -570,3 +593,32 @@ func ensureValidOAuth2Context(oAuth2Context string) bool {
}
return true
}
// GetTestResultDetails fetches full test result including per-operation results.
func (c *microcksClient) GetTestResultDetails(testResultID string) (*TestResult, error) {
rel := &url.URL{Path: "tests/" + testResultID}
u := c.APIURL.ResolveReference(rel)

req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+c.AuthToken)

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

result := TestResult{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse detailed test result: %w", err)
}
return &result, nil
}
Loading