Merge lp:~mandel/ubuntuone-dev-tools/proxy-testcase into lp:ubuntuone-dev-tools

Proposed by Manuel de la Peña
Status: Merged
Approved by: dobey
Approved revision: 83
Merged at revision: 52
Proposed branch: lp:~mandel/ubuntuone-dev-tools/proxy-testcase
Merge into: lp:ubuntuone-dev-tools
Diff against target: 1215 lines (+1098/-25)
10 files modified
data/squid.conf.in (+120/-0)
ubuntuone/devtools/services/__init__.py (+53/-0)
ubuntuone/devtools/services/dbus.py (+8/-23)
ubuntuone/devtools/services/squid.py (+245/-0)
ubuntuone/devtools/services/tests/test_dbus.py (+1/-1)
ubuntuone/devtools/services/tests/test_squid.py (+334/-0)
ubuntuone/devtools/testcases/dbus.py (+1/-1)
ubuntuone/devtools/testcases/squid.py (+58/-0)
ubuntuone/devtools/testcases/tests/__init__.py (+16/-0)
ubuntuone/devtools/testcases/tests/test_squid_testcase.py (+262/-0)
To merge this branch: bzr merge lp:~mandel/ubuntuone-dev-tools/proxy-testcase
Reviewer Review Type Date Requested Status
dobey (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+85019@code.launchpad.net

Commit message

- A SquidTestCase that does the following:
    - Creates a new password file.
    - Creates a squid.conf
    - Launches squid for tests listening to two random ports, one with auth and other without.
- Tests for all the above.
- Cleans a little the dbus testcase.

Description of the change

This branch adds the following:

- A SquidTestCase that does the following:
    - Creates a new password file.
    - Creates a squid.conf
    - Launches squid for tests listening to two random ports, one with auth and other without.
- Tests for all the above.
- Cleans a little the dbus testcase.

To post a comment you must log in.
Revision history for this message
dobey (dobey) wrote :

856 +"""Base swuid tests cases and test utilities."""

swuid -> squid

861 +# DBusRunner for DBusTestCase using tests
862 +from ubuntuone.devtools.services.squid import (

I don't think it's importing DBus bits here. :)

151 +def find_config_file(in_config_file, tempdir=None):

find_config_file doesn't actually even use tempdir, so you can remove the argument, and change the calls to it, to not pass it in.

review: Needs Fixing
Revision history for this message
dobey (dobey) wrote :

There's a conflict now with trunk. Please merge trunk and fix the conflict, as well as changing the remaining usage of xdg.BaseDirectory in your branch to use dirspec.basedir instead. The dirspec code is packaged in the nightlies PPA now, and is cross-platform and based on the code in ubuntu_sso and xdg.

Also, please fix the rest of the previously mentioned issues.

review: Needs Fixing
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Lovely and well tested branch!

A few small comments:

The first line of this should not be needed:
541 + template = None
542 + with open(path) as in_file:
543 + template = string.Template(in_file.read())

Regarding this:
   return dict(address='localhost'...
Can the key "address" be changed to "host"? I think it's a better name, and it matches other parts.
(PS: I know I might have suggested that name in the first place, sorry about that, but I think it's easier to change now than later)

review: Needs Fixing
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Great branch!
Thanks for working on this.

review: Approve
Revision history for this message
dobey (dobey) wrote :

So, it seems like we can combine the 2 squid config files. I copied the squid3.conf.in over top of squid2.conf.in, and the tests still pass for me, on Oneiric at least. The diff shows a small set of changes, most of which the older squid possibly just ignores.

>Starting squid version 2...
>squid: ERROR: Could not send signal 0 to process 32571: (3) No such process
>Waiting for squid to start..

This error happens with both versions of squid, both on Oneiric and Precise for me. Do we know why this is happening, and how we can get rid of it?

On Precise, I also get the following:

WARNING: cache_mem is larger than total disk cache space!

WARNING: (B) '::/0' is a subnetwork of (A) '::/0'
WARNING: because of this '::/0' is ignored to keep splay tree searching predictable
WARNING: You should probably remove '::/0' from the ACL named 'all'

Is there any way to get rid of the first warning, via the config? Perhaps setting a value of cache_mem to be quite small?
For the 'all' ACL issue, can we redirect the STDOUT/STDERR output from squid, to a log file (or two) in the tempdir?

review: Needs Fixing
Revision history for this message
Manuel de la Peña (mandel) wrote :

> So, it seems like we can combine the 2 squid config files. I copied the
> squid3.conf.in over top of squid2.conf.in, and the tests still pass for me, on
> Oneiric at least. The diff shows a small set of changes, most of which the
> older squid possibly just ignores.
>
> >Starting squid version 2...
> >squid: ERROR: Could not send signal 0 to process 32571: (3) No such process
> >Waiting for squid to start..

That occurs because I'm trying to talk with squid to ensure that is running before we start the the test case, mainly to ensure that there are no race condition. I can forward the stderr to a log file so that we see if happens but does not leave the terminal buffer full of junk.

>
> This error happens with both versions of squid, both on Oneiric and Precise
> for me. Do we know why this is happening, and how we can get rid of it?
>
> On Precise, I also get the following:
>
> WARNING: cache_mem is larger than total disk cache space!
>
> WARNING: (B) '::/0' is a subnetwork of (A) '::/0'
> WARNING: because of this '::/0' is ignored to keep splay tree searching
> predictable
> WARNING: You should probably remove '::/0' from the ACL named 'all'
>
> Is there any way to get rid of the first warning, via the config? Perhaps
> setting a value of cache_mem to be quite small?
> For the 'all' ACL issue, can we redirect the STDOUT/STDERR output from squid,
> to a log file (or two) in the tempdir?

I'll play around with the settings to see if I can solve the issues.

Revision history for this message
Manuel de la Peña (mandel) wrote :

Config and code have been updated to remove the errors. It was quite simple.

Revision history for this message
dobey (dobey) wrote :

With the fix to redirect the warnings and errors, we can keep the "acl all src all" in squid3.conf.in, and use it for both versions, it seems. I added it back, and copied squid3.conf.in to squid2.conf.in, and the tests work in both Precise and Oneiric for me, though squid3 does seem to take a bit long to start for some reason, even without that line in the config. Can we add this config back, and just have a single squid.conf.in for both versions?

Also, can you change the service code to look for squid3 first, rather than squid? It's better to try the new first, then fall back to the old version, I think.

Revision history for this message
Manuel de la Peña (mandel) wrote :

> With the fix to redirect the warnings and errors, we can keep the "acl all src
> all" in squid3.conf.in, and use it for both versions, it seems. I added it
> back, and copied squid3.conf.in to squid2.conf.in, and the tests work in both
> Precise and Oneiric for me, though squid3 does seem to take a bit long to
> start for some reason, even without that line in the config. Can we add this
> config back, and just have a single squid.conf.in for both versions?
>
Done

> Also, can you change the service code to look for squid3 first, rather than
> squid? It's better to try the new first, then fall back to the old version, I
> think.
Done

Revision history for this message
dobey (dobey) wrote :

Looks good now, though the comment is wrong for the block of code that finds the squid executable. :)

review: Approve
Revision history for this message
dobey (dobey) wrote :

Manuel, please add the shortened e-mail address used in your recent commits, to your LP account, so that LP knows who you are. :)

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (12.9 KiB)

The attempt to merge lp:~mandel/ubuntuone-dev-tools/proxy-testcase into lp:ubuntuone-dev-tools failed. Below is the output from the failed tests.

ubuntuone.devtools.testcases
  BaseTestCase
    runTest ... [OK]
ubuntuone.devtools.services.tests.test_squid
  EnvironTestCase
    test_delete_proxy_settings_present ... [OK]
    test_retrieve_proxy_settings ... [OK]
    test_store_settings ... [OK]
  PathsTestCase
    test_get_auth_temp_path ... [OK]
    test_get_basedir_missing ... [OK]
    test_get_basedir_present ... [OK]
    test_get_spool_temp_path_missing ... [OK]
    test_get_spool_temp_path_present ... [OK]
    test_get_squid_temp_path_missing ... [OK]
    test_get_squid_temp_path_present ... [OK]
  SquidRunnerInitTestCase
    test_htpasswd_missing ... [OK]
    test_squid_missing ... [OK]
  SquidRunnerTestCase
    test_generate_auth_file ... [OK]
    test_generate_config_file ... [OK]
    test_generate_swap ... [OK]
ubuntuone.devtools.testcases.dbus
  DBusTestCase
    runTest ... [OK]
ubuntuone.devtools.services.tests.test_dbus
  TestWithDBus
    test_config_file_path ... [OK]
    test_dbus_session_is_running ... [OK]
ubuntuone.devtools.tests.test_txcheck
  TestCheckTwistedTestClass
    test_bad_mixin_order ... [OK]
    test_bare_super ... [OK]
    test_inline_callbacks ... [OK]
    test_inline_callbacks_missing ... [OK]
    test_missing_return ... [OK]
    test_no_problems ... [OK]
    test_not_twisted ... [OK]
    test_ok_mixin_order ... [OK]
    test_super_not_called ... [OK]
  TestTwistedCheckSuite
    test_suite_catches_problems ... [OK]
    test_suite_runs_tests ... [OK]
twisted.trial.unittest
  TestCase
    runTest ... [OK]
ubuntuone.devtools.testcases
  BaseTestCase
    runTest ... [OK]
ubuntuone.devtools.tests.test_decorators
  Tes...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'data/squid.conf.in'
2--- data/squid.conf.in 1970-01-01 00:00:00 +0000
3+++ data/squid.conf.in 2012-01-05 16:48:29 +0000
4@@ -0,0 +1,120 @@
5+auth_param basic casesensitive on
6+# Use a default auth using ncsa and the passed generated file.
7+auth_param basic program ${auth_process} ${auth_file}
8+#Recommended minimum configuration:
9+acl manager proto cache_object
10+acl localhost src 127.0.0.1/32
11+acl to_localhost dst 127.0.0.0/32
12+#
13+# Example rule allowing access from your local networks.
14+# Adapt to list your (internal) IP networks from where browsing
15+# should be allowed
16+acl all src all
17+acl localnet src 10.0.0.0/8 # RFC1918 possible internal network
18+acl localnet src 172.16.0.0/12 # RFC1918 possible internal network
19+acl localnet src 192.168.0.0/16 # RFC1918 possible internal network
20+#
21+acl SSL_ports port 443 # https
22+acl SSL_ports port 563 # snews
23+acl SSL_ports port 873 # rsync
24+acl Safe_ports port 80 # http
25+acl Safe_ports port 21 # ftp
26+acl Safe_ports port 443 # https
27+acl Safe_ports port 70 # gopher
28+acl Safe_ports port 210 # wais
29+acl Safe_ports port 1025-65535 # unregistered ports
30+acl Safe_ports port 280 # http-mgmt
31+acl Safe_ports port 488 # gss-http
32+acl Safe_ports port 591 # filemaker
33+acl Safe_ports port 777 # multiling http
34+acl Safe_ports port 631 # cups
35+acl Safe_ports port 873 # rsync
36+acl Safe_ports port 901 # SWAT
37+acl purge method PURGE
38+acl CONNECT method CONNECT
39+
40+# make an acl for users that have auth
41+acl password proxy_auth REQUIRED myportname ${auth_port_number}
42+acl auth_port_connected myportname ${auth_port_number}
43+acl nonauth_port_connected myportname ${noauth_port_number}
44+
45+# Settings used for the tests:
46+# Allow users connected to the nonauth port
47+# Allow users authenticated AND connected to the auth port
48+http_access allow nonauth_port_connected
49+http_access allow password
50+
51+#Recommended minimum configuration:
52+#
53+# Only allow cachemgr access from localhost
54+http_access allow manager localhost
55+http_access deny manager
56+# Only allow purge requests from localhost
57+http_access allow purge localhost
58+http_access deny purge
59+# Deny requests to unknown ports
60+http_access deny !Safe_ports
61+# Deny CONNECT to other than SSL ports
62+http_access deny CONNECT !SSL_ports
63+# Example rule allowing access from your local networks.
64+# Adapt localnet in the ACL section to list your (internal) IP networks
65+# from where browsing should be allowed
66+#http_access allow localnet
67+http_access allow localhost
68+
69+# And finally deny all other access to this proxy
70+http_access deny all
71+
72+icp_access allow localnet
73+icp_access deny all
74+
75+# Squid normally listens to port 3128 but we are going to listento two
76+# different ports, one for auth one for nonauth.
77+http_port ${noauth_port_number}
78+http_port ${auth_port_number}
79+
80+#We recommend you to use at least the following line.
81+hierarchy_stoplist cgi-bin ?
82+
83+# Default cache settings.
84+cache_dir ufs ${spool_temp} 1000 16 256
85+
86+# access log settings
87+access_log ${squid_temp}/access.log squid
88+
89+# cache log settings
90+cache_log ${squid_temp}/cache.log
91+
92+# cache store log settings
93+cache_store_log ${squid_temp}/store.log
94+
95+# mime table conf
96+# mime_table /usr/share/squid/mime.conf
97+
98+#Default pid file name
99+pid_filename ${squid_temp}/squid.pid
100+
101+# debug options (Full debugging)
102+debug_options ALL,1
103+
104+#Default netdb_filename
105+
106+#Suggested default:
107+refresh_pattern ^ftp: 1440 20% 10080
108+refresh_pattern ^gopher: 1440 0% 1440
109+refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
110+refresh_pattern (Release|Packages(.gz)*)$ 0 20% 2880
111+# example line deb packages
112+refresh_pattern . 0 20% 4320
113+
114+# Don't upgrade ShoutCast responses to HTTP
115+acl shoutcast rep_header X-HTTP09-First-Line ^ICY.[0-9]
116+
117+# Apache mod_gzip and mod_deflate known to be broken so don't trust
118+# Apache to signal ETag correctly on such responses
119+acl apache rep_header Server ^Apache
120+
121+hosts_file /etc/hosts
122+
123+# Leave coredumps in the first cache dir
124+coredump_dir ${spool_temp}/squid
125
126=== modified file 'ubuntuone/devtools/services/__init__.py'
127--- ubuntuone/devtools/services/__init__.py 2010-11-03 21:04:06 +0000
128+++ ubuntuone/devtools/services/__init__.py 2012-01-05 16:48:29 +0000
129@@ -1,1 +1,54 @@
130+#
131+# Copyright 2011 Canonical Ltd.
132+#
133+# This program is free software: you can redistribute it and/or modify it
134+# under the terms of the GNU General Public License version 3, as published
135+# by the Free Software Foundation.
136+#
137+# This program is distributed in the hope that it will be useful, but
138+# WITHOUT ANY WARRANTY; without even the implied warranties of
139+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
140+# PURPOSE. See the GNU General Public License for more details.
141+#
142+# You should have received a copy of the GNU General Public License along
143+# with this program. If not, see <http://www.gnu.org/licenses/>.
144 """Service runners for testing."""
145+
146+import os
147+import socket
148+
149+from dirspec.basedir import load_data_paths
150+
151+
152+def find_config_file(in_config_file):
153+ """Find the first appropriate conf to use."""
154+ # In case we're running from within the source tree
155+ path = os.path.abspath(os.path.join(os.path.dirname(__file__),
156+ os.path.pardir, os.path.pardir,
157+ os.path.pardir,
158+ "data", in_config_file))
159+ if not os.path.exists(path):
160+ # Use the installed file in $pkgdatadir as source
161+ for path in load_data_paths("ubuntuone-dev-tools",
162+ in_config_file):
163+ if os.path.exists(path):
164+ break
165+
166+ # Check to make sure we didn't just fall out of the loop
167+ if not os.path.exists(path):
168+ raise IOError('Could not locate suitable %s' % in_config_file)
169+ return path
170+
171+
172+def get_arbitrary_port():
173+ """
174+ Find an unused port, and return it.
175+
176+ There might be a small race condition here, but we aren't
177+ worried about it.
178+ """
179+ sock = socket.socket()
180+ sock.bind(('localhost', 0))
181+ _, port = sock.getsockname()
182+ sock.close()
183+ return port
184
185=== modified file 'ubuntuone/devtools/services/dbus.py'
186--- ubuntuone/devtools/services/dbus.py 2011-12-19 22:37:31 +0000
187+++ ubuntuone/devtools/services/dbus.py 2012-01-05 16:48:29 +0000
188@@ -20,10 +20,12 @@
189 import signal
190 import subprocess
191
192-from dirspec.basedir import load_data_paths
193 from distutils.spawn import find_executable
194 from urllib import quote
195
196+from ubuntuone.devtools.services import find_config_file
197+DBUS_CONFIG_FILE = 'dbus-session.conf.in'
198+
199
200 class DBusLaunchError(Exception):
201 """Error while launching dbus-daemon"""
202@@ -44,42 +46,25 @@
203 self.running = False
204 self.config_file = None
205
206- def _find_config_file(self, tempdir=None):
207+ def _generate_config_file(self, tempdir=None):
208 """Find the first appropriate dbus-session.conf to use."""
209- # In case we're running from within the source tree
210- path = os.path.abspath(os.path.join(os.path.dirname(__file__),
211- os.path.pardir, os.path.pardir,
212- os.path.pardir,
213- "data", "dbus-session.conf.in"))
214- if not os.path.exists(path):
215- # Use the installed file in $pkgdatadir as source
216- for path in load_data_paths("ubuntuone-dev-tools",
217- "dbus-session.conf.in"):
218- if os.path.exists(path):
219- break
220-
221- # Check to make sure we didn't just fall out of the loop
222- if not os.path.exists(path):
223- raise IOError('Could not locate suitable dbus-session.conf.in')
224-
225+ # load the config file
226+ path = find_config_file(DBUS_CONFIG_FILE)
227+ # replace config settings
228 self.config_file = os.path.join(tempdir, 'dbus-session.conf')
229 dbus_address = 'unix:tmpdir=%s' % quote(tempdir)
230 with open(path) as in_file:
231 content = in_file.read()
232 with open(self.config_file, 'w') as out_file:
233 out_file.write(content.replace('@ADDRESS@', dbus_address))
234- out_file.close()
235- in_file.close()
236
237 def start_service(self, tempdir=None):
238 """Start our own session bus daemon for testing."""
239- if not tempdir:
240- tempdir = os.path.join(os.getcwd(), '_trial_temp')
241 dbus = find_executable("dbus-daemon")
242 if not dbus:
243 raise NotFoundError("dbus-daemon was not found.")
244
245- self._find_config_file(tempdir)
246+ self._generate_config_file(tempdir)
247
248 dbus_args = ["--fork",
249 "--config-file=" + self.config_file,
250
251=== added file 'ubuntuone/devtools/services/squid.py'
252--- ubuntuone/devtools/services/squid.py 1970-01-01 00:00:00 +0000
253+++ ubuntuone/devtools/services/squid.py 2012-01-05 16:48:29 +0000
254@@ -0,0 +1,245 @@
255+#
256+# Copyright 2011 Canonical Ltd.
257+#
258+# This program is free software: you can redistribute it and/or modify it
259+# under the terms of the GNU General Public License version 3, as published
260+# by the Free Software Foundation.
261+#
262+# This program is distributed in the hope that it will be useful, but
263+# WITHOUT ANY WARRANTY; without even the implied warranties of
264+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
265+# PURPOSE. See the GNU General Public License for more details.
266+#
267+# You should have received a copy of the GNU General Public License along
268+# with this program. If not, see <http://www.gnu.org/licenses/>.
269+"""Utilities for finding and running a squid proxy for testing."""
270+
271+import random
272+import signal
273+# pylint:disable=W0402
274+import string
275+# pylint:enable=W0402
276+import subprocess
277+import time
278+
279+from json import dumps, loads
280+from os import environ, makedirs, kill, unlink
281+from os.path import abspath, exists, join
282+
283+from distutils.spawn import find_executable
284+
285+from ubuntuone.devtools.services import (
286+ find_config_file,
287+ get_arbitrary_port,
288+)
289+
290+SQUID_CONFIG_FILE = 'squid.conf.in'
291+SQUID_DIR = 'squid'
292+AUTH_PROCESS_PATH = '/usr/lib/%s/ncsa_auth'
293+SPOOL_DIR = 'spool'
294+AUTH_FILE = 'htpasswd'
295+PROXY_ENV_VAR = 'SQUID_PROXY_SETTINGS'
296+
297+
298+def get_squid_executable():
299+ """Return the squid executable of the system."""
300+ # try with squid and if not present try with squid3 for newer systems
301+ # (Ubuntu P). We also return the path to the auth process so that we can
302+ # point to the correct one.
303+ squid = find_executable('squid3')
304+ auth_process = AUTH_PROCESS_PATH % 'squid3'
305+ if squid is None:
306+ squid = find_executable('squid')
307+ auth_process = AUTH_PROCESS_PATH % 'squid'
308+ return squid, auth_process
309+
310+
311+def get_htpasswd_executable():
312+ """Return the htpasswd executable."""
313+ return find_executable('htpasswd')
314+
315+
316+def _make_random_string(count):
317+ """Make a random string of the given length."""
318+ entropy = random.SystemRandom()
319+ return ''.join([entropy.choice(string.letters) for _ in
320+ range(count)])
321+
322+
323+def _get_basedir(tempdir):
324+ """Return the base squid config."""
325+ basedir = join(tempdir, SQUID_DIR)
326+ basedir = abspath(basedir)
327+ if not exists(basedir):
328+ makedirs(basedir)
329+ return basedir
330+
331+
332+def _get_spool_temp_path(tempdir=''):
333+ """Return the temp dir to be used for spool."""
334+ basedir = _get_basedir(tempdir)
335+ path = join(basedir, SPOOL_DIR)
336+ path = abspath(path)
337+ if not exists(path):
338+ makedirs(path)
339+ return path
340+
341+
342+def _get_squid_temp_path(tempdir=''):
343+ """Return the temp dir to be used by squid."""
344+ basedir = _get_basedir(tempdir)
345+ path = join(basedir, SQUID_DIR)
346+ path = abspath(path)
347+ if not exists(path):
348+ makedirs(path)
349+ return path
350+
351+
352+def _get_auth_temp_path(tempdir=''):
353+ """Return the path for the auth file."""
354+ basedir = _get_basedir(tempdir)
355+ auth_file = join(basedir, AUTH_FILE)
356+ if not exists(basedir):
357+ makedirs(basedir)
358+ return auth_file
359+
360+
361+def store_proxy_settings(settings):
362+ """Store the proxy setting in an env var."""
363+ environ[PROXY_ENV_VAR] = dumps(settings)
364+
365+
366+def retrieve_proxy_settings():
367+ """Return the proxy settings of the env."""
368+ if PROXY_ENV_VAR in environ:
369+ return loads(environ[PROXY_ENV_VAR])
370+ return None
371+
372+
373+def delete_proxy_settings():
374+ """Delete the proxy env settings."""
375+ if PROXY_ENV_VAR in environ:
376+ del environ[PROXY_ENV_VAR]
377+
378+
379+class SquidLaunchError(Exception):
380+ """Error while launching squid."""
381+
382+
383+class SquidRunner(object):
384+ """Class for running a squid proxy with the local config."""
385+
386+ def __init__(self):
387+ """Create a new instance."""
388+ self.squid, self.auth_process = get_squid_executable()
389+ if self.squid is None:
390+ raise SquidLaunchError('Could not locate "squid".')
391+
392+ self.htpasswd = get_htpasswd_executable()
393+ if self.htpasswd is None:
394+ raise SquidLaunchError('Could not locate "htpasswd".')
395+
396+ self.settings = dict(noauth_port=None, auth_port=None,
397+ username=None, password=None)
398+ self.squid_pid = None
399+ self.running = False
400+ self.config_file = None
401+ self.auth_file = None
402+
403+ def _generate_config_file(self, tempdir=''):
404+ """Find the first appropiate squid.conf to use."""
405+ # load the config file
406+ path = find_config_file(SQUID_CONFIG_FILE)
407+ # replace config settings
408+ basedir = join(tempdir, 'squid')
409+ basedir = abspath(basedir)
410+ if not exists(basedir):
411+ makedirs(basedir)
412+ self.config_file = join(basedir, 'squid.conf')
413+ with open(path) as in_file:
414+ template = string.Template(in_file.read())
415+
416+ self.settings['noauth_port'] = get_arbitrary_port()
417+ self.settings['auth_port'] = get_arbitrary_port()
418+ spool_path = _get_spool_temp_path(tempdir)
419+ squid_path = _get_squid_temp_path(tempdir)
420+ with open(self.config_file, 'w') as out_file:
421+ out_file.write(template.safe_substitute(
422+ auth_file=self.auth_file,
423+ auth_process=self.auth_process,
424+ noauth_port_number=self.settings['noauth_port'],
425+ auth_port_number=self.settings['auth_port'],
426+ spool_temp=spool_path,
427+ squid_temp=squid_path))
428+
429+ def _generate_swap(self, config_file):
430+ """Generate the squid swap files."""
431+ squid_args = ['-z', '-f', config_file]
432+ sp = subprocess.Popen([self.squid] + squid_args,
433+ stdout=subprocess.PIPE,
434+ stderr=subprocess.PIPE)
435+ sp.wait()
436+
437+ def _generate_auth_file(self, tempdir=''):
438+ """Generates a auth file using htpasswd."""
439+ if self.settings['username'] is None:
440+ self.settings['username'] = _make_random_string(10)
441+ if self.settings['password'] is None:
442+ self.settings['password'] = _make_random_string(10)
443+
444+ self.auth_file = _get_auth_temp_path(tempdir)
445+ # remove possible old auth file
446+ if exists(self.auth_file):
447+ unlink(self.auth_file)
448+ # create a new htpasswrd
449+ htpasswd_args = ['-bc',
450+ self.auth_file,
451+ self.settings['username'],
452+ self.settings['password']]
453+ sp = subprocess.Popen([self.htpasswd] + htpasswd_args,
454+ stdout=subprocess.PIPE,
455+ stderr=subprocess.PIPE)
456+ sp.wait()
457+
458+ def _is_squid_running(self):
459+ """Return if squid is running."""
460+ squid_args = ['-k', 'check', '-f', self.config_file]
461+ print 'Starting squid version...'
462+ message = 'Waiting for squid to start...'
463+ for timeout in (0.4, 0.1, 0.1, 0.2, 0.5, 1, 3, 5):
464+ try:
465+ # Do not use stdout=PIPE or stderr=PIPE with this function.
466+ subprocess.check_call([self.squid] + squid_args,
467+ stdout=subprocess.PIPE,
468+ stderr=subprocess.PIPE)
469+ return True
470+ except subprocess.CalledProcessError:
471+ message += '.'
472+ print message
473+ time.sleep(timeout)
474+ return False
475+
476+ def start_service(self, tempdir=None):
477+ """Start our own proxy."""
478+ # generate auth, config and swap dirs
479+ self._generate_auth_file(tempdir)
480+ self._generate_config_file(tempdir)
481+ self._generate_swap(self.config_file)
482+ squid_args = ['-N', '-X', '-f', self.config_file]
483+ sp = subprocess.Popen([self.squid] + squid_args,
484+ stdout=subprocess.PIPE,
485+ stderr=subprocess.PIPE)
486+ store_proxy_settings(self.settings)
487+ if not self._is_squid_running():
488+ raise SquidLaunchError('Could not start squid.')
489+ self.squid_pid = sp.pid
490+ self.running = True
491+
492+ def stop_service(self):
493+ """Stop our proxy,"""
494+ kill(self.squid_pid, signal.SIGKILL)
495+ delete_proxy_settings()
496+ self.running = False
497+ unlink(self.config_file)
498+ unlink(self.auth_file)
499+ self.config_file = None
500
501=== modified file 'ubuntuone/devtools/services/tests/test_dbus.py'
502--- ubuntuone/devtools/services/tests/test_dbus.py 2011-11-23 11:31:08 +0000
503+++ ubuntuone/devtools/services/tests/test_dbus.py 2012-01-05 16:48:29 +0000
504@@ -25,6 +25,6 @@
505 runner = DBusRunner()
506 # pylint: disable=W0212
507 os.makedirs(self.tmpdir)
508- runner._find_config_file(tempdir=self.tmpdir)
509+ runner._generate_config_file(tempdir=self.tmpdir)
510 shutil.rmtree(self.tmpdir)
511 self.assertEqual(expected, runner.config_file)
512
513=== added file 'ubuntuone/devtools/services/tests/test_squid.py'
514--- ubuntuone/devtools/services/tests/test_squid.py 1970-01-01 00:00:00 +0000
515+++ ubuntuone/devtools/services/tests/test_squid.py 2012-01-05 16:48:29 +0000
516@@ -0,0 +1,334 @@
517+#
518+# Copyright 2011 Canonical Ltd.
519+#
520+# This program is free software: you can redistribute it and/or modify it
521+# under the terms of the GNU General Public License version 3, as published
522+# by the Free Software Foundation.
523+#
524+# This program is distributed in the hope that it will be useful, but
525+# WITHOUT ANY WARRANTY; without even the implied warranties of
526+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
527+# PURPOSE. See the GNU General Public License for more details.
528+#
529+# You should have received a copy of the GNU General Public License along
530+# with this program. If not, see <http://www.gnu.org/licenses/>.
531+"""Test the squid service."""
532+
533+import json
534+import os
535+
536+from twisted.internet import defer
537+
538+from ubuntuone.devtools.testcases import BaseTestCase
539+from ubuntuone.devtools.services import squid
540+
541+# yes, we know names with _ are private
542+#pylint: disable=W0212
543+
544+
545+class PathsTestCase(BaseTestCase):
546+ """Test the different path functions."""
547+
548+ @defer.inlineCallbacks
549+ def setUp(self):
550+ """Set the different tests."""
551+ yield super(PathsTestCase, self).setUp()
552+ self.basedir_fn = squid._get_basedir
553+ self.basedir = self.mktemp('paths')
554+ self.path_exists = False
555+ self.created_paths = []
556+ self.called = []
557+
558+ def fake_basedir_fn(tempdir):
559+ """Retun the base dir."""
560+ self.called.append(('fake_basedir_fn', tempdir))
561+ return self.basedir
562+
563+ def fake_makedirs(path):
564+ """Fake the makedirs function."""
565+ self.called.append(('fake_makedirs', path))
566+ self.created_paths.append(path)
567+
568+ def fake_exists(path):
569+ """Fake the exists method."""
570+ self.called.append(('fake_exists', path))
571+ return path in self.created_paths or self.path_exists
572+
573+ self.patch(squid, '_get_basedir', fake_basedir_fn)
574+ self.patch(squid, 'makedirs', fake_makedirs)
575+ self.patch(squid, 'exists', fake_exists)
576+
577+ def test_get_basedir_missing(self):
578+ """Test the base dir creation."""
579+ basedir = self.basedir_fn(self.basedir)
580+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
581+ self.assertEqual(expected_path, basedir)
582+ self.assertTrue(('fake_makedirs', expected_path) in self.called)
583+ self.assertTrue(expected_path in self.created_paths)
584+ self.assertTrue(('fake_exists', expected_path) in self.called)
585+
586+ def test_get_basedir_present(self):
587+ """Test the base dir creation."""
588+ self.path_exists = True
589+ basedir = self.basedir_fn(self.basedir)
590+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
591+ self.assertEqual(expected_path, basedir)
592+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
593+ self.assertTrue(('fake_makedirs', expected_path) not in self.called)
594+ self.assertTrue(expected_path not in self.created_paths)
595+ self.assertTrue(('fake_exists', expected_path) in self.called)
596+
597+ def test_get_spool_temp_path_missing(self):
598+ """Test the spool path creation."""
599+ expected_path = os.path.join(self.basedir, squid.SPOOL_DIR)
600+ result = squid._get_spool_temp_path()
601+ self.assertEqual(expected_path, result)
602+ self.assertTrue(('fake_basedir_fn', '') in self.called)
603+ self.assertTrue(('fake_makedirs', expected_path) in self.called)
604+ self.assertTrue(expected_path in self.created_paths)
605+ self.assertTrue(('fake_exists', expected_path) in self.called)
606+
607+ def test_get_spool_temp_path_present(self):
608+ """Test the spool path creation."""
609+ self.path_exists = True
610+ expected_path = os.path.join(self.basedir, squid.SPOOL_DIR)
611+ result = squid._get_spool_temp_path()
612+ self.assertEqual(expected_path, result)
613+ self.assertTrue(('fake_basedir_fn', '') in self.called)
614+ self.assertTrue(('fake_makedirs', expected_path) not in self.called)
615+ self.assertTrue(expected_path not in self.created_paths)
616+ self.assertTrue(('fake_exists', expected_path) in self.called)
617+
618+ def test_get_squid_temp_path_missing(self):
619+ """Test the squid path creation."""
620+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
621+ result = squid._get_squid_temp_path()
622+ self.assertEqual(expected_path, result)
623+ self.assertTrue(('fake_basedir_fn', '') in self.called)
624+ self.assertTrue(('fake_makedirs', expected_path) in self.called)
625+ self.assertTrue(expected_path in self.created_paths)
626+ self.assertTrue(('fake_exists', expected_path) in self.called)
627+
628+ def test_get_squid_temp_path_present(self):
629+ """Test the squid path creation."""
630+ self.path_exists = True
631+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
632+ result = squid._get_squid_temp_path()
633+ self.assertEqual(expected_path, result)
634+ self.assertTrue(('fake_basedir_fn', '') in self.called)
635+ self.assertTrue(('fake_makedirs', expected_path) not in self.called)
636+ self.assertTrue(expected_path not in self.created_paths)
637+ self.assertTrue(('fake_exists', expected_path) in self.called)
638+
639+ def test_get_auth_temp_path(self):
640+ """Test the creation of the auth path."""
641+ self.path_exists = False
642+ expected_path = os.path.join(self.basedir, squid.AUTH_FILE)
643+ result = squid._get_auth_temp_path()
644+ self.assertEqual(expected_path, result)
645+ self.assertTrue(('fake_basedir_fn', '') in self.called)
646+ self.assertTrue(('fake_makedirs', self.basedir) in self.called)
647+ self.assertTrue(self.basedir in self.created_paths)
648+ self.assertTrue(('fake_exists', self.basedir) in self.called)
649+
650+
651+class EnvironTestCase(BaseTestCase):
652+ """Test the different environ functions."""
653+
654+ @defer.inlineCallbacks
655+ def setUp(self):
656+ """Set the tests."""
657+ yield super(EnvironTestCase, self).setUp()
658+ self.called = []
659+ self.settings = dict(noauth_port=3434, auth_port=232323,
660+ username='u1', password='test')
661+
662+ def fake_dumps(data):
663+ """Fake dumps."""
664+ self.called.append(('dumps', data))
665+ return json.dumps(data)
666+
667+ def fake_loads(data):
668+ """Fake loads."""
669+ self.called.append(('loads', data))
670+ return json.loads(data)
671+
672+ self.patch(squid, 'dumps', fake_dumps)
673+ self.patch(squid, 'loads', fake_loads)
674+ self.env = {}
675+ self.old_env = os.environ
676+ squid.environ = self.env
677+ self.addCleanup(self.set_back_environ)
678+
679+ def set_back_environ(self):
680+ """Set back the env."""
681+ squid.environ = self.old_env
682+
683+ def test_store_settings(self):
684+ """Test the storage of the settings."""
685+ squid.store_proxy_settings(self.settings)
686+ self.assertTrue(('dumps', self.settings) in self.called)
687+ self.assertEqual(self.env[squid.PROXY_ENV_VAR],
688+ json.dumps(self.settings))
689+
690+ def test_retrieve_proxy_settings(self):
691+ """Test reading the settings."""
692+ self.env[squid.PROXY_ENV_VAR] = json.dumps(self.settings)
693+ self.assertTrue(('loads', self.env[squid.PROXY_ENV_VAR]))
694+ self.assertEqual(squid.retrieve_proxy_settings(), self.settings)
695+
696+ def test_delete_proxy_settings_present(self):
697+ """Delete the proxy settings."""
698+ self.env[squid.PROXY_ENV_VAR] = json.dumps(self.settings)
699+ squid.delete_proxy_settings()
700+ self.assertFalse(squid.PROXY_ENV_VAR in self.env)
701+
702+
703+class SquidRunnerInitTestCase(BaseTestCase):
704+ """Test the creation of the runner."""
705+
706+ @defer.inlineCallbacks
707+ def setUp(self):
708+ """Set the different tests."""
709+ yield super(SquidRunnerInitTestCase, self).setUp()
710+ self.executables = {}
711+ self.called = []
712+
713+ def fake_find_executable(executable):
714+ """Fake the find executable."""
715+ self.called.append(('fake_find_executable', executable))
716+ return self.executables.get(executable, None)
717+
718+ self.patch(squid, 'find_executable', fake_find_executable)
719+
720+ def _assert_missing_binary(self, binary):
721+ """Perform the assertion when a bin is missing."""
722+ self.assertRaises(squid.SquidLaunchError, squid.SquidRunner)
723+ self.assertTrue(('fake_find_executable', binary) in self.called,
724+ self.called)
725+
726+ def test_squid_missing(self):
727+ """Test when squid is missing."""
728+ self.executables['htpasswd'] = 'htpasswd'
729+ self._assert_missing_binary('squid')
730+
731+ def test_htpasswd_missing(self):
732+ """Test when htpasswd is missing."""
733+ self.executables['squid'] = 'squid'
734+ self.executables['squid3'] = 'squid'
735+ self._assert_missing_binary('htpasswd')
736+
737+
738+class FakeSubprocess(object):
739+ """Fake the subprocess module."""
740+
741+ # pylint: disable=C0103
742+ def __init__(self):
743+ """Create a new instance."""
744+ self.called = []
745+ self.PIPE = 'PIPE'
746+
747+ def Popen(self, args, stdout=None, stderr=None):
748+ """Fake Popen."""
749+ self.called.append(('Popen', args, stdout, stderr))
750+ return self
751+ # pylint: enable=C0103
752+
753+ def wait(self):
754+ """Fake wait from a Popen object."""
755+ self.called.append(('wait',))
756+
757+
758+class FakeTemplate(object):
759+ """Fake the string.Template."""
760+
761+ def __init__(self):
762+ """Create a new instance."""
763+ self.data = None
764+ self.called = []
765+
766+ def __call__(self, data):
767+ """Fake constructor."""
768+ self.data = data
769+ return self
770+
771+ def safe_substitute(self, *args, **kwargs):
772+ """Fake the safe_substitute."""
773+ self.called.append(('safe_substitute', args, kwargs))
774+ return self.data
775+
776+
777+class SquidRunnerTestCase(BaseTestCase):
778+ """Test the default test case."""
779+
780+ @defer.inlineCallbacks
781+ def setUp(self):
782+ """Set the different tests."""
783+ yield super(SquidRunnerTestCase, self).setUp()
784+ self.subprocess = FakeSubprocess()
785+ self.patch(squid, 'subprocess', self.subprocess)
786+
787+ self.called = []
788+ self.executables = dict(squid='squid', htpasswd='htpasswd')
789+
790+ def fake_find_executable(executable):
791+ """Fake the find executable."""
792+ self.called.append(('fake_find_executable', executable))
793+ return self.executables.get(executable, None)
794+
795+ self.patch(squid, 'find_executable', fake_find_executable)
796+
797+ self.auth_temp = 'path/to/auth'
798+
799+ def fake_get_auth_temp_path(tempdir):
800+ """Return the path for the auth file."""
801+ self.called.append(('fake_get_auth_temp_path', tempdir))
802+ return self.auth_temp
803+
804+ self.patch(squid, '_get_auth_temp_path', fake_get_auth_temp_path)
805+
806+ self.port = 2324
807+
808+ def fake_get_port():
809+ """Fake the methos that returns the ports."""
810+ self.called.append(('fake_get_port',))
811+ return self.port
812+
813+ self.patch(squid, 'get_arbitrary_port', fake_get_port)
814+ self.template = FakeTemplate()
815+ self.patch(squid.string, 'Template', self.template)
816+ self.runner = squid.SquidRunner()
817+
818+ def test_generate_swap(self):
819+ """Test the generation of the squid swap."""
820+ config_file = 'path/to/config'
821+ expected_args = ['squid', '-z', '-f', config_file]
822+ self.runner._generate_swap(config_file)
823+ self.assertEqual(expected_args, self.subprocess.called[0][1])
824+ self.assertTrue('wait' in self.subprocess.called[1])
825+
826+ def test_generate_auth_file(self):
827+ """Test the generation of the auth file."""
828+ username = self.runner.settings['username'] = 'mandel'
829+ password = self.runner.settings['password'] = 'test'
830+ expected_args = ['htpasswd', '-bc', self.auth_temp,
831+ username, password]
832+ self.patch(squid, 'exists', lambda f: False)
833+ self.runner._generate_auth_file()
834+ self.assertEqual(expected_args, self.subprocess.called[0][1])
835+ self.assertTrue('wait' in self.subprocess.called[1])
836+
837+ def test_generate_config_file(self):
838+ """Test the generation of the config file."""
839+ self.runner.auth_file = self.auth_temp
840+ self.runner._generate_config_file(self.tmpdir)
841+ # remove the generated file
842+ self.addCleanup(os.unlink, self.runner.config_file)
843+ expected_parameters = ('safe_substitute', (),
844+ dict(auth_file=self.runner.auth_file,
845+ noauth_port_number=self.port,
846+ auth_port_number=self.port,
847+ spool_temp=squid._get_spool_temp_path(self.tmpdir),
848+ squid_temp=squid._get_squid_temp_path(self.tmpdir)))
849+ self.assertTrue(expected_parameters, self.template.called[0])
850+ self.assertEqual(2, self.called.count(('fake_get_port',)))
851
852=== modified file 'ubuntuone/devtools/testcases/dbus.py'
853--- ubuntuone/devtools/testcases/dbus.py 2011-11-23 11:14:00 +0000
854+++ ubuntuone/devtools/testcases/dbus.py 2012-01-05 16:48:29 +0000
855@@ -14,7 +14,7 @@
856 # You should have received a copy of the GNU General Public License along
857 # with this program. If not, see <http://www.gnu.org/licenses/>.
858
859-"""Base tests cases and test utilities."""
860+"""Base dbus tests cases and test utilities."""
861
862 from __future__ import absolute_import, with_statement
863
864
865=== added file 'ubuntuone/devtools/testcases/squid.py'
866--- ubuntuone/devtools/testcases/squid.py 1970-01-01 00:00:00 +0000
867+++ ubuntuone/devtools/testcases/squid.py 2012-01-05 16:48:29 +0000
868@@ -0,0 +1,58 @@
869+# -*- coding: utf-8 -*-
870+#
871+# Copyright 2011 Canonical Ltd.
872+#
873+# This program is free software: you can redistribute it and/or modify it
874+# under the terms of the GNU General Public License version 3, as published
875+# by the Free Software Foundation.
876+#
877+# This program is distributed in the hope that it will be useful, but
878+# WITHOUT ANY WARRANTY; without even the implied warranties of
879+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
880+# PURPOSE. See the GNU General Public License for more details.
881+#
882+# You should have received a copy of the GNU General Public License along
883+# with this program. If not, see <http://www.gnu.org/licenses/>.
884+
885+"""Base squid tests cases and test utilities."""
886+
887+from ubuntuone.devtools.testcase import BaseTestCase, skipIf
888+from ubuntuone.devtools.services.squid import (
889+ SquidRunner,
890+ SquidLaunchError,
891+ get_squid_executable,
892+ get_htpasswd_executable,
893+ retrieve_proxy_settings)
894+
895+# pylint: disable=C0103
896+squid, _ = get_squid_executable()
897+htpasswd = get_htpasswd_executable()
898+# pylint: enable=C0103
899+
900+
901+@skipIf(squid is None or htpasswd is None,
902+ 'The test requires squid and htpasswd.')
903+class SquidTestCase(BaseTestCase):
904+ """Test that uses a proxy."""
905+
906+ def required_services(self):
907+ """Return the list of required services for DBusTestCase."""
908+ services = super(SquidTestCase, self).required_services()
909+ services.extend([SquidRunner])
910+ return services
911+
912+ def get_nonauth_proxy_settings(self):
913+ """Return the settings of the noneauth proxy."""
914+ settings = retrieve_proxy_settings()
915+ if settings is None:
916+ raise SquidLaunchError('Proxy is not running.')
917+ return dict(host='localhost', port=settings['noauth_port'])
918+
919+ def get_auth_proxy_settings(self):
920+ """Return the settings of the auth proxy."""
921+ settings = retrieve_proxy_settings()
922+ if settings is None:
923+ raise SquidLaunchError('Proxy is not running.')
924+ return dict(host='localhost', port=settings['auth_port'],
925+ username=settings['username'],
926+ password=settings['password'])
927
928=== added directory 'ubuntuone/devtools/testcases/tests'
929=== added file 'ubuntuone/devtools/testcases/tests/__init__.py'
930--- ubuntuone/devtools/testcases/tests/__init__.py 1970-01-01 00:00:00 +0000
931+++ ubuntuone/devtools/testcases/tests/__init__.py 2012-01-05 16:48:29 +0000
932@@ -0,0 +1,16 @@
933+# -*- coding: utf-8 -*-
934+#
935+# Copyright 2011 Canonical Ltd.
936+#
937+# This program is free software: you can redistribute it and/or modify it
938+# under the terms of the GNU General Public License version 3, as published
939+# by the Free Software Foundation.
940+#
941+# This program is distributed in the hope that it will be useful, but
942+# WITHOUT ANY WARRANTY; without even the implied warranties of
943+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
944+# PURPOSE. See the GNU General Public License for more details.
945+#
946+# You should have received a copy of the GNU General Public License along
947+# with this program. If not, see <http://www.gnu.org/licenses/>.
948+"""Tests for the test cases."""
949
950=== added file 'ubuntuone/devtools/testcases/tests/test_squid_testcase.py'
951--- ubuntuone/devtools/testcases/tests/test_squid_testcase.py 1970-01-01 00:00:00 +0000
952+++ ubuntuone/devtools/testcases/tests/test_squid_testcase.py 2012-01-05 16:48:29 +0000
953@@ -0,0 +1,262 @@
954+# -*- coding: utf-8 -*-
955+#
956+# Copyright 2011 Canonical Ltd.
957+#
958+# This program is free software: you can redistribute it and/or modify it
959+# under the terms of the GNU General Public License version 3, as published
960+# by the Free Software Foundation.
961+#
962+# This program is distributed in the hope that it will be useful, but
963+# WITHOUT ANY WARRANTY; without even the implied warranties of
964+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
965+# PURPOSE. See the GNU General Public License for more details.
966+#
967+# You should have received a copy of the GNU General Public License along
968+# with this program. If not, see <http://www.gnu.org/licenses/>.
969+"""Test the squid test case."""
970+import base64
971+
972+from twisted.application import internet, service
973+from twisted.internet import defer, reactor
974+from twisted.web import client, error, http, resource, server
975+
976+from ubuntuone.devtools.testcases.squid import SquidTestCase
977+
978+
979+SAMPLE_RESOURCE = "<p>Hello World!</p>"
980+SIMPLERESOURCE = "simpleresource"
981+THROWERROR = "throwerror"
982+UNAUTHORIZED = "unauthorized"
983+
984+# ignore common twisted lint errors
985+# pylint: disable=C0103, W0212
986+
987+
988+class ProxyClientFactory(client.HTTPClientFactory):
989+ """Factory that supports proxy."""
990+
991+ def __init__(self, proxy_url, proxy_port, url, headers=None):
992+ # we set the proxy details before the init because the parent __init__
993+ # calls setURL
994+ self.proxy_url = proxy_url
995+ self.proxy_port = proxy_port
996+ self.disconnected_d = defer.Deferred()
997+ client.HTTPClientFactory.__init__(self, url, headers=headers)
998+
999+ def setURL(self, url):
1000+ self.host = self.proxy_url
1001+ self.port = self.proxy_port
1002+ self.url = url
1003+ self.path = url
1004+
1005+ def clientConnectionLost(self, connector, reason, reconnecting=0):
1006+ """Connection lost."""
1007+ self.disconnected_d.callback(self)
1008+
1009+
1010+class ProxyWebClient(object):
1011+ """Provide useful web methods with proxy."""
1012+
1013+ def __init__(self, proxy_url=None, proxy_port=None, username=None,
1014+ password=None):
1015+ """Create a new instance with the proxy settings."""
1016+ self.proxy_url = proxy_url
1017+ self.proxy_port = proxy_port
1018+ self.username = username
1019+ self.password = password
1020+ self.factory = None
1021+ self.connectors = []
1022+
1023+ def _connect(self, url, contextFactory):
1024+ """Perform the connection."""
1025+ scheme, _, _, _ = client._parse(url)
1026+ # pylint: disable=E1101
1027+ if scheme == 'https':
1028+ from twisted.internet import ssl
1029+ if contextFactory is None:
1030+ contextFactory = ssl.ClientContextFactory()
1031+ self.connectors.append(reactor.connectSSL(self.proxy_url,
1032+ self.proxy_port,
1033+ self.factory,
1034+ contextFactory))
1035+ else:
1036+ self.connectors.append(reactor.connectTCP(self.proxy_url,
1037+ self.proxy_port,
1038+ self.factory))
1039+ # pylint: enable=E1101
1040+
1041+ def _process_auth_error(self, failure, url, contextFactory):
1042+ """Process an auth failure."""
1043+ failure.trap(error.Error)
1044+ if failure.value.status == str(http.PROXY_AUTH_REQUIRED):
1045+ # we try to get the page using the basic auth
1046+ auth = base64.b64encode('%s:%s' % (self.username, self.password))
1047+ auth_header = 'Basic ' + auth.strip()
1048+ self.factory = ProxyClientFactory(self.proxy_url, self.proxy_port,
1049+ url, headers={'Proxy-Authorization': auth_header})
1050+ self._connect(url, contextFactory)
1051+ return self.factory.deferred
1052+ else:
1053+ return failure
1054+
1055+ def get_page(self, url, contextFactory=None, *args, **kwargs):
1056+ """Download a webpage as a string.
1057+
1058+ This method relies on the twisted.web.client.getPage but adds and extra
1059+ step. If there is an auth error the method will perform a second try
1060+ so that the username and password are used.
1061+ """
1062+ self.factory = ProxyClientFactory(self.proxy_url, self.proxy_port, url,
1063+ headers={'Connection': 'close'})
1064+ self._connect(url, contextFactory)
1065+ self.factory.deferred.addErrback(self._process_auth_error, url,
1066+ contextFactory)
1067+ return self.factory.deferred
1068+
1069+ @defer.inlineCallbacks
1070+ def shutdown(self):
1071+ """Clean all connectors."""
1072+ for connector in self.connectors:
1073+ yield connector.disconnect()
1074+ defer.returnValue(True)
1075+
1076+
1077+class SimpleResource(resource.Resource):
1078+ """A simple web resource."""
1079+
1080+ def render_GET(self, request):
1081+ """Make a bit of html out of these resource's
1082+ content."""
1083+ return SAMPLE_RESOURCE
1084+
1085+
1086+class SaveHTTPChannel(http.HTTPChannel):
1087+ """A save protocol to be used in tests."""
1088+
1089+ protocolInstance = None
1090+
1091+ def connectionMade(self):
1092+ """Keep track of the given protocol."""
1093+ SaveHTTPChannel.protocolInstance = self
1094+ http.HTTPChannel.connectionMade(self)
1095+
1096+
1097+class SaveSite(server.Site):
1098+ """A site that let us know when it closed."""
1099+
1100+ protocol = SaveHTTPChannel
1101+
1102+ def __init__(self, *args, **kwargs):
1103+ """Create a new instance."""
1104+ server.Site.__init__(self, *args, **kwargs)
1105+ # we disable the timeout in the tests, we will deal with it manually.
1106+ self.timeOut = None
1107+
1108+
1109+class MockWebServer(object):
1110+ """A mock webserver for testing"""
1111+
1112+ def __init__(self):
1113+ """Start up this instance."""
1114+ root = resource.Resource()
1115+ root.putChild(SIMPLERESOURCE, SimpleResource())
1116+
1117+ root.putChild(THROWERROR, resource.NoResource())
1118+
1119+ unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED,
1120+ "Unauthorized", "Unauthorized")
1121+ root.putChild(UNAUTHORIZED, unauthorized_resource)
1122+
1123+ self.site = SaveSite(root)
1124+ application = service.Application('web')
1125+ self.service_collection = service.IServiceCollection(application)
1126+ #pylint: disable=E1101
1127+ self.tcpserver = internet.TCPServer(0, self.site)
1128+ self.tcpserver.setServiceParent(self.service_collection)
1129+ self.service_collection.startService()
1130+
1131+ def get_url(self):
1132+ """Build the url for this mock server."""
1133+ #pylint: disable=W0212
1134+ port_num = self.tcpserver._port.getHost().port
1135+ return "http://localhost:%d/" % port_num
1136+
1137+ @defer.inlineCallbacks
1138+ def stop(self):
1139+ """Shut it down."""
1140+ #pylint: disable=E1101
1141+ # make the connection time out so that is works with squid3 when
1142+ # the connection is kept alive.
1143+ if self.site.protocol.protocolInstance:
1144+ self.site.protocol.protocolInstance.timeoutConnection()
1145+ yield self.service_collection.stopService()
1146+
1147+
1148+class ProxyTestCase(SquidTestCase):
1149+ """A squid test with no auth proxy."""
1150+
1151+ @defer.inlineCallbacks
1152+ def setUp(self):
1153+ """Set the tests."""
1154+ yield super(ProxyTestCase, self).setUp()
1155+ self.ws = MockWebServer()
1156+ self.proxy_client = None
1157+ self.addCleanup(self.teardown_client_server)
1158+ self.url = self.ws.get_url() + SIMPLERESOURCE
1159+
1160+ def teardown_client_server(self):
1161+ """Clean resources."""
1162+ if self.proxy_client is not None:
1163+ self.proxy_client.shutdown()
1164+ return defer.gatherResults([self.ws.stop(),
1165+ self.proxy_client.shutdown(),
1166+ self.proxy_client.factory.disconnected_d])
1167+ else:
1168+ return self.ws.stop()
1169+
1170+ def access_noauth_url(self, address, port):
1171+ """Access a url throught the proxy."""
1172+ self.proxy_client = ProxyWebClient(proxy_url=address, proxy_port=port)
1173+ return self.proxy_client.get_page(self.url)
1174+
1175+ def access_auth_url(self, address, port, username, password):
1176+ """Access a url throught the proxy."""
1177+ self.proxy_client = ProxyWebClient(proxy_url=address, proxy_port=port,
1178+ username=username, password=password)
1179+ return self.proxy_client.get_page(self.url)
1180+
1181+ @defer.inlineCallbacks
1182+ def test_noauth_url_access(self):
1183+ """Test accessing to the url."""
1184+ settings = self.get_nonauth_proxy_settings()
1185+ # if there is an exception we fail.
1186+ data = yield self.access_noauth_url(settings['host'],
1187+ settings['port'])
1188+ self.assertEqual(SAMPLE_RESOURCE, data)
1189+
1190+ @defer.inlineCallbacks
1191+ def test_auth_url_access(self):
1192+ """Test accessing to the url."""
1193+ settings = self.get_auth_proxy_settings()
1194+ # if there is an exception we fail.
1195+ data = yield self.access_auth_url(settings['host'],
1196+ settings['port'],
1197+ settings['username'],
1198+ settings['password'])
1199+ self.assertEqual(SAMPLE_RESOURCE, data)
1200+
1201+ def test_auth_url_401(self):
1202+ """Test failing accessing the url."""
1203+ settings = self.get_auth_proxy_settings()
1204+ # swap password for username to fail
1205+ d = self.failUnlessFailure(self.access_auth_url(settings['host'],
1206+ settings['port'], settings['password'],
1207+ settings['username']), error.Error)
1208+ return d
1209+
1210+ def test_auth_url_407(self):
1211+ """Test failing accessing the url."""
1212+ settings = self.get_auth_proxy_settings()
1213+ d = self.failUnlessFailure(self.access_noauth_url(settings['host'],
1214+ settings['port']), error.Error)
1215+ return d

Subscribers

People subscribed via source and target branches

to all changes: