feat: add project-owned error catalogs and HTTP 200 failure contracts
This commit is contained in:
205
cmd/go-omni/apigen/errorCatalog_test.go
Normal file
205
cmd/go-omni/apigen/errorCatalog_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestErrorCatalogDocumentAndValidation(t *testing.T) {
|
||||
path := filepath.Join(frameworkRoot(t), "examples/contracts/errors.api")
|
||||
source := readFile(t, path)
|
||||
spec, err := Parse(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compiled, err := Compile(spec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{"/token", "/raw"} {
|
||||
method := "post"
|
||||
if path == "/raw" {
|
||||
method = "get"
|
||||
}
|
||||
op := operation(t, document(t, compiled), path, method)
|
||||
response := op["responses"].(map[string]any)["200"].(map[string]any)
|
||||
media := response["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
alternatives := media["schema"].(map[string]any)["anyOf"].([]any)
|
||||
if len(alternatives) != 2 {
|
||||
t.Fatal("lost success or failure shape")
|
||||
}
|
||||
if path == "/raw" {
|
||||
if alternatives[0].(map[string]any)["properties"].(map[string]any)["token"] == nil {
|
||||
t.Fatal("raw success wrapped")
|
||||
}
|
||||
}
|
||||
examples := media["examples"].(map[string]any)
|
||||
if len(examples) != 2 {
|
||||
t.Fatal(examples)
|
||||
}
|
||||
for _, name := range []string{"UsernameInvalid", "PasswordInvalid"} {
|
||||
value := examples[name].(map[string]any)["value"].(map[string]any)
|
||||
if value["ok"] != false || value["code"] != float64(400002) || value["msg"] == nil {
|
||||
t.Fatal(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, change := range [][2]string{
|
||||
{`defaultStatus: 200`, `defaultStatus: 204`},
|
||||
{`"status": 403`, `"status": 304`},
|
||||
{`"PASSWORD_INVALID"`, `"USERNAME_INVALID"`},
|
||||
{`"code": 400002`, `"code": 2147483648`},
|
||||
{`"code": 400002`, `"code": "400002"`},
|
||||
{`"code": 400002`, `"code": 1.5`},
|
||||
{`"code": 400002`, `"code": null`},
|
||||
{`"code": 400002`, `"code": 400002, "code": 400003`},
|
||||
{`"message": "用户名格式不正确"`, `"message": "用户名格式不正确", "typo": true`},
|
||||
{`@handler token`, `@errors Unknown\n @handler token`},
|
||||
{`successCode: 0`, `successCode: 0\n codes: {"USERNAME_INVALID": 999}`},
|
||||
} {
|
||||
text := strings.Replace(source, change[0], strings.ReplaceAll(change[1], `\n`, "\n"), 1)
|
||||
s, err := parseText(t, text)
|
||||
if err == nil {
|
||||
_, err = Compile(s)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("accepted %s", change[1])
|
||||
}
|
||||
}
|
||||
// The legacy declaration is also allowed to carry a HTTP 200 failure.
|
||||
old := strings.Replace(declaredAPI, "EmailTaken 409", "EmailTaken 200", 1)
|
||||
s, err := parseText(t, old)
|
||||
if err == nil {
|
||||
_, err = Compile(s)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorCatalogConsumer(t *testing.T) {
|
||||
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") == "" {
|
||||
t.Skip("consumer integration")
|
||||
}
|
||||
root := frameworkRoot(t)
|
||||
dir := filepath.Join(t.TempDir(), "consumer")
|
||||
opts := Options{API: filepath.Join(root, "examples/contracts/errors.api"), Dir: dir, Module: "example.com/errorCatalog", FrameworkDir: root}
|
||||
cli := os.Getenv("GO_OMNI_RELEASE_CLI")
|
||||
generate := func(check bool) {
|
||||
t.Helper()
|
||||
if cli == "" {
|
||||
o := opts
|
||||
o.Check = check
|
||||
if _, err := Generate(o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
args := []string{"api", "generate", "--api", opts.API, "--dir", dir, "--module", opts.Module}
|
||||
if check {
|
||||
args = append(args, "--check")
|
||||
}
|
||||
cmd := exec.Command(cli, args...)
|
||||
cmd.Dir = filepath.Dir(dir)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("release generate %v\n%s", err, output)
|
||||
}
|
||||
}
|
||||
generate(false)
|
||||
|
||||
fixture := readFile(t, "testdata/errorCatalog/consumer_test.go")
|
||||
writeFile(t, filepath.Join(dir, "consumer_test.go"), fixture)
|
||||
generate(false)
|
||||
generate(true)
|
||||
if readFile(t, filepath.Join(dir, "consumer_test.go")) != fixture {
|
||||
t.Fatal("regeneration overwrote manual code")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "go", "test", "-race", "-mod=mod", "-count=1", "./...")
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("consumer %v\n%s", err, output)
|
||||
}
|
||||
if cli != "" {
|
||||
cmd = exec.CommandContext(ctx, "go", "list", "-m", "-json", frameworkModule)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(output))
|
||||
}
|
||||
var module struct {
|
||||
Version, Sum string
|
||||
Replace any
|
||||
}
|
||||
if err = json.Unmarshal(output, &module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if module.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || module.Replace != nil || module.Sum == "" {
|
||||
t.Fatal("not a fixed remote consumer")
|
||||
}
|
||||
t.Logf("remote framework %s checksum %s; no local replace", module.Version, module.Sum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorCatalogImportsAndRouteScopes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, filepath.Join(dir, "common.api"), `errorCatalog Numeric (
|
||||
defaultStatus: 200
|
||||
errors: {"NumberInvalid":{"key":"number.invalid","code":12,"message":"number"}}
|
||||
)
|
||||
errorCatalog Text (
|
||||
defaultStatus: 409
|
||||
errors: {"TextInvalid":{"key":"textInvalid","code":"000012","message":"text"}}
|
||||
)
|
||||
response NumericResponse (
|
||||
codeType: int32
|
||||
successCode: 0
|
||||
errorCode: 9999
|
||||
)
|
||||
`)
|
||||
writeFile(t, filepath.Join(dir, "service.api"), `import "common.api"
|
||||
@server (
|
||||
response: NumericResponse
|
||||
errors: NumberInvalid
|
||||
)
|
||||
service scopes-api {
|
||||
@handler numeric
|
||||
get /number
|
||||
}
|
||||
@server (
|
||||
errors: TextInvalid
|
||||
)
|
||||
service scopes-api {
|
||||
@handler text
|
||||
get /text
|
||||
}
|
||||
`)
|
||||
s, err := Parse(filepath.Join(dir, "service.api"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compiled, err := Compile(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc := document(t, compiled)
|
||||
for _, tc := range []struct {
|
||||
path, status, name string
|
||||
code any
|
||||
}{{"/number", "200", "NumberInvalid", float64(12)}, {"/text", "409", "TextInvalid", "000012"}} {
|
||||
response := operation(t, doc, tc.path, "get")["responses"].(map[string]any)[tc.status].(map[string]any)
|
||||
value := response["content"].(map[string]any)["application/json"].(map[string]any)["examples"].(map[string]any)[tc.name].(map[string]any)["value"].(map[string]any)
|
||||
if value["code"] != tc.code {
|
||||
t.Fatal(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user