package query_test import ( "fmt" "strings" "testing" "time" "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/internal/pubsub" "github.com/tendermint/tendermint/internal/pubsub/query" "github.com/tendermint/tendermint/internal/pubsub/query/syntax" ) var _ pubsub.Query = (*query.Query)(nil) // Example events from the OpenAPI documentation: // https://github.com/tendermint/tendermint/blob/master/rpc/openapi/openapi.yaml // // Redactions: // // - Add an explicit "tm" event for the built-in attributes. // - Remove Index fields (not relevant to tests). // - Add explicit balance values (to use in tests). // var apiEvents = []types.Event{ { Type: "tm", Attributes: []types.EventAttribute{ {Key: "event", Value: "Tx"}, {Key: "hash", Value: "XYZ"}, {Key: "height", Value: "5"}, }, }, { Type: "rewards.withdraw", Attributes: []types.EventAttribute{ {Key: "address", Value: "AddrA"}, {Key: "source", Value: "SrcX"}, {Key: "amount", Value: "100"}, {Key: "balance", Value: "1500"}, }, }, { Type: "rewards.withdraw", Attributes: []types.EventAttribute{ {Key: "address", Value: "AddrB"}, {Key: "source", Value: "SrcY"}, {Key: "amount", Value: "45"}, {Key: "balance", Value: "999"}, }, }, { Type: "transfer", Attributes: []types.EventAttribute{ {Key: "sender", Value: "AddrC"}, {Key: "recipient", Value: "AddrD"}, {Key: "amount", Value: "160"}, }, }, } func TestCompiledMatches(t *testing.T) { var ( txDate = "2017-01-01" txTime = "2018-05-03T14:45:00Z" ) testCases := []struct { s string events []types.Event matches bool }{ {`tm.events.type='NewBlock'`, newTestEvents(`tm|events.type=NewBlock`), true}, {`tx.gas > 7`, newTestEvents(`tx|gas=8`), true}, {`transfer.amount > 7`, newTestEvents(`transfer|amount=8stake`), true}, {`transfer.amount > 7`, newTestEvents(`transfer|amount=8.045`), true}, {`transfer.amount > 7.043`, newTestEvents(`transfer|amount=8.045stake`), true}, {`transfer.amount > 8.045`, newTestEvents(`transfer|amount=8.045stake`), false}, {`tx.gas > 7 AND tx.gas < 9`, newTestEvents(`tx|gas=8`), true}, {`body.weight >= 3.5`, newTestEvents(`body|weight=3.5`), true}, {`account.balance < 1000.0`, newTestEvents(`account|balance=900`), true}, {`apples.kg <= 4`, newTestEvents(`apples|kg=4.0`), true}, {`body.weight >= 4.5`, newTestEvents(`body|weight=4.5`), true}, {`oranges.kg < 4 AND watermellons.kg > 10`, newTestEvents(`oranges|kg=3`, `watermellons|kg=12`), true}, {`peaches.kg < 4`, newTestEvents(`peaches|kg=5`), false}, {`tx.date > DATE 2017-01-01`, newTestEvents(`tx|date=` + time.Now().Format(syntax.DateFormat)), true}, {`tx.date = DATE 2017-01-01`, newTestEvents(`tx|date=` + txDate), true}, {`tx.date = DATE 2018-01-01`, newTestEvents(`tx|date=` + txDate), false}, {`tx.time >= TIME 2013-05-03T14:45:00Z`, newTestEvents(`tx|time=` + time.Now().Format(syntax.TimeFormat)), true}, {`tx.time = TIME 2013-05-03T14:45:00Z`, newTestEvents(`tx|time=` + txTime), false}, {`abci.owner.name CONTAINS 'Igor'`, newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`), true}, {`abci.owner.name CONTAINS 'Igor'`, newTestEvents(`abci|owner.name=Pavel|owner.name=Ivan`), false}, {`abci.owner.name = 'Igor'`, newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`), true}, {`abci.owner.name = 'Ivan'`, newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`), true}, {`abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'`, newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`), true}, {`abci.owner.name = 'Ivan' AND abci.owner.name = 'John'`, newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`), false}, {`tm.events.type='NewBlock'`, newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`), true}, {`app.name = 'fuzzed'`, newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`), true}, {`tm.events.type='NewBlock' AND app.name = 'fuzzed'`, newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`), true}, {`tm.events.type='NewHeader' AND app.name = 'fuzzed'`, newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`), false}, {`slash EXISTS`, newTestEvents(`slash|reason=missing_signature|power=6000`), true}, {`slash EXISTS`, newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`), false}, {`slash.reason EXISTS AND slash.power > 1000`, newTestEvents(`slash|reason=missing_signature|power=6000`), true}, {`slash.reason EXISTS AND slash.power > 1000`, newTestEvents(`slash|reason=missing_signature|power=500`), false}, {`slash.reason EXISTS`, newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`), false}, // Test cases based on the OpenAPI examples. {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'`, apiEvents, true}, {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'SrcY'`, apiEvents, true}, {`tm.event = 'Tx' AND transfer.sender = 'AddrA'`, apiEvents, false}, {`tm.event = 'Tx' AND transfer.sender = 'AddrC'`, apiEvents, true}, {`tm.event = 'Tx' AND transfer.sender = 'AddrZ'`, apiEvents, false}, {`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'`, apiEvents, false}, {`tm.event = 'Tx' AND rewards.withdraw.source = 'W'`, apiEvents, false}, } // NOTE: The original implementation allowed arbitrary prefix matches on // attribute tags, e.g., "sl" would match "slash". // // That is weird and probably wrong: "foo.ba" should not match "foo.bar", // or there is no way to distinguish the case where there were two values // for "foo.bar" or one value each for "foo.ba" and "foo.bar". // // Apart from a single test case, I could not find any attested usage of // this implementation detail. It isn't documented in the OpenAPI docs and // is not shown in any of the example inputs. // // On that basis, I removed that test case. This implementation still does // correctly handle variable type/attribute splits ("x", "y.z" / "x.y", "z") // since that was required by the original "flattened" event representation. for i, tc := range testCases { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { c, err := query.New(tc.s) if err != nil { t.Fatalf("NewCompiled %#q: unexpected error: %v", tc.s, err) } got, err := c.Matches(tc.events) if err != nil { t.Errorf("Query: %#q\nInput: %+v\nMatches: got error %v", tc.s, tc.events, err) } if got != tc.matches { t.Errorf("Query: %#q\nInput: %+v\nMatches: got %v, want %v", tc.s, tc.events, got, tc.matches) } }) } } func TestAllMatchesAll(t *testing.T) { events := newTestEvents( ``, `Asher|Roth=`, `Route|66=`, `Rilly|Blue=`, ) for i := 0; i < len(events); i++ { match, err := query.All.Matches(events[:i]) if err != nil { t.Errorf("Matches failed: %w", err) } else if !match { t.Errorf("Did not match on %+v ", events[:i]) } } } // newTestEvent constructs an Event message from a template string. // The format is "type|attr1=val1|attr2=val2|...". func newTestEvent(s string) types.Event { var event types.Event parts := strings.Split(s, "|") event.Type = parts[0] if len(parts) == 1 { return event // type only, no attributes } for _, kv := range parts[1:] { key, val := splitKV(kv) event.Attributes = append(event.Attributes, types.EventAttribute{ Key: key, Value: val, }) } return event } // newTestEvents constructs a slice of Event messages by applying newTestEvent // to each element of ss. func newTestEvents(ss ...string) []types.Event { events := make([]types.Event, len(ss)) for i, s := range ss { events[i] = newTestEvent(s) } return events } func splitKV(s string) (key, value string) { kv := strings.SplitN(s, "=", 2) return kv[0], kv[1] }