mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
393 lines
9.6 KiB
Go
393 lines
9.6 KiB
Go
package testserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/messaging/publish"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
"github.com/hanzo-fi/payments/pkg/client/models/components"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/invopop/jsonschema"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/onsi/gomega/types"
|
|
"github.com/xeipuuv/gojsonschema"
|
|
)
|
|
|
|
type PayloadMatcher interface {
|
|
Match(actual interface{}) error
|
|
}
|
|
|
|
type NoOpPayloadMatcher struct{}
|
|
|
|
func (n NoOpPayloadMatcher) Match(interface{}) error {
|
|
return nil
|
|
}
|
|
|
|
var _ PayloadMatcher = (*NoOpPayloadMatcher)(nil)
|
|
|
|
type CallbackMatcher struct {
|
|
expected any
|
|
callback func(b []byte) error
|
|
}
|
|
|
|
func (m CallbackMatcher) Match(payload interface{}) error {
|
|
marshaledPayload, err := rawJson(m.expected, payload, true)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to process payload: %s", err)
|
|
}
|
|
return m.callback(marshaledPayload)
|
|
}
|
|
|
|
func WithCallback(expected any, callback func(b []byte) error) CallbackMatcher {
|
|
return CallbackMatcher{
|
|
expected: expected,
|
|
callback: callback,
|
|
}
|
|
}
|
|
|
|
var _ PayloadMatcher = (*CallbackMatcher)(nil)
|
|
|
|
type StructPayloadMatcher struct {
|
|
expected any
|
|
strict bool
|
|
}
|
|
|
|
func (s StructPayloadMatcher) Match(payload interface{}) error {
|
|
marshaledPayload, err := rawJson(s.expected, payload, s.strict)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to process payload: %s", err)
|
|
}
|
|
|
|
unmarshalledPayload := reflect.New(reflect.TypeOf(s.expected)).Interface()
|
|
if err := json.Unmarshal(marshaledPayload, unmarshalledPayload); err != nil {
|
|
return fmt.Errorf("unable to unmarshal payload: %s", err)
|
|
}
|
|
|
|
// unmarshalledPayload is actually a pointer
|
|
// as it is seen as "any" by the code, we use reflection to get the targeted valud
|
|
unmarshalledPayload = reflect.ValueOf(unmarshalledPayload).Elem().Interface()
|
|
|
|
diff := cmp.Diff(unmarshalledPayload, s.expected, cmp.Comparer(func(v1 *big.Int, v2 *big.Int) bool {
|
|
return v1.String() == v2.String()
|
|
}))
|
|
if diff != "" {
|
|
return errors.New(diff)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func WithPayload(v any) StructPayloadMatcher {
|
|
return StructPayloadMatcher{
|
|
expected: v,
|
|
strict: true,
|
|
}
|
|
}
|
|
|
|
// WithPayloadSubset is able to match partial structs
|
|
func WithPayloadSubset(v any) StructPayloadMatcher {
|
|
return StructPayloadMatcher{
|
|
expected: v,
|
|
strict: false,
|
|
}
|
|
}
|
|
|
|
var _ PayloadMatcher = (*StructPayloadMatcher)(nil)
|
|
|
|
// todo(libs): move in shared libs
|
|
type EventMatcher struct {
|
|
eventName string
|
|
matchers []PayloadMatcher
|
|
err error
|
|
}
|
|
|
|
func (e *EventMatcher) Match(actual any) (success bool, err error) {
|
|
msg, ok := actual.(*nats.Msg)
|
|
if !ok {
|
|
return false, fmt.Errorf("expected type %T", actual)
|
|
}
|
|
|
|
ev := publish.EventMessage{}
|
|
if err := json.Unmarshal(msg.Data, &ev); err != nil {
|
|
return false, fmt.Errorf("unable to unmarshal msg: %s", err)
|
|
}
|
|
|
|
if ev.Type != e.eventName {
|
|
return false, nil
|
|
}
|
|
|
|
for _, matcher := range e.matchers {
|
|
if e.err = matcher.Match(ev.Payload); e.err != nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (e *EventMatcher) FailureMessage(_ any) (message string) {
|
|
return fmt.Sprintf("event does not match expectations: %s", e.err)
|
|
}
|
|
|
|
func (e *EventMatcher) NegatedFailureMessage(_ any) (message string) {
|
|
return "event should not match"
|
|
}
|
|
|
|
var _ types.GomegaMatcher = (*EventMatcher)(nil)
|
|
|
|
func Event(eventName string, matchers ...PayloadMatcher) types.GomegaMatcher {
|
|
return &EventMatcher{
|
|
matchers: matchers,
|
|
eventName: eventName,
|
|
}
|
|
}
|
|
|
|
type LinkAttemptsLengthMatcher struct {
|
|
length int
|
|
matchers []PayloadMatcher
|
|
err error
|
|
}
|
|
|
|
func (m *LinkAttemptsLengthMatcher) Match(actual any) (success bool, err error) {
|
|
attempts, ok := actual.([]components.V3PaymentServiceUserLinkAttempt)
|
|
if !ok {
|
|
return false, fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if len(attempts) != m.length {
|
|
m.err = fmt.Errorf("expected %d link attempts, got %d", m.length, len(attempts))
|
|
return false, nil
|
|
}
|
|
|
|
for _, attempt := range attempts {
|
|
for _, matcher := range m.matchers {
|
|
if m.err = matcher.Match(attempt); m.err != nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (m *LinkAttemptsLengthMatcher) FailureMessage(_ any) (message string) {
|
|
return fmt.Sprintf("link attempt do not match expectations: %s", m.err)
|
|
}
|
|
|
|
func (m *LinkAttemptsLengthMatcher) NegatedFailureMessage(_ any) (message string) {
|
|
return "link attempt should not match"
|
|
}
|
|
|
|
var _ types.GomegaMatcher = (*LinkAttemptsLengthMatcher)(nil)
|
|
|
|
func HaveLinkAttemptsLengthMatcher(length int, matchers ...PayloadMatcher) types.GomegaMatcher {
|
|
return &LinkAttemptsLengthMatcher{
|
|
length: length,
|
|
matchers: matchers,
|
|
}
|
|
}
|
|
|
|
type LinkAttemptStatusMatcher struct {
|
|
status components.V3OpenBankingConnectionAttemptStatusEnum
|
|
}
|
|
|
|
func (t *LinkAttemptStatusMatcher) Match(actual any) error {
|
|
attempt, ok := actual.(components.V3PaymentServiceUserLinkAttempt)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if attempt.Status != t.status {
|
|
return fmt.Errorf("expected link attempt to have status %s, got %s", t.status, attempt.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func HaveLinkAttemptStatus(status components.V3OpenBankingConnectionAttemptStatusEnum) PayloadMatcher {
|
|
return &LinkAttemptStatusMatcher{
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
var _ PayloadMatcher = (*LinkAttemptStatusMatcher)(nil)
|
|
|
|
type UserConnectionsLengthMatcher struct {
|
|
length int
|
|
matchers []PayloadMatcher
|
|
err error
|
|
}
|
|
|
|
func (m *UserConnectionsLengthMatcher) Match(actual any) (success bool, err error) {
|
|
attempts, ok := actual.([]components.V3PaymentServiceUserConnection)
|
|
if !ok {
|
|
return false, fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if len(attempts) != m.length {
|
|
m.err = fmt.Errorf("expected %d connections, got %d", m.length, len(attempts))
|
|
return false, nil
|
|
}
|
|
|
|
for _, attempt := range attempts {
|
|
for _, matcher := range m.matchers {
|
|
if m.err = matcher.Match(attempt); m.err != nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (m *UserConnectionsLengthMatcher) FailureMessage(_ any) (message string) {
|
|
return fmt.Sprintf("connections do not match expectations: %s", m.err)
|
|
}
|
|
|
|
func (m *UserConnectionsLengthMatcher) NegatedFailureMessage(_ any) (message string) {
|
|
return "connections should not match"
|
|
}
|
|
|
|
var _ types.GomegaMatcher = (*UserConnectionsLengthMatcher)(nil)
|
|
|
|
func HaveUserConnectionsLengthMatcher(length int, matchers ...PayloadMatcher) types.GomegaMatcher {
|
|
return &UserConnectionsLengthMatcher{
|
|
length: length,
|
|
matchers: matchers,
|
|
}
|
|
}
|
|
|
|
type UserConnectionStatusMatcher struct {
|
|
status components.V3ConnectionStatusEnum
|
|
}
|
|
|
|
func (t *UserConnectionStatusMatcher) Match(actual any) error {
|
|
connection, ok := actual.(components.V3PaymentServiceUserConnection)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if connection.Status != t.status {
|
|
return fmt.Errorf("expected connection to have status %s, got %s", t.status, connection.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func HaveUserConnectionStatus(status components.V3ConnectionStatusEnum) PayloadMatcher {
|
|
return &UserConnectionStatusMatcher{
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
var _ PayloadMatcher = (*UserConnectionStatusMatcher)(nil)
|
|
|
|
type TaskMatcher struct {
|
|
status models.TaskStatus
|
|
matchers []PayloadMatcher
|
|
err error
|
|
}
|
|
|
|
func (t *TaskMatcher) Match(actual any) (success bool, err error) {
|
|
task, ok := actual.(models.Task)
|
|
if !ok {
|
|
return false, fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if task.Status != t.status {
|
|
t.err = fmt.Errorf("found task with status %s and error: %w", task.Status, task.Error)
|
|
return false, nil
|
|
}
|
|
|
|
for _, matcher := range t.matchers {
|
|
if t.err = matcher.Match(task); t.err != nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (t *TaskMatcher) FailureMessage(_ any) (message string) {
|
|
return fmt.Sprintf("event does not match expectations: %s", t.err)
|
|
}
|
|
|
|
func (t *TaskMatcher) NegatedFailureMessage(_ any) (message string) {
|
|
return "event should not match"
|
|
}
|
|
|
|
var _ types.GomegaMatcher = (*TaskMatcher)(nil)
|
|
|
|
func HaveTaskStatus(status models.TaskStatus, matchers ...PayloadMatcher) types.GomegaMatcher {
|
|
return &TaskMatcher{
|
|
matchers: matchers,
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
type TaskErrorMatcher struct {
|
|
expected error
|
|
}
|
|
|
|
func (m TaskErrorMatcher) Match(actual interface{}) error {
|
|
task, ok := actual.(models.Task)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected type %T", actual)
|
|
}
|
|
|
|
if task.Error == nil {
|
|
return fmt.Errorf("task with status %q did not contain an error", task.Status)
|
|
}
|
|
|
|
// not guaranteed to be able to unwrap the error, so just string match
|
|
if !strings.Contains(task.Error.Error(), m.expected.Error()) {
|
|
return fmt.Errorf("found unexpected error: %w", task.Error)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func WithError(expected error) TaskErrorMatcher {
|
|
return TaskErrorMatcher{
|
|
expected: expected,
|
|
}
|
|
}
|
|
|
|
var _ PayloadMatcher = (*TaskErrorMatcher)(nil)
|
|
|
|
func rawJson(expected any, payload interface{}, strict bool) (b []byte, err error) {
|
|
rawSchema := jsonschema.Reflect(expected)
|
|
data, err := json.Marshal(rawSchema)
|
|
if err != nil {
|
|
return b, fmt.Errorf("unable to marshal schema: %s", err)
|
|
}
|
|
|
|
schemaJSONLoader := gojsonschema.NewStringLoader(string(data))
|
|
schema, err := gojsonschema.NewSchema(schemaJSONLoader)
|
|
if err != nil {
|
|
return b, fmt.Errorf("unable to load json schema: %s", err)
|
|
}
|
|
|
|
dataJsonLoader := gojsonschema.NewRawLoader(payload)
|
|
|
|
if strict {
|
|
validate, err := schema.Validate(dataJsonLoader)
|
|
if err != nil {
|
|
return b, fmt.Errorf("failed to validate: %w", err)
|
|
}
|
|
|
|
if !validate.Valid() {
|
|
return b, fmt.Errorf("validation errors: %s", validate.Errors())
|
|
}
|
|
}
|
|
|
|
marshaledPayload, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return b, fmt.Errorf("unable to marshal payload: %s", err)
|
|
}
|
|
return marshaledPayload, nil
|
|
}
|