219 lines
7.7 KiB
Go
219 lines
7.7 KiB
Go
package apigen
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestManagedDefaultValidation(t *testing.T) {
|
|
source := readFile(t, filepath.Join(frameworkRoot(t), "examples/contracts/managed.api"))
|
|
spec, err := parseText(t, source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := Compile(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if spec.Routes[0].ResponseFormat != "" {
|
|
t.Fatal("compile mutated source")
|
|
}
|
|
var doc map[string]any
|
|
if err = json.Unmarshal(c.Document, &doc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if doc["x-omni-default-response"] != "CommonResponse" {
|
|
t.Fatal("missing default metadata")
|
|
}
|
|
for _, bad := range []string{
|
|
strings.Replace(source, "defaultResponse: CommonResponse", "defaultResponse: Missing", 1),
|
|
source + "\n@app (defaultResponse: CommonResponse)\n",
|
|
strings.Replace(source, "maxBytes: 2048", "maxBytes: 0", 1),
|
|
strings.Replace(source, "@produces \"text/plain\"", "@produces \"text/plain; charset=utf-8\"", 1),
|
|
} {
|
|
s, err := parseText(t, bad)
|
|
if err == nil {
|
|
_, err = Compile(s)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("invalid contract accepted")
|
|
}
|
|
}
|
|
root := t.TempDir()
|
|
writeFile(t, filepath.Join(root, "service.api"), "import \"child.api\"\nservice demo {\n@handler hello\nget /hello\n}")
|
|
writeFile(t, filepath.Join(root, "child.api"), "@app (defaultResponse: Shared)\nresponse Shared ()")
|
|
if _, err = Parse(filepath.Join(root, "service.api")); err == nil {
|
|
t.Fatal("import owns App defaults")
|
|
}
|
|
}
|
|
|
|
func TestManagedGeneratedProject(t *testing.T) {
|
|
root := frameworkRoot(t)
|
|
dir := filepath.Join(t.TempDir(), "managed")
|
|
o := Options{Dir: dir, API: filepath.Join(root, "examples/contracts/managed.api"), Module: "example.com/managed", FrameworkDir: root}
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o.API = filepath.Join(dir, "api/managed.api")
|
|
for _, path := range []string{"cmd/server/main.go", "internal/settings", "internal/servicecontext", "internal/service"} {
|
|
if _, err := os.Stat(filepath.Join(dir, path)); !os.IsNotExist(err) {
|
|
t.Fatalf("old layout remains: %s", path)
|
|
}
|
|
}
|
|
mainBefore := readFile(t, filepath.Join(dir, "main.go"))
|
|
files := map[string]string{
|
|
"consumer_test.go": "consumer_test.go", "internal/svc/options.go": "options.go", "internal/svc/client.go": "client.go", "internal/logic/demo/check.go": "operations.go",
|
|
"shutdown_test.go": "shutdown_test.go",
|
|
"phases_test.go": "phases_test.go", "internal/svc/background.go": "background.go",
|
|
}
|
|
for path, fixture := range files {
|
|
writeFile(t, filepath.Join(dir, path), readFile(t, filepath.Join("testdata/managed", fixture)))
|
|
}
|
|
for _, name := range []string{"other", "rawJSON", "upload", "download", "callback", "slow"} {
|
|
writeFile(t, filepath.Join(dir, "internal/logic/demo", name+".go"), "package demo\n")
|
|
}
|
|
configPath := filepath.Join(dir, "internal/config/config.go")
|
|
writeFile(t, configPath, strings.Replace(readFile(t, configPath), "type Config struct {", "type Config struct {\n BackgroundMode string `json:\"backgroundMode\"`\n FailInit bool `json:\"failInit\"`\n ShutdownInit bool `json:\"shutdownInit\"`", 1))
|
|
svcPath := filepath.Join(dir, "internal/svc/serviceContext.go")
|
|
source := strings.Replace(readFile(t, svcPath), "type ServiceContext struct {", "type ServiceContext struct {\n Client *Client", 1)
|
|
source = strings.Replace(source, "return nil", "return initClient(ctx,c)", 1)
|
|
writeFile(t, svcPath, source)
|
|
// Independent tooling stays outside generator ownership.
|
|
writeFile(t, filepath.Join(dir, "cmd/migrate/main.go"), "package main\nfunc main(){}\n")
|
|
before := snapshot(t, dir)
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !sameFiles(before, snapshot(t, dir)) {
|
|
t.Fatal("regeneration changed handwritten files")
|
|
}
|
|
o.Check = true
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o.Check = false
|
|
// Add an API endpoint: handler and Logic stub appear without touching main or business code.
|
|
api := readFile(t, o.API)
|
|
writeFile(t, o.API, api+"\n@server (group: extra)\nservice managed-api {\n@handler extra\nget /extra\n}\n")
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if readFile(t, filepath.Join(dir, "main.go")) != mainBefore {
|
|
t.Fatal("main requires business-specific edits")
|
|
}
|
|
for path := range files {
|
|
if readFile(t, filepath.Join(dir, path)) != string(before[path]) {
|
|
t.Fatal("business file changed", path)
|
|
}
|
|
}
|
|
// Main is owned, checked, and restored when missing. Hand edits are a conflict.
|
|
writeFile(t, filepath.Join(dir, "main.go"), mainBefore+"\n// drift\n")
|
|
o.Check = true
|
|
if err := generateManagedTestProject(t, o); err == nil {
|
|
t.Fatal("main drift accepted")
|
|
}
|
|
o.Check = false
|
|
if err := os.Remove(filepath.Join(dir, "main.go")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o.Check = true
|
|
if err := generateManagedTestProject(t, o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
defer cancel()
|
|
for _, args := range [][]string{{"test", "-race", "-mod=mod", "-count=1", "./..."}, {"build", "-mod=mod", "."}} {
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), "GOWORK=off")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("%v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
if os.Getenv("GO_OMNI_RELEASE_CLI") != "" {
|
|
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", frameworkModule)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), "GOWORK=off")
|
|
data, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatal(err, string(data))
|
|
}
|
|
var dependency struct {
|
|
Version, Sum string
|
|
Replace any
|
|
}
|
|
if err = json.Unmarshal(data, &dependency); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dependency.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dependency.Replace != nil || dependency.Sum == "" {
|
|
t.Fatal("not a fixed remote consumer")
|
|
}
|
|
t.Logf("remote framework %s checksum %s; no local replace", dependency.Version, dependency.Sum)
|
|
}
|
|
|
|
}
|
|
|
|
func generateManagedTestProject(t *testing.T, o Options) error {
|
|
t.Helper()
|
|
cli := os.Getenv("GO_OMNI_RELEASE_CLI")
|
|
if cli == "" {
|
|
_, err := Generate(o)
|
|
return err
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
defer cancel()
|
|
args := []string{"api", "generate", "--api", o.API, "--dir", o.Dir, "--module", o.Module}
|
|
if o.Check {
|
|
args = append(args, "--check")
|
|
}
|
|
if o.DryRun {
|
|
args = append(args, "--dry-run")
|
|
}
|
|
cmd := exec.CommandContext(ctx, cli, args...)
|
|
cmd.Dir = filepath.Dir(o.Dir)
|
|
if data, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("released CLI: %w\n%s", err, data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestManagedOwnershipRejectsOldOrHandwrittenEntry(t *testing.T) {
|
|
for _, oldLayout := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "handwrittenMain", true: "oldManifest"}[oldLayout], func(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "app")
|
|
if _, err := New(Options{Dir: dir, Module: "example.com/entry", FrameworkDir: frameworkRoot(t)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal([]byte(readFile(t, filepath.Join(dir, manifestPath))), &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if oldLayout {
|
|
m.Version = 1
|
|
} else {
|
|
delete(m.Generated, "main.go")
|
|
writeFile(t, filepath.Join(dir, "main.go"), "package main\nfunc main(){}\n")
|
|
}
|
|
data, _ := json.Marshal(m)
|
|
writeFile(t, filepath.Join(dir, manifestPath), string(data))
|
|
before := snapshot(t, dir)
|
|
if _, err := Generate(Options{Dir: dir, API: filepath.Join(dir, "api/service.api")}); err == nil {
|
|
t.Fatal("unowned layout accepted")
|
|
}
|
|
if !sameFiles(before, snapshot(t, dir)) {
|
|
t.Fatal("rejected migration modified project")
|
|
}
|
|
})
|
|
}
|
|
}
|