mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
194 lines
8.2 KiB
Go
194 lines
8.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package meta
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestField_CanonicalType(t *testing.T) {
|
|
cases := map[string]string{
|
|
"file": "string", // meta_data's non-standard "file" is a string with binary format
|
|
"list": "array", // "list" is meta_data's spelling of a JSON array
|
|
"integer": "integer",
|
|
"boolean": "boolean",
|
|
"string": "string",
|
|
"": "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := (Field{Type: in}).CanonicalType(); got != want {
|
|
t.Errorf("CanonicalType(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestField_EnumValues(t *testing.T) {
|
|
// string enum keeps source order (order can encode priority)
|
|
if got := (Field{Type: "string", Enum: []any{"b", "a", "c"}}).EnumValues(); !reflect.DeepEqual(got, []any{"b", "a", "c"}) {
|
|
t.Errorf("string enum = %v, want source order [b a c]", got)
|
|
}
|
|
// integer enum: string-stored literals coerced to int64 and numerically sorted
|
|
if got := (Field{Type: "integer", Enum: []any{"10", "2", "1"}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(10)}) {
|
|
t.Errorf("integer enum = %v, want [1 2 10] coerced+sorted", got)
|
|
}
|
|
// options used when enum absent, deduped, source order for strings
|
|
if got := (Field{Type: "string", Options: []Option{{Value: "x"}, {Value: "x"}, {Value: "y"}}}).EnumValues(); !reflect.DeepEqual(got, []any{"x", "y"}) {
|
|
t.Errorf("options enum = %v, want [x y] deduped", got)
|
|
}
|
|
// uncoercible literal dropped
|
|
if got := (Field{Type: "integer", Enum: []any{"1", "nope", "2"}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2)}) {
|
|
t.Errorf("bad enum = %v, want [1 2] (nope dropped)", got)
|
|
}
|
|
// no enum/options -> nil
|
|
if got := (Field{Type: "string"}).EnumValues(); got != nil {
|
|
t.Errorf("empty enum = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestField_EnumOptions(t *testing.T) {
|
|
// options carry descriptions, kept paired with their (string) value in source order
|
|
fo := Field{Type: "string", Options: []Option{
|
|
{Value: "open_id", Description: "以 open_id 标识"},
|
|
{Value: "open_id", Description: "dup ignored"}, // dedup keeps first
|
|
{Value: "user_id", Description: "以 user_id 标识"},
|
|
}}
|
|
got := fo.EnumOptions()
|
|
want := []EnumOption{
|
|
{Value: "open_id", Description: "以 open_id 标识"},
|
|
{Value: "user_id", Description: "以 user_id 标识"},
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("EnumOptions = %+v, want %+v", got, want)
|
|
}
|
|
|
|
// integer enum (bare form): values coerced + numerically sorted, no descriptions
|
|
fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}}
|
|
gi := fi.EnumOptions()
|
|
if len(gi) != 3 || gi[0].Value != int64(1) || gi[2].Value != int64(10) || gi[0].Description != "" {
|
|
t.Errorf("EnumOptions(integer) = %+v, want [1 2 10] coerced+sorted, no desc", gi)
|
|
}
|
|
|
|
// EnumValues stays the value projection of EnumOptions (golden-critical)
|
|
if !reflect.DeepEqual(fo.EnumValues(), []any{"open_id", "user_id"}) {
|
|
t.Errorf("EnumValues diverged from EnumOptions values: %v", fo.EnumValues())
|
|
}
|
|
// unconstrained -> nil
|
|
if (Field{Type: "string"}).EnumOptions() != nil {
|
|
t.Error("EnumOptions should be nil when unconstrained")
|
|
}
|
|
}
|
|
|
|
func TestField_EnumOptions_BothEnumAndOptions(t *testing.T) {
|
|
// enum is the value set; descriptions backfilled from options, empty where absent
|
|
f := Field{Type: "string", Enum: []any{"1", "2", "3", "4", "6"}, Options: []Option{
|
|
{Value: "1", Description: "from"},
|
|
{Value: "2", Description: "to"},
|
|
{Value: "6", Description: "subject"},
|
|
}}
|
|
want := []EnumOption{
|
|
{Value: "1", Description: "from"},
|
|
{Value: "2", Description: "to"},
|
|
{Value: "3", Description: ""},
|
|
{Value: "4", Description: ""},
|
|
{Value: "6", Description: "subject"},
|
|
}
|
|
if got := f.EnumOptions(); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("EnumOptions(enum+options) = %+v, want %+v", got, want)
|
|
}
|
|
|
|
// enum values stored as strings match option values stored as numbers
|
|
fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}, Options: []Option{
|
|
{Value: 1, Description: "one"},
|
|
{Value: 2, Description: "two"},
|
|
}}
|
|
wantI := []EnumOption{
|
|
{Value: int64(1), Description: "one"},
|
|
{Value: int64(2), Description: "two"},
|
|
{Value: int64(10), Description: ""},
|
|
}
|
|
if got := fi.EnumOptions(); !reflect.DeepEqual(got, wantI) {
|
|
t.Errorf("EnumOptions(integer enum+options) = %+v, want %+v", got, wantI)
|
|
}
|
|
}
|
|
|
|
func TestField_Enum_NumberAndBoolean(t *testing.T) {
|
|
// number: string-stored floats coerced to float64 and numerically sorted
|
|
if got := (Field{Type: "number", Enum: []any{"2.5", "1.5", "10"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5, float64(10)}) {
|
|
t.Errorf("number enum = %v, want [1.5 2.5 10] coerced+sorted", got)
|
|
}
|
|
// number: uncoercible literal dropped
|
|
if got := (Field{Type: "number", Enum: []any{"1.5", "x", "2.5"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5}) {
|
|
t.Errorf("number enum with bad value = %v, want [1.5 2.5]", got)
|
|
}
|
|
// boolean: true/false coerced and sorted (false before true); invalid dropped
|
|
if got := (Field{Type: "boolean", Enum: []any{"true", "maybe", "false"}}).EnumValues(); !reflect.DeepEqual(got, []any{false, true}) {
|
|
t.Errorf("boolean enum = %v, want [false true]", got)
|
|
}
|
|
}
|
|
|
|
func TestField_EnumOptions_NonStringValuesNormalized(t *testing.T) {
|
|
// JSON numbers/bools arrive already-typed (a number is float64, not a
|
|
// string) — e.g. options[].value: 0. They must still be normalized to the
|
|
// field's canonical type (int64 for "integer") and sorted numerically;
|
|
// leaving them as float64 both yields the wrong type and defeats enumLess,
|
|
// whose integer branch asserts int64 and would otherwise treat every value
|
|
// as zero (no sort).
|
|
if got := (Field{Type: "integer", Options: []Option{
|
|
{Value: float64(10)}, {Value: float64(2)}, {Value: float64(1)},
|
|
}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(10)}) {
|
|
t.Errorf("integer options from float64 = %#v, want [int64(1) int64(2) int64(10)]", got)
|
|
}
|
|
// bare enum form, JSON numbers
|
|
if got := (Field{Type: "integer", Enum: []any{float64(3), float64(1), float64(2)}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(3)}) {
|
|
t.Errorf("integer enum from float64 = %#v, want [int64(1) int64(2) int64(3)]", got)
|
|
}
|
|
// number field: a whole-valued float stays float64
|
|
if got := (Field{Type: "number", Enum: []any{float64(2), float64(1)}}).EnumValues(); !reflect.DeepEqual(got, []any{float64(1), float64(2)}) {
|
|
t.Errorf("number enum from float64 = %#v, want [float64(1) float64(2)]", got)
|
|
}
|
|
// boolean field: native bools coerce + sort (false < true)
|
|
if got := (Field{Type: "boolean", Enum: []any{true, false}}).EnumValues(); !reflect.DeepEqual(got, []any{false, true}) {
|
|
t.Errorf("boolean enum from bool = %#v, want [false true]", got)
|
|
}
|
|
// non-integral float under integer is uncoercible -> dropped (mirrors how "2.5" fails ParseInt)
|
|
if got := (Field{Type: "integer", Enum: []any{float64(1), float64(2.5), float64(3)}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(3)}) {
|
|
t.Errorf("integer enum with fractional float = %#v, want [int64(1) int64(3)]", got)
|
|
}
|
|
}
|
|
|
|
func TestField_CoercedDefaultAndExample(t *testing.T) {
|
|
if got := (Field{Type: "integer", Default: "5"}).CoercedDefault(); got != int64(5) {
|
|
t.Errorf("CoercedDefault integer = %v (%T), want int64(5)", got, got)
|
|
}
|
|
if got := (Field{Type: "integer", Default: "bad"}).CoercedDefault(); got != nil {
|
|
t.Errorf("CoercedDefault uncoercible = %v, want nil", got)
|
|
}
|
|
if got := (Field{Type: "string"}).CoercedDefault(); got != nil {
|
|
t.Errorf("CoercedDefault absent = %v, want nil", got)
|
|
}
|
|
if got := (Field{Type: "boolean", Example: "true"}).CoercedExample(); got != true {
|
|
t.Errorf("CoercedExample boolean = %v, want true", got)
|
|
}
|
|
}
|
|
|
|
func TestField_Bounds(t *testing.T) {
|
|
f := Field{Min: "1", Max: "100"}
|
|
if v := f.MinBound(); v == nil || *v != 1 {
|
|
t.Errorf("MinBound = %v, want 1", v)
|
|
}
|
|
if v := f.MaxBound(); v == nil || *v != 100 {
|
|
t.Errorf("MaxBound = %v, want 100", v)
|
|
}
|
|
if v := (Field{Min: "0.5"}).MinBound(); v == nil || *v != 0.5 {
|
|
t.Errorf("MinBound fractional = %v, want 0.5", v)
|
|
}
|
|
if v := (Field{}).MinBound(); v != nil {
|
|
t.Errorf("MinBound absent = %v, want nil", v)
|
|
}
|
|
if v := (Field{Max: "not_a_number"}).MaxBound(); v != nil {
|
|
t.Errorf("MaxBound unparseable = %v, want nil", v)
|
|
}
|
|
}
|