Skip to content

Commit c3aa0fa

Browse files
MimoJanraclaude
andcommitted
fix: API compliance fixes for v2.0.3
- update_test_case: inject id into PATCH body (required by Allure API) - manual_scenario: type ScenarioDto with required type discriminator per spec - add_test_case_defect: fix endpoint POST /testcase/{id}/defect/{defectId} - remove_test_case_members: replace DELETE /members with POST bulk/member/remove - resolve_test_result / bulk_resolve: add required status field - remove update_launch_environment (PUT /launch/{id}/env not in spec) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 455825a commit c3aa0fa

9 files changed

Lines changed: 116 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.3] - 2026-06-01 - API Compliance Fixes
11+
12+
### Fixed
13+
14+
- **`update_test_case``id` missing from request body**`PATCH /api/testcase/{id}` requires the test case ID both in the URL path and in the JSON body. The body field was absent, causing the API to reject or misroute update requests.
15+
16+
- **`update_test_case` — scenario steps lacked required `type` discriminator** — The Allure API uses a polymorphic step schema (`ScenarioStepDto`) with a required `type` field (`"body"` or `"expected"`). Steps were sent without this field, so the API could not determine the step variant. The `manual_scenario` field is now typed as `ScenarioDto{Steps []ScenarioStepDto}` with `type` enforced. Tool description and schema updated to make the step format explicit.
17+
18+
- **`add_test_case_defect` — wrong endpoint** — was calling `POST /api/testcase/{id}/defect` (GET-only in spec). Corrected to `POST /api/testcase/{testCaseId}/defect/{defectId}` with no request body.
19+
20+
- **`remove_test_case_members` — wrong endpoint and method** — was calling `DELETE /api/testcase/{id}/members` (not in spec). Replaced with `POST /api/testcase/bulk/member/remove` using the correct `{ids, selection}` body. Tool schema updated to require `project_id` (needed for the `selection` object).
21+
22+
- **`resolve_test_result` — missing required `status` field**`POST /api/testresult/{id}/resolve` requires a `status` body (`failed`, `broken`, `passed`, `skipped`, `unknown`). Was sending no body. Both the client method and tool schema now require `status`.
23+
24+
- **`bulk_resolve_test_results` — missing required `status` field**`POST /api/testresult/bulk/resolve` requires `status` alongside `selection`. The `TestResultBulkResolveDto` struct and tool schema updated accordingly.
25+
26+
### Removed
27+
28+
- **`update_launch_environment`**`PUT /api/launch/{id}/env` does not exist in the Allure TestOps API (spec defines only `GET` on this path). Tool and client method removed to prevent silent failures.
29+
1030
## [2.0.2] - 2026-06-01 - Tool Discovery & Test Case Management
1131

1232
### Fixed

internal/adapters/allure/client.go

Lines changed: 17 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ func (c *Client) CreateTestCase(ctx context.Context, projectID int64, name, desc
823823
}
824824

825825
func (c *Client) UpdateTestCase(ctx context.Context, testCaseID int64, req UpdateTestCaseRequest) error {
826+
req.ID = testCaseID
826827
body, err := json.Marshal(req)
827828
if err != nil {
828829
return fmt.Errorf("marshal request: %w", err)
@@ -2174,13 +2175,14 @@ func (c *Client) BulkUnmuteTestResults(ctx context.Context, launchID int64, test
21742175
return nil
21752176
}
21762177

2177-
func (c *Client) BulkResolveTestResults(ctx context.Context, launchID int64, testResultIDs []int64) error {
2178+
func (c *Client) BulkResolveTestResults(ctx context.Context, launchID int64, testResultIDs []int64, status string) error {
21782179
selection := TestResultTreeSelectionDto{
21792180
LaunchID: launchID,
21802181
LeafsInclude: testResultIDs,
21812182
}
21822183
body, err := json.Marshal(TestResultBulkResolveDto{
21832184
Selection: selection,
2185+
Status: status,
21842186
})
21852187
if err != nil {
21862188
return fmt.Errorf("marshal request: %w", err)
@@ -2354,8 +2356,12 @@ func (c *Client) CopyLaunch(ctx context.Context, launchID int64) (*LaunchRespons
23542356
return &result, nil
23552357
}
23562358

2357-
func (c *Client) ResolveTestResult(ctx context.Context, testResultID int64) error {
2358-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url(fmt.Sprintf("/api/testresult/%d/resolve", testResultID)), nil)
2359+
func (c *Client) ResolveTestResult(ctx context.Context, testResultID int64, status string) error {
2360+
body, err := json.Marshal(map[string]any{"status": status})
2361+
if err != nil {
2362+
return fmt.Errorf("marshal request: %w", err)
2363+
}
2364+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url(fmt.Sprintf("/api/testresult/%d/resolve", testResultID)), bytes.NewBuffer(body))
23592365
if err != nil {
23602366
return fmt.Errorf("create request: %w", err)
23612367
}
@@ -2428,34 +2434,6 @@ func (c *Client) GetLaunchEnvironment(ctx context.Context, launchID int64) (map[
24282434
return result, nil
24292435
}
24302436

2431-
func (c *Client) UpdateLaunchEnvironment(ctx context.Context, launchID int64, env map[string]any) error {
2432-
body, err := json.Marshal(env)
2433-
if err != nil {
2434-
return fmt.Errorf("marshal request: %w", err)
2435-
}
2436-
2437-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, c.url(fmt.Sprintf("/api/launch/%d/env", launchID)), bytes.NewBuffer(body))
2438-
if err != nil {
2439-
return fmt.Errorf("create request: %w", err)
2440-
}
2441-
if err := c.setAuthHeader(ctx, httpReq); err != nil {
2442-
return fmt.Errorf("set auth: %w", err)
2443-
}
2444-
httpReq.Header.Set("Content-Type", "application/json")
2445-
2446-
resp, err := c.httpClient.Do(httpReq)
2447-
if err != nil {
2448-
return fmt.Errorf("http request: %w", err)
2449-
}
2450-
defer resp.Body.Close()
2451-
2452-
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
2453-
return errFromResponse(resp)
2454-
}
2455-
2456-
return nil
2457-
}
2458-
24592437
func (c *Client) GetTestCaseHistory(ctx context.Context, testCaseID int64, page, size int) (map[string]any, error) {
24602438
url := fmt.Sprintf("/api/testcase/%d/history?page=%d&size=%d", testCaseID, page, size)
24612439
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url(url), nil)
@@ -2582,21 +2560,13 @@ func (c *Client) MergeLaunches(ctx context.Context, launchIDs []int64, launchNam
25822560
}
25832561

25842562
func (c *Client) AddTestCaseDefect(ctx context.Context, testCaseID, defectID int64) error {
2585-
body, err := json.Marshal(map[string]any{
2586-
"defectId": defectID,
2587-
})
2588-
if err != nil {
2589-
return fmt.Errorf("marshal request: %w", err)
2590-
}
2591-
2592-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url(fmt.Sprintf("/api/testcase/%d/defect", testCaseID)), bytes.NewBuffer(body))
2563+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url(fmt.Sprintf("/api/testcase/%d/defect/%d", testCaseID, defectID)), nil)
25932564
if err != nil {
25942565
return fmt.Errorf("create request: %w", err)
25952566
}
25962567
if err := c.setAuthHeader(ctx, httpReq); err != nil {
25972568
return fmt.Errorf("set auth: %w", err)
25982569
}
2599-
httpReq.Header.Set("Content-Type", "application/json")
26002570

26012571
resp, err := c.httpClient.Do(httpReq)
26022572
if err != nil {
@@ -2689,34 +2659,14 @@ func (c *Client) AddTestCaseMembers(ctx context.Context, testCaseID int64, membe
26892659
return nil
26902660
}
26912661

2692-
func (c *Client) RemoveTestCaseMembers(ctx context.Context, testCaseID int64, memberIDs []int64) error {
2693-
body, err := json.Marshal(map[string]any{
2694-
"memberIds": memberIDs,
2662+
func (c *Client) RemoveTestCaseMembers(ctx context.Context, projectID, testCaseID int64, memberIDs []int64) error {
2663+
return c.bulkPost(ctx, "/api/testcase/bulk/member/remove", map[string]any{
2664+
"ids": memberIDs,
2665+
"selection": TestCaseTreeSelectionDto{
2666+
ProjectID: projectID,
2667+
LeafsInclude: []int64{testCaseID},
2668+
},
26952669
})
2696-
if err != nil {
2697-
return fmt.Errorf("marshal request: %w", err)
2698-
}
2699-
2700-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.url(fmt.Sprintf("/api/testcase/%d/members", testCaseID)), bytes.NewBuffer(body))
2701-
if err != nil {
2702-
return fmt.Errorf("create request: %w", err)
2703-
}
2704-
if err := c.setAuthHeader(ctx, httpReq); err != nil {
2705-
return fmt.Errorf("set auth: %w", err)
2706-
}
2707-
httpReq.Header.Set("Content-Type", "application/json")
2708-
2709-
resp, err := c.httpClient.Do(httpReq)
2710-
if err != nil {
2711-
return fmt.Errorf("http request: %w", err)
2712-
}
2713-
defer resp.Body.Close()
2714-
2715-
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
2716-
return errFromResponse(resp)
2717-
}
2718-
2719-
return nil
27202670
}
27212671

27222672
// GetTestCaseExternalLinks returns the external URL links attached to a test case.

internal/adapters/allure/models.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ type CreateTestCaseRequest struct {
418418

419419
// UpdateTestCaseRequest maps TestCasePatchV2Dto.
420420
type UpdateTestCaseRequest struct {
421+
ID int64 `json:"id,omitempty"`
421422
Name string `json:"name,omitempty"`
422423
Description string `json:"description,omitempty"`
423424
FullName string `json:"fullName,omitempty"`
@@ -432,10 +433,23 @@ type UpdateTestCaseRequest struct {
432433
Tags []TestTagDto `json:"tags,omitempty"`
433434
Members []MemberDto `json:"members,omitempty"`
434435
Links []ExternalLinkDto `json:"links,omitempty"`
435-
Scenario map[string]any `json:"scenario,omitempty"`
436+
Scenario *ScenarioDto `json:"scenario,omitempty"`
436437
CustomFields []CustomFieldValueWithCfDto `json:"customFields,omitempty"`
437438
}
438439

440+
// ScenarioDto is the top-level scenario object used in PATCH /api/testcase/{id}.
441+
type ScenarioDto struct {
442+
Steps []ScenarioStepDto `json:"steps"`
443+
}
444+
445+
// ScenarioStepDto represents a single manual step inside a scenario.
446+
// Type is required by the API discriminator: use "body" for a regular step,
447+
// "expected" for an expected-result entry.
448+
type ScenarioStepDto struct {
449+
Type string `json:"type"`
450+
Body string `json:"body,omitempty"`
451+
}
452+
439453
// ── Shared DTOs ───────────────────────────────────────────────────────────────
440454

441455
type ExternalLinkDto struct {
@@ -667,6 +681,7 @@ type TestResultBulkMuteDto struct {
667681

668682
type TestResultBulkResolveDto struct {
669683
Selection TestResultTreeSelectionDto `json:"selection"`
684+
Status string `json:"status"`
670685
Issues []interface{} `json:"issues,omitempty"`
671686
}
672687

internal/tools/registry_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ func TestRegistry_HasExpectedTools(t *testing.T) {
7272
"resolve_test_result",
7373
"unmute_test_result",
7474
"get_launch_environment",
75-
"update_launch_environment",
7675
"get_test_case_history",
7776
"get_launch_defects",
7877
"get_test_case_defects",
@@ -150,8 +149,8 @@ func TestRegistry_HasExpectedTools(t *testing.T) {
150149
has_search := r.GetTool("search_testops_operations") != nil
151150
has_execute := r.GetTool("execute_testops_operation") != nil
152151

153-
// Expected count: 110 base tools + 2 OpenAPI tools (if spec found)
154-
expected_count := 110
152+
// Expected count: 109 base tools + 2 OpenAPI tools (if spec found)
153+
expected_count := 109
155154
if has_search && has_execute {
156155
expected_count = 112
157156
}

internal/tools/tools_bulk.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,13 @@ func (r *Registry) registerBulkTools() {
238238
},
239239
"description": "Test result IDs",
240240
},
241+
"status": map[string]any{
242+
"type": "string",
243+
"enum": []string{"failed", "broken", "passed", "skipped", "unknown"},
244+
"description": "Resolution status to set on all results",
245+
},
241246
},
242-
"required": []string{"launch_id", "test_result_ids"},
247+
"required": []string{"launch_id", "test_result_ids", "status"},
243248
},
244249
Handler: Typed(r.bulkResolveTestResults),
245250
})
@@ -1003,6 +1008,7 @@ func (r *Registry) bulkUnmuteTestResults(ctx context.Context, args bulkUnmuteTes
10031008
type bulkResolveTestResultsArgs struct {
10041009
LaunchID int64 `json:"launch_id"`
10051010
TestResultIDs []int64 `json:"test_result_ids"`
1011+
Status string `json:"status"`
10061012
}
10071013

10081014
func (r *Registry) bulkResolveTestResults(ctx context.Context, args bulkResolveTestResultsArgs) (any, error) {
@@ -1012,10 +1018,13 @@ func (r *Registry) bulkResolveTestResults(ctx context.Context, args bulkResolveT
10121018
if len(args.TestResultIDs) == 0 {
10131019
return nil, fmt.Errorf("test_result_ids must not be empty")
10141020
}
1021+
if args.Status == "" {
1022+
return nil, fmt.Errorf("status is required")
1023+
}
10151024

10161025
r.logger.Info("bulk resolving test results", map[string]any{"launch_id": args.LaunchID, "count": len(args.TestResultIDs)})
10171026

1018-
if err := r.allure.BulkResolveTestResults(ctx, args.LaunchID, args.TestResultIDs); err != nil {
1027+
if err := r.allure.BulkResolveTestResults(ctx, args.LaunchID, args.TestResultIDs, args.Status); err != nil {
10191028
r.logger.Error("bulk resolve test results", err, map[string]any{"launch_id": args.LaunchID})
10201029
return nil, fmt.Errorf("bulk resolve: %w", err)
10211030
}

internal/tools/tools_launches.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -150,26 +150,6 @@ func (r *Registry) registerLaunchTools() {
150150
Handler: Typed(r.getLaunchEnvironment),
151151
})
152152

153-
r.register(&Tool{
154-
Name: "update_launch_environment",
155-
Description: "Update launch environment variables",
156-
InputSchema: map[string]any{
157-
"type": "object",
158-
"properties": map[string]any{
159-
"launch_id": map[string]any{
160-
"type": "integer",
161-
"description": "Allure launch ID",
162-
},
163-
"environment": map[string]any{
164-
"type": "object",
165-
"description": "Environment variables as key-value pairs",
166-
},
167-
},
168-
"required": []string{"launch_id", "environment"},
169-
},
170-
Handler: Typed(r.updateLaunchEnvironment),
171-
})
172-
173153
r.register(&Tool{
174154
Name: "copy_launch",
175155
Description: "Copy/duplicate a launch",
@@ -532,29 +512,6 @@ func (r *Registry) getLaunchEnvironment(ctx context.Context, args getLaunchEnvir
532512
return map[string]any{"environment": env}, nil
533513
}
534514

535-
type updateLaunchEnvironmentArgs struct {
536-
LaunchID int64 `json:"launch_id"`
537-
Environment map[string]any `json:"environment"`
538-
}
539-
540-
func (r *Registry) updateLaunchEnvironment(ctx context.Context, args updateLaunchEnvironmentArgs) (any, error) {
541-
if args.LaunchID <= 0 {
542-
return nil, fmt.Errorf("launch_id must be positive")
543-
}
544-
if len(args.Environment) == 0 {
545-
return nil, fmt.Errorf("environment must not be empty")
546-
}
547-
548-
r.logger.Info("updating launch environment", map[string]any{"launch_id": args.LaunchID})
549-
550-
if err := r.allure.UpdateLaunchEnvironment(ctx, args.LaunchID, args.Environment); err != nil {
551-
r.logger.Error("update launch environment", err, map[string]any{"launch_id": args.LaunchID})
552-
return nil, fmt.Errorf("update launch environment: %w", err)
553-
}
554-
555-
return map[string]any{"status": "updated"}, nil
556-
}
557-
558515
type copyLaunchArgs struct {
559516
LaunchID int64 `json:"launch_id"`
560517
}

internal/tools/tools_relations.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ func (r *Registry) registerRelationTools() {
123123
InputSchema: map[string]any{
124124
"type": "object",
125125
"properties": map[string]any{
126+
"project_id": map[string]any{
127+
"type": "integer",
128+
"description": "Allure project ID (required by the API)",
129+
},
126130
"test_case_id": map[string]any{
127131
"type": "integer",
128132
"description": "Allure test case ID",
@@ -135,7 +139,7 @@ func (r *Registry) registerRelationTools() {
135139
"description": "Member IDs to remove",
136140
},
137141
},
138-
"required": []string{"test_case_id", "member_ids"},
142+
"required": []string{"project_id", "test_case_id", "member_ids"},
139143
},
140144
Handler: Typed(r.removeTestCaseMembers),
141145
})
@@ -345,11 +349,15 @@ func (r *Registry) addTestCaseMembers(ctx context.Context, args addTestCaseMembe
345349
}
346350

347351
type removeTestCaseMembersArgs struct {
352+
ProjectID int64 `json:"project_id"`
348353
TestCaseID int64 `json:"test_case_id"`
349354
MemberIDs []int64 `json:"member_ids"`
350355
}
351356

352357
func (r *Registry) removeTestCaseMembers(ctx context.Context, args removeTestCaseMembersArgs) (any, error) {
358+
if args.ProjectID <= 0 {
359+
return nil, fmt.Errorf("project_id must be positive")
360+
}
353361
if args.TestCaseID <= 0 {
354362
return nil, fmt.Errorf("test_case_id must be positive")
355363
}
@@ -362,7 +370,7 @@ func (r *Registry) removeTestCaseMembers(ctx context.Context, args removeTestCas
362370
"count": len(args.MemberIDs),
363371
})
364372

365-
if err := r.allure.RemoveTestCaseMembers(ctx, args.TestCaseID, args.MemberIDs); err != nil {
373+
if err := r.allure.RemoveTestCaseMembers(ctx, args.ProjectID, args.TestCaseID, args.MemberIDs); err != nil {
366374
r.logger.Error("remove test case members", err, map[string]any{"test_case_id": args.TestCaseID})
367375
return nil, fmt.Errorf("remove test case members: %w", err)
368376
}

0 commit comments

Comments
 (0)