connpt.py
  1  """
  2  Tests for client connect-point functionality.
  3  """
  4  
  5  from arti_rpc_tests import arti_test
  6  from arti_rpc import (
  7      ArtiRpcError,
  8      ArtiRpcErrorStatus,
  9      ArtiRpcConn,
 10      ArtiRpcConnBuilder,
 11  )
 12  
 13  import tempfile
 14  import os
 15  import urllib.parse
 16  
 17  
 18  def connpt_abort():
 19      """
 20      Return a connect point that fails with an explicit abort.
 21      """
 22      return """\
 23  [builtin]
 24  builtin = "abort"
 25  """
 26  
 27  
 28  def connpt_unix(context):
 29      """
 30      Return a connect point that uses an AF_UNIX connection
 31      to connect to arti.
 32      """
 33      return f"""
 34  [connect]
 35  socket = "unix:{context.socket_path}"
 36  auth = "none"
 37  """
 38  
 39  
 40  def connpt_working(context):
 41      """
 42      Return a connect point that should work for connecting to the
 43      arti instance in `context`.
 44  
 45      (Prefer this function to `connpt_unix()`, so that once we have
 46      something that works on windows, we can make our tests pass there too.)
 47      """
 48      return connpt_unix(context)
 49  
 50  
 51  class Tempdir:
 52      """
 53      Helper for creating files within a temporary directory.
 54  
 55      When this object is destroyed, the directory and its contents are deleted.
 56      """
 57  
 58      def __init__(self):
 59          self.tmpdir = tempfile.TemporaryDirectory()
 60  
 61      def dirname(self):
 62          """
 63          Return the name of the temporary directory
 64          """
 65          return self.tmpdir.name
 66  
 67      def fname(self, name):
 68          """
 69          Return the name of a file within the temporary directory
 70          """
 71          return os.path.join(self.tmpdir.name, name)
 72  
 73      def write(self, fname, text):
 74          """
 75          Store `text` into the file called `fname` within the temporary directory.
 76          """
 77          with open(self.fname(fname), "w") as f:
 78              f.write(text)
 79  
 80  
 81  class SavedEnviron:
 82      """
 83      Context manager to preserve os.environ while a test is changing it.
 84      """
 85  
 86      def __init__(self):
 87          pass
 88  
 89      def __enter__(self):
 90          self.env = dict(os.environ)
 91  
 92      def __exit__(self, _et, _ev, _tb):
 93          os.environ.update(self.env)
 94          for k in os.environ.keys() - self.env.keys():
 95              del os.environ[k]
 96  
 97  
 98  def assert_builder_aborts(bld: ArtiRpcConnBuilder):
 99      """
100      Try to open a RPC connection with `bld`, and assert that the attempt aborts.
101      """
102      try:
103          _ = bld.connect()
104          assert False  # shouldn't be reached.
105      except ArtiRpcError as e:
106          assert e.status_code() == ArtiRpcErrorStatus.ALL_CONNECT_ATTEMPTS_FAILED
107          assert 'Encountered an explicit "abort"' in str(e)
108  
109  
110  def assert_builder_connects(bld):
111      """
112      Try to open a RPC connection with `bld`, and assert that the attempt succeeds.
113      """
114      c = bld.connect()
115      assert c is not None
116  
117  
118  @arti_test
119  def ordering_literal_manual(context):
120      # Assert that prepend_literal_connect_point respects ordering.
121      bld = ArtiRpcConnBuilder()
122      bld.prepend_literal_connect_point(connpt_working(context))
123      bld.prepend_literal_connect_point(connpt_abort())
124      assert_builder_aborts(bld)
125  
126      bld = ArtiRpcConnBuilder()
127      bld.prepend_literal_connect_point(connpt_abort())
128      bld.prepend_literal_connect_point(connpt_working(context))
129      assert_builder_connects(bld)
130  
131  
132  @arti_test
133  def ordering_paths_manual(context):
134      # Assert that prepend_literal_path respects ordering.
135      tmp = Tempdir()
136      tmp.write("abort.toml", connpt_abort())
137      tmp.write("working.toml", connpt_working(context))
138  
139      bld = ArtiRpcConnBuilder()
140      bld.prepend_literal_path(tmp.fname("working.toml"))
141      bld.prepend_literal_path(tmp.fname("abort.toml"))
142      assert_builder_aborts(bld)
143  
144      bld = ArtiRpcConnBuilder()
145      bld.prepend_literal_path(tmp.fname("abort.toml"))
146      bld.prepend_literal_path(tmp.fname("working.toml"))
147      assert_builder_connects(bld)
148  
149  
150  @arti_test
151  def ordering_env_path(context):
152      # Assert that paths within our envvars respect ordering.
153      tmp = Tempdir()
154      tmp.write("abort.toml", connpt_abort())
155      tmp.write("working.toml", connpt_working(context))
156  
157      fn_a = tmp.fname("abort.toml")
158      fn_w = tmp.fname("working.toml")
159      assert ":" not in fn_a
160      assert ":" not in fn_w
161  
162      for varname in ["ARTI_RPC_CONNECT_PATH", "ARTI_RPC_CONNECT_PATH_OVERRIDE"]:
163          assert varname not in os.environ
164          with SavedEnviron():
165              bld = ArtiRpcConnBuilder()
166              os.environ[varname] = f"{fn_a}:{fn_w}"
167              assert_builder_aborts(bld)
168  
169              bld = ArtiRpcConnBuilder()
170              os.environ[varname] = f"{fn_w}:{fn_a}"
171              assert_builder_connects(bld)
172          assert varname not in os.environ
173  
174  
175  @arti_test
176  def ordering_env_literal(context):
177      # Assert that literal connect points within our envvars respect ordering.
178      q_a = urllib.parse.quote(connpt_abort())
179      q_w = urllib.parse.quote(connpt_working(context))
180  
181      for varname in ["ARTI_RPC_CONNECT_PATH", "ARTI_RPC_CONNECT_PATH_OVERRIDE"]:
182          assert varname not in os.environ
183          with SavedEnviron():
184              bld = ArtiRpcConnBuilder()
185              os.environ[varname] = f"{q_a}:{q_w}"
186              assert_builder_aborts(bld)
187  
188              bld = ArtiRpcConnBuilder()
189              os.environ[varname] = f"{q_w}:{q_a}"
190              assert_builder_connects(bld)
191          assert varname not in os.environ
192  
193  
194  @arti_test
195  def ordering_dir(context):
196      # Assert that files within a directory respect ordering.
197      tmp = Tempdir()
198      tmp.write("00_name_ignored", connpt_abort())  # no ".toml" suffix
199      tmp.write("01_abort.toml", connpt_abort())
200      tmp.write("02_connect.toml", connpt_working(context))
201      tmp.write("03_abort.toml", connpt_abort())
202  
203      bld = ArtiRpcConnBuilder()
204      bld.prepend_literal_path(tmp.dirname())
205      assert_builder_aborts(bld)
206  
207      os.unlink(tmp.fname("01_abort.toml"))
208      assert_builder_connects(bld)
209  
210  
211  @arti_test
212  def ordering_multi(context):
213      # Assert that our envvars and manually inserted items respect ordering
214      # wrt one another.
215      tmp = Tempdir()
216      tmp.write("abort.toml", connpt_abort())
217      tmp.write("working.toml", connpt_working(context))
218  
219      fn_a = tmp.fname("abort.toml")
220      fn_w = tmp.fname("working.toml")
221  
222      with SavedEnviron():
223          bld = ArtiRpcConnBuilder()
224          os.environ["ARTI_RPC_CONNECT_PATH"] = fn_a
225          assert_builder_aborts(bld)
226  
227          bld.prepend_literal_path(fn_w)
228          assert_builder_connects(bld)
229  
230          os.environ["ARTI_RPC_CONNECT_PATH_OVERRIDE"] = fn_a
231          assert_builder_aborts(bld)
232  
233  
234  @arti_test
235  def connect_nobuilder(context):
236      # Assert that we can use ArtiRpcConn constructor
237      # without a builder.
238      tmp = Tempdir()
239      tmp.write("abort.toml", connpt_abort())
240      tmp.write("working.toml", connpt_working(context))
241  
242      fn_a = tmp.fname("abort.toml")
243      fn_w = tmp.fname("working.toml")
244  
245      with SavedEnviron():
246          os.environ["ARTI_RPC_CONNECT_PATH"] = fn_a
247          try:
248              _ = ArtiRpcConn()
249              assert False  # Shouldn't be reached.
250          except ArtiRpcError as e:
251              assert e.status_code() == ArtiRpcErrorStatus.ALL_CONNECT_ATTEMPTS_FAILED
252              assert 'Encountered an explicit "abort"' in str(e)
253  
254          os.environ["ARTI_RPC_CONNECT_PATH"] = fn_w
255          c = ArtiRpcConn()
256          assert c is not None