Initial release of go-telegram

A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
This commit is contained in:
2026-05-09 13:09:27 +01:00
commit ac7cae8fa7
164 changed files with 100239 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// loadIR
// ---------------------------------------------------------------------------
func TestLoadIR_ValidFile(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
data, err := json.Marshal(api)
require.NoError(t, err)
tmp := filepath.Join(t.TempDir(), "api.json")
require.NoError(t, os.WriteFile(tmp, data, 0o600))
loaded, err := loadIR(tmp)
require.NoError(t, err)
require.Len(t, loaded.Methods, 1)
require.Equal(t, "getMe", loaded.Methods[0].Name)
}
func TestLoadIR_MissingFile(t *testing.T) {
_, err := loadIR("/nonexistent/path/api.json")
require.Error(t, err)
require.Contains(t, err.Error(), "open IR")
}
func TestLoadIR_InvalidJSON(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "bad.json")
require.NoError(t, os.WriteFile(tmp, []byte("not json"), 0o600))
_, err := loadIR(tmp)
require.Error(t, err)
require.Contains(t, err.Error(), "decode IR")
}
// ---------------------------------------------------------------------------
// auditBool
// ---------------------------------------------------------------------------
func TestAuditBool_LongDocTruncated(t *testing.T) {
longDoc := make([]byte, 200)
for i := range longDoc {
longDoc[i] = 'a'
}
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "myMethod", Doc: string(longDoc), Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
problems := auditBool(api, &spec.Overrides{})
require.Len(t, problems, 1)
require.Contains(t, problems[0], "…")
}
func TestAuditBool_TrueIsReturnedVariant(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "doThing", Doc: "true is returned on success.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
require.Empty(t, auditBool(api, &spec.Overrides{}))
}
func TestAuditBool_ReturnsBoolean(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "doThing", Doc: "Returns Boolean on success.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
require.Empty(t, auditBool(api, &spec.Overrides{}))
}
// ---------------------------------------------------------------------------
// formatTypeRef
// ---------------------------------------------------------------------------
func TestFormatTypeRef_AllBranches(t *testing.T) {
cases := []struct {
tr spec.TypeRef
want string
}{
{spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}, "bool"},
{spec.TypeRef{Kind: spec.KindNamed, Name: "User"}, "User"},
{spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}}, "[]Update"},
{spec.TypeRef{Kind: spec.KindArray}, "[]any"},
{spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64", "string"}}, "(int64 | string)"},
{spec.TypeRef{Kind: spec.Kind(99)}, "?"},
}
for _, c := range cases {
got := formatTypeRef(c.tr)
require.Equal(t, c.want, got, "for kind=%v name=%v", c.tr.Kind, c.tr.Name)
}
}
// ---------------------------------------------------------------------------
// auditDrift
// ---------------------------------------------------------------------------
func TestAuditDrift_InvalidRefReturnsError(t *testing.T) {
cur := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
_, err := auditDrift("internal/spec/api.json", "THIS_REF_DOES_NOT_EXIST", cur)
require.Error(t, err)
}
func TestAuditDrift_SameRefNoDrift(t *testing.T) {
irPath := "../../internal/spec/api.json"
cur, err := loadIR(irPath)
if err != nil {
t.Skip("api.json not available, skipping drift test")
}
changes, err := auditDrift(irPath, "HEAD", cur)
require.NoError(t, err)
require.Empty(t, changes)
}
func TestAuditDrift_InvalidJSONFromGit(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script not supported on Windows")
}
tmp := t.TempDir()
fakeGit := filepath.Join(tmp, "git")
require.NoError(t, os.WriteFile(fakeGit, []byte("#!/bin/sh\necho 'not valid json'\n"), 0o600))
require.NoError(t, os.Chmod(fakeGit, 0o755))
origPATH := os.Getenv("PATH")
t.Cleanup(func() { _ = os.Setenv("PATH", origPATH) })
_ = os.Setenv("PATH", tmp+string(os.PathListSeparator)+origPATH)
_, err := auditDrift("internal/spec/api.json", "HEAD", &spec.API{})
require.Error(t, err)
require.Contains(t, err.Error(), "decode")
}
// ---------------------------------------------------------------------------
// auditAny
// ---------------------------------------------------------------------------
func TestAuditAny_FlagsUnknownMethodReturn(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{
Name: "weirdMethod",
Returns: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B", "C"}},
},
},
}
out := auditAny(api)
require.Len(t, out, 1)
require.Contains(t, out[0], "any return: weirdMethod")
}
func TestAuditAny_FlagsUnknownMethodParam(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{
Name: "weirdMethod",
Params: []spec.Field{
{Name: "Thing", JSONName: "thing", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"X", "Y", "Z"}}},
},
},
},
}
out := auditAny(api)
require.Len(t, out, 1)
require.Contains(t, out[0], "any param: weirdMethod.Thing")
}
// ---------------------------------------------------------------------------
// diffSignatures
// ---------------------------------------------------------------------------
func TestDiffSignatures_UnchangedNoDrift(t *testing.T) {
prev := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
cur := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
require.Empty(t, diffSignatures(prev, cur))
}
// ---------------------------------------------------------------------------
// typeRefEqual
// ---------------------------------------------------------------------------
func TestTypeRefEqual_ArrayNilElemDiffers(t *testing.T) {
a := spec.TypeRef{Kind: spec.KindArray}
b := spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}}
require.False(t, typeRefEqual(a, b))
require.False(t, typeRefEqual(b, a))
}
// ---------------------------------------------------------------------------
// TestHelperMain: subprocess helper for main() coverage.
//
// When AUDIT_HELPER_MAIN=1 is set, this function:
// 1. Resets flag.CommandLine so main()'s flag.Parse() gets a clean slate.
// 2. Sets os.Args to the args encoded in AUDIT_HELPER_ARGS.
// 3. Calls main() which calls os.Exit — the test process terminates with
// main's exit code, which the parent test captures.
// ---------------------------------------------------------------------------
func TestHelperMain(t *testing.T) {
if os.Getenv("AUDIT_HELPER_MAIN") != "1" {
t.Skip("not running as subprocess helper")
}
// Reset flag.CommandLine so main()'s flag.Parse() gets a clean slate.
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// Decode args from JSON-encoded env var.
encoded := os.Getenv("AUDIT_HELPER_ARGS")
if encoded != "" {
var args []string
if err := json.Unmarshal([]byte(encoded), &args); err == nil {
os.Args = append([]string{os.Args[0]}, args...)
}
} else {
os.Args = os.Args[:1]
}
main()
}
// runMain runs main() via the test binary subprocess so that coverage counters
// from main() are included in the profile. Args are JSON-encoded in an env var
// to avoid conflicts with the test binary's own flag parsing.
func runMain(t *testing.T, extraEnv []string, args ...string) (string, int) {
t.Helper()
argsJSON, _ := json.Marshal(args)
cmd := exec.Command(os.Args[0], "-test.run=TestHelperMain", "-test.v=false")
cmd.Env = append(os.Environ(), "AUDIT_HELPER_MAIN=1", "AUDIT_HELPER_ARGS="+string(argsJSON))
cmd.Env = append(cmd.Env, extraEnv...)
out, err := cmd.CombinedOutput()
code := 0
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
}
}
return string(out), code
}
// ---------------------------------------------------------------------------
// main() integration tests — exercise main() code paths via subprocess
// ---------------------------------------------------------------------------
func TestMain_CleanExitsZero(t *testing.T) {
tmp := t.TempDir()
ir := writeIR(t, tmp, &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Doc: "Returns True on success.", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
})
ov := writeOverrides(t, tmp)
out, code := runMain(t, nil, "-ir", ir, "-overrides", ov)
require.Equal(t, exitClean, code, "expected exit 0 (clean)\nout: %s", out)
require.Contains(t, out, "clean")
}
func TestMain_FallbackExitsOne(t *testing.T) {
tmp := t.TempDir()
ir := writeIR(t, tmp, &spec.API{
Methods: []spec.MethodDecl{
{Name: "doSomething", Doc: "Does something.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
})
ov := writeOverrides(t, tmp)
out, code := runMain(t, nil, "-ir", ir, "-overrides", ov)
require.Equal(t, exitFallback, code, "expected exit 1 (fallback)\nout: %s", out)
require.Contains(t, out, "bool fallback")
}
func TestMain_InvalidIRExitsThree(t *testing.T) {
tmp := t.TempDir()
bad := filepath.Join(tmp, "bad.json")
require.NoError(t, os.WriteFile(bad, []byte("not json"), 0o600))
ov := writeOverrides(t, tmp)
out, code := runMain(t, nil, "-ir", bad, "-overrides", ov)
require.Equal(t, exitInvalid, code, "expected exit 3 (invalid IR)\nout: %s", out)
}
func TestMain_InvalidOverridesExitsThree(t *testing.T) {
tmp := t.TempDir()
ir := writeIR(t, tmp, &spec.API{})
bad := filepath.Join(tmp, "bad_ov.json")
require.NoError(t, os.WriteFile(bad, []byte("not json"), 0o600))
out, code := runMain(t, nil, "-ir", ir, "-overrides", bad)
require.Equal(t, exitInvalid, code, "expected exit 3 (invalid overrides)\nout: %s", out)
}
func TestMain_DriftDetectedExitsTwo(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script not supported on Windows")
}
tmp := t.TempDir()
prevAPI := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
curAPI := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}},
},
}
curIR := writeIR(t, tmp, curAPI)
ov := writeOverrides(t, tmp)
prevData, err := json.Marshal(prevAPI)
require.NoError(t, err)
prevFile := filepath.Join(tmp, "prev.json")
require.NoError(t, os.WriteFile(prevFile, prevData, 0o600))
fakeGit := filepath.Join(tmp, "git")
script := fmt.Sprintf("#!/bin/sh\ncat %s\n", prevFile)
require.NoError(t, os.WriteFile(fakeGit, []byte(script), 0o600))
require.NoError(t, os.Chmod(fakeGit, 0o755))
newPATH := tmp + string(os.PathListSeparator) + os.Getenv("PATH")
out, code := runMain(t,
[]string{"PATH=" + newPATH},
"-ir", curIR, "-overrides", ov, "-drift", "-against", "HEAD~1",
)
require.Equal(t, exitDrift, code, "expected exit 2 (drift)\nout: %s", out)
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
func writeIR(t *testing.T, dir string, api *spec.API) string {
t.Helper()
data, err := json.Marshal(api)
require.NoError(t, err)
p := filepath.Join(dir, "api.json")
require.NoError(t, os.WriteFile(p, data, 0o600))
return p
}
func writeOverrides(t *testing.T, dir string) string {
t.Helper()
p := filepath.Join(dir, "overrides.json")
require.NoError(t, os.WriteFile(p, []byte("{}"), 0o600))
return p
}
+291
View File
@@ -0,0 +1,291 @@
// Command audit reports IR-level codegen fallbacks and signature drift.
//
// Usage:
//
// audit -ir <path> (default internal/spec/api.json)
// audit -overrides <path> (default internal/spec/overrides.json)
// audit -drift (compare against -against ref's IR; off by default)
// audit -against <ref> (git ref to diff drift against; default HEAD~1)
//
// Exit codes:
//
// 0 — clean
// 1 — unaccounted bool fallbacks or any-typed fields
// 2 — drift detected (signature changed)
// 3 — invalid IR or overrides
package main
import (
"flag"
"fmt"
"github.com/goccy/go-json"
"os"
"os/exec"
"strings"
"github.com/lukaszraczylo/go-telegram/internal/spec"
)
const (
exitClean = 0
exitFallback = 1
exitDrift = 2
exitInvalid = 3
)
func main() {
irPath := flag.String("ir", "internal/spec/api.json", "path to IR JSON")
ovPath := flag.String("overrides", "internal/spec/overrides.json", "path to overrides JSON")
checkDrift := flag.Bool("drift", false, "compare against -against ref's IR for signature changes")
againstRef := flag.String("against", "HEAD~1", "git ref to diff drift against (e.g. origin/main, HEAD~1)")
flag.Parse()
api, err := loadIR(*irPath)
if err != nil {
fmt.Fprintln(os.Stderr, "audit:", err)
os.Exit(exitInvalid)
}
overrides, err := spec.LoadOverrides(*ovPath)
if err != nil {
fmt.Fprintln(os.Stderr, "audit:", err)
os.Exit(exitInvalid)
}
var problems []string
problems = append(problems, auditBool(api, overrides)...)
problems = append(problems, auditAny(api)...)
driftFound := false
if *checkDrift {
if d, err := auditDrift(*irPath, *againstRef, api); err != nil {
fmt.Fprintln(os.Stderr, "audit: drift check skipped:", err)
} else if len(d) > 0 {
fmt.Println("Drift detected (signatures changed since HEAD):")
for _, p := range d {
fmt.Println(" ", p)
}
driftFound = true
}
}
if len(problems) == 0 && !driftFound {
fmt.Println("audit: clean")
os.Exit(exitClean)
}
if len(problems) > 0 {
fmt.Println("Codegen fallbacks requiring action:")
for _, p := range problems {
fmt.Println(" ", p)
}
fmt.Println()
fmt.Println("To resolve: extend cmd/scrape/method.go regex patterns OR")
fmt.Println("add an entry to internal/spec/overrides.json.")
os.Exit(exitFallback)
}
// drift only, no fallbacks
os.Exit(exitDrift)
}
func loadIR(path string) (*spec.API, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open IR: %w", err)
}
defer func() { _ = f.Close() }()
var api spec.API
if err := json.NewDecoder(f).Decode(&api); err != nil {
return nil, fmt.Errorf("decode IR: %w", err)
}
return &api, nil
}
// auditBool returns problems for methods returning bool whose docs don't
// actually say "Returns True" / etc. and which aren't in the approved list.
func auditBool(api *spec.API, ov *spec.Overrides) []string {
var out []string
for _, m := range api.Methods {
if m.Returns.Kind != spec.KindPrimitive || m.Returns.Name != "bool" {
continue
}
if ov.IsBoolApproved(m.Name) {
continue
}
if looksGenuinelyBool(m.Doc) {
continue
}
snippet := m.Doc
if len(snippet) > 120 {
snippet = snippet[:120] + "…"
}
out = append(out, fmt.Sprintf("bool fallback: %s — doc: %q", m.Name, snippet))
}
return out
}
func looksGenuinelyBool(doc string) bool {
for _, p := range []string{
"Returns True", "Returns true",
"True is returned", "true is returned",
"Returns Boolean", "Returns Bool",
} {
if strings.Contains(doc, p) {
return true
}
}
return false
}
// auditAny scans the IR for any KindOneOf TypeRef that would render as
// `any` in generated code (not matched by ChatID/InputFile-or-string/known
// union heuristics). Reports each occurrence with location.
func auditAny(api *spec.API) []string {
var out []string
isKnownUnion := func(variants []string) bool {
if hasVariants(variants, "int64", "string") {
return true // ChatID
}
if hasVariants(variants, "InputFile", "string") {
return true // *InputFile
}
// ReplyMarkup union: all four keyboard types — emitter renders as `any` intentionally
if hasVariants(variants, "InlineKeyboardMarkup", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ForceReply") {
return true
}
for _, t := range api.Types {
if len(t.OneOf) > 0 && sameSet(variants, t.OneOf) {
return true
}
}
return false
}
isAny := func(tr spec.TypeRef) bool {
return tr.Kind == spec.KindOneOf && !isKnownUnion(tr.Variants)
}
for _, t := range api.Types {
for _, f := range t.Fields {
if isAny(f.Type) {
out = append(out, fmt.Sprintf("any field: %s.%s (variants=%v)", t.Name, f.Name, f.Type.Variants))
}
}
}
for _, m := range api.Methods {
if isAny(m.Returns) {
out = append(out, fmt.Sprintf("any return: %s (variants=%v)", m.Name, m.Returns.Variants))
}
for _, p := range m.Params {
if isAny(p.Type) {
out = append(out, fmt.Sprintf("any param: %s.%s (variants=%v)", m.Name, p.Name, p.Type.Variants))
}
}
}
return out
}
func hasVariants(got []string, want ...string) bool {
if len(got) != len(want) {
return false
}
seen := map[string]int{}
for _, g := range got {
seen[g]++
}
for _, w := range want {
seen[w]--
}
for _, v := range seen {
if v != 0 {
return false
}
}
return true
}
func sameSet(a, b []string) bool {
if len(a) != len(b) {
return false
}
return hasVariants(a, b...)
}
// auditDrift compares method/return signatures between the given git ref's
// version of irPath and the in-memory current IR. Returns a list of
// human-readable change descriptions.
func auditDrift(irPath, againstRef string, current *spec.API) ([]string, error) {
cmd := exec.Command("git", "show", againstRef+":"+irPath) // #nosec G204 - operator tool, ref controlled by caller
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git show %s: %w", againstRef, err)
}
var prev spec.API
if err := json.Unmarshal(out, &prev); err != nil {
return nil, fmt.Errorf("decode %s IR: %w", againstRef, err)
}
return diffSignatures(&prev, current), nil
}
func diffSignatures(prev, cur *spec.API) []string {
var changes []string
pmeth := indexByName(prev.Methods, func(m spec.MethodDecl) string { return m.Name })
cmeth := indexByName(cur.Methods, func(m spec.MethodDecl) string { return m.Name })
for name, p := range pmeth {
c, ok := cmeth[name]
if !ok {
changes = append(changes, fmt.Sprintf("removed method: %s", name))
continue
}
if !typeRefEqual(p.Returns, c.Returns) {
changes = append(changes, fmt.Sprintf(
"method %s return changed: %s → %s",
name, formatTypeRef(p.Returns), formatTypeRef(c.Returns)))
}
}
for name := range cmeth {
if _, ok := pmeth[name]; !ok {
changes = append(changes, fmt.Sprintf("added method: %s", name))
}
}
return changes
}
func indexByName[T any](xs []T, f func(T) string) map[string]T {
out := map[string]T{}
for _, x := range xs {
out[f(x)] = x
}
return out
}
func typeRefEqual(a, b spec.TypeRef) bool {
if a.Kind != b.Kind || a.Name != b.Name {
return false
}
if (a.ElemType == nil) != (b.ElemType == nil) {
return false
}
if a.ElemType != nil && !typeRefEqual(*a.ElemType, *b.ElemType) {
return false
}
return sameSet(a.Variants, b.Variants)
}
func formatTypeRef(t spec.TypeRef) string {
switch t.Kind {
case spec.KindPrimitive:
return t.Name
case spec.KindNamed:
return t.Name
case spec.KindArray:
if t.ElemType != nil {
return "[]" + formatTypeRef(*t.ElemType)
}
return "[]any"
case spec.KindOneOf:
return "(" + strings.Join(t.Variants, " | ") + ")"
}
return "?"
}
+216
View File
@@ -0,0 +1,216 @@
package main
import (
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// ---- auditBool -----------------------------------------------------------
func TestAuditBool_FlagsUnapprovedBoolMethod(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Doc: "A simple method.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
ov := &spec.Overrides{}
problems := auditBool(api, ov)
require.Len(t, problems, 1)
require.Contains(t, problems[0], "bool fallback: getMe")
}
func TestAuditBool_SkipsApprovedBoolMethod(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "setWebhook", Doc: "Use this to set webhook.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
ov := &spec.Overrides{ApprovedBoolMethods: []string{"setWebhook"}}
problems := auditBool(api, ov)
require.Empty(t, problems)
}
func TestAuditBool_SkipsMethodWithReturnsTrueDoc(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "doThing", Doc: "Returns True on success.", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
ov := &spec.Overrides{}
problems := auditBool(api, ov)
require.Empty(t, problems)
}
func TestAuditBool_SkipsNonBoolMethods(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Doc: "Gets user.", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
ov := &spec.Overrides{}
require.Empty(t, auditBool(api, ov))
}
// ---- auditAny ------------------------------------------------------------
func TestAuditAny_FlagsUnrecognisedOneOf(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{
Name: "Foo",
Fields: []spec.Field{
{Name: "Bar", JSONName: "bar", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B", "C"}}},
},
},
},
}
out := auditAny(api)
require.Len(t, out, 1)
require.Contains(t, out[0], "any field: Foo.Bar")
}
func TestAuditAny_SkipsChatIDShape(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{
Name: "SendMessage",
Fields: []spec.Field{
{Name: "ChatID", JSONName: "chat_id", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64", "string"}}},
},
},
},
}
require.Empty(t, auditAny(api))
}
func TestAuditAny_SkipsKnownUnion(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{Name: "InputMedia", OneOf: []string{"InputMediaPhoto", "InputMediaVideo"}},
{
Name: "SomeMethod",
Fields: []spec.Field{
{Name: "Media", JSONName: "media", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"InputMediaPhoto", "InputMediaVideo"}}},
},
},
},
}
require.Empty(t, auditAny(api))
}
func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{
Name: "sendMessage",
Params: []spec.Field{
{Name: "ReplyMarkup", JSONName: "reply_markup", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"InlineKeyboardMarkup", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ForceReply"}}},
},
},
},
}
require.Empty(t, auditAny(api))
}
func TestAuditAny_SkipsInputFileShape(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{
Name: "sendPhoto",
Params: []spec.Field{
{Name: "Photo", JSONName: "photo", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"InputFile", "string"}}},
},
},
},
}
require.Empty(t, auditAny(api))
}
// ---- diffSignatures ------------------------------------------------------
func TestDiffSignatures_AddedMethod(t *testing.T) {
prev := &spec.API{}
cur := &spec.API{
Methods: []spec.MethodDecl{
{Name: "newMethod", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
changes := diffSignatures(prev, cur)
require.Len(t, changes, 1)
require.Contains(t, changes[0], "added method: newMethod")
}
func TestDiffSignatures_RemovedMethod(t *testing.T) {
prev := &spec.API{
Methods: []spec.MethodDecl{
{Name: "oldMethod", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
cur := &spec.API{}
changes := diffSignatures(prev, cur)
require.Len(t, changes, 1)
require.Contains(t, changes[0], "removed method: oldMethod")
}
func TestDiffSignatures_ChangedReturn(t *testing.T) {
prev := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}},
},
}
cur := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
changes := diffSignatures(prev, cur)
require.Len(t, changes, 1)
require.Contains(t, changes[0], "getMe")
require.Contains(t, changes[0], "bool")
require.Contains(t, changes[0], "User")
}
func TestDiffSignatures_Clean(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{Name: "getMe", Returns: spec.TypeRef{Kind: spec.KindNamed, Name: "User"}},
},
}
require.Empty(t, diffSignatures(api, api))
}
// ---- typeRefEqual --------------------------------------------------------
func TestTypeRefEqual_Primitive(t *testing.T) {
a := spec.TypeRef{Kind: spec.KindPrimitive, Name: "bool"}
require.True(t, typeRefEqual(a, a))
b := spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}
require.False(t, typeRefEqual(a, b))
}
func TestTypeRefEqual_Array(t *testing.T) {
elem := &spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}
a := spec.TypeRef{Kind: spec.KindArray, ElemType: elem}
b := spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}}
require.True(t, typeRefEqual(a, b))
c := spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}}
require.False(t, typeRefEqual(a, c))
}
func TestTypeRefEqual_OneOf(t *testing.T) {
a := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64", "string"}}
b := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"string", "int64"}}
require.True(t, typeRefEqual(a, b))
c := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"int64"}}
require.False(t, typeRefEqual(a, c))
}
func TestTypeRefEqual_NilVsNonNilElem(t *testing.T) {
a := spec.TypeRef{Kind: spec.KindArray}
b := spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Update"}}
require.False(t, typeRefEqual(a, b))
}