openmeter / ledger /entry_identity_test.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 5)
cee2387 verified
Raw
History Blame Contribute Delete
8.55 kB
package ledger_test
import (
"testing"
"time"
"github.com/alpacahq/alpacadecimal"
"github.com/stretchr/testify/require"
"github.com/openmeterio/openmeter/openmeter/ledger"
ledgeraccount "github.com/openmeterio/openmeter/openmeter/ledger/account"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/models"
)
func TestEntryIdentityParts_Text(t *testing.T) {
sourceChargeID := "01JABCDEF0123456789ABCDEFG"
spendChargeID := "01JBCDEFG0123456789ABCDEFG"
collectionSource := "0"
correctionSource := "entry:1"
tests := []struct {
name string
parts ledger.EntryIdentityParts
expectedText ledger.EntryIdentityKeyText
expectedVersion ledger.EntryIdentityVersion
}{
{
name: "empty v1",
expectedText: "",
expectedVersion: ledger.EntryIdentityVersion1,
},
{
name: "collection source v1",
parts: ledger.EntryIdentityParts{
CollectionSource: &collectionSource,
},
expectedText: "collection-source:0",
expectedVersion: ledger.EntryIdentityVersion1,
},
{
name: "correction source v1",
parts: ledger.EntryIdentityParts{
CorrectionSource: &correctionSource,
},
expectedText: "correction-source:entry:1",
expectedVersion: ledger.EntryIdentityVersion1,
},
{
name: "source charge v2",
parts: ledger.EntryIdentityParts{
SourceChargeID: &sourceChargeID,
},
expectedText: "entry-identity:v2:||01JABCDEF0123456789ABCDEFG|",
expectedVersion: ledger.EntryIdentityVersion2,
},
{
name: "collection source plus charge provenance v2",
parts: ledger.EntryIdentityParts{
CollectionSource: &collectionSource,
SourceChargeID: &sourceChargeID,
SpendChargeID: &spendChargeID,
},
expectedText: "entry-identity:v2:0||01JABCDEF0123456789ABCDEFG|01JBCDEFG0123456789ABCDEFG",
expectedVersion: ledger.EntryIdentityVersion2,
},
{
name: "correction source plus spend provenance v2",
parts: ledger.EntryIdentityParts{
CorrectionSource: &correctionSource,
SpendChargeID: &spendChargeID,
},
expectedText: "entry-identity:v2:|entry%3A1||01JBCDEFG0123456789ABCDEFG",
expectedVersion: ledger.EntryIdentityVersion2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actualText, actualVersion := tt.parts.Text()
require.Equal(t, tt.expectedText, actualText)
require.Equal(t, tt.expectedVersion, actualVersion)
require.Equal(t, tt.expectedVersion, actualText.Version())
parsedVersion, parsedParts, err := actualText.Parse()
require.NoError(t, err)
require.Equal(t, tt.expectedVersion, parsedVersion)
require.Equal(t, tt.parts, parsedParts)
})
}
}
func TestEntryIdentityKeyText_Parse(t *testing.T) {
t.Run("returns malformed v2 error", func(t *testing.T) {
_, _, err := ledger.EntryIdentityKeyText("entry-identity:v2:too|few").Parse()
require.ErrorContains(t, err, "invalid ledger entry identity key format")
})
}
func TestValidateEntryIdentityKey(t *testing.T) {
sourceChargeID := "01JABCDEF0123456789ABCDEFG"
spendChargeID := "01JBCDEFG0123456789ABCDEFG"
collectionSource := "0"
address := testEntryIdentityAddress(t, ledger.AccountTypeCustomerFBO, "subaccount", ledger.Route{
Currency: currencyx.Code("USD"),
})
identityKey, _ := ledger.EntryIdentityParts{
CollectionSource: &collectionSource,
SourceChargeID: &sourceChargeID,
SpendChargeID: &spendChargeID,
}.Text()
t.Run("accepts canonical identity", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: string(identityKey),
sourceChargeID: &sourceChargeID,
spendChargeID: &spendChargeID,
}
require.NoError(t, ledger.ValidateEntryInput(t.Context(), entry))
})
t.Run("accepts current schema without charge provenance", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: "collection-source:0",
}
require.NoError(t, ledger.ValidateEntryInput(t.Context(), entry))
})
t.Run("rejects provenance without v2 identity", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: "collection-source:0",
sourceChargeID: &sourceChargeID,
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "identity_key version must be 2")
})
t.Run("rejects mismatched source charge", func(t *testing.T) {
otherSourceChargeID := "01JCDEFGH0123456789ABCDEFG"
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: string(identityKey),
sourceChargeID: &otherSourceChargeID,
spendChargeID: &spendChargeID,
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "source_charge_id does not match identity_key")
})
t.Run("rejects legacy schema with charge provenance", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: "collection-source:0",
schemaVersion: ledger.EntrySchemaVersionLegacy,
sourceChargeID: &sourceChargeID,
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "schema_version 1 cannot contain charge provenance")
})
t.Run("rejects legacy schema with v2 identity", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: string(identityKey),
schemaVersion: ledger.EntrySchemaVersionLegacy,
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "identity_key version 2 requires schema_version 2")
})
t.Run("rejects unsupported schema version", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: "collection-source:0",
schemaVersion: ledger.EntrySchemaVersion(99),
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "unsupported schema_version 99")
})
t.Run("rejects non-canonical identity", func(t *testing.T) {
entry := validationEntryInput{
address: address,
amount: alpacadecimal.NewFromInt(1),
identityKey: "entry-identity:v2:0||01JABCDEF0123456789ABCDEFG|01JBCDEFG0123456789ABCDEFG|extra",
sourceChargeID: &sourceChargeID,
spendChargeID: &spendChargeID,
}
err := ledger.ValidateEntryInput(t.Context(), entry)
require.Error(t, err)
require.ErrorContains(t, ledger.ValidateEntryIdentityKey(entry), "invalid ledger entry identity key format")
})
}
type validationEntryInput struct {
address ledger.PostingAddress
amount alpacadecimal.Decimal
identityKey string
schemaVersion ledger.EntrySchemaVersion
sourceChargeID *string
spendChargeID *string
}
func (e validationEntryInput) PostingAddress() ledger.PostingAddress {
return e.address
}
func (e validationEntryInput) Amount() alpacadecimal.Decimal {
return e.amount
}
func (e validationEntryInput) IdentityKey() string {
return e.identityKey
}
func (e validationEntryInput) SchemaVersion() ledger.EntrySchemaVersion {
if e.schemaVersion == 0 {
return ledger.EntrySchemaVersionCurrent
}
return e.schemaVersion
}
func (e validationEntryInput) SourceChargeID() *string {
return e.sourceChargeID
}
func (e validationEntryInput) SpendChargeID() *string {
return e.spendChargeID
}
func (e validationEntryInput) Annotations() models.Annotations {
return nil
}
func testEntryIdentityAddress(t *testing.T, accountType ledger.AccountType, subAccountID string, route ledger.Route) ledger.PostingAddress {
t.Helper()
key, err := ledger.BuildRoutingKey(route)
require.NoError(t, err)
addr, err := ledgeraccount.NewAddressFromData(ledgeraccount.AddressData{
SubAccountID: subAccountID,
AccountType: accountType,
Route: route,
RouteID: "route-" + subAccountID + "-" + time.Now().UTC().Format("150405.000000000"),
RoutingKey: key,
})
require.NoError(t, err)
return addr
}