Publisher integration tests (48 tests): dry-run verification for all 8 publishers (GitHub, Docker, Homebrew, Scoop, AUR, Chocolatey, npm, LinuxKit), command building, config parsing, repository detection, artifact handling, cross-publisher name uniqueness, nil relCfg handling, checksum mapping, interface compliance. SDK generation tests (38 tests): orchestration, generator registry, interface compliance for all 4 languages (TypeScript, Python, Go, PHP), config defaults, SetVersion, spec detection priority across all 8 common paths. Breaking change detection tests (30 tests): oasdiff integration covering add/remove endpoints, required/optional params, response type changes, HTTP method removal, identical specs, multiple breaking changes, JSON format support, error handling, DiffExitCode, DiffResult structure. Co-Authored-By: Virgil <virgil@lethean.io>
630 lines
14 KiB
Go
630 lines
14 KiB
Go
package sdk
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- Breaking Change Detection Tests (oasdiff integration) ---
|
|
|
|
func TestDiff_Good_AddEndpoint_NonBreaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.1.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/status:
|
|
get:
|
|
operationId: getStatus
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Breaking, "adding endpoints should not be breaking")
|
|
assert.Empty(t, result.Changes)
|
|
assert.Equal(t, "No breaking changes", result.Summary)
|
|
}
|
|
|
|
func TestDiff_Good_RemoveEndpoint_Breaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/orders:
|
|
get:
|
|
operationId: listOrders
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Breaking, "removing endpoints should be breaking")
|
|
assert.NotEmpty(t, result.Changes)
|
|
assert.Contains(t, result.Summary, "breaking change")
|
|
}
|
|
|
|
func TestDiff_Good_AddRequiredParam_Breaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.1.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
parameters:
|
|
- name: tenant_id
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Breaking, "adding required parameter should be breaking")
|
|
assert.NotEmpty(t, result.Changes)
|
|
}
|
|
|
|
func TestDiff_Good_AddOptionalParam_NonBreaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.1.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Breaking, "adding optional parameter should not be breaking")
|
|
}
|
|
|
|
func TestDiff_Good_ChangeResponseType_Breaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
name:
|
|
type: string
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Breaking, "changing response schema type should be breaking")
|
|
}
|
|
|
|
func TestDiff_Good_RemoveHTTPMethod_Breaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
post:
|
|
operationId: createUser
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Breaking, "removing HTTP method should be breaking")
|
|
assert.NotEmpty(t, result.Changes)
|
|
}
|
|
|
|
func TestDiff_Good_IdenticalSpecs_NonBreaking(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
spec := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
post:
|
|
operationId: createUser
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(spec), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(spec), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Breaking, "identical specs should not be breaking")
|
|
assert.Empty(t, result.Changes)
|
|
assert.Equal(t, "No breaking changes", result.Summary)
|
|
}
|
|
|
|
// --- Error Handling Tests ---
|
|
|
|
func TestDiff_Bad_NonExistentBase(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
`), 0644))
|
|
|
|
_, err := Diff(filepath.Join(tmpDir, "nonexistent.yaml"), revPath)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to load base spec")
|
|
}
|
|
|
|
func TestDiff_Bad_NonExistentRevision(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(`openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
`), 0644))
|
|
|
|
_, err := Diff(basePath, filepath.Join(tmpDir, "nonexistent.yaml"))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to load revision spec")
|
|
}
|
|
|
|
func TestDiff_Bad_InvalidYAML(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte("not: valid: openapi: spec: {{{{"), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
`), 0644))
|
|
|
|
_, err := Diff(basePath, revPath)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// --- DiffExitCode Tests ---
|
|
|
|
func TestDiffExitCode_Good(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
result *DiffResult
|
|
err error
|
|
expected int
|
|
}{
|
|
{
|
|
name: "no breaking changes returns 0",
|
|
result: &DiffResult{Breaking: false},
|
|
err: nil,
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "breaking changes returns 1",
|
|
result: &DiffResult{Breaking: true, Changes: []string{"removed endpoint"}},
|
|
err: nil,
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "error returns 2",
|
|
result: nil,
|
|
err: assert.AnError,
|
|
expected: 2,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
code := DiffExitCode(tc.result, tc.err)
|
|
assert.Equal(t, tc.expected, code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- DiffResult Structure Tests ---
|
|
|
|
func TestDiffResult_Good_Summary(t *testing.T) {
|
|
t.Run("breaking result has count in summary", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create specs with 2 removed endpoints
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
/orders:
|
|
get:
|
|
operationId: listOrders
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths:
|
|
/health:
|
|
get:
|
|
operationId: getHealth
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, result.Breaking)
|
|
assert.Contains(t, result.Summary, "breaking change")
|
|
// Should have at least 2 changes (removed /users and /orders)
|
|
assert.GreaterOrEqual(t, len(result.Changes), 2)
|
|
})
|
|
}
|
|
|
|
func TestDiffResult_Good_ChangesAreHumanReadable(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/removed-endpoint:
|
|
get:
|
|
operationId: removedEndpoint
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths: {}
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, result.Breaking)
|
|
// Changes should contain human-readable descriptions from oasdiff
|
|
for _, change := range result.Changes {
|
|
assert.NotEmpty(t, change, "each change should have a description")
|
|
}
|
|
}
|
|
|
|
// --- Multiple Changes Detection Tests ---
|
|
|
|
func TestDiff_Good_MultipleBreakingChanges(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
base := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
post:
|
|
operationId: createUser
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
delete:
|
|
operationId: deleteAllUsers
|
|
responses:
|
|
"204":
|
|
description: No Content
|
|
`
|
|
revision := `openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "2.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: listUsers
|
|
parameters:
|
|
- name: required_filter
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
`
|
|
basePath := filepath.Join(tmpDir, "base.yaml")
|
|
revPath := filepath.Join(tmpDir, "rev.yaml")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, result.Breaking)
|
|
// Should detect: removed POST, removed DELETE, and possibly added required param
|
|
assert.GreaterOrEqual(t, len(result.Changes), 2,
|
|
"should detect multiple breaking changes, got: %v", result.Changes)
|
|
}
|
|
|
|
// --- JSON Spec Support Tests ---
|
|
|
|
func TestDiff_Good_JSONSpecs(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
baseJSON := `{
|
|
"openapi": "3.0.0",
|
|
"info": {"title": "Test API", "version": "1.0.0"},
|
|
"paths": {
|
|
"/health": {
|
|
"get": {
|
|
"operationId": "getHealth",
|
|
"responses": {"200": {"description": "OK"}}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
revJSON := `{
|
|
"openapi": "3.0.0",
|
|
"info": {"title": "Test API", "version": "1.1.0"},
|
|
"paths": {
|
|
"/health": {
|
|
"get": {
|
|
"operationId": "getHealth",
|
|
"responses": {"200": {"description": "OK"}}
|
|
}
|
|
},
|
|
"/status": {
|
|
"get": {
|
|
"operationId": "getStatus",
|
|
"responses": {"200": {"description": "OK"}}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
basePath := filepath.Join(tmpDir, "base.json")
|
|
revPath := filepath.Join(tmpDir, "rev.json")
|
|
require.NoError(t, os.WriteFile(basePath, []byte(baseJSON), 0644))
|
|
require.NoError(t, os.WriteFile(revPath, []byte(revJSON), 0644))
|
|
|
|
result, err := Diff(basePath, revPath)
|
|
require.NoError(t, err)
|
|
assert.False(t, result.Breaking, "adding endpoint in JSON format should not be breaking")
|
|
}
|