Files
new-api/pkg/jsplugin/fixture.go
T

117 lines
3.9 KiB
Go

package jsplugin
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
)
// Fixture describes deterministic calls into a plugin. Arguments and expected
// values stay as JSON so fixtures remain portable across engine implementations.
type Fixture struct {
UnixNow *int64 `json:"unixNow"`
Cases []FixtureCase `json:"cases"`
}
type FixtureCase struct {
Name string `json:"name"`
Hook string `json:"hook"`
Member string `json:"member,omitempty"`
Path []string `json:"path,omitempty"`
Args []json.RawMessage `json:"args"`
Expected json.RawMessage `json:"expected"`
ExpectedError string `json:"expectedError,omitempty"`
}
type FixtureReport struct {
Total int
Passed int
}
// ReplayFixture compiles a plugin and runs every fixture case in declaration
// order. unixNow is fixed by the fixture to keep signing and timestamp hooks
// reproducible.
func ReplayFixture(ctx context.Context, source string, data []byte) (FixtureReport, error) {
var fixture Fixture
if err := common.Unmarshal(data, &fixture); err != nil {
return FixtureReport{}, fmt.Errorf("decode fixture: %w", err)
}
if len(fixture.Cases) == 0 {
return FixtureReport{}, fmt.Errorf("fixture must contain at least one case")
}
options := Options{Key: "fixture", Version: "fixture"}
if fixture.UnixNow != nil {
fixed := time.Unix(*fixture.UnixNow, 0)
options.Now = func() time.Time { return fixed }
}
plugin, err := NewRegistry().Register(source, options)
if err != nil {
return FixtureReport{}, fmt.Errorf("compile plugin: %w", err)
}
report := FixtureReport{Total: len(fixture.Cases)}
for index, testCase := range fixture.Cases {
caseName := strings.TrimSpace(testCase.Name)
if caseName == "" {
caseName = fmt.Sprintf("case %d", index+1)
}
if strings.TrimSpace(testCase.Hook) == "" {
return report, fmt.Errorf("%s: hook is required", caseName)
}
args := make([]any, len(testCase.Args))
for argumentIndex, raw := range testCase.Args {
if err = common.Unmarshal(raw, &args[argumentIndex]); err != nil {
return report, fmt.Errorf("%s: decode argument %d: %w", caseName, argumentIndex+1, err)
}
}
var result any
if testCase.Member != "" && len(testCase.Path) > 0 {
return report, fmt.Errorf("%s: member and path are mutually exclusive", caseName)
}
if len(testCase.Path) > 0 {
result, err = plugin.Engine.CallPath(ctx, testCase.Hook, testCase.Path, args...)
} else if testCase.Member == "" {
result, err = plugin.Engine.Call(ctx, testCase.Hook, args...)
} else {
result, err = plugin.Engine.CallMember(ctx, testCase.Hook, testCase.Member, args...)
}
if testCase.ExpectedError != "" {
if err == nil || !strings.Contains(err.Error(), testCase.ExpectedError) {
return report, fmt.Errorf("%s: expected error containing %q, got %v", caseName, testCase.ExpectedError, err)
}
report.Passed++
continue
}
if err != nil {
return report, fmt.Errorf("%s: %w", caseName, err)
}
if len(testCase.Expected) == 0 {
return report, fmt.Errorf("%s: expected is required when expectedError is empty", caseName)
}
var expected any
if err = common.Unmarshal(testCase.Expected, &expected); err != nil {
return report, fmt.Errorf("%s: decode expected value: %w", caseName, err)
}
actualData, marshalErr := common.Marshal(result)
if marshalErr != nil {
return report, fmt.Errorf("%s: encode actual value: %w", caseName, marshalErr)
}
var actual any
if err = common.Unmarshal(actualData, &actual); err != nil {
return report, fmt.Errorf("%s: normalize actual value: %w", caseName, err)
}
if !reflect.DeepEqual(expected, actual) {
return report, fmt.Errorf("%s: result mismatch: expected %s, got %s", caseName, testCase.Expected, actualData)
}
report.Passed++
}
return report, nil
}