diff options
author | Tulir Asokan <tulir@maunium.net> | 2018-04-15 15:36:01 +0300 |
---|---|---|
committer | Tulir Asokan <tulir@maunium.net> | 2018-04-15 15:36:01 +0300 |
commit | 0cdde557a3ed7624de31aa844929037b65e1fe11 (patch) | |
tree | 0d592f9e5d1071c00729dda03571731646242c31 /coverage.html | |
parent | bb9ed4558b6ee64d81a8092e1bb0304c75b49c22 (diff) |
Add tests for pushrule conditions and fix bugs found when making tests
Diffstat (limited to 'coverage.html')
-rw-r--r-- | coverage.html | 682 |
1 files changed, 682 insertions, 0 deletions
diff --git a/coverage.html b/coverage.html new file mode 100644 index 0000000..c3d77dd --- /dev/null +++ b/coverage.html @@ -0,0 +1,682 @@ + +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + body { + background: black; + color: rgb(80, 80, 80); + } + body, pre, #legend span { + font-family: Menlo, monospace; + font-weight: bold; + } + #topbar { + background: black; + position: fixed; + top: 0; left: 0; right: 0; + height: 42px; + border-bottom: 1px solid rgb(80, 80, 80); + } + #content { + margin-top: 50px; + } + #nav, #legend { + float: left; + margin-left: 10px; + } + #legend { + margin-top: 12px; + } + #nav { + margin-top: 10px; + } + #legend span { + margin: 0 5px; + } + .cov0 { color: rgb(192, 0, 0) } +.cov1 { color: rgb(128, 128, 128) } +.cov2 { color: rgb(116, 140, 131) } +.cov3 { color: rgb(104, 152, 134) } +.cov4 { color: rgb(92, 164, 137) } +.cov5 { color: rgb(80, 176, 140) } +.cov6 { color: rgb(68, 188, 143) } +.cov7 { color: rgb(56, 200, 146) } +.cov8 { color: rgb(44, 212, 149) } +.cov9 { color: rgb(32, 224, 152) } +.cov10 { color: rgb(20, 236, 155) } + + </style> + </head> + <body> + <div id="topbar"> + <div id="nav"> + <select id="files"> + + <option value="file0">maunium.net/go/gomuks/matrix/pushrules/action.go (51.6%)</option> + + <option value="file1">maunium.net/go/gomuks/matrix/pushrules/condition.go (97.5%)</option> + + <option value="file2">maunium.net/go/gomuks/matrix/pushrules/pushrules.go (50.0%)</option> + + <option value="file3">maunium.net/go/gomuks/matrix/pushrules/rule.go (13.6%)</option> + + <option value="file4">maunium.net/go/gomuks/matrix/pushrules/ruleset.go (52.9%)</option> + + </select> + </div> + <div id="legend"> + <span>not tracked</span> + + <span class="cov0">not covered</span> + <span class="cov8">covered</span> + + </div> + </div> + <div id="content"> + + <pre class="file" id="file0" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import "encoding/json" + +// PushActionType is the type of a PushAction +type PushActionType string + +// The allowed push action types as specified in spec section 11.12.1.4.1. +const ( + ActionNotify PushActionType = "notify" + ActionDontNotify PushActionType = "dont_notify" + ActionCoalesce PushActionType = "coalesce" + ActionSetTweak PushActionType = "set_tweak" +) + +// PushActionTweak is the type of the tweak in SetTweak push actions. +type PushActionTweak string + +// The allowed tweak types as specified in spec section 11.12.1.4.1.1. +const ( + TweakSound PushActionTweak = "sound" + TweakHighlight PushActionTweak = "highlight" +) + +// PushActionArray is an array of PushActions. +type PushActionArray []*PushAction + +// PushActionArrayShould contains the important information parsed from a PushActionArray. +type PushActionArrayShould struct { + // Whether or not the array contained a Notify, DontNotify or Coalesce action type. + NotifySpecified bool + // Whether or not the event in question should trigger a notification. + Notify bool + // Whether or not the event in question should be highlighted. + Highlight bool + + // Whether or not the event in question should trigger a sound alert. + PlaySound bool + // The name of the sound to play if PlaySound is true. + SoundName string +} + +// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct. +func (actions PushActionArray) Should() (should PushActionArrayShould) <span class="cov8" title="1">{ + for _, action := range actions </span><span class="cov8" title="1">{ + switch action.Action </span>{ + case ActionNotify, ActionCoalesce:<span class="cov0" title="0"> + should.Notify = true + should.NotifySpecified = true</span> + case ActionDontNotify:<span class="cov8" title="1"> + should.Notify = false + should.NotifySpecified = true</span> + case ActionSetTweak:<span class="cov0" title="0"> + switch action.Tweak </span>{ + case TweakHighlight:<span class="cov0" title="0"> + var ok bool + should.Highlight, ok = action.Value.(bool) + if !ok </span><span class="cov0" title="0">{ + // Highlight value not specified, so assume true since the tweak is set. + should.Highlight = true + }</span> + case TweakSound:<span class="cov0" title="0"> + should.SoundName = action.Value.(string) + should.PlaySound = len(should.SoundName) > 0</span> + } + } + } + <span class="cov8" title="1">return</span> +} + +// PushAction is a single action that should be triggered when receiving a message. +type PushAction struct { + Action PushActionType + Tweak PushActionTweak + Value interface{} +} + +// UnmarshalJSON parses JSON into this PushAction. +// +// * If the JSON is a single string, the value is stored in the Action field. +// * If the JSON is an object with the set_tweak field, Action will be set to +// "set_tweak", Tweak will be set to the value of the set_tweak field and +// and Value will be set to the value of the value field. +// * In any other case, the function does nothing. +func (action *PushAction) UnmarshalJSON(raw []byte) error <span class="cov8" title="1">{ + var data interface{} + + err := json.Unmarshal(raw, &data) + if err != nil </span><span class="cov0" title="0">{ + return err + }</span> + + <span class="cov8" title="1">switch val := data.(type) </span>{ + case string:<span class="cov8" title="1"> + action.Action = PushActionType(val)</span> + case map[string]interface{}:<span class="cov8" title="1"> + tweak, ok := val["set_tweak"].(string) + if ok </span><span class="cov8" title="1">{ + action.Action = ActionSetTweak + action.Tweak = PushActionTweak(tweak) + action.Value, _ = val["value"] + }</span> + } + <span class="cov8" title="1">return nil</span> +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (action *PushAction) MarshalJSON() (raw []byte, err error) <span class="cov0" title="0">{ + if action.Action == ActionSetTweak </span><span class="cov0" title="0">{ + data := map[string]interface{}{ + "set_tweak": action.Tweak, + "value": action.Value, + } + return json.Marshal(&data) + }</span><span class="cov0" title="0"> else { + data := string(action.Action) + return json.Marshal(&data) + }</span> +} +</pre> + + <pre class="file" id="file1" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "regexp" + "strconv" + "strings" + + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" + "maunium.net/go/gomuks/matrix/rooms" +) + +// Room is an interface with the functions that are needed for processing room-specific push conditions +type Room interface { + GetMember(mxid string) *rooms.Member + GetMembers() map[string]*rooms.Member + GetSessionOwner() *rooms.Member +} + +// PushCondKind is the type of a push condition. +type PushCondKind string + +// The allowed push condition kinds as specified in section 11.12.1.4.3 of r0.3.0 of the Client-Server API. +const ( + KindEventMatch PushCondKind = "event_match" + KindContainsDisplayName PushCondKind = "contains_display_name" + KindRoomMemberCount PushCondKind = "room_member_count" +) + +// PushCondition wraps a condition that is required for a specific PushRule to be used. +type PushCondition struct { + // The type of the condition. + Kind PushCondKind `json:"kind"` + // The dot-separated field of the event to match. Only applicable if kind is EventMatch. + Key string `json:"key,omitempty"` + // The glob-style pattern to match the field against. Only applicable if kind is EventMatch. + Pattern string `json:"pattern,omitempty"` + // The condition that needs to be fulfilled for RoomMemberCount-type conditions. + // A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found. + MemberCountCondition string `json:"is,omitempty"` +} + +// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions. +var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$") + +// Match checks if this condition is fulfilled for the given event in the given room. +func (cond *PushCondition) Match(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + switch cond.Kind </span>{ + case KindEventMatch:<span class="cov8" title="1"> + return cond.matchValue(room, event)</span> + case KindContainsDisplayName:<span class="cov8" title="1"> + return cond.matchDisplayName(room, event)</span> + case KindRoomMemberCount:<span class="cov8" title="1"> + return cond.matchMemberCount(room, event)</span> + default:<span class="cov8" title="1"> + return false</span> + } +} + +func (cond *PushCondition) matchValue(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + index := strings.IndexRune(cond.Key, '.') + key := cond.Key + subkey := "" + if index > 0 </span><span class="cov8" title="1">{ + subkey = key[index+1:] + key = key[0:index] + }</span> + + <span class="cov8" title="1">pattern, _ := glob.Compile(cond.Pattern) + + switch key </span>{ + case "type":<span class="cov8" title="1"> + return pattern.MatchString(event.Type)</span> + case "sender":<span class="cov8" title="1"> + return pattern.MatchString(event.Sender)</span> + case "room_id":<span class="cov8" title="1"> + return pattern.MatchString(event.RoomID)</span> + case "state_key":<span class="cov8" title="1"> + if event.StateKey == nil </span><span class="cov8" title="1">{ + return cond.Pattern == "" + }</span> + <span class="cov8" title="1">return pattern.MatchString(*event.StateKey)</span> + case "content":<span class="cov8" title="1"> + val, _ := event.Content[subkey].(string) + return pattern.MatchString(val)</span> + default:<span class="cov8" title="1"> + return false</span> + } +} + +func (cond *PushCondition) matchDisplayName(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + member := room.GetSessionOwner() + if member == nil || member.UserID == event.Sender </span><span class="cov8" title="1">{ + return false + }</span> + <span class="cov8" title="1">text, _ := event.Content["body"].(string) + return strings.Contains(text, member.DisplayName)</span> +} + +func (cond *PushCondition) matchMemberCount(room Room, event *gomatrix.Event) bool <span class="cov8" title="1">{ + group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition) + if len(group) != 3 </span><span class="cov8" title="1">{ + return false + }</span> + + <span class="cov8" title="1">operator := group[1] + wantedMemberCount, _ := strconv.Atoi(group[2]) + + memberCount := len(room.GetMembers()) + + switch operator </span>{ + case "==", "":<span class="cov8" title="1"> + return memberCount == wantedMemberCount</span> + case ">":<span class="cov8" title="1"> + return memberCount > wantedMemberCount</span> + case ">=":<span class="cov8" title="1"> + return memberCount >= wantedMemberCount</span> + case "<":<span class="cov8" title="1"> + return memberCount < wantedMemberCount</span> + case "<=":<span class="cov8" title="1"> + return memberCount <= wantedMemberCount</span> + default:<span class="cov0" title="0"> + // Should be impossible due to regex. + return false</span> + } +} +</pre> + + <pre class="file" id="file2" style="display: none">package pushrules + +import ( + "encoding/json" + "net/url" + + "maunium.net/go/gomatrix" +) + +// GetPushRules returns the push notification rules for the global scope. +func GetPushRules(client *gomatrix.Client) (*PushRuleset, error) <span class="cov0" title="0">{ + return GetScopedPushRules(client, "global") +}</span> + +// GetScopedPushRules returns the push notification rules for the given scope. +func GetScopedPushRules(client *gomatrix.Client, scope string) (resp *PushRuleset, err error) <span class="cov0" title="0">{ + u, _ := url.Parse(client.BuildURL("pushrules", scope)) + // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. + u.Path += "/" + _, err = client.MakeRequest("GET", u.String(), nil, &resp) + return +}</span> + +// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON. +func EventToPushRules(event *gomatrix.Event) (*PushRuleset, error) <span class="cov8" title="1">{ + content, _ := event.Content["global"] + raw, err := json.Marshal(content) + if err != nil </span><span class="cov0" title="0">{ + return nil, err + }</span> + + <span class="cov8" title="1">ruleset := &PushRuleset{} + err = json.Unmarshal(raw, ruleset) + if err != nil </span><span class="cov0" title="0">{ + return nil, err + }</span> + + <span class="cov8" title="1">return ruleset, nil</span> +} +</pre> + + <pre class="file" id="file3" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "github.com/zyedidia/glob" + "maunium.net/go/gomatrix" +) + +type PushRuleCollection interface { + GetActions(room Room, event *gomatrix.Event) PushActionArray +} + +type PushRuleArray []*PushRule + +func (rules PushRuleArray) setType(typ PushRuleType) PushRuleArray <span class="cov8" title="1">{ + for _, rule := range rules </span><span class="cov8" title="1">{ + rule.Type = typ + }</span> + <span class="cov8" title="1">return rules</span> +} + +func (rules PushRuleArray) GetActions(room Room, event *gomatrix.Event) PushActionArray <span class="cov0" title="0">{ + for _, rule := range rules </span><span class="cov0" title="0">{ + if !rule.Match(room, event) </span><span class="cov0" title="0">{ + continue</span> + } + <span class="cov0" title="0">return rule.Actions</span> + } + <span class="cov0" title="0">return nil</span> +} + +type PushRuleMap struct { + Map map[string]*PushRule + Type PushRuleType +} + +func (rules PushRuleArray) setTypeAndMap(typ PushRuleType) PushRuleMap <span class="cov8" title="1">{ + data := PushRuleMap{ + Map: make(map[string]*PushRule), + Type: typ, + } + for _, rule := range rules </span><span class="cov0" title="0">{ + rule.Type = typ + data.Map[rule.RuleID] = rule + }</span> + <span class="cov8" title="1">return data</span> +} + +func (ruleMap PushRuleMap) GetActions(room Room, event *gomatrix.Event) PushActionArray <span class="cov0" title="0">{ + var rule *PushRule + var found bool + switch ruleMap.Type </span>{ + case RoomRule:<span class="cov0" title="0"> + rule, found = ruleMap.Map[event.RoomID]</span> + case SenderRule:<span class="cov0" title="0"> + rule, found = ruleMap.Map[event.Sender]</span> + } + <span class="cov0" title="0">if found && rule.Match(room, event) </span><span class="cov0" title="0">{ + return rule.Actions + }</span> + <span class="cov0" title="0">return nil</span> +} + +func (ruleMap PushRuleMap) unmap() PushRuleArray <span class="cov0" title="0">{ + array := make(PushRuleArray, len(ruleMap.Map)) + index := 0 + for _, rule := range ruleMap.Map </span><span class="cov0" title="0">{ + array[index] = rule + index++ + }</span> + <span class="cov0" title="0">return array</span> +} + +type PushRuleType string + +const ( + OverrideRule PushRuleType = "override" + ContentRule PushRuleType = "content" + RoomRule PushRuleType = "room" + SenderRule PushRuleType = "sender" + UnderrideRule PushRuleType = "underride" +) + +type PushRule struct { + // The type of this rule. + Type PushRuleType `json:"-"` + // The ID of this rule. + // For room-specific rules and user-specific rules, this is the room or user ID (respectively) + // For other types of rules, this doesn't affect anything. + RuleID string `json:"rule_id"` + // The actions this rule should trigger when matched. + Actions PushActionArray `json:"actions"` + // Whether this is a default rule, or has been set explicitly. + Default bool `json:"default"` + // Whether or not this push rule is enabled. + Enabled bool `json:"enabled"` + // The conditions to match in order to trigger this rule. + // Only applicable to generic underride/override rules. + Conditions []*PushCondition `json:"conditions,omitempty"` + // Pattern for content-specific push rules + Pattern string `json:"pattern,omitempty"` +} + +func (rule *PushRule) Match(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + if !rule.Enabled </span><span class="cov0" title="0">{ + return false + }</span> + <span class="cov0" title="0">switch rule.Type </span>{ + case OverrideRule, UnderrideRule:<span class="cov0" title="0"> + return rule.matchConditions(room, event)</span> + case ContentRule:<span class="cov0" title="0"> + return rule.matchPattern(room, event)</span> + case RoomRule:<span class="cov0" title="0"> + return rule.RuleID == event.RoomID</span> + case SenderRule:<span class="cov0" title="0"> + return rule.RuleID == event.Sender</span> + default:<span class="cov0" title="0"> + return false</span> + } +} + +func (rule *PushRule) matchConditions(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + for _, cond := range rule.Conditions </span><span class="cov0" title="0">{ + if !cond.Match(room, event) </span><span class="cov0" title="0">{ + return false + }</span> + } + <span class="cov0" title="0">return true</span> +} + +func (rule *PushRule) matchPattern(room Room, event *gomatrix.Event) bool <span class="cov0" title="0">{ + pattern, err := glob.Compile(rule.Pattern) + if err != nil </span><span class="cov0" title="0">{ + return false + }</span> + <span class="cov0" title="0">text, _ := event.Content["body"].(string) + return pattern.MatchString(text)</span> +} +</pre> + + <pre class="file" id="file4" style="display: none">// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package pushrules + +import ( + "encoding/json" + + "maunium.net/go/gomatrix" +) + +type PushRuleset struct { + Override PushRuleArray + Content PushRuleArray + Room PushRuleMap + Sender PushRuleMap + Underride PushRuleArray +} + +type rawPushRuleset struct { + Override PushRuleArray `json:"override"` + Content PushRuleArray `json:"content"` + Room PushRuleArray `json:"room"` + Sender PushRuleArray `json:"sender"` + Underride PushRuleArray `json:"underride"` +} + +// UnmarshalJSON parses JSON into this PushRuleset. +// +// For override, sender and underride push rule arrays, the type is added +// to each PushRule and the array is used as-is. +// +// For room and sender push rule arrays, the type is added to each PushRule +// and the array is converted to a map with the rule ID as the key and the +// PushRule as the value. +func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) <span class="cov8" title="1">{ + data := rawPushRuleset{} + err = json.Unmarshal(raw, &data) + if err != nil </span><span class="cov0" title="0">{ + return + }</span> + + <span class="cov8" title="1">rs.Override = data.Override.setType(OverrideRule) + rs.Content = data.Content.setType(ContentRule) + rs.Room = data.Room.setTypeAndMap(RoomRule) + rs.Sender = data.Sender.setTypeAndMap(SenderRule) + rs.Underride = data.Underride.setType(UnderrideRule) + return</span> +} + +// MarshalJSON is the reverse of UnmarshalJSON() +func (rs *PushRuleset) MarshalJSON() ([]byte, error) <span class="cov0" title="0">{ + data := rawPushRuleset{ + Override: rs.Override, + Content: rs.Content, + Room: rs.Room.unmap(), + Sender: rs.Sender.unmap(), + Underride: rs.Underride, + } + return json.Marshal(&data) +}</span> + +// DefaultPushActions is the value returned if none of the rule +// collections in a Ruleset match the event given to GetActions() +var DefaultPushActions = make(PushActionArray, 0) + +// GetActions matches the given event against all of the push rule +// collections in this push ruleset in the order of priority as +// specified in spec section 11.12.1.4. +func (rs *PushRuleset) GetActions(room Room, event *gomatrix.Event) (match PushActionArray) <span class="cov0" title="0">{ + // Add push rule collections to array in priority order + arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride} + // Loop until one of the push rule collections matches the room/event combo. + for _, pra := range arrays </span><span class="cov0" title="0">{ + if match = pra.GetActions(room, event); match != nil </span><span class="cov0" title="0">{ + // Match found, return it. + return + }</span> + } + // No match found, return default actions. + <span class="cov0" title="0">return DefaultPushActions</span> +} +</pre> + + </div> + </body> + <script> + (function() { + var files = document.getElementById('files'); + var visible; + files.addEventListener('change', onChange, false); + function select(part) { + if (visible) + visible.style.display = 'none'; + visible = document.getElementById(part); + if (!visible) + return; + files.value = part; + visible.style.display = 'block'; + location.hash = part; + } + function onChange() { + select(files.value); + window.scrollTo(0, 0); + } + if (location.hash != "") { + select(location.hash.substr(1)); + } + if (!visible) { + select("file0"); + } + })(); + </script> +</html> |