mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
port agent webhook trigger, agent file upload/download, component input-form + debug endpoints from Python - [x] New Feature (non-breaking change which adds functionality)
243 lines
7.2 KiB
Go
243 lines
7.2 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package handler
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestExtractBySchema_RequiredMissing pins the required-field-missing
|
|
// branch (mirrors python agent_api.py:1913).
|
|
func TestExtractBySchema_RequiredMissing(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"q": map[string]any{"type": "string"},
|
|
},
|
|
"required": []any{"q"},
|
|
}
|
|
_, err := extractBySchema(map[string]any{}, schema, "query")
|
|
if err == nil || !strings.Contains(err.Error(), "missing required field") {
|
|
t.Errorf("err = %v, want missing required field", err)
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_OptionalDefault confirms that optional fields get
|
|
// the type-default value when absent.
|
|
func TestExtractBySchema_OptionalDefault(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"s": map[string]any{"type": "string"},
|
|
"b": map[string]any{"type": "boolean"},
|
|
"o": map[string]any{"type": "object"},
|
|
"a": map[string]any{"type": "array"},
|
|
"f": map[string]any{"type": "file"},
|
|
"n": map[string]any{"type": "number"},
|
|
},
|
|
}
|
|
got, err := extractBySchema(map[string]any{}, schema, "query")
|
|
if err != nil {
|
|
t.Fatalf("extractBySchema: %v", err)
|
|
}
|
|
if got["s"] != "" {
|
|
t.Errorf("default for string: got %v", got["s"])
|
|
}
|
|
if got["b"] != false {
|
|
t.Errorf("default for boolean: got %v", got["b"])
|
|
}
|
|
if _, ok := got["o"].(map[string]any); !ok {
|
|
t.Errorf("default for object: got %T", got["o"])
|
|
}
|
|
if _, ok := got["a"].([]any); !ok {
|
|
t.Errorf("default for array: got %T", got["a"])
|
|
}
|
|
if _, ok := got["f"].([]any); !ok {
|
|
t.Errorf("default for file: got %T", got["f"])
|
|
}
|
|
if got["n"] != float64(0) {
|
|
t.Errorf("default for number: got %v", got["n"])
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_NumberCoercion pins string→number auto-cast.
|
|
func TestExtractBySchema_NumberCoercion(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"n": map[string]any{"type": "number"},
|
|
},
|
|
}
|
|
got, err := extractBySchema(map[string]any{"n": "42"}, schema, "body")
|
|
if err != nil {
|
|
t.Fatalf("extractBySchema: %v", err)
|
|
}
|
|
if got["n"] != float64(42) {
|
|
t.Errorf("number coercion: got %v (%T)", got["n"], got["n"])
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_NumberCoercionFloat covers decimal strings.
|
|
func TestExtractBySchema_NumberCoercionFloat(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"n": map[string]any{"type": "number"},
|
|
},
|
|
}
|
|
got, err := extractBySchema(map[string]any{"n": "3.14"}, schema, "body")
|
|
if err != nil {
|
|
t.Fatalf("extractBySchema: %v", err)
|
|
}
|
|
if got["n"] != 3.14 {
|
|
t.Errorf("number float coercion: got %v", got["n"])
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_BooleanCoercion pins true/false string parsing.
|
|
func TestExtractBySchema_BooleanCoercion(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"b": map[string]any{"type": "boolean"},
|
|
},
|
|
}
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"true", true},
|
|
{"TRUE", true},
|
|
{"1", true},
|
|
{"false", false},
|
|
{"0", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := extractBySchema(map[string]any{"b": tc.in}, schema, "body")
|
|
if err != nil {
|
|
t.Errorf("coerce %q: err=%v", tc.in, err)
|
|
continue
|
|
}
|
|
if got["b"] != tc.want {
|
|
t.Errorf("coerce %q: got %v, want %v", tc.in, got["b"], tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_BooleanCoercionBad confirms non-boolean strings fail.
|
|
func TestExtractBySchema_BooleanCoercionBad(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"b": map[string]any{"type": "boolean"},
|
|
},
|
|
}
|
|
_, err := extractBySchema(map[string]any{"b": "maybe"}, schema, "body")
|
|
if err == nil || !strings.Contains(err.Error(), "auto-cast failed") {
|
|
t.Errorf("err = %v, want auto-cast failed", err)
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_TypeMismatch covers wrong-type validation.
|
|
func TestExtractBySchema_TypeMismatch(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"n": map[string]any{"type": "number"},
|
|
},
|
|
}
|
|
_, err := extractBySchema(map[string]any{"n": []any{1}}, schema, "body")
|
|
if err == nil || !strings.Contains(err.Error(), "type mismatch") {
|
|
t.Errorf("err = %v, want type mismatch", err)
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_ArrayInnerType confirms array<inner> validation.
|
|
func TestExtractBySchema_ArrayInnerType(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"xs": map[string]any{"type": "array<number>"},
|
|
},
|
|
}
|
|
// valid
|
|
if _, err := extractBySchema(map[string]any{"xs": []any{1, 2, 3}}, schema, "body"); err != nil {
|
|
t.Errorf("valid array: err=%v", err)
|
|
}
|
|
// invalid inner type
|
|
_, err := extractBySchema(map[string]any{"xs": []any{1, "two", 3}}, schema, "body")
|
|
if err == nil || !strings.Contains(err.Error(), "type mismatch") {
|
|
t.Errorf("invalid array inner: err=%v, want type mismatch", err)
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_ObjectCoercion pins string→object auto-cast.
|
|
func TestExtractBySchema_ObjectCoercion(t *testing.T) {
|
|
schema := map[string]any{
|
|
"properties": map[string]any{
|
|
"o": map[string]any{"type": "object"},
|
|
},
|
|
}
|
|
got, err := extractBySchema(map[string]any{"o": `{"k":"v"}`}, schema, "body")
|
|
if err != nil {
|
|
t.Fatalf("extractBySchema: %v", err)
|
|
}
|
|
m, ok := got["o"].(map[string]any)
|
|
if !ok || m["k"] != "v" {
|
|
t.Errorf("object coercion: got %v", got["o"])
|
|
}
|
|
}
|
|
|
|
// TestExtractBySchema_NilData confirms nil data yields empty map.
|
|
func TestExtractBySchema_NilData(t *testing.T) {
|
|
got, err := extractBySchema(nil, nil, "query")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Errorf("expected empty map, got nil")
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("expected empty map, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestValidateType_FileAndArray confirms validateType handles the
|
|
// edge cases at the python agent_api.py:2021-2051 boundary.
|
|
func TestValidateType_FileAndArray(t *testing.T) {
|
|
if !validateType([]any{map[string]any{}}, "file") {
|
|
t.Error("file should accept []any")
|
|
}
|
|
if !validateType([]any{1, 2, 3}, "array<number>") {
|
|
t.Error("array<number> should accept []int-equivalent")
|
|
}
|
|
if validateType([]any{1, "x"}, "array<number>") {
|
|
t.Error("array<number> should reject mixed types")
|
|
}
|
|
}
|
|
|
|
// TestDefaultForType covers the python default_for_type table.
|
|
func TestDefaultForType(t *testing.T) {
|
|
cases := []struct {
|
|
t string
|
|
// We compare via fmt-friendly assertions rather than reflect.DeepEqual
|
|
// because the python reference returns [] / {} / false / 0 / "" / nil
|
|
// directly, and Go equivalents may differ in concrete type.
|
|
}{
|
|
{"file"}, {"object"}, {"boolean"}, {"number"}, {"string"},
|
|
{"array"}, {"array<object>"}, {"null"}, {"unknown"},
|
|
}
|
|
for _, tc := range cases {
|
|
got := defaultForType(tc.t)
|
|
// Just ensure no panic and we get a sensible zero value.
|
|
_ = got
|
|
}
|
|
}
|