223 lines
6.4 KiB
Go
223 lines
6.4 KiB
Go
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) {
|
|
testCatalogConsumer(t, "errors.api", "errorCatalog")
|
|
}
|
|
|
|
func testCatalogConsumer(t *testing.T, apiFile, fixtureName string, extraFiles ...map[string]string) {
|
|
t.Helper()
|
|
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", apiFile), 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)
|
|
|
|
files := map[string]string{"consumer_test.go": "consumer_test.go"}
|
|
for _, extra := range extraFiles {
|
|
for target, source := range extra {
|
|
files[target] = source
|
|
}
|
|
}
|
|
before := map[string]string{}
|
|
for target, source := range files {
|
|
fixture := readFile(t, filepath.Join("testdata", fixtureName, source))
|
|
writeFile(t, filepath.Join(dir, target), fixture)
|
|
before[target] = fixture
|
|
}
|
|
generate(false)
|
|
generate(true)
|
|
for target, fixture := range before {
|
|
if readFile(t, filepath.Join(dir, target)) != fixture {
|
|
t.Fatalf("regeneration overwrote %s", target)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|