Merge pull request 'Support filtering by nature and asset country' (#24) from filters into main
All checks were successful
Badges / coveralls (push) Successful in 33s
All checks were successful
Badges / coveralls (push) Successful in 33s
Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
@@ -16,21 +16,32 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: once we support more brokers or exchanges we should make this parameter required and
|
||||
// remove/change default
|
||||
var platform = pflag.StringP("platform", "p", "trading212", "one of the supported platforms")
|
||||
platform = pflag.StringP("platform", "p", "trading212", "One of the supported platforms")
|
||||
lang = pflag.StringP("language", "l", language.Portuguese.String(), "The 2 letter language code")
|
||||
// TODO: improve documentation on selectors
|
||||
selectors = pflag.StringSlice("selectors", nil, "Only process entries that conform to all the selectors:")
|
||||
|
||||
var lang = pflag.StringP("language", "l", language.Portuguese.String(), "2 letter language code")
|
||||
|
||||
var readerFactories = map[string]func() internal.RecordReader{
|
||||
readerFactories = map[string]func() internal.RecordReader{
|
||||
"trading212": func() internal.RecordReader {
|
||||
return trading212.NewRecordReader(os.Stdin, internal.NewOpenFIGI(&http.Client{Timeout: 5 * time.Second}))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
pflag.Parse()
|
||||
|
||||
err := run(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("found a fatal issue", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
if platform == nil || len(*platform) == 0 {
|
||||
slog.Error("--platform flag is required")
|
||||
os.Exit(1)
|
||||
@@ -41,14 +52,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := run(context.Background(), *platform, *lang)
|
||||
if err != nil {
|
||||
slog.Error("found a fatal issue", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, platform, lang string) error {
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Kill, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
@@ -56,25 +59,30 @@ func run(ctx context.Context, platform, lang string) error {
|
||||
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
|
||||
factory, ok := readerFactories[platform]
|
||||
factory, ok := readerFactories[*platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported platform: %s", platform)
|
||||
return fmt.Errorf("unsupported platform: %s", *platform)
|
||||
}
|
||||
|
||||
reader := factory()
|
||||
|
||||
writer := internal.NewAggregatorWriter()
|
||||
|
||||
selector, err := internal.ParseSelectors(*selectors)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing selectors: %w", err)
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
return internal.BuildReport(ctx, reader, writer)
|
||||
return internal.BuildReport(ctx, reader, writer, selector)
|
||||
})
|
||||
|
||||
err := eg.Wait()
|
||||
err = eg.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loc, err := NewLocalizer(lang)
|
||||
loc, err := NewLocalizer(*lang)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create localizer: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,17 @@ const (
|
||||
)
|
||||
|
||||
func (n Nature) String() string {
|
||||
if n == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if n.Valid() {
|
||||
return string(n)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (n Nature) Valid() bool {
|
||||
switch n {
|
||||
case NatureG01, NatureG20:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ func TestNature_String(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "return unknown",
|
||||
name: "return unknown for empty",
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "return unknown for bad value",
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,7 +50,12 @@ type ReportWriter interface {
|
||||
Write(context.Context, ReportItem) error
|
||||
}
|
||||
|
||||
func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter) error {
|
||||
// Selector returns true if a record should be selected for processing, false otherwise.
|
||||
type Selector func(Record) bool
|
||||
|
||||
// BuildReport reads records from a RecordReader and, if the record passes the Selector, it is
|
||||
// processed into the report
|
||||
func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter, s Selector) error {
|
||||
buys := make(map[string]*FillerQueue)
|
||||
|
||||
for {
|
||||
@@ -66,6 +71,10 @@ func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter)
|
||||
return err
|
||||
}
|
||||
|
||||
if !s(rec) {
|
||||
continue
|
||||
}
|
||||
|
||||
buyQueue, ok := buys[rec.Symbol()]
|
||||
if !ok {
|
||||
buyQueue = new(FillerQueue)
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestBuildReport(t *testing.T) {
|
||||
Taxes: decimal.Decimal{},
|
||||
})).Times(1)
|
||||
|
||||
gotErr := internal.BuildReport(t.Context(), reader, writer)
|
||||
gotErr := internal.BuildReport(t.Context(), reader, writer, internal.Any())
|
||||
if gotErr != nil {
|
||||
t.Fatalf("got unexpected err: %v", gotErr)
|
||||
}
|
||||
|
||||
114
internal/selectors.go
Normal file
114
internal/selectors.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Any() Selector {
|
||||
return func(r Record) bool { return true }
|
||||
}
|
||||
|
||||
func And(a, b Selector) Selector {
|
||||
return func(r Record) bool {
|
||||
return a(r) && b(r)
|
||||
}
|
||||
}
|
||||
|
||||
func OnlyNature(n Nature) Selector {
|
||||
return func(r Record) bool {
|
||||
return r.Nature() == n
|
||||
}
|
||||
}
|
||||
|
||||
func OnlyAssetCountry(c int64) Selector {
|
||||
return func(r Record) bool {
|
||||
return r.AssetCountry() == c
|
||||
}
|
||||
}
|
||||
|
||||
// selectorParser is a function that parses a selector value string into a Selector
|
||||
type selectorParser func(string) (Selector, error)
|
||||
|
||||
// parsers maps selector keys to their parser functions
|
||||
var parsers = map[string]selectorParser{
|
||||
"code": parseNature,
|
||||
"assetCountry": parseAssetCountry,
|
||||
}
|
||||
|
||||
func parseNature(value string) (Selector, error) {
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("code selector requires a non-empty value")
|
||||
}
|
||||
nature := Nature(value)
|
||||
if !nature.Valid() {
|
||||
return nil, fmt.Errorf("invalid nature code %q", value)
|
||||
}
|
||||
return OnlyNature(nature), nil
|
||||
}
|
||||
|
||||
func parseAssetCountry(value string) (Selector, error) {
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("assetCountry selector requires a non-empty value")
|
||||
}
|
||||
i, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("assetCountry value must be a valid integer: %w", err)
|
||||
}
|
||||
return OnlyAssetCountry(i), nil
|
||||
}
|
||||
|
||||
// ParseSelectors parses a list of selector strings into a composed Selector.
|
||||
// Each selector string must be in the format "key:value" where key is one of:
|
||||
// - code: filter by nature (e.g., "code:G01")
|
||||
// - assetCountry: filter by asset country code (e.g., "assetCountry:840")
|
||||
//
|
||||
// Multiple selectors are combined with AND logic.
|
||||
// If no selectors are provided, returns Any() which matches all records.
|
||||
func ParseSelectors(sl []string) (Selector, error) {
|
||||
if len(sl) == 0 {
|
||||
return Any(), nil
|
||||
}
|
||||
|
||||
// Parse the first selector
|
||||
first, err := parseSingleSelector(sl[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there's only one, return it
|
||||
if len(sl) == 1 {
|
||||
return first, nil
|
||||
}
|
||||
|
||||
// Recursively parse the rest and combine with AND
|
||||
rest, err := ParseSelectors(sl[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return And(first, rest), nil
|
||||
}
|
||||
|
||||
func parseSingleSelector(s string) (Selector, error) {
|
||||
key, value, found := strings.Cut(s, ":")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("invalid selector format %q: must be 'key:value'", s)
|
||||
}
|
||||
|
||||
parser, ok := parsers[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown selector key %q: supported keys are %v", key, supportedKeys())
|
||||
}
|
||||
|
||||
return parser(value)
|
||||
}
|
||||
|
||||
func supportedKeys() []string {
|
||||
keys := make([]string, 0, len(parsers))
|
||||
for k := range parsers {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
434
internal/selectors_test.go
Normal file
434
internal/selectors_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type testRecord struct {
|
||||
symbol string
|
||||
nature internal.Nature
|
||||
brokerCountry int64
|
||||
assetCountry int64
|
||||
side internal.Side
|
||||
price decimal.Decimal
|
||||
quantity decimal.Decimal
|
||||
timestamp time.Time
|
||||
fees decimal.Decimal
|
||||
taxes decimal.Decimal
|
||||
}
|
||||
|
||||
func (m testRecord) Symbol() string { return m.symbol }
|
||||
func (m testRecord) Nature() internal.Nature { return m.nature }
|
||||
func (m testRecord) BrokerCountry() int64 { return m.brokerCountry }
|
||||
func (m testRecord) AssetCountry() int64 { return m.assetCountry }
|
||||
func (m testRecord) Side() internal.Side { return m.side }
|
||||
func (m testRecord) Price() decimal.Decimal { return m.price }
|
||||
func (m testRecord) Quantity() decimal.Decimal { return m.quantity }
|
||||
func (m testRecord) Timestamp() time.Time { return m.timestamp }
|
||||
func (m testRecord) Fees() decimal.Decimal { return m.fees }
|
||||
func (m testRecord) Taxes() decimal.Decimal { return m.taxes }
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
selector := internal.Any()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record internal.Record
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "returns true for any record",
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: 1,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns true for record with unknown nature",
|
||||
record: testRecord{
|
||||
symbol: "MSFT",
|
||||
nature: internal.NatureUnknown,
|
||||
assetCountry: 2,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns true for empty record",
|
||||
record: testRecord{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := selector(tt.record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyNature(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nature internal.Nature
|
||||
record internal.Record
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches G01 nature",
|
||||
nature: internal.NatureG01,
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
nature: internal.NatureG01,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches G20 nature",
|
||||
nature: internal.NatureG20,
|
||||
record: testRecord{
|
||||
symbol: "ETF",
|
||||
nature: internal.NatureG20,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match different nature",
|
||||
nature: internal.NatureG01,
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
nature: internal.NatureG20,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "does not match unknown nature",
|
||||
nature: internal.NatureG01,
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
nature: internal.NatureUnknown,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := internal.OnlyNature(tt.nature)
|
||||
got := selector(tt.record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyAssetCountry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
country int64
|
||||
record internal.Record
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches asset country",
|
||||
country: 840, // USA
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
assetCountry: 840,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches different country",
|
||||
country: 826, // UK
|
||||
record: testRecord{
|
||||
symbol: "BP",
|
||||
assetCountry: 826,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match different asset country",
|
||||
country: 840,
|
||||
record: testRecord{
|
||||
symbol: "BP",
|
||||
assetCountry: 826,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "does not match zero country",
|
||||
country: 0,
|
||||
record: testRecord{
|
||||
symbol: "AAPL",
|
||||
assetCountry: 840,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := internal.OnlyAssetCountry(tt.country)
|
||||
got := selector(tt.record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnd(t *testing.T) {
|
||||
record := testRecord{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selectorA internal.Selector
|
||||
selectorB internal.Selector
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "both selectors return true",
|
||||
selectorA: func(internal.Record) bool { return true },
|
||||
selectorB: func(internal.Record) bool { return true },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "first selector returns true, second returns false",
|
||||
selectorA: func(internal.Record) bool { return true },
|
||||
selectorB: func(internal.Record) bool { return false },
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "first selector returns false, second returns true",
|
||||
selectorA: func(internal.Record) bool { return false },
|
||||
selectorB: func(internal.Record) bool { return true },
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "both selectors return false",
|
||||
selectorA: func(internal.Record) bool { return false },
|
||||
selectorB: func(internal.Record) bool { return false },
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector := internal.And(tt.selectorA, tt.selectorB)
|
||||
got := selector(record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectors_Empty(t *testing.T) {
|
||||
selector, err := internal.ParseSelectors([]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for empty selectors: %v", err)
|
||||
}
|
||||
record := testRecord{
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: 840,
|
||||
}
|
||||
|
||||
if !selector(record) {
|
||||
t.Fatalf("empty selectors should match all records")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectors_OnlyNature(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selector string
|
||||
nature internal.Nature
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches G01",
|
||||
selector: "code:G01",
|
||||
nature: internal.NatureG01,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches G20",
|
||||
selector: "code:G20",
|
||||
nature: internal.NatureG20,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match different nature",
|
||||
selector: "code:G01",
|
||||
nature: internal.NatureG20,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector, err := internal.ParseSelectors([]string{tt.selector})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
record := testRecord{
|
||||
nature: tt.nature,
|
||||
assetCountry: 840,
|
||||
}
|
||||
|
||||
got := selector(record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectors_OnlyAssetCountry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selector string
|
||||
assetCountry int64
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "matches USA",
|
||||
selector: "assetCountry:840",
|
||||
assetCountry: 840,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "matches UK",
|
||||
selector: "assetCountry:826",
|
||||
assetCountry: 826,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "does not match different country",
|
||||
selector: "assetCountry:840",
|
||||
assetCountry: 826,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector, err := internal.ParseSelectors([]string{tt.selector})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
record := testRecord{
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: tt.assetCountry,
|
||||
}
|
||||
|
||||
got := selector(record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectors_Multiple(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
nature internal.Nature
|
||||
assetCountry int64
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "both selectors match",
|
||||
selectors: []string{"code:G01", "assetCountry:840"},
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: 840,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "first selector matches, second does not",
|
||||
selectors: []string{"code:G01", "assetCountry:826"},
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: 840,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "first selector does not match, second does",
|
||||
selectors: []string{"code:G20", "assetCountry:840"},
|
||||
nature: internal.NatureG01,
|
||||
assetCountry: 840,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selector, err := internal.ParseSelectors(tt.selectors)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
record := testRecord{
|
||||
nature: tt.nature,
|
||||
assetCountry: tt.assetCountry,
|
||||
}
|
||||
|
||||
got := selector(record)
|
||||
if got != tt.want {
|
||||
t.Fatalf("want %v but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectors_InvalidAssetCountry(t *testing.T) {
|
||||
t.Run("returns error for non-numeric asset country", func(t *testing.T) {
|
||||
_, err := internal.ParseSelectors([]string{"assetCountry:notanumber"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for non-numeric asset country")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSelectors_UnknownSelector(t *testing.T) {
|
||||
t.Run("returns error for unknown selector type", func(t *testing.T) {
|
||||
_, err := internal.ParseSelectors([]string{"unknown:value"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown selector type")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSelectors_EmptyValue(t *testing.T) {
|
||||
t.Run("rejects code with empty value", func(t *testing.T) {
|
||||
_, err := internal.ParseSelectors([]string{"code:"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty code value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects assetCountry with empty value", func(t *testing.T) {
|
||||
_, err := internal.ParseSelectors([]string{"assetCountry:"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for empty assetCountry value")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSelectors_MissingColon(t *testing.T) {
|
||||
t.Run("rejects input without colon", func(t *testing.T) {
|
||||
_, err := internal.ParseSelectors([]string{"code"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for input without colon")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user