go-devops/sdk/generation_test.go
Snider 7aaa2154b6 test(release): Phase 3 — publisher integration, SDK generation, breaking change detection
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>
2026-02-20 05:28:20 +00:00

337 lines
9.3 KiB
Go

package sdk
import (
"context"
"os"
"path/filepath"
"testing"
"forge.lthn.ai/core/go-devops/sdk/generators"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- SDK Generation Orchestration Tests ---
func TestSDK_Generate_Good_AllLanguages(t *testing.T) {
t.Run("Generate iterates all configured languages", func(t *testing.T) {
tmpDir := t.TempDir()
// Create a minimal OpenAPI spec
specPath := filepath.Join(tmpDir, "openapi.yaml")
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
require.NoError(t, err)
cfg := &Config{
Spec: "openapi.yaml",
Languages: []string{"nonexistent-lang"},
Output: "sdk",
Package: PackageConfig{
Name: "testclient",
Version: "1.0.0",
},
}
s := New(tmpDir, cfg)
s.SetVersion("v1.0.0")
// Generate should fail on unknown language
err = s.Generate(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown language")
})
}
func TestSDK_GenerateLanguage_Good_OutputDir(t *testing.T) {
t.Run("output directory uses language subdirectory", func(t *testing.T) {
tmpDir := t.TempDir()
specPath := filepath.Join(tmpDir, "openapi.yaml")
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
require.NoError(t, err)
cfg := &Config{
Spec: "openapi.yaml",
Languages: []string{"typescript"},
Output: "custom-sdk",
Package: PackageConfig{
Name: "my-client",
Version: "2.0.0",
},
}
s := New(tmpDir, cfg)
s.SetVersion("v2.0.0")
// This will fail because generators aren't installed, but we can verify
// the spec detection works correctly
specResult, err := s.DetectSpec()
require.NoError(t, err)
assert.Equal(t, specPath, specResult)
})
}
func TestSDK_GenerateLanguage_Bad_NoSpec(t *testing.T) {
t.Run("fails when no OpenAPI spec exists", func(t *testing.T) {
tmpDir := t.TempDir()
s := New(tmpDir, &Config{
Languages: []string{"typescript"},
Output: "sdk",
})
err := s.GenerateLanguage(context.Background(), "typescript")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no OpenAPI spec found")
})
}
func TestSDK_GenerateLanguage_Bad_UnknownLanguage(t *testing.T) {
t.Run("fails for unregistered language", func(t *testing.T) {
tmpDir := t.TempDir()
specPath := filepath.Join(tmpDir, "openapi.yaml")
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
require.NoError(t, err)
s := New(tmpDir, nil)
err = s.GenerateLanguage(context.Background(), "cobol")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown language: cobol")
})
}
// --- Generator Registry Tests ---
func TestRegistry_Good_RegisterAndGet(t *testing.T) {
t.Run("register and retrieve all generators", func(t *testing.T) {
registry := generators.NewRegistry()
registry.Register(generators.NewTypeScriptGenerator())
registry.Register(generators.NewPythonGenerator())
registry.Register(generators.NewGoGenerator())
registry.Register(generators.NewPHPGenerator())
// Verify all languages are registered
languages := registry.Languages()
assert.Len(t, languages, 4)
assert.Contains(t, languages, "typescript")
assert.Contains(t, languages, "python")
assert.Contains(t, languages, "go")
assert.Contains(t, languages, "php")
// Verify retrieval
for _, lang := range []string{"typescript", "python", "go", "php"} {
gen, ok := registry.Get(lang)
assert.True(t, ok, "should find generator for %s", lang)
assert.Equal(t, lang, gen.Language())
}
})
t.Run("Get returns false for unregistered language", func(t *testing.T) {
registry := generators.NewRegistry()
gen, ok := registry.Get("rust")
assert.False(t, ok)
assert.Nil(t, gen)
})
}
func TestRegistry_Good_OverwritesDuplicateLanguage(t *testing.T) {
registry := generators.NewRegistry()
registry.Register(generators.NewTypeScriptGenerator())
registry.Register(generators.NewTypeScriptGenerator()) // register again
languages := registry.Languages()
count := 0
for _, lang := range languages {
if lang == "typescript" {
count++
}
}
assert.Equal(t, 1, count, "should have exactly one typescript entry")
}
// --- Generator Interface Compliance Tests ---
func TestGenerators_Good_LanguageIdentifiers(t *testing.T) {
tests := []struct {
generator generators.Generator
expected string
}{
{generators.NewTypeScriptGenerator(), "typescript"},
{generators.NewPythonGenerator(), "python"},
{generators.NewGoGenerator(), "go"},
{generators.NewPHPGenerator(), "php"},
}
for _, tc := range tests {
t.Run(tc.expected, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.generator.Language())
})
}
}
func TestGenerators_Good_InstallInstructions(t *testing.T) {
tests := []struct {
language string
gen generators.Generator
contains string
}{
{"typescript", generators.NewTypeScriptGenerator(), "npm install"},
{"python", generators.NewPythonGenerator(), "pip install"},
{"go", generators.NewGoGenerator(), "go install"},
{"php", generators.NewPHPGenerator(), "Docker"},
}
for _, tc := range tests {
t.Run(tc.language, func(t *testing.T) {
instructions := tc.gen.Install()
assert.NotEmpty(t, instructions)
assert.Contains(t, instructions, tc.contains)
})
}
}
func TestGenerators_Good_AvailableDoesNotPanic(t *testing.T) {
// Available() should never panic regardless of system state
gens := []generators.Generator{
generators.NewTypeScriptGenerator(),
generators.NewPythonGenerator(),
generators.NewGoGenerator(),
generators.NewPHPGenerator(),
}
for _, gen := range gens {
t.Run(gen.Language(), func(t *testing.T) {
// Should not panic — result depends on system
_ = gen.Available()
})
}
}
// --- SDK Config Tests ---
func TestSDKConfig_Good_DefaultConfig(t *testing.T) {
t.Run("default config has all four languages", func(t *testing.T) {
cfg := DefaultConfig()
assert.Contains(t, cfg.Languages, "typescript")
assert.Contains(t, cfg.Languages, "python")
assert.Contains(t, cfg.Languages, "go")
assert.Contains(t, cfg.Languages, "php")
assert.Len(t, cfg.Languages, 4)
})
t.Run("default config enables diff", func(t *testing.T) {
cfg := DefaultConfig()
assert.True(t, cfg.Diff.Enabled)
assert.False(t, cfg.Diff.FailOnBreaking)
})
t.Run("default config uses sdk/ output", func(t *testing.T) {
cfg := DefaultConfig()
assert.Equal(t, "sdk", cfg.Output)
})
}
func TestSDKConfig_Good_SetVersion(t *testing.T) {
t.Run("SetVersion updates both fields", func(t *testing.T) {
s := New("/tmp", &Config{
Package: PackageConfig{
Name: "test",
Version: "old",
},
})
s.SetVersion("v3.0.0")
assert.Equal(t, "v3.0.0", s.version)
assert.Equal(t, "v3.0.0", s.config.Package.Version)
})
t.Run("SetVersion on nil config is safe", func(t *testing.T) {
s := &SDK{}
// Should not panic
s.SetVersion("v1.0.0")
assert.Equal(t, "v1.0.0", s.version)
})
}
func TestSDKConfig_Good_NewWithNilConfig(t *testing.T) {
s := New("/project", nil)
assert.NotNil(t, s.config)
assert.Equal(t, "sdk", s.config.Output)
assert.True(t, s.config.Diff.Enabled)
}
// --- Spec Detection Integration Tests ---
func TestSpecDetection_Good_Priority(t *testing.T) {
t.Run("configured spec takes priority over common paths", func(t *testing.T) {
tmpDir := t.TempDir()
// Create both a common path spec and a configured spec
commonSpec := filepath.Join(tmpDir, "openapi.yaml")
err := os.WriteFile(commonSpec, []byte(minimalSpec), 0644)
require.NoError(t, err)
configuredSpec := filepath.Join(tmpDir, "custom", "api.yaml")
require.NoError(t, os.MkdirAll(filepath.Dir(configuredSpec), 0755))
err = os.WriteFile(configuredSpec, []byte(minimalSpec), 0644)
require.NoError(t, err)
s := New(tmpDir, &Config{Spec: "custom/api.yaml"})
specPath, err := s.DetectSpec()
require.NoError(t, err)
assert.Equal(t, configuredSpec, specPath)
})
t.Run("common paths checked in order", func(t *testing.T) {
tmpDir := t.TempDir()
// Create the second common path only (api/openapi.yaml is first)
apiDir := filepath.Join(tmpDir, "api")
require.NoError(t, os.MkdirAll(apiDir, 0755))
apiSpec := filepath.Join(apiDir, "openapi.json")
err := os.WriteFile(apiSpec, []byte(`{"openapi":"3.0.0"}`), 0644)
require.NoError(t, err)
s := New(tmpDir, nil)
specPath, err := s.DetectSpec()
require.NoError(t, err)
assert.Equal(t, apiSpec, specPath)
})
}
func TestSpecDetection_Good_AllCommonPaths(t *testing.T) {
for _, commonPath := range commonSpecPaths {
t.Run(commonPath, func(t *testing.T) {
tmpDir := t.TempDir()
specPath := filepath.Join(tmpDir, commonPath)
require.NoError(t, os.MkdirAll(filepath.Dir(specPath), 0755))
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
require.NoError(t, err)
s := New(tmpDir, nil)
detected, err := s.DetectSpec()
require.NoError(t, err)
assert.Equal(t, specPath, detected)
})
}
}
// --- Compile-time interface checks ---
var _ generators.Generator = (*generators.TypeScriptGenerator)(nil)
var _ generators.Generator = (*generators.PythonGenerator)(nil)
var _ generators.Generator = (*generators.GoGenerator)(nil)
var _ generators.Generator = (*generators.PHPGenerator)(nil)
// minimalSpec is a valid OpenAPI 3.0 spec used across tests.
const minimalSpec = `openapi: "3.0.0"
info:
title: Test API
version: "1.0.0"
paths:
/health:
get:
operationId: getHealth
responses:
"200":
description: OK
`