Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
@@ -0,0 +1,438 @@
package httpapi
import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
func TestV1OpenAPIYAMLParses(t *testing.T) {
t.Parallel()
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
if root["openapi"] == nil {
t.Fatal("missing openapi version field")
}
if root["paths"] == nil {
t.Fatal("missing paths field")
}
}
// TestV1OpenAPIYAMLDefaultServerIsProduction ensures public docs default to
// Descrybe-hosted https://descrybe.io/api/v1 (customers do not self-host the API).
// Verified against legacy openapi + descrybe-api-documentation.md; no api.descrybe.io.
func TestV1OpenAPIYAMLDefaultServerIsProduction(t *testing.T) {
t.Parallel()
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
servers, ok := root["servers"].([]any)
if !ok || len(servers) == 0 {
t.Fatal("missing servers list")
}
first, ok := servers[0].(map[string]any)
if !ok {
t.Fatalf("servers[0] must be a mapping, got %T", servers[0])
}
url, _ := first["url"].(string)
if url != "https://descrybe.io/api/v1" {
t.Fatalf("servers[0].url = %q, want https://descrybe.io/api/v1", url)
}
}
// TestV1OpenAPIYAMLHandlerServesValidYAML ensures GET /api/v1/openapi.yaml body
// (what Scalar/js-yaml consumes) parses with a real YAML parser.
func TestV1OpenAPIYAMLHandlerServesValidYAML(t *testing.T) {
t.Parallel()
s := &Server{}
r := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
w := httptest.NewRecorder()
s.handleV1OpenAPI(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d", w.Code)
}
ct := w.Header().Get("Content-Type")
if !strings.Contains(ct, "yaml") {
t.Fatalf("Content-Type=%q, want yaml", ct)
}
_ = mustParseOpenAPIRoot(t, w.Body.Bytes())
}
// TestV1OpenAPIYAMLNoUnquotedBraceProse guards against Scalar/js-yaml failures like:
//
// YAMLParseError: Nested mappings are not allowed in compact mappings
//
// which happen when an unquoted description/summary/title/example contains `{ key: ... }` mid-prose.
// Also rejects unquoted `[]`/`{}` fragments, `: ` sequences, and `#` that are not a pure flow node.
func TestV1OpenAPIYAMLNoUnquotedBraceProse(t *testing.T) {
t.Parallel()
lineRe := regexp.MustCompile(`^(\s*)(description|summary|title|example):\s*(.*)$`)
pureFlowRe := regexp.MustCompile(`^(\{[^}]*\}|\[[^\]]*\])$`)
nestedColonRe := regexp.MustCompile(`\{[^}\n]*:`)
bracketRe := regexp.MustCompile(`[{}\[\]]`)
var bad []string
for i, line := range strings.Split(string(v1OpenAPIYAML), "\n") {
m := lineRe.FindStringSubmatch(line)
if m == nil {
continue
}
val := strings.TrimSpace(m[3])
if val == "" || val == "|" || val == ">" || val == "|-" || val == ">-" || val == "|+" || val == ">+" {
continue
}
if strings.HasPrefix(val, `"`) || strings.HasPrefix(val, "'") {
continue
}
if pureFlowRe.MatchString(val) {
continue
}
reason := ""
switch {
case nestedColonRe.MatchString(val):
reason = "{ key: } prose"
case bracketRe.MatchString(val):
reason = "unquoted []/{}"
case strings.Contains(val, ": "):
reason = "unquoted ': ' sequence"
case strings.Contains(val, "#"):
reason = "unquoted #"
}
if reason == "" {
continue
}
trimmed := strings.TrimRight(line, "\r")
if len(trimmed) > 160 {
trimmed = trimmed[:160] + "…"
}
bad = append(bad, fmt.Sprintf("line %d (%s): %s", i+1, reason, strings.TrimSpace(trimmed)))
}
if len(bad) > 0 {
t.Fatalf("unquoted description/summary/title/example prose breaks YAML parsers:\n%s", strings.Join(bad, "\n"))
}
}
func TestV1OpenAPIYAMLRapiDocCompatibleExamples(t *testing.T) {
t.Parallel()
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
var bad []string
collectRapiDocUnsafeExamples(root, "root", &bad)
if len(bad) > 0 {
t.Fatalf("RapiDoc standardizeExample throws on singular example objects with null fields; wrap as examples.*.value:\n%s", strings.Join(bad, "\n"))
}
}
func collectRapiDocUnsafeExamples(node any, path string, bad *[]string) {
switch v := node.(type) {
case map[string]any:
if ex, ok := v["example"]; ok {
if m, ok := ex.(map[string]any); ok {
if _, hasValue := m["value"]; !hasValue {
for key, val := range m {
if val == nil {
*bad = append(*bad, fmt.Sprintf("%s.example.%s is null", path, key))
}
}
}
}
}
for key, child := range v {
collectRapiDocUnsafeExamples(child, path+"."+key, bad)
}
case []any:
for i, child := range v {
collectRapiDocUnsafeExamples(child, fmt.Sprintf("%s[%d]", path, i), bad)
}
}
}
func mustParseOpenAPIRoot(t *testing.T, raw []byte) map[string]any {
t.Helper()
var doc any
if err := yaml.Unmarshal(raw, &doc); err != nil {
t.Fatalf("openapi YAML must parse: %v", err)
}
root, ok := doc.(map[string]any)
if !ok {
t.Fatalf("openapi root must be a mapping, got %T", doc)
}
return root
}
func TestV1OpenAPIYAMLNoTabs(t *testing.T) {
t.Parallel()
for i, line := range strings.Split(string(v1OpenAPIYAML), "\n") {
if strings.Contains(line, "\t") {
t.Fatalf("tabs break YAML indentation (line %d)", i+1)
}
}
}
func TestV1OpenAPIYAMLNoDuplicateKeys(t *testing.T) {
t.Parallel()
var root yaml.Node
if err := yaml.Unmarshal(v1OpenAPIYAML, &root); err != nil {
t.Fatalf("parse: %v", err)
}
var bad []string
collectDuplicateYAMLKeys(&root, "", &bad)
if len(bad) > 0 {
t.Fatalf("duplicate YAML keys:\n%s", strings.Join(bad, "\n"))
}
}
func collectDuplicateYAMLKeys(n *yaml.Node, path string, bad *[]string) {
if n == nil {
return
}
switch n.Kind {
case yaml.DocumentNode:
for _, c := range n.Content {
collectDuplicateYAMLKeys(c, path, bad)
}
case yaml.MappingNode:
seen := make(map[string]int, len(n.Content)/2)
for i := 0; i+1 < len(n.Content); i += 2 {
k := n.Content[i]
v := n.Content[i+1]
key := k.Value
childPath := path + "/" + key
if prev, ok := seen[key]; ok {
*bad = append(*bad, fmt.Sprintf("%s (lines %d and %d)", childPath, prev, k.Line))
} else {
seen[key] = k.Line
}
collectDuplicateYAMLKeys(v, childPath, bad)
}
case yaml.SequenceNode:
for i, c := range n.Content {
collectDuplicateYAMLKeys(c, fmt.Sprintf("%s/%d", path, i), bad)
}
}
}
func TestV1OpenAPIYAMLRefsResolve(t *testing.T) {
t.Parallel()
var doc map[string]any
if err := yaml.Unmarshal(v1OpenAPIYAML, &doc); err != nil {
t.Fatalf("parse: %v", err)
}
comps, _ := doc["components"].(map[string]any)
if comps == nil {
t.Fatal("missing components")
}
var missing []string
walkOpenAPIRefs(doc, comps, &missing)
if len(missing) > 0 {
t.Fatalf("unresolved $ref targets:\n%s", strings.Join(missing, "\n"))
}
}
func walkOpenAPIRefs(node any, comps map[string]any, missing *[]string) {
switch v := node.(type) {
case map[string]any:
if ref, ok := v["$ref"].(string); ok {
if err := resolveComponentRef(ref, comps); err != nil {
*missing = append(*missing, err.Error())
}
}
for _, child := range v {
walkOpenAPIRefs(child, comps, missing)
}
case []any:
for _, child := range v {
walkOpenAPIRefs(child, comps, missing)
}
}
}
func resolveComponentRef(ref string, comps map[string]any) error {
const prefix = "#/components/"
if !strings.HasPrefix(ref, prefix) {
return fmt.Errorf("%q: only local #/components/… refs are supported", ref)
}
rest := strings.TrimPrefix(ref, prefix)
section, name, ok := strings.Cut(rest, "/")
if !ok || section == "" || name == "" {
return fmt.Errorf("%q: bad component ref format", ref)
}
name = strings.ReplaceAll(strings.ReplaceAll(name, "~1", "/"), "~0", "~")
bucket, _ := comps[section].(map[string]any)
if bucket == nil {
return fmt.Errorf("%q: unknown components section %q", ref, section)
}
if _, ok := bucket[name]; !ok {
return fmt.Errorf("%q: missing target", ref)
}
return nil
}
// TestV1OpenAPIYAMLSecuredOpsDocumentErrors ensures every public v1 operation
// documents realistic error responses via shared components (401/403/404/409/422/429/500).
func TestV1OpenAPIYAMLSecuredOpsDocumentErrors(t *testing.T) {
t.Parallel()
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
comps, _ := root["components"].(map[string]any)
responses, _ := comps["responses"].(map[string]any)
for _, name := range []string{
"Unauthorized", "Forbidden", "NotFound", "BadRequest",
"ValidationError", "Conflict", "TooManyRequests", "InternalServerError",
} {
if _, ok := responses[name]; !ok {
t.Fatalf("missing components.responses.%s", name)
}
}
schemas, _ := comps["schemas"].(map[string]any)
if _, ok := schemas["FlatAPIError"]; !ok {
t.Fatal("missing components.schemas.FlatAPIError")
}
paths, _ := root["paths"].(map[string]any)
rootSec := root["security"]
var bad []string
for path, raw := range paths {
item, _ := raw.(map[string]any)
for method, opRaw := range item {
switch method {
case "get", "post", "put", "patch", "delete":
default:
continue
}
op, _ := opRaw.(map[string]any)
sec := op["security"]
if sec == nil {
sec = rootSec
}
unauth := false
if sl, ok := sec.([]any); ok && len(sl) == 0 {
unauth = true
}
resp, _ := op["responses"].(map[string]any)
codes := make(map[string]bool, len(resp))
for c := range resp {
codes[c] = true
}
need := map[string]bool{"500": true}
if !unauth {
need["401"] = true
if strings.Contains(path, "{") {
need["403"] = true
need["404"] = true
need["400"] = true
need["422"] = true
}
if method == "post" || method == "put" || method == "patch" {
need["400"] = true
need["422"] = true
}
if isOpenAPIHeavyMutation(path, method) {
need["429"] = true
}
if strings.HasPrefix(path, "/team") || strings.HasPrefix(path, "/admin") {
need["403"] = true
need["409"] = true
}
}
for code := range need {
if !codes[code] {
bad = append(bad, fmt.Sprintf("%s %s missing %s", strings.ToUpper(method), path, code))
}
}
}
}
if len(bad) > 0 {
t.Fatalf("incomplete error responses:\n%s", strings.Join(bad, "\n"))
}
}
func isOpenAPIHeavyMutation(path, method string) bool {
if method != "post" {
return false
}
switch path {
case "/process", "/products/process":
return true
}
if strings.HasSuffix(path, "/sync-process-sample") ||
strings.HasSuffix(path, "/extract-schema") ||
strings.HasSuffix(path, "/generate") ||
strings.HasSuffix(path, "/export-products") {
return true
}
if strings.HasSuffix(path, "/retry") && strings.Contains(path, "/process/") {
return true
}
return strings.HasSuffix(path, "/sync") && strings.Contains(path, "/feeds/")
}
// TestV1OpenAPIYAMLProcessDualIDsAndGatesDocumentsContracts locks the
// products/process dual-ID + plan-gate docs against PresentProduct.id footguns.
func TestV1OpenAPIYAMLProcessDualIDsAndGatesDocumentsContracts(t *testing.T) {
t.Parallel()
doc := string(v1OpenAPIYAML)
for _, needle := range []string{
"Dual IDs (do not confuse)",
"never PresentProduct.id",
"data[].raw_product_id",
"assertV1ProcessGates runs before EnsureRaw",
"normalize_only",
"plan_gate",
"feature_disabled",
} {
if !strings.Contains(doc, needle) {
t.Fatalf("openapi missing process dual-id/gates contract text %q", needle)
}
}
root := mustParseOpenAPIRoot(t, v1OpenAPIYAML)
comps, _ := root["components"].(map[string]any)
schemas, _ := comps["schemas"].(map[string]any)
present, _ := schemas["PresentProduct"].(map[string]any)
props, _ := present["properties"].(map[string]any)
if _, ok := props["raw_product_id"]; !ok {
t.Fatal("PresentProduct must document raw_product_id for dual-mode clients")
}
paths, _ := root["paths"].(map[string]any)
processPath, _ := paths["/products/process"].(map[string]any)
post, _ := processPath["post"].(map[string]any)
resps, _ := post["responses"].(map[string]any)
if _, ok := resps["402"]; !ok {
t.Fatal("POST /products/process must document HTTP 402 plan gates")
}
}
// TestV1OpenAPIYAMLDocumentsAPIKeyCutoverReissue locks cutover honesty: legacy
// API keys were not migrated and clients must create new keys.
func TestV1OpenAPIYAMLDocumentsAPIKeyCutoverReissue(t *testing.T) {
t.Parallel()
doc := string(v1OpenAPIYAML)
for _, needle := range []string{
"Cutover / migration (reissue)",
"were not migrated",
"/settings?tab=api-keys",
"non-migrated (pre-cutover)",
"create a new dk_ key",
} {
if !strings.Contains(doc, needle) {
t.Fatalf("openapi missing API key cutover reissue text %q", needle)
}
}
if strings.Contains(doc, "must mint a new") {
t.Fatal("openapi should say create (not mint) for API key reissue copy")
}
}