Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 47 additions & 0 deletions integration-tests/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//go:build e2e

package e2e_test

import (
"context"
"errors"
"os"
"path/filepath"
"testing"

"github.com/spencercjh/spec-forge/internal/executor"
)

// TestE2E_ErrorHandling_CommandNotFound tests error handling when build tool is not found.
func TestE2E_ErrorHandling_CommandNotFound(t *testing.T) {
// Create a temp directory with pom.xml but no maven installed
tmpDir := t.TempDir()
pomPath := filepath.Join(tmpDir, "pom.xml")
if err := os.WriteFile(pomPath, []byte(`<project></project>`), 0o644); err != nil {
t.Fatal(err)
}

// Create target directory with a dummy spec (simulating pre-existing spec)
targetDir := filepath.Join(tmpDir, "target")
if err := os.Mkdir(targetDir, 0o755); err != nil {
t.Fatal(err)
}

Comment thread
spencercjh marked this conversation as resolved.
Outdated
ctx := context.Background()
exec := executor.NewExecutor()

// Try to run a non-existent command
_, err := exec.Execute(ctx, &executor.ExecuteOptions{
Command: "nonexistent_command_12345",
Args: []string{},
})

if err == nil {
t.Fatal("Expected error for non-existent command")
}

// Verify it's a CommandNotFoundError
if _, ok := errors.AsType[*executor.CommandNotFoundError](err); !ok {
t.Logf("Got error type %T: %v", err, err)
Comment thread
spencercjh marked this conversation as resolved.
Outdated
}
}
177 changes: 177 additions & 0 deletions integration-tests/gozero_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//go:build e2e

package e2e_test

import (
"context"
"os"
"path/filepath"
"testing"
"time"

"github.com/spencercjh/spec-forge/internal/extractor"
"github.com/spencercjh/spec-forge/internal/extractor/gozero"
"github.com/spencercjh/spec-forge/internal/validator"
)

// TestE2E_GoZero_Generate tests the generate flow for a go-zero project.
func TestE2E_GoZero_Generate(t *testing.T) {
projectPath := "gozero-demo"

// Check if project exists
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
t.Skip("go-zero demo project not found")
}

// Check if go.mod exists
goModPath := filepath.Join(projectPath, "go.mod")
if _, err := os.Stat(goModPath); os.IsNotExist(err) {
t.Skip("go.mod not found, skipping test")
}

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

// Step 1: Detect project
detector := gozero.NewDetector()
info, err := detector.Detect(projectPath)
if err != nil {
t.Fatalf("Failed to detect project: %v", err)
}

// Verify it's detected as go-zero
if info.Framework != gozero.FrameworkGoZero {
t.Errorf("Expected framework %s, got %s", gozero.FrameworkGoZero, info.Framework)
}

// Verify go-zero specific info
gozeroInfo, ok := info.FrameworkData.(*gozero.Info)
if !ok {
t.Fatal("Expected FrameworkData to be *gozero.Info")
}

if !gozeroInfo.HasGoZeroDeps {
t.Error("Expected go-zero dependencies to be present")
}

if len(gozeroInfo.APIFiles) == 0 {
t.Error("Expected at least one .api file to be found")
}

t.Logf("Detected go-zero project with %d API files: %v", len(gozeroInfo.APIFiles), gozeroInfo.APIFiles)

// Step 2: Patch (verify goctl availability)
patcher := gozero.NewPatcher()
patchResult, err := patcher.Patch(projectPath)
if err != nil {
t.Fatalf("Failed to patch project: %v", err)
}

if !patchResult.HasGoctl {

Check failure on line 70 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

patchResult.HasGoctl undefined (type *gozero.PatchResult has no field or method HasGoctl)
t.Skip("goctl not found in PATH, skipping generation test")
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
spencercjh marked this conversation as resolved.

t.Logf("goctl is available: %s", patchResult.GoctlVersion)
Comment thread
spencercjh marked this conversation as resolved.

// Step 3: Generate OpenAPI spec
gen := gozero.NewGenerator()
result, err := gen.Generate(ctx, projectPath, info, &extractor.GenerateOptions{
Format: "yaml",
Comment on lines +78 to +79
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This e2e test writes openapi.yaml into the checked-in demo project directory (hence the added .gitignore entries). To avoid polluting the working tree and to make runs more hermetic, set GenerateOptions.OutputDir (and optionally OutputFile) to a t.TempDir() location.

Suggested change
result, err := gen.Generate(ctx, projectPath, info, &extractor.GenerateOptions{
Format: "yaml",
outputDir := t.TempDir()
result, err := gen.Generate(ctx, projectPath, info, &extractor.GenerateOptions{
Format: "yaml",
OutputDir: outputDir,
OutputFile: "openapi.yaml",

Copilot uses AI. Check for mistakes.
})
if err != nil {
t.Fatalf("Failed to generate spec: %v", err)
}

if result.SpecFilePath == "" {
t.Fatal("Expected spec file path to be set")
}

// Step 4: Validate generated spec
v := validator.NewValidator()
validateResult, err := v.Validate(ctx, result.SpecFilePath)
if err != nil {
t.Fatalf("Failed to validate spec: %v", err)
}

if !validateResult.Valid {
t.Errorf("Generated spec is invalid: %v", validateResult.Errors)
}

t.Logf("Successfully generated valid OpenAPI spec at: %s", result.SpecFilePath)
}

// TestE2E_GoZero_Detect tests the detection of go-zero project characteristics.
func TestE2E_GoZero_Detect(t *testing.T) {
projectPath := "gozero-demo"

if _, err := os.Stat(projectPath); os.IsNotExist(err) {
t.Skip("go-zero demo project not found")
}

detector := gozero.NewDetector()
info, err := detector.Detect(projectPath)
if err != nil {
t.Fatalf("Failed to detect project: %v", err)
}

// Verify basic project info
if info.GoVersion == "" {

Check failure on line 118 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

info.GoVersion undefined (type *extractor.ProjectInfo has no field or method GoVersion)
t.Error("Expected Go version to be detected")
}

if info.ModuleName == "" {

Check failure on line 122 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

info.ModuleName undefined (type *extractor.ProjectInfo has no field or method ModuleName)
t.Error("Expected module name to be detected")
}

gozeroInfo, ok := info.FrameworkData.(*gozero.Info)
if !ok {
t.Fatal("Expected FrameworkData to be *gozero.Info")
}

// Verify go-zero specific detection
t.Logf("Go Version: %s", info.GoVersion)

Check failure on line 132 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

info.GoVersion undefined (type *extractor.ProjectInfo has no field or method GoVersion)
t.Logf("Module Name: %s", info.ModuleName)

Check failure on line 133 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

info.ModuleName undefined (type *extractor.ProjectInfo has no field or method ModuleName)
Comment thread
spencercjh marked this conversation as resolved.
Outdated
t.Logf("Has go-zero deps: %v", gozeroInfo.HasGoZeroDeps)
t.Logf("go-zero version: %s", gozeroInfo.GoZeroVersion)
t.Logf("API files: %v", gozeroInfo.APIFiles)
t.Logf("Has goctl: %v", gozeroInfo.HasGoctl)
}

// TestE2E_GoZero_NoGoctl tests graceful handling when goctl is not available.
func TestE2E_GoZero_NoGoctl(t *testing.T) {
projectPath := "gozero-demo"

if _, err := os.Stat(projectPath); os.IsNotExist(err) {
t.Skip("go-zero demo project not found")
}

// Check if goctl is available
patcher := gozero.NewPatcher()
patchResult, err := patcher.Patch(projectPath)
if err != nil {
t.Fatalf("Failed to check goctl: %v", err)
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

if patchResult.HasGoctl {

Check failure on line 155 in integration-tests/gozero_test.go

View workflow job for this annotation

GitHub Actions / E2E Tests

patchResult.HasGoctl undefined (type *gozero.PatchResult has no field or method HasGoctl)
t.Skip("goctl is available, skip this test")
}
Comment thread
spencercjh marked this conversation as resolved.

// When goctl is not available, generation should fail with a clear error
ctx := context.Background()
detector := gozero.NewDetector()
info, err := detector.Detect(projectPath)
if err != nil {
t.Fatalf("Failed to detect project: %v", err)
}

Comment thread
spencercjh marked this conversation as resolved.
gen := gozero.NewGenerator()
_, err = gen.Generate(ctx, projectPath, info, &extractor.GenerateOptions{
Format: "yaml",
})

if err == nil {
t.Error("Expected error when goctl is not available")
}

t.Logf("Got expected error when goctl not available: %v", err)
}
29 changes: 29 additions & 0 deletions integration-tests/mock_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build e2e

package e2e_test

import (
"context"

"github.com/spencercjh/spec-forge/internal/enricher/provider"
)

// countingMockProvider tracks call counts for verification.
type countingMockProvider struct {
callCount int
responses map[string]string
}

func (m *countingMockProvider) Generate(ctx context.Context, prompt string) (string, error) {
m.callCount++
if resp, ok := m.responses["default"]; ok {
return resp, nil
}
return `{"description": "Mock response"}`, nil
}

func (m *countingMockProvider) Name() string {
return "mock"
}

var _ provider.Provider = (*countingMockProvider)(nil)
67 changes: 67 additions & 0 deletions integration-tests/spring_gradle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build e2e

package e2e_test

import (
"context"
"os"
"path/filepath"
"testing"
"time"

"github.com/spencercjh/spec-forge/internal/extractor"
"github.com/spencercjh/spec-forge/internal/extractor/spring"
"github.com/spencercjh/spec-forge/internal/validator"
)

// TestE2E_GradleSpringBoot_Generate tests the generate flow for a Gradle Spring Boot project.
func TestE2E_GradleSpringBoot_Generate(t *testing.T) {
projectPath := "gradle-springboot-openapi-demo"

if _, err := os.Stat(projectPath); os.IsNotExist(err) {
t.Skip("Gradle Spring Boot demo project not found")
}

// Check if gradlew wrapper exists
gradlewPath := filepath.Join(projectPath, "gradlew")
if _, err := os.Stat(gradlewPath); os.IsNotExist(err) {
t.Skip("Gradle wrapper not found, skipping test")
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

// Step 1: Detect project
detector := spring.NewDetector()
info, err := detector.Detect(projectPath)
if err != nil {
t.Fatalf("Failed to detect project: %v", err)
}

if info.BuildTool != spring.BuildToolGradle {
t.Errorf("Expected Gradle build tool, got %s", info.BuildTool)
}

// Step 2: Generate OpenAPI spec (uses gradlew wrapper)
gen := spring.NewGenerator()
result, err := gen.Generate(ctx, projectPath, info, &extractor.GenerateOptions{
Format: "json",
SkipTests: true,
})
if err != nil {
t.Fatalf("Failed to generate spec: %v", err)
}

// Step 3: Validate
v := validator.NewValidator()
validateResult, err := v.Validate(ctx, result.SpecFilePath)
if err != nil {
t.Fatalf("Failed to validate spec: %v", err)
}

if !validateResult.Valid {
t.Errorf("Generated spec is invalid: %v", validateResult.Errors)
}

t.Logf("Successfully generated valid OpenAPI spec at: %s", result.SpecFilePath)
}
Loading
Loading