From 604041aa135d48f5f2a571086082c1318265220c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 14:43:35 +0200 Subject: [PATCH 01/11] Copy metadata.py to metadata2.py --- irctest/server_tests/metadata_2.py | 244 +++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 irctest/server_tests/metadata_2.py diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py new file mode 100644 index 00000000..abfde8b5 --- /dev/null +++ b/irctest/server_tests/metadata_2.py @@ -0,0 +1,244 @@ +""" +`Deprecated IRCv3 Metadata `_ +""" + +from irctest import cases + + +class MetadataTestCase(cases.BaseServerTestCase): + valid_metadata_keys = {"valid_key1", "valid_key2"} + invalid_metadata_keys = {"invalid_key1", "invalid_key2"} + + @cases.mark_specifications("IRCv3", deprecated=True) + def testInIsupport(self): + """“If METADATA is supported, it MUST be specified in RPL_ISUPPORT + using the METADATA key.” + -- + """ + self.addClient() + self.sendLine(1, "CAP LS 302") + self.getCapLs(1) + self.sendLine(1, "USER foo foo foo :foo") + self.sendLine(1, "NICK foo") + self.sendLine(1, "CAP END") + self.skipToWelcome(1) + m = self.getMessage(1) + while m.command != "005": # RPL_ISUPPORT + m = self.getMessage(1) + self.assertIn( + "METADATA", + {x.split("=")[0] for x in m.params[1:-1]}, + fail_msg="{item} missing from RPL_ISUPPORT", + ) + self.getMessages(1) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testGetOneUnsetValid(self): + """""" + self.connectClient("foo") + self.sendLine(1, "METADATA * GET valid_key1") + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="766", # ERR_NOMATCHINGKEY + fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a " + "request to an unset valid METADATA key.", + ) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testGetTwoUnsetValid(self): + """“Multiple keys may be given. The response will be either RPL_KEYVALUE, + ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.” + -- + """ + self.connectClient("foo") + self.sendLine(1, "METADATA * GET valid_key1 valid_key2") + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="766", # ERR_NOMATCHINGKEY + fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a " + "request to two unset valid METADATA key: {msg}", + ) + self.assertEqual( + m.params[1], + "valid_key1", + m, + fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " + "did not respond to valid_key1 first: {msg}", + ) + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="766", # ERR_NOMATCHINGKEY + fail_msg="Did not reply with two 766 (ERR_NOMATCHINGKEY) to a " + "request to two unset valid METADATA key: {msg}", + ) + self.assertEqual( + m.params[1], + "valid_key2", + m, + fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " + "did not respond to valid_key2 as second response: {msg}", + ) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testListNoSet(self): + """“This subcommand MUST list all currently-set metadata keys along + with their values. The response will be zero or more RPL_KEYVALUE + events, following by RPL_METADATAEND event.” + -- + """ + self.connectClient("foo") + self.sendLine(1, "METADATA * LIST") + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="762", # RPL_METADATAEND + fail_msg="Response to “METADATA * LIST” was not " + "762 (RPL_METADATAEND) but: {msg}", + ) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testListInvalidTarget(self): + """“In case of invalid target RPL_METADATAEND MUST NOT be sent.” + -- + """ + self.connectClient("foo") + self.sendLine(1, "METADATA foobar LIST") + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="765", # ERR_TARGETINVALID + fail_msg="Response to “METADATA LIST” was " + "not 765 (ERR_TARGETINVALID) but: {msg}", + ) + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn( + "762", + commands, + fail_msg="Sent “METADATA LIST”, got 765 " + "(ERR_TARGETINVALID), and then 762 (RPL_METADATAEND)", + ) + + def assertSetValue(self, target, key, value, displayable_value=None): + if displayable_value is None: + displayable_value = value + self.sendLine(1, "METADATA {} SET {} :{}".format(target, key, value)) + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="761", # RPL_KEYVALUE + fail_msg="Did not reply with 761 (RPL_KEYVALUE) to a valid " + "“METADATA * SET {} :{}”: {msg}", + extra_format=(key, displayable_value), + ) + self.assertEqual( + m.params[1], + "valid_key1", + m, + fail_msg="Second param of 761 after setting “{expects}” to " + "“{}” is not “{expects}”: {msg}.", + extra_format=(displayable_value,), + ) + self.assertEqual( + m.params[3], + value, + m, + fail_msg="Fourth param of 761 after setting “{0}” to " + "“{1}” is not “{1}”: {msg}.", + extra_format=(key, displayable_value), + ) + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="762", # RPL_METADATAEND + fail_msg="Did not send RPL_METADATAEND after setting " + "a valid METADATA key.", + ) + + def assertGetValue(self, target, key, value, displayable_value=None): + self.sendLine(1, "METADATA * GET {}".format(key)) + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="761", # RPL_KEYVALUE + fail_msg="Did not reply with 761 (RPL_KEYVALUE) to a valid " + "“METADATA * GET” when the key is set is set: {msg}", + ) + self.assertEqual( + m.params[1], + key, + m, + fail_msg="Second param of 761 after getting “{expects}” " + "(which is set) is not “{expects}”: {msg}.", + ) + self.assertEqual( + m.params[3], + value, + m, + fail_msg="Fourth param of 761 after getting “{0}” " + "(which is set to “{1}”) is not ”{1}”: {msg}.", + extra_format=(key, displayable_value), + ) + + def assertSetGetValue(self, target, key, value, displayable_value=None): + self.assertSetValue(target, key, value, displayable_value) + self.assertGetValue(target, key, value, displayable_value) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testSetGetValid(self): + """""" + self.connectClient("foo") + self.assertSetGetValue("*", "valid_key1", "myvalue") + + @cases.mark_specifications("IRCv3", deprecated=True) + def testSetGetZeroCharInValue(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + self.connectClient("foo") + self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero") + + @cases.mark_specifications("IRCv3", deprecated=True) + def testSetGetHeartInValue(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + heart = b"\xf0\x9f\x92\x9c".decode() + self.connectClient("foo") + self.assertSetGetValue( + "*", + "valid_key1", + "->{}<-".format(heart), + "zero->{}<-zero".format(heart.encode()), + ) + + @cases.mark_specifications("IRCv3", deprecated=True) + def testSetInvalidUtf8(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + self.connectClient("foo") + # Sending directly because it is not valid UTF-8 so Python would + # not like it + self.clients[1].conn.sendall( + b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n" + ) + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn( + "761", + commands, # RPL_KEYVALUE + fail_msg="Setting METADATA key to a value containing invalid " + "UTF-8 was answered with 761 (RPL_KEYVALUE)", + ) + self.clients[1].conn.sendall( + b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n" + ) + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn( + "761", + commands, # RPL_KEYVALUE + fail_msg="Setting METADATA key to a value containing invalid " + "UTF-8 was answered with 761 (RPL_KEYVALUE)", + ) From e17ab347f403df73bfcf169fc053f0f259a724a2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 14:44:58 +0200 Subject: [PATCH 02/11] Adapt metadata_2 to stdreplies, and run it on Unreal Passes on https://github.com/progval/unrealircd-contrib/commit/3968354247d07e341c1ede6e7f3d29688ed702c7 --- irctest/controllers/unrealircd.py | 1 + irctest/server_tests/metadata_2.py | 184 +++++++++++------------------ pytest.ini | 1 + 3 files changed, 70 insertions(+), 116 deletions(-) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 8d7e643c..1f29cde5 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -11,6 +11,7 @@ TEMPLATE_CONFIG = """ include "modules.default.conf"; +loadmodule "third/metadata2"; include "operclass.default.conf"; {extras} include "help/help.conf"; diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index abfde8b5..d5844c0d 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -1,41 +1,23 @@ """ -`Deprecated IRCv3 Metadata `_ +`IRCv3 Metadata 2 `_ +(not to be confused with the `deprecated IRCv3 Metadata +`_) """ from irctest import cases +from irctest.patma import ANYSTR, StrRe class MetadataTestCase(cases.BaseServerTestCase): valid_metadata_keys = {"valid_key1", "valid_key2"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"} - @cases.mark_specifications("IRCv3", deprecated=True) - def testInIsupport(self): - """“If METADATA is supported, it MUST be specified in RPL_ISUPPORT - using the METADATA key.” - -- - """ - self.addClient() - self.sendLine(1, "CAP LS 302") - self.getCapLs(1) - self.sendLine(1, "USER foo foo foo :foo") - self.sendLine(1, "NICK foo") - self.sendLine(1, "CAP END") - self.skipToWelcome(1) - m = self.getMessage(1) - while m.command != "005": # RPL_ISUPPORT - m = self.getMessage(1) - self.assertIn( - "METADATA", - {x.split("=")[0] for x in m.params[1:-1]}, - fail_msg="{item} missing from RPL_ISUPPORT", - ) - self.getMessages(1) - - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testGetOneUnsetValid(self): """""" - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.sendLine(1, "METADATA * GET valid_key1") m = self.getMessage(1) self.assertMessageMatch( @@ -45,25 +27,26 @@ def testGetOneUnsetValid(self): "request to an unset valid METADATA key.", ) - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testGetTwoUnsetValid(self): """“Multiple keys may be given. The response will be either RPL_KEYVALUE, ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.” -- """ - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.sendLine(1, "METADATA * GET valid_key1 valid_key2") m = self.getMessage(1) self.assertMessageMatch( m, - command="766", # ERR_NOMATCHINGKEY - fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a " + command="766", # RPL_NOMATCHINGKEY + fail_msg="Did not reply with 766 (RPL_NOMATCHINGKEY) to a " "request to two unset valid METADATA key: {msg}", ) - self.assertEqual( - m.params[1], - "valid_key1", + self.assertMessageMatch( m, + params=["foo", "foo", "valid_key1", ANYSTR], fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " "did not respond to valid_key1 first: {msg}", ) @@ -74,42 +57,45 @@ def testGetTwoUnsetValid(self): fail_msg="Did not reply with two 766 (ERR_NOMATCHINGKEY) to a " "request to two unset valid METADATA key: {msg}", ) - self.assertEqual( - m.params[1], - "valid_key2", + self.assertMessageMatch( m, + params=["foo", "foo", "valid_key2", ANYSTR], fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " "did not respond to valid_key2 as second response: {msg}", ) - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testListNoSet(self): """“This subcommand MUST list all currently-set metadata keys along with their values. The response will be zero or more RPL_KEYVALUE events, following by RPL_METADATAEND event.” -- """ - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.sendLine(1, "METADATA * LIST") m = self.getMessage(1) self.assertMessageMatch( m, command="762", # RPL_METADATAEND - fail_msg="Response to “METADATA * LIST” was not " - "762 (RPL_METADATAEND) but: {msg}", + params=["foo", ANYSTR], ) - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testListInvalidTarget(self): """“In case of invalid target RPL_METADATAEND MUST NOT be sent.” -- """ - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.sendLine(1, "METADATA foobar LIST") m = self.getMessage(1) self.assertMessageMatch( m, - command="765", # ERR_TARGETINVALID + command="FAIL", + params=["METADATA", "INVALID_TARGET", "foobar", ANYSTR], fail_msg="Response to “METADATA LIST” was " "not 765 (ERR_TARGETINVALID) but: {msg}", ) @@ -117,115 +103,81 @@ def testListInvalidTarget(self): self.assertNotIn( "762", commands, - fail_msg="Sent “METADATA LIST”, got 765 " - "(ERR_TARGETINVALID), and then 762 (RPL_METADATAEND)", + fail_msg="Sent “METADATA LIST”, got FAIL INVALID_TARGET, " + "and then 762 (RPL_METADATAEND)", ) - def assertSetValue(self, target, key, value, displayable_value=None): - if displayable_value is None: - displayable_value = value + def assertSetValue(self, target, key, value): self.sendLine(1, "METADATA {} SET {} :{}".format(target, key, value)) - m = self.getMessage(1) + + if target == "*": + target = StrRe(r"(\*|foo)") + self.assertMessageMatch( - m, + self.getMessage(1), command="761", # RPL_KEYVALUE - fail_msg="Did not reply with 761 (RPL_KEYVALUE) to a valid " - "“METADATA * SET {} :{}”: {msg}", - extra_format=(key, displayable_value), - ) - self.assertEqual( - m.params[1], - "valid_key1", - m, - fail_msg="Second param of 761 after setting “{expects}” to " - "“{}” is not “{expects}”: {msg}.", - extra_format=(displayable_value,), - ) - self.assertEqual( - m.params[3], - value, - m, - fail_msg="Fourth param of 761 after setting “{0}” to " - "“{1}” is not “{1}”: {msg}.", - extra_format=(key, displayable_value), - ) - m = self.getMessage(1) - self.assertMessageMatch( - m, - command="762", # RPL_METADATAEND - fail_msg="Did not send RPL_METADATAEND after setting " - "a valid METADATA key.", + params=["foo", target, key, ANYSTR, value], ) - def assertGetValue(self, target, key, value, displayable_value=None): - self.sendLine(1, "METADATA * GET {}".format(key)) - m = self.getMessage(1) + def assertGetValue(self, target, key, value): + self.sendLine(1, "METADATA {} GET {}".format(target, key)) + + if target == "*": + target = StrRe(r"(\*|foo)") + self.assertMessageMatch( - m, + self.getMessage(1), command="761", # RPL_KEYVALUE - fail_msg="Did not reply with 761 (RPL_KEYVALUE) to a valid " - "“METADATA * GET” when the key is set is set: {msg}", - ) - self.assertEqual( - m.params[1], - key, - m, - fail_msg="Second param of 761 after getting “{expects}” " - "(which is set) is not “{expects}”: {msg}.", - ) - self.assertEqual( - m.params[3], - value, - m, - fail_msg="Fourth param of 761 after getting “{0}” " - "(which is set to “{1}”) is not ”{1}”: {msg}.", - extra_format=(key, displayable_value), + params=["foo", target, key, ANYSTR, value], ) - def assertSetGetValue(self, target, key, value, displayable_value=None): - self.assertSetValue(target, key, value, displayable_value) - self.assertGetValue(target, key, value, displayable_value) + def assertSetGetValue(self, target, key, value): + self.assertSetValue(target, key, value) + self.assertGetValue(target, key, value) - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testSetGetValid(self): """""" - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.assertSetGetValue("*", "valid_key1", "myvalue") - @cases.mark_specifications("IRCv3", deprecated=True) - def testSetGetZeroCharInValue(self): - """“Values are unrestricted, except that they MUST be UTF-8.” - -- - """ - self.connectClient("foo") - self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero") - - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.mark_specifications("IRCv3") def testSetGetHeartInValue(self): """“Values are unrestricted, except that they MUST be UTF-8.” -- """ heart = b"\xf0\x9f\x92\x9c".decode() - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) self.assertSetGetValue( "*", "valid_key1", "->{}<-".format(heart), - "zero->{}<-zero".format(heart.encode()), ) - @cases.mark_specifications("IRCv3", deprecated=True) + @cases.xfailIfSoftware( + ["UnrealIRCd"], "UnrealIRCd does not validate UTF-8 in metadata values" + ) + @cases.mark_specifications("IRCv3") def testSetInvalidUtf8(self): """“Values are unrestricted, except that they MUST be UTF-8.” -- """ - self.connectClient("foo") + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) # Sending directly because it is not valid UTF-8 so Python would # not like it self.clients[1].conn.sendall( b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n" ) - commands = {m.command for m in self.getMessages(1)} + try: + commands = {m.command for m in self.getMessages(1)} + except UnicodeDecodeError: + assert False, "Server sent invalid UTF-8" self.assertNotIn( "761", commands, # RPL_KEYVALUE diff --git a/pytest.ini b/pytest.ini index 375f2bb3..6cef85d3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,7 @@ markers = extended-monitor labeled-response message-tags + metadata-2 draft/multiline multi-prefix server-time From 88f8be0ff63959346b9561e74f8debd4b98b6cc1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 17:52:54 +0200 Subject: [PATCH 03/11] expect batches --- irctest/server_tests/metadata_2.py | 48 +++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index d5844c0d..98b1a81e 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -5,13 +5,26 @@ """ from irctest import cases -from irctest.patma import ANYSTR, StrRe +from irctest.patma import ANYDICT, ANYSTR, StrRe class MetadataTestCase(cases.BaseServerTestCase): valid_metadata_keys = {"valid_key1", "valid_key2"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"} + def getBatchMessages(self, client): + messages = self.getMessages(1) + + first_msg = messages.pop(0) + last_msg = messages.pop(-1) + self.assertMessageMatch( + first_msg, command="BATCH", params=[StrRe(r"\+.*"), "metadata"] + ) + batch_id = first_msg.params[0][1:] + self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id]) + + return (batch_id, messages) + @cases.mark_specifications("IRCv3") def testGetOneUnsetValid(self): """""" @@ -19,12 +32,15 @@ def testGetOneUnsetValid(self): "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) self.sendLine(1, "METADATA * GET valid_key1") - m = self.getMessage(1) + + (batch_id, messages) = self.getBatchMessages(1) + self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY") self.assertMessageMatch( - m, + messages[0], + tags={"batch": batch_id, **ANYDICT}, command="766", # ERR_NOMATCHINGKEY fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a " - "request to an unset valid METADATA key.", + "request to an unset valid METADATA key: {msg}", ) @cases.mark_specifications("IRCv3") @@ -37,28 +53,28 @@ def testGetTwoUnsetValid(self): "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) self.sendLine(1, "METADATA * GET valid_key1 valid_key2") - m = self.getMessage(1) + (batch_id, messages) = self.getBatchMessages(1) + self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY") self.assertMessageMatch( - m, + messages[0], command="766", # RPL_NOMATCHINGKEY fail_msg="Did not reply with 766 (RPL_NOMATCHINGKEY) to a " "request to two unset valid METADATA key: {msg}", ) self.assertMessageMatch( - m, + messages[0], params=["foo", "foo", "valid_key1", ANYSTR], fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " "did not respond to valid_key1 first: {msg}", ) - m = self.getMessage(1) self.assertMessageMatch( - m, + messages[1], command="766", # ERR_NOMATCHINGKEY fail_msg="Did not reply with two 766 (ERR_NOMATCHINGKEY) to a " "request to two unset valid METADATA key: {msg}", ) self.assertMessageMatch( - m, + messages[1], params=["foo", "foo", "valid_key2", ANYSTR], fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " "did not respond to valid_key2 as second response: {msg}", @@ -75,12 +91,8 @@ def testListNoSet(self): "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) self.sendLine(1, "METADATA * LIST") - m = self.getMessage(1) - self.assertMessageMatch( - m, - command="762", # RPL_METADATAEND - params=["foo", ANYSTR], - ) + (batch_id, messages) = self.getBatchMessages(1) + self.assertEqual(len(messages), 0, fail_msg="Expected empty batch") @cases.mark_specifications("IRCv3") def testListInvalidTarget(self): @@ -125,8 +137,10 @@ def assertGetValue(self, target, key, value): if target == "*": target = StrRe(r"(\*|foo)") + (batch_id, messages) = self.getBatchMessages(1) + self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE") self.assertMessageMatch( - self.getMessage(1), + messages[0], command="761", # RPL_KEYVALUE params=["foo", target, key, ANYSTR, value], ) From 8d01063aee836a2a7891cfd508cfc5da8cf41154 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 17:54:06 +0200 Subject: [PATCH 04/11] Improve testing for invalid UTF8, and add test for too-long values --- irctest/client_mock.py | 14 +++++++- irctest/server_tests/metadata_2.py | 51 ++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/irctest/client_mock.py b/irctest/client_mock.py index e6aaa131..89cf58ca 100644 --- a/irctest/client_mock.py +++ b/irctest/client_mock.py @@ -74,7 +74,19 @@ def getMessages( continue if not synchronize: got_pong = True - for line in data.decode().split("\r\n"): + try: + decoded_data = data.decode() + except UnicodeDecodeError: + print( + "{time:.3f}{ssl} S -> {client} - failed to decode: {data!r}".format( + time=time.time(), + ssl=" (ssl)" if self.ssl else "", + client=self.name, + data=data, + ) + ) + raise + for line in decoded_data.split("\r\n"): if line: if self.show_io: print( diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index 98b1a81e..f8dd4b37 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -172,14 +172,7 @@ def testSetGetHeartInValue(self): "->{}<-".format(heart), ) - @cases.xfailIfSoftware( - ["UnrealIRCd"], "UnrealIRCd does not validate UTF-8 in metadata values" - ) - @cases.mark_specifications("IRCv3") - def testSetInvalidUtf8(self): - """“Values are unrestricted, except that they MUST be UTF-8.” - -- - """ + def _testSetInvalidValue(self, value): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) @@ -198,13 +191,45 @@ def testSetInvalidUtf8(self): fail_msg="Setting METADATA key to a value containing invalid " "UTF-8 was answered with 761 (RPL_KEYVALUE)", ) - self.clients[1].conn.sendall( - b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n" + self.clients[1].conn.sendall(b"METADATA * SET valid_key1 :" + value + b"\r\n") + self.assertMessageMatch( + self.getMessage(1), + command="FAIL", + params=["METADATA", "INVALID_VALUE", ANYSTR], ) - commands = {m.command for m in self.getMessages(1)} + messages = self.getMessages(1) self.assertNotIn( - "761", - commands, # RPL_KEYVALUE + "761", # RPL_KEYVALUE + {m.command for m in messages}, fail_msg="Setting METADATA key to a value containing invalid " "UTF-8 was answered with 761 (RPL_KEYVALUE)", ) + self.assertEqual( + messages, + [], + fail_msg="Unexpected response to METADATA SET with invalid value: {got}", + ) + + @cases.mark_specifications("IRCv3") + def testSetInvalidUtf8(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + self._testSetInvalidValue(b"invalid UTF-8: \xc3") + + @cases.mark_specifications("IRCv3") + def testSetTooManyChars(self): + """Assumes all servers reject values over 480 bytes. This isn't required by the + spec, but makes them risk overflowing when printing the value, so they probably + won't allow that. + """ + self._testSetInvalidValue(b"abcd" * 120) + + @cases.mark_specifications("IRCv3") + def testSetTooManyBytes(self): + """Assumes all servers reject values over 480 bytes. This isn't required by the + spec, but makes them risk overflowing when printing the value, so they probably + won't allow that. + """ + heart = b"\xf0\x9f\x92\x9c" + self._testSetInvalidValue(heart * 120) From 0318a37075b77d67990570c1156b962cae81d7a4 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 17:56:00 +0200 Subject: [PATCH 05/11] Test for nick instead of * as target --- irctest/server_tests/metadata_2.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index f8dd4b37..9acd519c 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -4,6 +4,10 @@ `_) """ +import itertools + +import pytest + from irctest import cases from irctest.patma import ANYDICT, ANYSTR, StrRe @@ -149,13 +153,17 @@ def assertSetGetValue(self, target, key, value): self.assertSetValue(target, key, value) self.assertGetValue(target, key, value) + @pytest.mark.parametrize( + "set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"]) + ) @cases.mark_specifications("IRCv3") - def testSetGetValid(self): + def testSetGetValid(self, set_target, get_target): """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.assertSetGetValue("*", "valid_key1", "myvalue") + self.assertSetValue(set_target, "valid_key1", "myvalue") + self.assertGetValue(get_target, "valid_key1", "myvalue") @cases.mark_specifications("IRCv3") def testSetGetHeartInValue(self): From 8a6a12e7f78f79e9ea02378b65b621d97a8f20dd Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 18:56:17 +0200 Subject: [PATCH 06/11] Fix fail code --- irctest/server_tests/metadata_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index 9acd519c..fad89f01 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -203,7 +203,7 @@ def _testSetInvalidValue(self, value): self.assertMessageMatch( self.getMessage(1), command="FAIL", - params=["METADATA", "INVALID_VALUE", ANYSTR], + params=["METADATA", "VALUE_INVALID", ANYSTR], ) messages = self.getMessages(1) self.assertNotIn( From f18162c02ddabb4599d3208f6ed4a7be242b4bb1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 18:56:27 +0200 Subject: [PATCH 07/11] Add untested 'before-connect' test --- irctest/server_tests/metadata_2.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index fad89f01..a77cf5b0 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -8,7 +8,7 @@ import pytest -from irctest import cases +from irctest import cases, runner from irctest.patma import ANYDICT, ANYSTR, StrRe @@ -165,6 +165,29 @@ def testSetGetValid(self, set_target, get_target): self.assertSetValue(set_target, "valid_key1", "myvalue") self.assertGetValue(get_target, "valid_key1", "myvalue") + @cases.mark_specifications("IRCv3") + def testSetGetValidBeforeConnect(self): + """""" + self.addClient(1) + + self.sendLine(1, "CAP LS 302") + caps = self.getCapLs(1) + if "before-connect" not in (caps["draft/metadata-2"] or "").split(","): + raise runner.OptionalExtensionNotSupported( + "draft/metadata-2=before-connect" + ) + + self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True) + + self.assertSetValue("*", "valid_key1", "myvalue") + + self.sendLine(1, "NICK foo") + self.sendLine(1, "USER foo 0 * :foo") + self.sendLine(1, "CAP END") + self.skipToWelcome(1) + + self.assertGetValue("*", "valid_key1", "myvalue") + @cases.mark_specifications("IRCv3") def testSetGetHeartInValue(self): """“Values are unrestricted, except that they MUST be UTF-8.” From 34f078e04a0cd3afc4efdec2d40d57ddcbf253da Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 19:02:04 +0200 Subject: [PATCH 08/11] Add a bunch of GetSet tests --- irctest/server_tests/metadata_2.py | 101 +++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 19 deletions(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index a77cf5b0..7c43413b 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -11,13 +11,18 @@ from irctest import cases, runner from irctest.patma import ANYDICT, ANYSTR, StrRe +CLIENT_NICKS = { + 1: "foo", + 2: "bar", +} + class MetadataTestCase(cases.BaseServerTestCase): valid_metadata_keys = {"valid_key1", "valid_key2"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"} def getBatchMessages(self, client): - messages = self.getMessages(1) + messages = self.getMessages(client) first_msg = messages.pop(0) last_msg = messages.pop(-1) @@ -123,47 +128,104 @@ def testListInvalidTarget(self): "and then 762 (RPL_METADATAEND)", ) - def assertSetValue(self, target, key, value): - self.sendLine(1, "METADATA {} SET {} :{}".format(target, key, value)) + def assertSetValue(self, client, target, key, value): + self.sendLine(client, "METADATA {} SET {} :{}".format(target, key, value)) if target == "*": - target = StrRe(r"(\*|foo)") + target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")") self.assertMessageMatch( - self.getMessage(1), + self.getMessage(client), command="761", # RPL_KEYVALUE - params=["foo", target, key, ANYSTR, value], + params=[CLIENT_NICKS[client], target, key, ANYSTR, value], ) - def assertGetValue(self, target, key, value): - self.sendLine(1, "METADATA {} GET {}".format(target, key)) + def assertGetValue(self, client, target, key, value): + self.sendLine(client, "METADATA {} GET {}".format(target, key)) if target == "*": - target = StrRe(r"(\*|foo)") + target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")") - (batch_id, messages) = self.getBatchMessages(1) + (batch_id, messages) = self.getBatchMessages(client) self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE") self.assertMessageMatch( messages[0], command="761", # RPL_KEYVALUE - params=["foo", target, key, ANYSTR, value], + params=[CLIENT_NICKS[client], target, key, ANYSTR, value], ) - def assertSetGetValue(self, target, key, value): - self.assertSetValue(target, key, value) - self.assertGetValue(target, key, value) + def assertSetGetValue(self, client, target, key, value): + self.assertSetValue(client, target, key, value) + self.assertGetValue(client, target, key, value) @pytest.mark.parametrize( "set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"]) ) @cases.mark_specifications("IRCv3") - def testSetGetValid(self, set_target, get_target): + def testSetGet(self, set_target, get_target): """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.assertSetValue(set_target, "valid_key1", "myvalue") - self.assertGetValue(get_target, "valid_key1", "myvalue") + self.assertSetValue(1, set_target, "valid_key1", "myvalue") + self.assertGetValue(1, get_target, "valid_key1", "myvalue") + + @cases.mark_specifications("IRCv3") + def testSetGetAgain(self): + """""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.assertSetGetValue(1, "*", "valid_key1", "myvalue1") + self.assertSetGetValue(1, "*", "valid_key1", "myvalue2") + + @cases.mark_specifications("IRCv3") + def testSetGetChannel(self): + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.assertSetGetValue(1, "#chan", "valid_key1", "myvalue1") + self.assertEqual( + self.getMessages(2), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) + self.assertGetValue(2, "#chan", "valid_key1", "myvalue1") + + @cases.mark_specifications("IRCv3") + def testSetGetOtherUser(self): + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + # As of 2023-04-15, the Unreal module requires users to share a channel for + # metadata to be visible to each other + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.assertSetValue(1, "*", "valid_key1", "myvalue1") + self.assertEqual( + self.getMessages(2), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) + self.assertGetValue(2, "foo", "valid_key1", "myvalue1") @cases.mark_specifications("IRCv3") def testSetGetValidBeforeConnect(self): @@ -179,14 +241,14 @@ def testSetGetValidBeforeConnect(self): self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True) - self.assertSetValue("*", "valid_key1", "myvalue") + self.assertSetValue(1, "*", "valid_key1", "myvalue") self.sendLine(1, "NICK foo") self.sendLine(1, "USER foo 0 * :foo") self.sendLine(1, "CAP END") self.skipToWelcome(1) - self.assertGetValue("*", "valid_key1", "myvalue") + self.assertGetValue(1, "*", "valid_key1", "myvalue") @cases.mark_specifications("IRCv3") def testSetGetHeartInValue(self): @@ -198,6 +260,7 @@ def testSetGetHeartInValue(self): "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) self.assertSetGetValue( + 1, "*", "valid_key1", "->{}<-".format(heart), From 6c5595539c55b21dd8ab9c1224bbf8ad971c8ac7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 19:42:44 +0200 Subject: [PATCH 09/11] Add tests for KEY_NO_PERMISSION --- irctest/server_tests/metadata_2.py | 62 +++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index 7c43413b..087cab29 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -203,7 +203,7 @@ def testSetGetChannel(self): self.assertGetValue(2, "#chan", "valid_key1", "myvalue1") @cases.mark_specifications("IRCv3") - def testSetGetOtherUser(self): + def testGetOtherUser(self): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) @@ -227,6 +227,66 @@ def testSetGetOtherUser(self): ) self.assertGetValue(2, "foo", "valid_key1", "myvalue1") + @cases.mark_specifications("IRCv3") + def testSetGetChannelNotOp(self): + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.sendLine(2, "METADATA #chan SET valid_key1 myvalue") + self.assertMessageMatch( + self.getMessage(2), + command="FAIL", + params=["METADATA", "KEY_NO_PERMISSION", "#chan", "valid_key1", ANYSTR], + ) + + self.assertEqual( + self.getMessages(1), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) + + @cases.mark_specifications("IRCv3") + def testSetOtherUser(self): + """Not required by the spec, but it makes little sense to allow anyone to + write a channel's metadata""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + # As of 2023-04-15, the Unreal module requires users to share a channel for + # metadata to be visible to each other. + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.sendLine(1, "METADATA bar SET valid_key1 myvalue") + self.assertMessageMatch( + self.getMessage(1), + command="FAIL", + params=["METADATA", "KEY_NO_PERMISSION", "bar", "valid_key1", ANYSTR], + ) + + self.assertEqual( + self.getMessages(2), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) + @cases.mark_specifications("IRCv3") def testSetGetValidBeforeConnect(self): """""" From 10c17721213e107509cd42ed1df78a2852464b67 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 20:32:18 +0200 Subject: [PATCH 10/11] Use real metadata keys --- irctest/server_tests/metadata_2.py | 53 ++++++++++++++---------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index 087cab29..afffcccb 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -18,9 +18,6 @@ class MetadataTestCase(cases.BaseServerTestCase): - valid_metadata_keys = {"valid_key1", "valid_key2"} - invalid_metadata_keys = {"invalid_key1", "invalid_key2"} - def getBatchMessages(self, client): messages = self.getMessages(client) @@ -40,7 +37,7 @@ def testGetOneUnsetValid(self): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.sendLine(1, "METADATA * GET valid_key1") + self.sendLine(1, "METADATA * GET display-name") (batch_id, messages) = self.getBatchMessages(1) self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY") @@ -61,7 +58,7 @@ def testGetTwoUnsetValid(self): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.sendLine(1, "METADATA * GET valid_key1 valid_key2") + self.sendLine(1, "METADATA * GET display-name avatar") (batch_id, messages) = self.getBatchMessages(1) self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY") self.assertMessageMatch( @@ -72,9 +69,9 @@ def testGetTwoUnsetValid(self): ) self.assertMessageMatch( messages[0], - params=["foo", "foo", "valid_key1", ANYSTR], - fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " - "did not respond to valid_key1 first: {msg}", + params=["foo", "foo", "display-name", ANYSTR], + fail_msg="Response to “METADATA * GET display-name avatar” " + "did not respond to display-name first: {msg}", ) self.assertMessageMatch( messages[1], @@ -84,9 +81,9 @@ def testGetTwoUnsetValid(self): ) self.assertMessageMatch( messages[1], - params=["foo", "foo", "valid_key2", ANYSTR], - fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " - "did not respond to valid_key2 as second response: {msg}", + params=["foo", "foo", "avatar", ANYSTR], + fail_msg="Response to “METADATA * GET display-name avatar” " + "did not respond to avatar as second response: {msg}", ) @cases.mark_specifications("IRCv3") @@ -167,8 +164,8 @@ def testSetGet(self, set_target, get_target): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.assertSetValue(1, set_target, "valid_key1", "myvalue") - self.assertGetValue(1, get_target, "valid_key1", "myvalue") + self.assertSetValue(1, set_target, "display-name", "Foo The First") + self.assertGetValue(1, get_target, "display-name", "Foo The First") @cases.mark_specifications("IRCv3") def testSetGetAgain(self): @@ -176,8 +173,8 @@ def testSetGetAgain(self): self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.assertSetGetValue(1, "*", "valid_key1", "myvalue1") - self.assertSetGetValue(1, "*", "valid_key1", "myvalue2") + self.assertSetGetValue(1, "*", "display-name", "Foo The First") + self.assertSetGetValue(1, "*", "display-name", "Foo The Second") @cases.mark_specifications("IRCv3") def testSetGetChannel(self): @@ -194,13 +191,13 @@ def testSetGetChannel(self): self.getMessages(2) self.getMessages(1) - self.assertSetGetValue(1, "#chan", "valid_key1", "myvalue1") + self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel") self.assertEqual( self.getMessages(2), [], fail_msg="Unexpected messages after other user used METADATA SET: {got}", ) - self.assertGetValue(2, "#chan", "valid_key1", "myvalue1") + self.assertGetValue(2, "#chan", "display-name", "Hash Channel") @cases.mark_specifications("IRCv3") def testGetOtherUser(self): @@ -219,13 +216,13 @@ def testGetOtherUser(self): self.getMessages(2) self.getMessages(1) - self.assertSetValue(1, "*", "valid_key1", "myvalue1") + self.assertSetValue(1, "*", "display-name", "Foo The First") self.assertEqual( self.getMessages(2), [], fail_msg="Unexpected messages after other user used METADATA SET: {got}", ) - self.assertGetValue(2, "foo", "valid_key1", "myvalue1") + self.assertGetValue(2, "foo", "display-name", "Foo The First") @cases.mark_specifications("IRCv3") def testSetGetChannelNotOp(self): @@ -242,11 +239,11 @@ def testSetGetChannelNotOp(self): self.getMessages(2) self.getMessages(1) - self.sendLine(2, "METADATA #chan SET valid_key1 myvalue") + self.sendLine(2, "METADATA #chan SET display-name :Sharp Channel") self.assertMessageMatch( self.getMessage(2), command="FAIL", - params=["METADATA", "KEY_NO_PERMISSION", "#chan", "valid_key1", ANYSTR], + params=["METADATA", "KEY_NO_PERMISSION", "#chan", "display-name", ANYSTR], ) self.assertEqual( @@ -274,11 +271,11 @@ def testSetOtherUser(self): self.getMessages(2) self.getMessages(1) - self.sendLine(1, "METADATA bar SET valid_key1 myvalue") + self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo") self.assertMessageMatch( self.getMessage(1), command="FAIL", - params=["METADATA", "KEY_NO_PERMISSION", "bar", "valid_key1", ANYSTR], + params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR], ) self.assertEqual( @@ -301,14 +298,14 @@ def testSetGetValidBeforeConnect(self): self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True) - self.assertSetValue(1, "*", "valid_key1", "myvalue") + self.assertSetValue(1, "*", "display-name", "Foo The First") self.sendLine(1, "NICK foo") self.sendLine(1, "USER foo 0 * :foo") self.sendLine(1, "CAP END") self.skipToWelcome(1) - self.assertGetValue(1, "*", "valid_key1", "myvalue") + self.assertGetValue(1, "*", "display-name", "Foo The First") @cases.mark_specifications("IRCv3") def testSetGetHeartInValue(self): @@ -322,7 +319,7 @@ def testSetGetHeartInValue(self): self.assertSetGetValue( 1, "*", - "valid_key1", + "display-name", "->{}<-".format(heart), ) @@ -333,7 +330,7 @@ def _testSetInvalidValue(self, value): # Sending directly because it is not valid UTF-8 so Python would # not like it self.clients[1].conn.sendall( - b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n" + b"METADATA * SET display-name :invalid UTF-8 ->\xc3<-\r\n" ) try: commands = {m.command for m in self.getMessages(1)} @@ -345,7 +342,7 @@ def _testSetInvalidValue(self, value): fail_msg="Setting METADATA key to a value containing invalid " "UTF-8 was answered with 761 (RPL_KEYVALUE)", ) - self.clients[1].conn.sendall(b"METADATA * SET valid_key1 :" + value + b"\r\n") + self.clients[1].conn.sendall(b"METADATA * SET display-name :" + value + b"\r\n") self.assertMessageMatch( self.getMessage(1), command="FAIL", From 754df829d4da9c2a59e6c44c97ca4cf1002f3a52 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 22:44:47 +0200 Subject: [PATCH 11/11] Add tests for SUB --- irctest/server_tests/metadata_2.py | 279 ++++++++++++++++++++++++++--- 1 file changed, 255 insertions(+), 24 deletions(-) diff --git a/irctest/server_tests/metadata_2.py b/irctest/server_tests/metadata_2.py index afffcccb..e666ec6c 100644 --- a/irctest/server_tests/metadata_2.py +++ b/irctest/server_tests/metadata_2.py @@ -9,7 +9,7 @@ import pytest from irctest import cases, runner -from irctest.patma import ANYDICT, ANYSTR, StrRe +from irctest.patma import ANYDICT, ANYLIST, ANYSTR, StrRe CLIENT_NICKS = { 1: "foo", @@ -31,6 +31,22 @@ def getBatchMessages(self, client): return (batch_id, messages) + def sub(self, client, keys): + self.sendLine(2, "METADATA * SUB " + " ".join(keys)) + acknowledged_subs = [] + for m in self.getMessages(2): + self.assertMessageMatch( + m, + command="770", # RPL_METADATASUBOK + params=["bar", *ANYLIST], + ) + acknowledged_subs.extend(m.params[1:]) + self.assertEqual( + sorted(acknowledged_subs), + sorted(keys), + fail_msg="Expected RPL_METADATASUBOK to ack {expects}, got {got}", + ) + @cases.mark_specifications("IRCv3") def testGetOneUnsetValid(self): """""" @@ -63,8 +79,8 @@ def testGetTwoUnsetValid(self): self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY") self.assertMessageMatch( messages[0], - command="766", # RPL_NOMATCHINGKEY - fail_msg="Did not reply with 766 (RPL_NOMATCHINGKEY) to a " + command="766", # RPL_KEYNOTSET + fail_msg="Did not reply with 766 (RPL_KEYNOTSET) to a " "request to two unset valid METADATA key: {msg}", ) self.assertMessageMatch( @@ -75,8 +91,8 @@ def testGetTwoUnsetValid(self): ) self.assertMessageMatch( messages[1], - command="766", # ERR_NOMATCHINGKEY - fail_msg="Did not reply with two 766 (ERR_NOMATCHINGKEY) to a " + command="766", # RPL_KEYNOTSET + fail_msg="Did not reply with two 766 (RPL_KEYNOTSET) to a " "request to two unset valid METADATA key: {msg}", ) self.assertMessageMatch( @@ -137,6 +153,18 @@ def assertSetValue(self, client, target, key, value): params=[CLIENT_NICKS[client], target, key, ANYSTR, value], ) + def assertUnsetValue(self, client, target, key): + self.sendLine(client, "METADATA {} SET {}".format(target, key)) + + if target == "*": + target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")") + + self.assertMessageMatch( + self.getMessage(client), + command="766", # RPL_KEYNOTSET + params=[CLIENT_NICKS[client], target, key, ANYSTR], + ) + def assertGetValue(self, client, target, key, value): self.sendLine(client, "METADATA {} GET {}".format(target, key)) @@ -151,30 +179,192 @@ def assertGetValue(self, client, target, key, value): params=[CLIENT_NICKS[client], target, key, ANYSTR, value], ) + def assertValueNotSet(self, client, target, key): + self.sendLine(client, "METADATA {} GET {}".format(target, key)) + + if target == "*": + target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")") + + (batch_id, messages) = self.getBatchMessages(client) + self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE") + self.assertMessageMatch( + messages[0], + command="766", # RPL_KEYNOTSET + params=[CLIENT_NICKS[client], target, key, ANYSTR], + ) + def assertSetGetValue(self, client, target, key, value): self.assertSetValue(client, target, key, value) self.assertGetValue(client, target, key, value) + def assertUnsetGetValue(self, client, target, key): + self.assertUnsetValue(client, target, key) + self.assertValueNotSet(client, target, key) + @pytest.mark.parametrize( "set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"]) ) @cases.mark_specifications("IRCv3") - def testSetGet(self, set_target, get_target): + def testSetGetUser(self, set_target, get_target): """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - self.assertSetValue(1, set_target, "display-name", "Foo The First") - self.assertGetValue(1, get_target, "display-name", "Foo The First") + self.assertSetGetValue(1, set_target, "display-name", "Foo The First") + + @cases.mark_specifications("IRCv3") + def testSetGetUserAgain(self): + """""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.assertSetGetValue(1, "*", "display-name", "Foo The First") + self.assertSetGetValue(1, "*", "display-name", "Foo The Second") + + @cases.mark_specifications("IRCv3") + def testSetUnsetUser(self): + """""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.assertSetGetValue(1, "*", "display-name", "Foo The First") + self.assertUnsetGetValue(1, "*", "display-name") + + @cases.mark_specifications("IRCv3") + def testGetOtherUser(self): + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + # As of 2023-04-15, the Unreal module requires users to share a channel for + # metadata to be visible to each other + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.assertSetValue(1, "*", "display-name", "Foo The First") + self.assertEqual( + self.getMessages(2), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) + self.assertGetValue(2, "foo", "display-name", "Foo The First") + + @cases.mark_specifications("IRCv3") + def testSetOtherUser(self): + """Not required by the spec, but it makes little sense to allow anyone to + write a channel's metadata""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + # As of 2023-04-15, the Unreal module requires users to share a channel for + # metadata to be visible to each other. + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + + self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo") + self.assertMessageMatch( + self.getMessage(1), + command="FAIL", + params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR], + ) + + self.assertEqual( + self.getMessages(2), + [], + fail_msg="Unexpected messages after other user used METADATA SET: {got}", + ) @cases.mark_specifications("IRCv3") - def testSetGetAgain(self): + def testSubUser(self): """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + self.sub(2, ["avatar", "display-name"]) + + self.sendLine(1, "JOIN #chan") + self.sendLine(2, "JOIN #chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(1) + self.assertSetGetValue(1, "*", "display-name", "Foo The First") + self.assertMessageMatch( + self.getMessage(2), + command="METADATA", + params=["foo", "display-name", ANYSTR, "Foo The First"], + ) + self.assertSetGetValue(1, "*", "display-name", "Foo The Second") + self.assertMessageMatch( + self.getMessage(2), + command="METADATA", + params=["foo", "display-name", ANYSTR, "Foo The Second"], + ) + + self.assertUnsetGetValue(1, "*", "display-name") + self.assertMessageMatch( + self.getMessage(2), + command="METADATA", + params=["foo", "display-name", ANYSTR], + ) + + @cases.mark_specifications("IRCv3") + def testSubUserSetBeforeJoin(self): + """""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + self.sub(2, ["display-name", "avatar"]) + + self.assertSetGetValue(1, "*", "display-name", "Foo The First") + self.assertEqual( + self.getMessages(2), + [], + fail_msg="'bar' got message when 'foo' set its display-name even though " + "they don't share a channel", + ) + + self.sendLine(1, "JOIN #chan") + self.getMessages(1) + self.sendLine(2, "JOIN #chan") + + messages = self.getMessages(2) + metadata_messages = [m for m in messages if m.command == "METADATA"] + + self.assertEqual( + len(metadata_messages), + 1, + fail_msg="Expected exactly one METADATA message when joining a channel, " + "got: {got}", + ) + + self.assertMessageMatch( + metadata_messages[0], + command="METADATA", + params=["foo", "display-name", ANYSTR, "Foo The First"], + ) @cases.mark_specifications("IRCv3") def testSetGetChannel(self): @@ -200,7 +390,8 @@ def testSetGetChannel(self): self.assertGetValue(2, "#chan", "display-name", "Hash Channel") @cases.mark_specifications("IRCv3") - def testGetOtherUser(self): + def testSetUnsetChannel(self): + """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) @@ -208,21 +399,20 @@ def testGetOtherUser(self): "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - # As of 2023-04-15, the Unreal module requires users to share a channel for - # metadata to be visible to each other self.sendLine(1, "JOIN #chan") self.sendLine(2, "JOIN #chan") self.getMessages(1) self.getMessages(2) self.getMessages(1) - self.assertSetValue(1, "*", "display-name", "Foo The First") + self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel") + self.assertUnsetGetValue(1, "#chan", "display-name") self.assertEqual( self.getMessages(2), [], fail_msg="Unexpected messages after other user used METADATA SET: {got}", ) - self.assertGetValue(2, "foo", "display-name", "Foo The First") + self.assertValueNotSet(2, "#chan", "display-name") @cases.mark_specifications("IRCv3") def testSetGetChannelNotOp(self): @@ -253,9 +443,8 @@ def testSetGetChannelNotOp(self): ) @cases.mark_specifications("IRCv3") - def testSetOtherUser(self): - """Not required by the spec, but it makes little sense to allow anyone to - write a channel's metadata""" + def testSubChannel(self): + """""" self.connectClient( "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) @@ -263,25 +452,67 @@ def testSetOtherUser(self): "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True ) - # As of 2023-04-15, the Unreal module requires users to share a channel for - # metadata to be visible to each other. + self.sub(2, ["avatar", "display-name"]) + self.sendLine(1, "JOIN #chan") self.sendLine(2, "JOIN #chan") self.getMessages(1) self.getMessages(2) self.getMessages(1) - self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo") + self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel") self.assertMessageMatch( - self.getMessage(1), - command="FAIL", - params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR], + self.getMessage(2), + command="METADATA", + params=["#chan", "display-name", ANYSTR, "Hash Channel"], + ) + + self.assertSetGetValue(1, "#chan", "display-name", "Harsh Channel") + self.assertMessageMatch( + self.getMessage(2), + command="METADATA", + params=["#chan", "display-name", ANYSTR, "Harsh Channel"], ) + @cases.mark_specifications("IRCv3") + def testSubChannelSetBeforeJoin(self): + """""" + self.connectClient( + "foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + self.connectClient( + "bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True + ) + + self.sub(2, ["display-name", "avatar"]) + + self.sendLine(1, "JOIN #chan") + self.getMessages(1) + + self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel") self.assertEqual( self.getMessages(2), [], - fail_msg="Unexpected messages after other user used METADATA SET: {got}", + fail_msg="'bar' got message when 'foo' set #chan's display-name even " + "though they are not in it", + ) + + self.sendLine(2, "JOIN #chan") + + messages = self.getMessages(2) + metadata_messages = [m for m in messages if m.command == "METADATA"] + + self.assertEqual( + len(metadata_messages), + 1, + fail_msg="Expected exactly one METADATA message when joining a channel, " + "got: {got}", + ) + + self.assertMessageMatch( + metadata_messages[0], + command="METADATA", + params=["#chan", "display-name", ANYSTR, "Hash Channel"], ) @cases.mark_specifications("IRCv3")