|
18 | 18 | # Keep this in sync with validate_chathistory() |
19 | 19 | SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"] |
20 | 20 |
|
21 | | -MYSQL_PASSWORD = "" |
22 | | - |
23 | 21 |
|
24 | 22 | def skip_ngircd(f): |
25 | 23 | @functools.wraps(f) |
@@ -813,6 +811,253 @@ def validate_msg(msg): |
813 | 811 | relay = self.getMessage(2) |
814 | 812 | validate_msg(relay) |
815 | 813 |
|
| 814 | + @pytest.mark.private_chathistory |
| 815 | + @skip_ngircd |
| 816 | + def testChathistoryTargets(self): |
| 817 | + """Tests the CHATHISTORY TARGETS command, which lists channels the user has visible |
| 818 | + history in and users with which the user has exchanged direct messages.""" |
| 819 | + c1 = random_name("foo") |
| 820 | + c2 = random_name("bar") |
| 821 | + c3 = random_name("baz") |
| 822 | + |
| 823 | + # Register users |
| 824 | + self.controller.registerUser(self, c1, "sesame1") |
| 825 | + self.controller.registerUser(self, c2, "sesame2") |
| 826 | + self.controller.registerUser(self, c3, "sesame3") |
| 827 | + |
| 828 | + # Connect with all capabilities |
| 829 | + self.connectClient( |
| 830 | + c1, |
| 831 | + capabilities=[ |
| 832 | + "message-tags", |
| 833 | + "server-time", |
| 834 | + "echo-message", |
| 835 | + "batch", |
| 836 | + "labeled-response", |
| 837 | + "sasl", |
| 838 | + CHATHISTORY_CAP, |
| 839 | + ], |
| 840 | + password="sesame1", |
| 841 | + skip_if_cap_nak=True, |
| 842 | + ) |
| 843 | + |
| 844 | + self.connectClient( |
| 845 | + c2, |
| 846 | + capabilities=[ |
| 847 | + "message-tags", |
| 848 | + "server-time", |
| 849 | + "echo-message", |
| 850 | + "batch", |
| 851 | + "labeled-response", |
| 852 | + "sasl", |
| 853 | + CHATHISTORY_CAP, |
| 854 | + ], |
| 855 | + password="sesame2", |
| 856 | + ) |
| 857 | + |
| 858 | + self.connectClient( |
| 859 | + c3, |
| 860 | + capabilities=[ |
| 861 | + "message-tags", |
| 862 | + "server-time", |
| 863 | + "echo-message", |
| 864 | + "batch", |
| 865 | + "labeled-response", |
| 866 | + "sasl", |
| 867 | + CHATHISTORY_CAP, |
| 868 | + ], |
| 869 | + password="sesame3", |
| 870 | + ) |
| 871 | + |
| 872 | + # Clear any initial messages |
| 873 | + self.getMessages(1) |
| 874 | + self.getMessages(2) |
| 875 | + self.getMessages(3) |
| 876 | + |
| 877 | + # Create a few channels for testing |
| 878 | + ch1 = "#chan" + secrets.token_hex(8) |
| 879 | + ch2 = "#chan" + secrets.token_hex(8) |
| 880 | + |
| 881 | + # Join channels and exchange messages |
| 882 | + self.joinChannel(1, ch1) |
| 883 | + self.joinChannel(2, ch1) |
| 884 | + self.joinChannel(1, ch2) |
| 885 | + self.joinChannel(3, ch2) |
| 886 | + self.getMessages(1) |
| 887 | + self.getMessages(2) |
| 888 | + self.getMessages(3) |
| 889 | + |
| 890 | + target_times = {} # Store target and their latest message times |
| 891 | + |
| 892 | + # Exchange messages between users and in channels |
| 893 | + # First, exchange messages in ch1 |
| 894 | + self.sendLine(1, f"PRIVMSG {ch1} :Hello channel 1") |
| 895 | + self.getMessages(1) |
| 896 | + ch1_msg = self.getMessages(2)[0] |
| 897 | + target_times[ch1] = ch1_msg.tags.get("time", "") |
| 898 | + time.sleep(0.002) |
| 899 | + |
| 900 | + # Then, DM between c1 and c2 |
| 901 | + self.sendLine(1, f"PRIVMSG {c2} :Hello user 2") |
| 902 | + self.getMessages(1) |
| 903 | + c2_msg = self.getMessages(2)[0] |
| 904 | + target_times[c2] = c2_msg.tags.get("time", "") |
| 905 | + time.sleep(0.002) |
| 906 | + |
| 907 | + # Then, messages in ch2 |
| 908 | + self.sendLine(1, f"PRIVMSG {ch2} :Hello channel 2") |
| 909 | + self.getMessages(1) |
| 910 | + ch2_msg = self.getMessages(3)[0] |
| 911 | + target_times[ch2] = ch2_msg.tags.get("time", "") |
| 912 | + time.sleep(0.002) |
| 913 | + |
| 914 | + # Finally, DM between c1 and c3 |
| 915 | + self.sendLine(1, f"PRIVMSG {c3} :Hello user 3") |
| 916 | + self.getMessages(1) |
| 917 | + c3_msg = self.getMessages(3)[0] |
| 918 | + target_times[c3] = c3_msg.tags.get("time", "") |
| 919 | + |
| 920 | + # Now test TARGETS command |
| 921 | + # Get a timestamp before all messages |
| 922 | + before_all = "timestamp=2020-01-01T00:00:00.000Z" |
| 923 | + # Get a timestamp after all messages |
| 924 | + after_all = "timestamp=2262-01-01T00:00:00.000Z" |
| 925 | + |
| 926 | + # Execute TARGETS command |
| 927 | + self.sendLine(1, f"CHATHISTORY TARGETS {before_all} {after_all} 100") |
| 928 | + |
| 929 | + # Verify response |
| 930 | + batch_messages = self.getMessages(1) |
| 931 | + |
| 932 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 933 | + # Check that each target we messaged is in the results |
| 934 | + # Targets should be sorted by time of latest message (earliest first) |
| 935 | + expected_order = [ch1, c2, ch2, c3] |
| 936 | + expected_results = [(target, target_times[target]) for target in expected_order] |
| 937 | + self.assertEqual(targets_results, expected_results) |
| 938 | + |
| 939 | + # Test with a limit parameter |
| 940 | + self.sendLine(1, f"CHATHISTORY TARGETS {before_all} {after_all} 2") |
| 941 | + batch_messages = self.getMessages(1) |
| 942 | + |
| 943 | + # Extract targets again |
| 944 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 945 | + |
| 946 | + # Should only get 2 targets due to limit |
| 947 | + self.assertEqual([t[0] for t in targets_results], [ch1, c2]) |
| 948 | + |
| 949 | + # Test with timestamp range that excludes some targets |
| 950 | + # Get the timestamp from the first message in ch1 |
| 951 | + ch1_time = target_times[ch1] |
| 952 | + # Send the command to get targets after this time |
| 953 | + self.sendLine(1, f"CHATHISTORY TARGETS timestamp={ch1_time} {after_all} 100") |
| 954 | + batch_messages = self.getMessages(1) |
| 955 | + |
| 956 | + # Should only get targets that had messages after ch1_time |
| 957 | + # That would be c2, ch2, and c3, but not ch1 |
| 958 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 959 | + self.assertEqual([t[0] for t in targets_results], [c2, ch2, c3]) |
| 960 | + |
| 961 | + # test limits on both sides |
| 962 | + ch3_time = target_times[c3] |
| 963 | + self.sendLine( |
| 964 | + 1, f"CHATHISTORY TARGETS timestamp={ch1_time} timestamp={ch3_time} 100" |
| 965 | + ) |
| 966 | + batch_messages = self.getMessages(1) |
| 967 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 968 | + self.assertEqual([t[0] for t in targets_results], [c2, ch2]) |
| 969 | + |
| 970 | + @pytest.mark.private_chathistory |
| 971 | + @skip_ngircd |
| 972 | + def testChathistoryTargetsExcludesUpdatedTargets(self): |
| 973 | + """Tests that CHATHISTORY TARGETS does not match targets that have messages |
| 974 | + within the selection window, but where the latest message is outside the |
| 975 | + selection window.""" |
| 976 | + c1 = random_name("foo") |
| 977 | + |
| 978 | + # Register users |
| 979 | + self.controller.registerUser(self, c1, "sesame1") |
| 980 | + |
| 981 | + # Connect with all capabilities |
| 982 | + self.connectClient( |
| 983 | + c1, |
| 984 | + capabilities=[ |
| 985 | + "message-tags", |
| 986 | + "server-time", |
| 987 | + "echo-message", |
| 988 | + "batch", |
| 989 | + "labeled-response", |
| 990 | + "sasl", |
| 991 | + CHATHISTORY_CAP, |
| 992 | + ], |
| 993 | + password="sesame1", |
| 994 | + skip_if_cap_nak=True, |
| 995 | + ) |
| 996 | + |
| 997 | + # Clear any initial messages |
| 998 | + self.getMessages(1) |
| 999 | + |
| 1000 | + # Create a few channels for testing |
| 1001 | + ch1 = "#chan" + secrets.token_hex(8) |
| 1002 | + |
| 1003 | + # Join channels and exchange messages |
| 1004 | + self.joinChannel(1, ch1) |
| 1005 | + self.getMessages(1) |
| 1006 | + |
| 1007 | + target_times = [] |
| 1008 | + |
| 1009 | + for i in range(3): |
| 1010 | + self.sendLine(1, f"PRIVMSG {ch1} :Hello channel {i}") |
| 1011 | + ch1_msg = self.getMessages(1)[0] |
| 1012 | + target_times.append(ch1_msg.tags["time"]) |
| 1013 | + time.sleep(0.002) |
| 1014 | + |
| 1015 | + # Now test TARGETS command |
| 1016 | + # Get a timestamp before all messages |
| 1017 | + before_all = "timestamp=2020-01-01T00:00:00.000Z" |
| 1018 | + # Get a timestamp after all messages |
| 1019 | + after_all = "timestamp=2262-01-01T00:00:00.000Z" |
| 1020 | + |
| 1021 | + # Execute TARGETS command |
| 1022 | + self.sendLine(1, f"CHATHISTORY TARGETS {before_all} {after_all} 100") |
| 1023 | + # Verify response |
| 1024 | + batch_messages = self.getMessages(1) |
| 1025 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 1026 | + expected_results = [(ch1, target_times[2])] |
| 1027 | + self.assertEqual(targets_results, expected_results) |
| 1028 | + |
| 1029 | + # Execute TARGETS command with a time window excluding the latest message |
| 1030 | + self.sendLine( |
| 1031 | + 1, f"CHATHISTORY TARGETS {before_all} timestamp={target_times[1]} 100" |
| 1032 | + ) |
| 1033 | + # Verify response |
| 1034 | + batch_messages = self.getMessages(1) |
| 1035 | + targets_results = self.summarize_chathistory_targets(batch_messages) |
| 1036 | + # should not receive any targets |
| 1037 | + self.assertEqual(targets_results, []) |
| 1038 | + |
| 1039 | + def summarize_chathistory_targets(self, batch_messages): |
| 1040 | + # Validate batch format |
| 1041 | + batch_start = batch_messages[0] |
| 1042 | + batch_end = batch_messages[-1] |
| 1043 | + self.assertMessageMatch( |
| 1044 | + batch_start, |
| 1045 | + command="BATCH", |
| 1046 | + params=[StrRe(r"\+.*"), "draft/chathistory-targets"], |
| 1047 | + ) |
| 1048 | + batch_tag = batch_start.params[0][1:] |
| 1049 | + self.assertMessageMatch(batch_end, command="BATCH", params=["-" + batch_tag]) |
| 1050 | + # Extract actual targets from the batch |
| 1051 | + targets_results = [] |
| 1052 | + for msg in batch_messages: |
| 1053 | + if ( |
| 1054 | + msg.command == "CHATHISTORY" |
| 1055 | + and msg.params[0] == "TARGETS" |
| 1056 | + and len(msg.params) >= 3 |
| 1057 | + ): |
| 1058 | + targets_results.append((msg.params[1], msg.params[2])) |
| 1059 | + return targets_results |
| 1060 | + |
816 | 1061 |
|
817 | 1062 | assert {f"_validate_chathistory_{cmd}" for cmd in SUBCOMMANDS} == { |
818 | 1063 | meth_name |
|
0 commit comments