418 lines
10 KiB
Go
418 lines
10 KiB
Go
package ledger
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tech/sendico/ledger/storage/model"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func TestParseObjectID(t *testing.T) {
|
|
t.Run("ValidObjectID", func(t *testing.T) {
|
|
validID := primitive.NewObjectID()
|
|
result, err := parseObjectID(validID.Hex())
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, validID, result)
|
|
})
|
|
|
|
t.Run("EmptyString", func(t *testing.T) {
|
|
result, err := parseObjectID("")
|
|
|
|
require.Error(t, err)
|
|
assert.Equal(t, primitive.NilObjectID, result)
|
|
assert.Contains(t, err.Error(), "empty object ID")
|
|
})
|
|
|
|
t.Run("InvalidHexString", func(t *testing.T) {
|
|
result, err := parseObjectID("invalid-hex-string")
|
|
|
|
require.Error(t, err)
|
|
assert.Equal(t, primitive.NilObjectID, result)
|
|
assert.Contains(t, err.Error(), "invalid object ID")
|
|
})
|
|
|
|
t.Run("IncorrectLength", func(t *testing.T) {
|
|
result, err := parseObjectID("abc123")
|
|
|
|
require.Error(t, err)
|
|
assert.Equal(t, primitive.NilObjectID, result)
|
|
})
|
|
}
|
|
|
|
func TestParseDecimal(t *testing.T) {
|
|
t.Run("ValidDecimal", func(t *testing.T) {
|
|
result, err := parseDecimal("123.45")
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Equal(decimal.NewFromFloat(123.45)))
|
|
})
|
|
|
|
t.Run("EmptyString", func(t *testing.T) {
|
|
result, err := parseDecimal("")
|
|
|
|
require.Error(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Contains(t, err.Error(), "empty amount")
|
|
})
|
|
|
|
t.Run("InvalidDecimal", func(t *testing.T) {
|
|
result, err := parseDecimal("not-a-number")
|
|
|
|
require.Error(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Contains(t, err.Error(), "invalid decimal amount")
|
|
})
|
|
|
|
t.Run("NegativeDecimal", func(t *testing.T) {
|
|
result, err := parseDecimal("-100.50")
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Equal(decimal.NewFromFloat(-100.50)))
|
|
})
|
|
|
|
t.Run("ZeroDecimal", func(t *testing.T) {
|
|
result, err := parseDecimal("0")
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.IsZero())
|
|
})
|
|
}
|
|
|
|
func TestValidateMoney(t *testing.T) {
|
|
t.Run("ValidMoney", func(t *testing.T) {
|
|
money := &moneyv1.Money{
|
|
Amount: "100.50",
|
|
Currency: "USD",
|
|
}
|
|
|
|
err := validateMoney(money, "test_field")
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("NilMoney", func(t *testing.T) {
|
|
err := validateMoney(nil, "test_field")
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "test_field: money is required")
|
|
})
|
|
|
|
t.Run("EmptyAmount", func(t *testing.T) {
|
|
money := &moneyv1.Money{
|
|
Amount: "",
|
|
Currency: "USD",
|
|
}
|
|
|
|
err := validateMoney(money, "test_field")
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "test_field: amount is required")
|
|
})
|
|
|
|
t.Run("EmptyCurrency", func(t *testing.T) {
|
|
money := &moneyv1.Money{
|
|
Amount: "100.50",
|
|
Currency: "",
|
|
}
|
|
|
|
err := validateMoney(money, "test_field")
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "test_field: currency is required")
|
|
})
|
|
|
|
t.Run("InvalidAmount", func(t *testing.T) {
|
|
money := &moneyv1.Money{
|
|
Amount: "invalid",
|
|
Currency: "USD",
|
|
}
|
|
|
|
err := validateMoney(money, "test_field")
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid decimal amount")
|
|
})
|
|
}
|
|
|
|
func TestValidatePostingLines(t *testing.T) {
|
|
t.Run("ValidPostingLines", func(t *testing.T) {
|
|
lines := []*ledgerv1.PostingLine{
|
|
{
|
|
LedgerAccountRef: primitive.NewObjectID().Hex(),
|
|
Money: &moneyv1.Money{
|
|
Amount: "10.00",
|
|
Currency: "USD",
|
|
},
|
|
LineType: ledgerv1.LineType_LINE_FEE,
|
|
},
|
|
}
|
|
|
|
err := validatePostingLines(lines)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("EmptyLines", func(t *testing.T) {
|
|
err := validatePostingLines([]*ledgerv1.PostingLine{})
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("NilLine", func(t *testing.T) {
|
|
lines := []*ledgerv1.PostingLine{nil}
|
|
|
|
err := validatePostingLines(lines)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "nil posting line")
|
|
})
|
|
|
|
t.Run("EmptyAccountRef", func(t *testing.T) {
|
|
lines := []*ledgerv1.PostingLine{
|
|
{
|
|
LedgerAccountRef: "",
|
|
Money: &moneyv1.Money{
|
|
Amount: "10.00",
|
|
Currency: "USD",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := validatePostingLines(lines)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "ledger_account_ref is required")
|
|
})
|
|
|
|
t.Run("NilMoney", func(t *testing.T) {
|
|
lines := []*ledgerv1.PostingLine{
|
|
{
|
|
LedgerAccountRef: primitive.NewObjectID().Hex(),
|
|
Money: nil,
|
|
},
|
|
}
|
|
|
|
err := validatePostingLines(lines)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "money is required")
|
|
})
|
|
|
|
t.Run("MainLineType", func(t *testing.T) {
|
|
lines := []*ledgerv1.PostingLine{
|
|
{
|
|
LedgerAccountRef: primitive.NewObjectID().Hex(),
|
|
Money: &moneyv1.Money{
|
|
Amount: "10.00",
|
|
Currency: "USD",
|
|
},
|
|
LineType: ledgerv1.LineType_LINE_MAIN,
|
|
},
|
|
}
|
|
|
|
err := validatePostingLines(lines)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cannot have LINE_MAIN type")
|
|
})
|
|
}
|
|
|
|
func TestGetEventTime(t *testing.T) {
|
|
t.Run("ValidTimestamp", func(t *testing.T) {
|
|
now := time.Now()
|
|
ts := timestamppb.New(now)
|
|
|
|
result := getEventTime(ts)
|
|
|
|
assert.True(t, result.Sub(now) < time.Second)
|
|
})
|
|
|
|
t.Run("NilTimestamp", func(t *testing.T) {
|
|
before := time.Now()
|
|
result := getEventTime(nil)
|
|
after := time.Now()
|
|
|
|
assert.True(t, result.After(before) || result.Equal(before))
|
|
assert.True(t, result.Before(after) || result.Equal(after))
|
|
})
|
|
|
|
t.Run("InvalidTimestamp", func(t *testing.T) {
|
|
// Create an invalid timestamp with negative seconds
|
|
ts := ×tamppb.Timestamp{Seconds: -1, Nanos: -1}
|
|
|
|
// Invalid timestamp should return current time
|
|
before := time.Now()
|
|
result := getEventTime(ts)
|
|
after := time.Now()
|
|
|
|
// Result should be close to now since timestamp is invalid
|
|
assert.True(t, result.After(before.Add(-time.Second)) || result.Equal(before))
|
|
assert.True(t, result.Before(after.Add(time.Second)) || result.Equal(after))
|
|
})
|
|
}
|
|
|
|
func TestProtoLineTypeToModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input ledgerv1.LineType
|
|
expected model.LineType
|
|
}{
|
|
{"Main", ledgerv1.LineType_LINE_MAIN, model.LineTypeMain},
|
|
{"Fee", ledgerv1.LineType_LINE_FEE, model.LineTypeFee},
|
|
{"Spread", ledgerv1.LineType_LINE_SPREAD, model.LineTypeSpread},
|
|
{"Reversal", ledgerv1.LineType_LINE_REVERSAL, model.LineTypeReversal},
|
|
{"Unspecified", ledgerv1.LineType_LINE_TYPE_UNSPECIFIED, model.LineTypeMain},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := protoLineTypeToModel(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModelLineTypeToProto(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input model.LineType
|
|
expected ledgerv1.LineType
|
|
}{
|
|
{"Main", model.LineTypeMain, ledgerv1.LineType_LINE_MAIN},
|
|
{"Fee", model.LineTypeFee, ledgerv1.LineType_LINE_FEE},
|
|
{"Spread", model.LineTypeSpread, ledgerv1.LineType_LINE_SPREAD},
|
|
{"Reversal", model.LineTypeReversal, ledgerv1.LineType_LINE_REVERSAL},
|
|
{"Unknown", model.LineType("unknown"), ledgerv1.LineType_LINE_TYPE_UNSPECIFIED},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := modelLineTypeToProto(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModelEntryTypeToProto(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input model.EntryType
|
|
expected ledgerv1.EntryType
|
|
}{
|
|
{"Credit", model.EntryTypeCredit, ledgerv1.EntryType_ENTRY_CREDIT},
|
|
{"Debit", model.EntryTypeDebit, ledgerv1.EntryType_ENTRY_DEBIT},
|
|
{"Transfer", model.EntryTypeTransfer, ledgerv1.EntryType_ENTRY_TRANSFER},
|
|
{"FX", model.EntryTypeFX, ledgerv1.EntryType_ENTRY_FX},
|
|
{"Fee", model.EntryTypeFee, ledgerv1.EntryType_ENTRY_FEE},
|
|
{"Adjust", model.EntryTypeAdjust, ledgerv1.EntryType_ENTRY_ADJUST},
|
|
{"Reverse", model.EntryTypeReverse, ledgerv1.EntryType_ENTRY_REVERSE},
|
|
{"Unknown", model.EntryType("unknown"), ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := modelEntryTypeToProto(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalculateBalance(t *testing.T) {
|
|
t.Run("PositiveBalance", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "100.00"},
|
|
{Amount: "50.00"},
|
|
}
|
|
|
|
result, err := calculateBalance(lines)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Equal(decimal.NewFromFloat(150.00)))
|
|
})
|
|
|
|
t.Run("NegativeBalance", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "-100.00"},
|
|
{Amount: "-50.00"},
|
|
}
|
|
|
|
result, err := calculateBalance(lines)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Equal(decimal.NewFromFloat(-150.00)))
|
|
})
|
|
|
|
t.Run("ZeroBalance", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "100.00"},
|
|
{Amount: "-100.00"},
|
|
}
|
|
|
|
result, err := calculateBalance(lines)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.IsZero())
|
|
})
|
|
|
|
t.Run("EmptyLines", func(t *testing.T) {
|
|
result, err := calculateBalance([]*model.PostingLine{})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, result.IsZero())
|
|
})
|
|
|
|
t.Run("InvalidAmount", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "invalid"},
|
|
}
|
|
|
|
_, err := calculateBalance(lines)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestValidateBalanced(t *testing.T) {
|
|
t.Run("BalancedEntry", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "100.00"}, // credit
|
|
{Amount: "-100.00"}, // debit
|
|
}
|
|
|
|
err := validateBalanced(lines)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("BalancedWithMultipleLines", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "100.00"}, // credit
|
|
{Amount: "-50.00"}, // debit
|
|
{Amount: "-50.00"}, // debit
|
|
}
|
|
|
|
err := validateBalanced(lines)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("UnbalancedEntry", func(t *testing.T) {
|
|
lines := []*model.PostingLine{
|
|
{Amount: "100.00"},
|
|
{Amount: "-50.00"},
|
|
}
|
|
|
|
err := validateBalanced(lines)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "must balance")
|
|
})
|
|
|
|
t.Run("EmptyLines", func(t *testing.T) {
|
|
err := validateBalanced([]*model.PostingLine{})
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|