Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Adding tests and fixing bugs the tests found

  • Loading branch information...
commit 76e20fd99ee832ac7c9ec0dbfa5c98d1032d4dd5 1 parent 04b65ea
Aaron Frank authored
3  app.yaml
@@ -8,6 +8,9 @@ handlers:
8 8 - url: /examples/twiml
9 9 static_dir: examples/twiml
10 10
  11 +- url: /test.*
  12 + script: gaeunit.py
  13 +
11 14 #Handler for all callbacks as of right now.
12 15 - url: /Callbacks/.*
13 16 script: handlers/callbacks.py
470 gaeunit.py
... ... @@ -0,0 +1,470 @@
  1 +#!/usr/bin/env python
  2 +'''
  3 +GAEUnit: Google App Engine Unit Test Framework
  4 +
  5 +Usage:
  6 +
  7 +1. Put gaeunit.py into your application directory. Modify 'app.yaml' by
  8 + adding the following mapping below the 'handlers:' section:
  9 +
  10 + - url: /test.*
  11 + script: gaeunit.py
  12 +
  13 +2. Write your own test cases by extending unittest.TestCase.
  14 +
  15 +3. Launch the development web server. To run all tests, point your browser to:
  16 +
  17 + http://localhost:8080/test (Modify the port if necessary.)
  18 +
  19 + For plain text output add '?format=plain' to the above URL.
  20 + See README.TXT for information on how to run specific tests.
  21 +
  22 +4. The results are displayed as the tests are run.
  23 +
  24 +Visit http://code.google.com/p/gaeunit for more information and updates.
  25 +
  26 +------------------------------------------------------------------------------
  27 +Copyright (c) 2008-2009, George Lei and Steven R. Farley. All rights reserved.
  28 +
  29 +Distributed under the following BSD license:
  30 +
  31 +Redistribution and use in source and binary forms, with or without
  32 +modification, are permitted provided that the following conditions are met:
  33 +
  34 +* Redistributions of source code must retain the above copyright notice,
  35 + this list of conditions and the following disclaimer.
  36 +
  37 +* Redistributions in binary form must reproduce the above copyright notice,
  38 + this list of conditions and the following disclaimer in the documentation
  39 + and/or other materials provided with the distribution.
  40 +
  41 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  42 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  43 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  44 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
  45 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  46 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  47 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  48 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  49 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  50 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  51 +------------------------------------------------------------------------------
  52 +'''
  53 +
  54 +__author__ = "George Lei and Steven R. Farley"
  55 +__email__ = "George.Z.Lei@Gmail.com"
  56 +__version__ = "#Revision: 1.2.8 $"[11:-2]
  57 +__copyright__= "Copyright (c) 2008-2009, George Lei and Steven R. Farley"
  58 +__license__ = "BSD"
  59 +__url__ = "http://code.google.com/p/gaeunit"
  60 +
  61 +import sys
  62 +import os
  63 +import unittest
  64 +import time
  65 +import logging
  66 +import cgi
  67 +import django.utils.simplejson
  68 +
  69 +from google.appengine.ext import webapp
  70 +from google.appengine.api import apiproxy_stub_map
  71 +from google.appengine.api import datastore_file_stub
  72 +from google.appengine.ext.webapp.util import run_wsgi_app
  73 +
  74 +_LOCAL_TEST_DIR = 'test' # location of files
  75 +_WEB_TEST_DIR = '/test' # how you want to refer to tests on your web server
  76 +
  77 +# or:
  78 +# _WEB_TEST_DIR = '/u/test'
  79 +# then in app.yaml:
  80 +# - url: /u/test.*
  81 +# script: gaeunit.py
  82 +
  83 +
  84 +##############################################################################
  85 +# Main request handler
  86 +##############################################################################
  87 +
  88 +
  89 +class MainTestPageHandler(webapp.RequestHandler):
  90 + def get(self):
  91 + unknown_args = [arg for arg in self.request.arguments()
  92 + if arg not in ("format", "package", "name")]
  93 + if len(unknown_args) > 0:
  94 + errors = []
  95 + for arg in unknown_args:
  96 + errors.append(_log_error("The request parameter '%s' is not valid." % arg))
  97 + self.error(404)
  98 + self.response.out.write(" ".join(errors))
  99 + return
  100 +
  101 + format = self.request.get("format", "html")
  102 + if format == "html":
  103 + self._render_html()
  104 + elif format == "plain":
  105 + self._render_plain()
  106 + else:
  107 + error = _log_error("The format '%s' is not valid." % cgi.escape(format))
  108 + self.error(404)
  109 + self.response.out.write(error)
  110 +
  111 + def _render_html(self):
  112 + suite, error = _create_suite(self.request)
  113 + if not error:
  114 + self.response.out.write(_MAIN_PAGE_CONTENT % (_test_suite_to_json(suite), _WEB_TEST_DIR, __version__))
  115 + else:
  116 + self.error(404)
  117 + self.response.out.write(error)
  118 +
  119 + def _render_plain(self):
  120 + self.response.headers["Content-Type"] = "text/plain"
  121 + runner = unittest.TextTestRunner(self.response.out)
  122 + suite, error = _create_suite(self.request)
  123 + if not error:
  124 + self.response.out.write("====================\n" \
  125 + "GAEUnit Test Results\n" \
  126 + "====================\n\n")
  127 + _run_test_suite(runner, suite)
  128 + else:
  129 + self.error(404)
  130 + self.response.out.write(error)
  131 +
  132 +
  133 +##############################################################################
  134 +# JSON test classes
  135 +##############################################################################
  136 +
  137 +
  138 +class JsonTestResult(unittest.TestResult):
  139 + def __init__(self):
  140 + unittest.TestResult.__init__(self)
  141 + self.testNumber = 0
  142 +
  143 + def render_to(self, stream):
  144 + result = {
  145 + 'runs': self.testsRun,
  146 + 'total': self.testNumber,
  147 + 'errors': self._list(self.errors),
  148 + 'failures': self._list(self.failures),
  149 + }
  150 +
  151 + stream.write(django.utils.simplejson.dumps(result).replace('},', '},\n'))
  152 +
  153 + def _list(self, list):
  154 + dict = []
  155 + for test, err in list:
  156 + d = {
  157 + 'desc': test.shortDescription() or str(test),
  158 + 'detail': err,
  159 + }
  160 + dict.append(d)
  161 + return dict
  162 +
  163 +
  164 +class JsonTestRunner:
  165 + def run(self, test):
  166 + self.result = JsonTestResult()
  167 + self.result.testNumber = test.countTestCases()
  168 + startTime = time.time()
  169 + test(self.result)
  170 + stopTime = time.time()
  171 + timeTaken = stopTime - startTime
  172 + return self.result
  173 +
  174 +
  175 +class JsonTestRunHandler(webapp.RequestHandler):
  176 + def get(self):
  177 + self.response.headers["Content-Type"] = "text/javascript"
  178 + test_name = self.request.get("name")
  179 + _load_default_test_modules()
  180 + suite = unittest.defaultTestLoader.loadTestsFromName(test_name)
  181 + runner = JsonTestRunner()
  182 + _run_test_suite(runner, suite)
  183 + runner.result.render_to(self.response.out)
  184 +
  185 +
  186 +# This is not used by the HTML page, but it may be useful for other client test runners.
  187 +class JsonTestListHandler(webapp.RequestHandler):
  188 + def get(self):
  189 + self.response.headers["Content-Type"] = "text/javascript"
  190 + suite, error = _create_suite(self.request)
  191 + if not error:
  192 + self.response.out.write(_test_suite_to_json(suite))
  193 + else:
  194 + self.error(404)
  195 + self.response.out.write(error)
  196 +
  197 +
  198 +##############################################################################
  199 +# Module helper functions
  200 +##############################################################################
  201 +
  202 +
  203 +def _create_suite(request):
  204 + package_name = request.get("package")
  205 + test_name = request.get("name")
  206 +
  207 + loader = unittest.defaultTestLoader
  208 + suite = unittest.TestSuite()
  209 +
  210 + error = None
  211 +
  212 + try:
  213 + if not package_name and not test_name:
  214 + modules = _load_default_test_modules()
  215 + for module in modules:
  216 + suite.addTest(loader.loadTestsFromModule(module))
  217 + elif test_name:
  218 + _load_default_test_modules()
  219 + suite.addTest(loader.loadTestsFromName(test_name))
  220 + elif package_name:
  221 + package = reload(__import__(package_name))
  222 + module_names = package.__all__
  223 + for module_name in module_names:
  224 + suite.addTest(loader.loadTestsFromName('%s.%s' % (package_name, module_name)))
  225 +
  226 + if suite.countTestCases() == 0:
  227 + raise Exception("'%s' is not found or does not contain any tests." % \
  228 + (test_name or package_name or 'local directory: \"%s\"' % _LOCAL_TEST_DIR))
  229 + except Exception, e:
  230 + error = str(e)
  231 + _log_error(error)
  232 +
  233 + return (suite, error)
  234 +
  235 +
  236 +def _load_default_test_modules():
  237 + if not _LOCAL_TEST_DIR in sys.path:
  238 + sys.path.append(_LOCAL_TEST_DIR)
  239 + module_names = [mf[0:-3] for mf in os.listdir(_LOCAL_TEST_DIR) if mf.endswith(".py")]
  240 + return [reload(__import__(name)) for name in module_names]
  241 +
  242 +
  243 +def _get_tests_from_suite(suite, tests):
  244 + for test in suite:
  245 + if isinstance(test, unittest.TestSuite):
  246 + _get_tests_from_suite(test, tests)
  247 + else:
  248 + tests.append(test)
  249 +
  250 +
  251 +def _test_suite_to_json(suite):
  252 + tests = []
  253 + _get_tests_from_suite(suite, tests)
  254 + test_tuples = [(type(test).__module__, type(test).__name__, test._testMethodName) \
  255 + for test in tests]
  256 + test_dict = {}
  257 + for test_tuple in test_tuples:
  258 + module_name, class_name, method_name = test_tuple
  259 + if module_name not in test_dict:
  260 + mod_dict = {}
  261 + method_list = []
  262 + method_list.append(method_name)
  263 + mod_dict[class_name] = method_list
  264 + test_dict[module_name] = mod_dict
  265 + else:
  266 + mod_dict = test_dict[module_name]
  267 + if class_name not in mod_dict:
  268 + method_list = []
  269 + method_list.append(method_name)
  270 + mod_dict[class_name] = method_list
  271 + else:
  272 + method_list = mod_dict[class_name]
  273 + method_list.append(method_name)
  274 +
  275 + return django.utils.simplejson.dumps(test_dict)
  276 +
  277 +
  278 +def _run_test_suite(runner, suite):
  279 + """Run the test suite.
  280 +
  281 + Preserve the current development apiproxy, create a new apiproxy and
  282 + replace the datastore with a temporary one that will be used for this
  283 + test suite, run the test suite, and restore the development apiproxy.
  284 + This isolates the test datastore from the development datastore.
  285 +
  286 + """
  287 + original_apiproxy = apiproxy_stub_map.apiproxy
  288 + try:
  289 + apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
  290 + temp_stub = datastore_file_stub.DatastoreFileStub('GAEUnitDataStore', None, None, trusted=True)
  291 + apiproxy_stub_map.apiproxy.RegisterStub('datastore', temp_stub)
  292 + # Allow the other services to be used as-is for tests.
  293 + for name in ['user', 'urlfetch', 'mail', 'memcache', 'images']:
  294 + apiproxy_stub_map.apiproxy.RegisterStub(name, original_apiproxy.GetStub(name))
  295 + runner.run(suite)
  296 + finally:
  297 + apiproxy_stub_map.apiproxy = original_apiproxy
  298 +
  299 +
  300 +def _log_error(s):
  301 + logging.warn(s)
  302 + return s
  303 +
  304 +
  305 +################################################
  306 +# Browser HTML, CSS, and Javascript
  307 +################################################
  308 +
  309 +
  310 +# This string uses Python string formatting, so be sure to escape percents as %%.
  311 +_MAIN_PAGE_CONTENT = """
  312 +<html>
  313 +<head>
  314 + <style>
  315 + body {font-family:arial,sans-serif; text-align:center}
  316 + #title {font-family:"Times New Roman","Times Roman",TimesNR,times,serif; font-size:28px; font-weight:bold; text-align:center}
  317 + #version {font-size:87%%; text-align:center;}
  318 + #weblink {font-style:italic; text-align:center; padding-top:7px; padding-bottom:7px}
  319 + #results {padding-top:20px; margin:0pt auto; text-align:center; font-weight:bold}
  320 + #testindicator {width:750px; height:16px; border-style:solid; border-width:2px 1px 1px 2px; background-color:#f8f8f8;}
  321 + #footerarea {text-align:center; font-size:83%%; padding-top:25px}
  322 + #errorarea {padding-top:25px}
  323 + .error {border-color: #c3d9ff; border-style: solid; border-width: 2px 1px 2px 1px; width:750px; padding:1px; margin:0pt auto; text-align:left}
  324 + .errtitle {background-color:#c3d9ff; font-weight:bold}
  325 + </style>
  326 + <script language="javascript" type="text/javascript">
  327 + var testsToRun = %s;
  328 + var totalRuns = 0;
  329 + var totalErrors = 0;
  330 + var totalFailures = 0;
  331 +
  332 + function newXmlHttp() {
  333 + try { return new XMLHttpRequest(); } catch(e) {}
  334 + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
  335 + try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
  336 + alert("XMLHttpRequest not supported");
  337 + return null;
  338 + }
  339 +
  340 + function requestTestRun(moduleName, className, methodName) {
  341 + var methodSuffix = "";
  342 + if (methodName) {
  343 + methodSuffix = "." + methodName;
  344 + }
  345 + var xmlHttp = newXmlHttp();
  346 + xmlHttp.open("GET", "%s/run?name=" + moduleName + "." + className + methodSuffix, true);
  347 + xmlHttp.onreadystatechange = function() {
  348 + if (xmlHttp.readyState != 4) {
  349 + return;
  350 + }
  351 + if (xmlHttp.status == 200) {
  352 + var result = eval("(" + xmlHttp.responseText + ")");
  353 + totalRuns += parseInt(result.runs);
  354 + totalErrors += result.errors.length;
  355 + totalFailures += result.failures.length;
  356 + document.getElementById("testran").innerHTML = totalRuns;
  357 + document.getElementById("testerror").innerHTML = totalErrors;
  358 + document.getElementById("testfailure").innerHTML = totalFailures;
  359 + if (totalErrors == 0 && totalFailures == 0) {
  360 + testSucceed();
  361 + } else {
  362 + testFailed();
  363 + }
  364 + var errors = result.errors;
  365 + var failures = result.failures;
  366 + var details = "";
  367 + for(var i=0; i<errors.length; i++) {
  368 + details += '<p><div class="error"><div class="errtitle">ERROR ' +
  369 + errors[i].desc +
  370 + '</div><div class="errdetail"><pre>'+errors[i].detail +
  371 + '</pre></div></div></p>';
  372 + }
  373 + for(var i=0; i<failures.length; i++) {
  374 + details += '<p><div class="error"><div class="errtitle">FAILURE ' +
  375 + failures[i].desc +
  376 + '</div><div class="errdetail"><pre>' +
  377 + failures[i].detail +
  378 + '</pre></div></div></p>';
  379 + }
  380 + var errorArea = document.getElementById("errorarea");
  381 + errorArea.innerHTML += details;
  382 + } else {
  383 + document.getElementById("errorarea").innerHTML = xmlHttp.responseText;
  384 + testFailed();
  385 + }
  386 + };
  387 + xmlHttp.send(null);
  388 + }
  389 +
  390 + function testFailed() {
  391 + document.getElementById("testindicator").style.backgroundColor="red";
  392 + }
  393 +
  394 + function testSucceed() {
  395 + document.getElementById("testindicator").style.backgroundColor="green";
  396 + }
  397 +
  398 + function runTests() {
  399 + // Run each test asynchronously (concurrently).
  400 + var totalTests = 0;
  401 + for (var moduleName in testsToRun) {
  402 + var classes = testsToRun[moduleName];
  403 + for (var className in classes) {
  404 + // TODO: Optimize for the case where tests are run by class so we don't
  405 + // have to always execute each method separately. This should be
  406 + // possible when we have a UI that allows the user to select tests
  407 + // by module, class, and method.
  408 + //requestTestRun(moduleName, className);
  409 + methods = classes[className];
  410 + for (var i = 0; i < methods.length; i++) {
  411 + totalTests += 1;
  412 + var methodName = methods[i];
  413 + requestTestRun(moduleName, className, methodName);
  414 + }
  415 + }
  416 + }
  417 + document.getElementById("testtotal").innerHTML = totalTests;
  418 + }
  419 +
  420 + </script>
  421 + <title>GAEUnit: Google App Engine Unit Test Framework</title>
  422 +</head>
  423 +<body onload="runTests()">
  424 + <div id="headerarea">
  425 + <div id="title">GAEUnit: Google App Engine Unit Test Framework</div>
  426 + <div id="version">Version %s</div>
  427 + </div>
  428 + <div id="resultarea">
  429 + <table id="results"><tbody>
  430 + <tr><td colspan="3"><div id="testindicator"> </div></td</tr>
  431 + <tr>
  432 + <td>Runs: <span id="testran">0</span>/<span id="testtotal">0</span></td>
  433 + <td>Errors: <span id="testerror">0</span></td>
  434 + <td>Failures: <span id="testfailure">0</span></td>
  435 + </tr>
  436 + </tbody></table>
  437 + </div>
  438 + <div id="errorarea"></div>
  439 + <div id="footerarea">
  440 + <div id="weblink">
  441 + <p>
  442 + Please visit the <a href="http://code.google.com/p/gaeunit">project home page</a>
  443 + for the latest version or to report problems.
  444 + </p>
  445 + <p>
  446 + Copyright 2008-2009 <a href="mailto:George.Z.Lei@Gmail.com">George Lei</a>
  447 + and <a href="mailto:srfarley@gmail.com>Steven R. Farley</a>
  448 + </p>
  449 + </div>
  450 + </div>
  451 +</body>
  452 +</html>
  453 +"""
  454 +
  455 +
  456 +##############################################################################
  457 +# Script setup and execution
  458 +##############################################################################
  459 +
  460 +
  461 +application = webapp.WSGIApplication([('%s' % _WEB_TEST_DIR, MainTestPageHandler),
  462 + ('%s/run' % _WEB_TEST_DIR, JsonTestRunHandler),
  463 + ('%s/list' % _WEB_TEST_DIR, JsonTestListHandler)],
  464 + debug=True)
  465 +
  466 +def main():
  467 + run_wsgi_app(application)
  468 +
  469 +if __name__ == '__main__':
  470 + main()
2  handlers/calls.py
@@ -107,7 +107,7 @@ def post(self,API_VERSION,ACCOUNT_SID,*args):
107 107 PhoneNumberSid = Phone_Number.Sid,
108 108 AccountSid = ACCOUNT_SID,
109 109 Status = 'queued',
110   - Direction = 'outgoing-api'
  110 + Direction = 'outbound-api'
111 111 )
112 112 Call.put()
113 113 response_data = Call.get_dict()
2  handlers/main.py
@@ -120,7 +120,7 @@ def post(self,Sid):
120 120 Valid = True
121 121 for arg in self.request.arguments():
122 122 if Valid:
123   - Valid,TwilioCode,TwilioMsg = self.data['PhoneNumber'].validate(self.request, arg, self.request.get( arg ,None))
  123 + Valid,TwilioCode,TwilioMsg = self.data['PhoneNumber'].validate(self.request, arg, self.request.get( arg ,None),{})
124 124 setattr(self.data['PhoneNumber'], arg, self.data['PhoneNumber'].sanitize( self.request, arg, self.request.get( arg ,None)))
125 125
126 126 if Valid:
83 helpers/parameters.py
... ... @@ -1,10 +1,37 @@
1 1 import urlparse
2 2 import re
3 3 import logging
  4 +
  5 +from models import incoming_phone_numbers, outgoing_caller_ids
  6 +def arg_or_request(arg_value,request, arg_name, default = None):
  7 + return arg_value if ((arg_value is not None and arg_value != '') or request is None) else request.get(arg_name, default)
4 8 #Parses the given phone number, and then makes sure that it can be put into a valid twilio format.
5 9 #Returns the twilio formatted phone_number and whether or not it went well, Valid or not
  10 +
6 11 def parse_phone_number(phone_number):
7   - return phone_number, True
  12 + #TAKEN FROM DIVE INTO PYTHON
  13 + # http://diveintopython.org/
  14 + phonePattern = re.compile(r'''
  15 + (\+1)* # optional +1 capture don't match beginning of string, number can start anywhere
  16 + (\d{3}) # area code is 3 digits (e.g. '800')
  17 + \D* # optional separator is any number of non-digits
  18 + (\d{3}) # trunk is 3 digits (e.g. '555')
  19 + \D* # optional separator
  20 + (\d{4}) # rest of number is 4 digits (e.g. '1212')
  21 + \D* # optional separator
  22 + (\d*) # extension is optional and can be any number of digits
  23 + $ # end of string
  24 + ''', re.VERBOSE)
  25 + try:
  26 + phoneGroups = phonePattern.search(phone_number).groups()
  27 + pn = '+1'+str(phoneGroups[1])+str(phoneGroups[2])+str(phoneGroups[3])
  28 + except Exception, e:
  29 + logging.info('having trouble parsing phone number: '+phone_number)
  30 + return phone_number, False
  31 + else:
  32 + logging.info('successful parsing phone number: '+phone_number)
  33 + return pn, True
  34 + #should actually parse # and check for truth
8 35
9 36 def valid_to_phone_number(phone_number,required = False):
10 37 if phone_number is None:
@@ -14,23 +41,51 @@ def valid_to_phone_number(phone_number,required = False):
14 41 if Valid:
15 42 return True, 0, ''
16 43 else:
17   - return False, 21401, 'http://www.twilio.com/docs/errors/21401'
  44 + if required:
  45 + return False, 21401, 'http://www.twilio.com/docs/errors/21401'
  46 + else:
  47 + return True, 21401, 'http://www.twilio.com/docs/errors/21401'
18 48
19   -def valid_from_phone_number(phone_number,required = False):
20   - if phone_number is None and required:
  49 +def valid_from_phone_number(phone_number, required = False, Direction = 'outbound-api', SMS = False):
  50 + if (phone_number is None or phone_number == '') and required:
21 51 return False, 21603, 'http://www.twilio.com/docs/errors/21603'
22 52 else:
  53 +# logging.info('from phone number not none, and required')
23 54 number_parsed, Valid = parse_phone_number(phone_number)
24 55 if Valid:
25   - return True, 0, ''
  56 + logging.info('valid from phone number, but is it outgoing')
  57 + logging.info('Direction is: '+str(Direction))
  58 + if Direction in ['outbound-call','outbound-api','outbound-reply']:
  59 + logging.info('outgoing direction from phone number')
  60 + #need to check numbers
  61 + #first check if we have that phone number as an incoming phone number
  62 + PN = incoming_phone_numbers.Incoming_Phone_Number.all().filter('PhoneNumber =',number_parsed).get()
  63 + if PN is None:
  64 + logging.info('no incoming phone number')
  65 + if SMS:
  66 + return False, 14108, 'http://www.twilio.com/docs/error/14108'
  67 + else:
  68 + PN = outgoing_caller_ids.Outgoing_Caller_Ids.all().filter('PhoneNumber =',number_parsed).get()
  69 + if PN is None:
  70 + return False, 14108, 'http://www.twilio.com/docs/error/14108'
  71 + else:
  72 + return True, 0, ''
  73 + else:
  74 + return True, 0, ''
  75 + else:
  76 + return True, 0, ''
26 77 else:
  78 + logging.info('not a valid from phone number')
27 79 return False, 21401, 'http://www.twilio.com/docs/errors/21401'
28 80
29 81 def valid_body(body, required=True):
30   - if body is None and required:
  82 + if (body is None or body == '') and required:
31 83 return False, 14103, 'http://www.twilio.com/docs/errors/14103'
32 84 else:
33   - return True, 0, ''
  85 + if body is not None and len(body) > 160:
  86 + return False, 21605, 'http://www.twilio.com/docs/errors/21605'
  87 + else:
  88 + return True, 0, ''
34 89
35 90 def required(required_list,request):
36 91 Valid = True
@@ -77,9 +132,9 @@ def check_url(URL):
77 132 return True
78 133
79 134 # Checks for the normal callback url to make sure they are valid
80   -def standard_urls(request,StandardArgName):
81   - if request.get(StandardArgName, None) is not None:
82   - if check_url(request.get(StandardArgName,None)):
  135 +def standard_urls(ArgValue):
  136 + if ArgValue is not None:
  137 + if check_url(ArgValue):
83 138 return True, 0, ''
84 139 else:
85 140 return False, 21502, 'http://www.twilio.com/docs/errors/21502'
@@ -89,12 +144,12 @@ def standard_urls(request,StandardArgName):
89 144 #What should this pass back?
90 145 #Passed,Twilio error code, Twilio error message
91 146 #Checks fallback urls for validity, normal callback being created (cant have fallback without normal callback)
92   -def fallback_urls(request, FallbackArgName, StandardArgName, Instance, method = 'Voice'):
  147 +def fallback_urls(fallback_arg_value, standard_arg_value, StandardArgName, Instance, method = 'Voice'):
93 148 # need to check that its a valid url,
94   - if request.get(FallbackArgName,None) is not None and request.get(FallbackArgName,None) != '':
95   - if check_url(request.get(FallbackArgName,None)):
  149 + if fallback_arg_value != '': #if not passed, then returns ''
  150 + if check_url(fallback_arg_value):
96 151 # need to check that a standard url is passed, or set already
97   - if (request.get(StandardArgName,None) is not None and request.get(StandardArgName,None) != '') or (getattr(Instance,StandardArgName) is not None and getattr(Instance,StandardArgName) != ''):
  152 + if standard_arg_value != '' or getattr(Instance,StandardArgName) is not None:
98 153 return True, 0, ''
99 154 else:
100 155 #Hack to check for sms fallback missing or not.
BIN  helpers/parameters.pyc
Binary file not shown
2  models/accounts.py
@@ -44,7 +44,7 @@ def sanitize(self, request, arg_name, arg_value):
44 44 else:
45 45 return arg_value
46 46
47   - def validate(self, request, arg_name, arg_value):
  47 + def validate(self, request, arg_name,arg_value, **kwargs):
48 48 validators = {
49 49 'FriendlyName' : parameters.friendlyname_length(request.get('FriendlyName',''))
50 50 }
BIN  models/accounts.pyc
Binary file not shown
4 models/base.py
@@ -28,7 +28,7 @@ def new(cls, request, AccountSid = None, **kwargs):
28 28 arg_length = len(kwargs)
29 29 for keyword in kwargs:
30 30 if hasattr(cls,keyword) and kwargs[keyword] is not None:
31   - Valid, TwilioCode, TwilioMsg = cls().validate( request, keyword, kwargs[keyword] )
  31 + Valid, TwilioCode, TwilioMsg = cls().validate( request, keyword, kwargs[keyword], **kwargs)
32 32 if not Valid:
33 33 break
34 34 else:
@@ -49,5 +49,5 @@ def new(cls, request, AccountSid = None, **kwargs):
49 49 def sanitize(self, request, arg_name, arg_value):
50 50 return arg_value
51 51
52   - def validate(self, request, arg_name, arg_value):
  52 + def validate(self, request, arg_name,arg_value, **kwargs):
53 53 return True, 0, ''
BIN  models/base.pyc
Binary file not shown
6 models/calls.py
@@ -67,10 +67,10 @@ def new(cls, ParentCallSid, AccountSid,To,From,PhoneNumberSid,Status,StartTime =
67 67 )
68 68 """
69 69
70   - def validate(self, request, arg_name,arg_value):
  70 + def validate(self, request, arg_name,arg_value, **kwargs):
71 71 validators = {
72 72 'To' : parameters.valid_to_phone_number(arg_value if arg_value is not None else request.get('To',None),required=True),
73   - 'From' : parameters.valid_from_phone_number(arg_value if arg_value is not None else request.get('From',None),required=True)
  73 + 'From' : parameters.valid_from_phone_number(arg_value if arg_value is not None else request.get('From',None),required=True, self = self)
74 74 }
75 75 if arg_name in validators:
76 76 return validators[arg_name]
@@ -125,7 +125,7 @@ def disconnect(self,StatusCallback = None,StatusCallbackMethod = 'POST'):
125 125 self.Status = 'complete'
126 126 self.EndTime = datetime.datetime.now()
127 127 self.Duration = (self.EndTime - self.StartTime).seconds
128   - if self.Direction == 'outgoing-api' or self.Direction == 'outbound-dial':
  128 + if self.Direction == 'outbound-api' or self.Direction == 'outbound-dial':
129 129 #should be dependent on country code, but will need more work
130 130 self.Price = self.Duration * (0.02)
131 131 elif self.Direction == 'inbound':
2  models/conferences.py
@@ -32,7 +32,7 @@ def sanitize(self, request, arg_name, arg_value):
32 32 else:
33 33 return arg_value
34 34
35   - def validate(self, request, arg_name, arg_value):
  35 + def validate(self, request, arg_name,arg_value, **kwargs):
36 36 validators = {
37 37 'FriendlyName' : parameters.friendlyname_length(request.get('FriendlyName',''))
38 38 }
10 models/incoming_phone_numbers.py
... ... @@ -1,12 +1,4 @@
1   -from google.appengine.ext import db
2   -
3   -from models import base, phone_numbers
4   -
5   -from random import random
6   -
7   -from hashlib import sha256
8   -
9   -from helpers import parameters
  1 +from models import phone_numbers
10 2
11 3 class Incoming_Phone_Number(phone_numbers.Phone_Number):
12 4 pass
10 models/messages.py
@@ -44,12 +44,12 @@ def error(self):
44 44 self.Price = 0.00
45 45 self.put()
46 46
47   - def validate(self, request, arg_name,arg_value):
  47 + def validate(self, request, arg_name,arg_value, **kwargs):
48 48 validators = {
49   - 'To' : parameters.valid_to_phone_number(arg_value if arg_value is not None else request.get('To',None),required=True),
50   - 'From' : parameters.valid_from_phone_number(arg_value if arg_value is not None else request.get('From',None),required=True),
51   - 'Body' : parameters.valid_body(arg_value if arg_value is not None else request.get('Body',None),required=True),
52   - 'StatusCallback' : arg_value if (arg_value is not None or request is None) else parameters.standard_urls(request,'StatusCallback')
  49 + 'To' : parameters.valid_to_phone_number(parameters.arg_or_request(arg_value, request, arg_name),required=True),
  50 + 'From' : parameters.valid_from_phone_number(parameters.arg_or_request(arg_value, request, arg_name),required=True, Direction = kwargs['Direction'] if 'Direction' in kwargs else None, SMS = True),
  51 + 'Body' : parameters.valid_body(parameters.arg_or_request(arg_value, request, arg_name), required=True),
  52 + 'StatusCallback' : parameters.standard_urls(parameters.arg_or_request(arg_value, request, arg_name))
53 53 }
54 54 if arg_name in validators:
55 55 return validators[arg_name]
10 models/outgoing_caller_ids.py
... ... @@ -1,12 +1,4 @@
1   -from google.appengine.ext import db
2   -
3   -from models import base, phone_numbers
4   -
5   -from random import random
6   -
7   -from hashlib import sha256
8   -
9   -from helpers import parameters
  1 +from models import phone_numbers
10 2
11 3 class Outgoing_Caller_Id(phone_numbers.Phone_Number):
12 4 pass
2  models/participants.py
@@ -38,7 +38,7 @@ def sanitize(self, request, arg_name, arg_value):
38 38 else:
39 39 return arg_value
40 40
41   - def validate(self, request, arg_name, arg_value):
  41 + def validate(self, request, arg_name,arg_value, **kwargs):
42 42 validators = {
43 43 'Muted' : parameters.allow_boolean(arg_value if arg_value is not None else request.get( arg_name, None) )
44 44 }
68 models/phone_numbers.py
@@ -3,8 +3,6 @@
3 3 from random import random
4 4 from hashlib import sha256
5 5
6   -from helpers import parameters
7   -
8 6 """
9 7 Sid A 34 character string that uniquely idetifies this resource.
10 8 DateCreated The date that this resource was created, given as GMT RFC 2822 format.
@@ -48,20 +46,34 @@ def new_Sid(self):
48 46 return 'PN'+sha256(str(random())).hexdigest()
49 47
50 48 #Validators for all properties that are user-editable.
51   - def validate(self, request, arg_name, arg_value):
  49 + def validate(self, request, arg_name, arg_value, **kwargs):
  50 + from helpers import parameters
  51 +
52 52 validators = {
53   - 'FriendlyName' : parameters.friendlyname_length(request.get('FriendlyName','')),
54   - 'VoiceCallerIdLookup' : parameters.allowed_boolean(request.get('VoiceCallerIdLookup',None)),
55   - 'VoiceUrl' : parameters.standard_urls(request,'VoiceUrl'),
56   - 'VoiceMethod' : parameters.phone_allowed_methods(arg_value,['GET','POST']),
57   - 'VoiceFallbackUrl' : parameters.fallback_urls(request, 'VoiceFallbackUrl', 'VoiceUrl', self, 'Voice'),
58   - 'VoiceFallbackMethod' : parameters.phone_allowed_methods(arg_value,['GET','POST']),
59   - 'StatusCallback' : parameters.standard_urls(request,'StatusCallback'),
60   - 'StatusCallbackMethod' : parameters.phone_allowed_methods(arg_value,['GET','POST']),
61   - 'SmsUrl' : parameters.standard_urls(request,'SmsUrl'),
62   - 'SmsMethod' : parameters.sms_allowed_methods(arg_value,['GET','POST']),
63   - 'SmsFallbackUrl' : parameters.fallback_urls(request, 'SmsFallbackUrl', 'SmsUrl', self, 'SMS'),
64   - 'SmsFallbackMethod' : parameters.sms_allowed_methods(arg_value,['GET','POST'])
  53 + 'FriendlyName' : parameters.friendlyname_length(parameters.arg_or_request(arg_value, request, arg_name)),
  54 +
  55 + 'VoiceCallerIdLookup' : parameters.allowed_boolean(parameters.arg_or_request(arg_value, request, arg_name,False)),
  56 +
  57 + 'VoiceUrl' : parameters.standard_urls(parameters.arg_or_request(arg_value, request, arg_name)),
  58 +
  59 + 'VoiceMethod' : parameters.phone_allowed_methods(parameters.arg_or_request(arg_value, request, arg_name,'POST'),['GET','POST']),
  60 +
  61 + 'VoiceFallbackUrl' : parameters.fallback_urls(parameters.arg_or_request(arg_value, request, arg_name,''), parameters.arg_or_request(arg_value, request, 'VoiceUrl',''), 'VoiceUrl', self, 'Voice'),
  62 +
  63 + 'VoiceFallbackMethod' : parameters.phone_allowed_methods(parameters.arg_or_request(arg_value, request, arg_name,'POST'),['GET','POST']),
  64 +
  65 + 'StatusCallback' : parameters.standard_urls(parameters.arg_or_request(arg_value, request, arg_name)),
  66 +
  67 + 'StatusCallbackMethod' : parameters.phone_allowed_methods(parameters.arg_or_request(arg_value, request, arg_name,'POST'),['GET','POST']),
  68 +
  69 + 'SmsUrl' : parameters.standard_urls(parameters.arg_or_request(arg_value, request, arg_name)),
  70 +
  71 + 'SmsMethod' : parameters.sms_allowed_methods(parameters.arg_or_request(arg_value, request, arg_name,'POST'),['GET','POST']),
  72 +
  73 + 'SmsFallbackUrl' : parameters.fallback_urls(parameters.arg_or_request(arg_value, request, arg_name,''), parameters.arg_or_request(arg_value, request, 'SmsUrl',''), 'SmsUrl', self, 'SMS'),
  74 +
  75 + 'SmsFallbackMethod' : parameters.sms_allowed_methods(parameters.arg_or_request(arg_value, request, arg_name,'POST'),['GET','POST'])
  76 +
65 77 }
66 78
67 79 if arg_name in validators:
@@ -70,19 +82,21 @@ def validate(self, request, arg_name, arg_value):
70 82 return True, 0, ''
71 83 #to be used, but for now will leave as is, minus standardizing how I do method saving
72 84 def sanitize(self, request, arg_name, arg_value):
  85 + from helpers import parameters
  86 +
73 87 sanitizers = {
74   - 'FriendlyName' : request.get('FriendlyName',None),
75   - 'VoiceCallerIdLookup' : request.get('VoiceCallerIdLookup',None),
76   - 'VoiceUrl' : request.get('VoiceUrl',None),
77   - 'VoiceMethod' : request.get('VoiceMethod','POST').upper(),
78   - 'VoiceFallbackUrl' : request.get('VoiceFallbackUrl',None),
79   - 'VoiceFallbackMethod' : request.get('VoiceFallbackMethod','POST').upper(),
80   - 'StatusCallback' : request.get('StatusCallback',None),
81   - 'StatusCallbackMethod' : request.get('StatusCallbackMethod','POST').upper(),
82   - 'SmsUrl' : request.get('SmsUrl',None),
83   - 'SmsMethod' : request.get('SmsMethod','POST').upper(),
84   - 'SmsFallbackUrl' : request.get('SmsFallbackUrl',None),
85   - 'SmsFallbackMethod' : request.get('SmsFallbackMethod','POST').upper()
  88 + 'FriendlyName' : parameters.arg_or_request(arg_value, request, arg_name),
  89 + 'VoiceCallerIdLookup' : parameters.arg_or_request(arg_value, request, arg_name,False),
  90 + 'VoiceUrl' : parameters.arg_or_request(arg_value, request, arg_name),
  91 + 'VoiceMethod' : parameters.arg_or_request(arg_value, request, arg_name,'POST').upper(),
  92 + 'VoiceFallbackUrl' : parameters.arg_or_request(arg_value, request, arg_name),
  93 + 'VoiceFallbackMethod' : parameters.arg_or_request(arg_value, request, arg_name,'POST').upper(),
  94 + 'StatusCallback' : parameters.arg_or_request(arg_value, request, arg_name),
  95 + 'StatusCallbackMethod' : parameters.arg_or_request(arg_value, request, arg_name,'POST').upper(),
  96 + 'SmsUrl' :parameters.arg_or_request(arg_value, request, arg_name),
  97 + 'SmsMethod' : parameters.arg_or_request(arg_value, request, arg_name,'POST').upper(),
  98 + 'SmsFallbackUrl' : parameters.arg_or_request(arg_value, request, arg_name),
  99 + 'SmsFallbackMethod' : parameters.arg_or_request(arg_value, request, arg_name,'POST').upper()
86 100 }
87 101 if arg_name in sanitizers:
88 102 return sanitizers[arg_name]
0  tests/handlers/base_handler.py → test/handlers/base_handler.py
File renamed without changes
0  tests/models/base.py → test/models/base.py
File renamed without changes
51 test/models_test.py
... ... @@ -0,0 +1,51 @@
  1 +import unittest
  2 +import logging
  3 +from google.appengine.ext import db
  4 +import models
  5 +
  6 +class MessagesModel(unittest.TestCase):
  7 + def setUp(self):
  8 + #create account
  9 + self.Account = models.accounts.Account.new(key_name='email@email.com',email='email@email.com',password='password')
  10 + self.Account.put()
  11 + self.PhoneNumber,Valid,TwilioCode, TwilioMsg = models.incoming_phone_numbers.Incoming_Phone_Number.new(PhoneNumber = '+13015559999', request = None, AccountSid = self.Account.Sid)
  12 + self.PhoneNumber.put()
  13 + self.FakeToNumber = '+12405551234'
  14 + self.BodyText = 'Fake Body Text'
  15 + self.LongBodyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.'
  16 +
  17 + def test_Message_creation_success(self):
  18 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = self.PhoneNumber.PhoneNumber, Body = self.BodyText, Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  19 + self.assertTrue(Valid)
  20 + self.assertEqual(Message.To,self.FakeToNumber)
  21 + self.assertEqual(Message.From,self.PhoneNumber.PhoneNumber)
  22 + self.assertEqual(Message.Body,self.BodyText)
  23 +
  24 + def test_Message_creation_to_failure(self):
  25 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = 'adsfadsfasdf', From = self.PhoneNumber.PhoneNumber, Body = 'Good body text', Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  26 + self.assertFalse(Valid)
  27 + self.assertEqual(TwilioCode, 21401)
  28 +
  29 + def test_Message_creation_from_failure_mistyped_number(self):
  30 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = '+240555a123', Body = 'Good body text', Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  31 + self.assertFalse(Valid)
  32 + self.assertEqual(TwilioCode, 21401)
  33 +
  34 + def test_Message_creation_from_failure_not_allowed_number(self):
  35 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = '+5555555555', Body = 'Good body text', Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  36 + self.assertFalse(Valid)
  37 + self.assertEqual(TwilioCode, 14108)
  38 +
  39 + def test_Message_creation_body_blank_failure(self):
  40 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = self.PhoneNumber.PhoneNumber, Body = '', Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  41 + self.assertFalse(Valid)
  42 + self.assertEqual(TwilioCode, 14103)
  43 +
  44 + def test_Message_creation_body_long_failure(self):
  45 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = self.PhoneNumber.PhoneNumber, Body = self.LongBodyText, Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  46 + self.assertFalse(Valid)
  47 + self.assertEqual(TwilioCode, 21605)
  48 +
  49 + def test_Message_creation_callback_failure(self):
  50 + Message, Valid, TwilioCode, TwilioMsg = models.messages.Message.new(To = self.FakeToNumber, From = self.PhoneNumber.PhoneNumber, Body = 'Good body text', Direction = 'outbound-api', request = None, AccountSid = self.Account.Sid)
  51 +
1,311 webtest/__init__.py
... ... @@ -0,0 +1,1311 @@
  1 +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2 +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3 +"""
  4 +Routines for testing WSGI applications.
  5 +
  6 +Most interesting is TestApp
  7 +"""
  8 +
  9 +import sys
  10 +import random
  11 +import urllib
  12 +import urlparse
  13 +import mimetypes
  14 +import time
  15 +import cgi
  16 +import os
  17 +#import webbrowser
  18 +from Cookie import BaseCookie
  19 +try:
  20 + from cStringIO import StringIO
  21 +except ImportError:
  22 + from StringIO import StringIO
  23 +import re
  24 +from webob import Response, Request
  25 +from wsgiref.validate import validator
  26 +
  27 +__all__ = ['TestApp']
  28 +
  29 +def tempnam_no_warning(*args):
  30 + """
  31 + An os.tempnam with the warning turned off, because sometimes
  32 + you just need to use this and don't care about the stupid
  33 + security warning.
  34 + """
  35 + return os.tempnam(*args)
  36 +
  37 +class NoDefault(object):
  38 + pass
  39 +
  40 +try:
  41 + sorted
  42 +except NameError:
  43 + def sorted(l):
  44 + l = list(l)
  45 + l.sort()
  46 + return l
  47 +
  48 +class AppError(Exception):
  49 + pass
  50 +
  51 +class TestApp(object):
  52 +
  53 + # for py.test
  54 + disabled = True
  55 +
  56 + def __init__(self, app, extra_environ=None, relative_to=None):
  57 + """
  58 + Wraps a WSGI application in a more convenient interface for
  59 + testing.
  60 +
  61 + ``app`` may be an application, or a Paste Deploy app
  62 + URI, like ``'config:filename.ini#test'``.
  63 +
  64 + ``extra_environ`` is a dictionary of values that should go
  65 + into the environment for each request. These can provide a
  66 + communication channel with the application.
  67 +
  68 + ``relative_to`` is a directory, and filenames used for file
  69 + uploads are calculated relative to this. Also ``config:``
  70 + URIs that aren't absolute.
  71 + """
  72 + if isinstance(app, (str, unicode)):
  73 + from paste.deploy import loadapp
  74 + # @@: Should pick up relative_to from calling module's
  75 + # __file__
  76 + app = loadapp(app, relative_to=relative_to)
  77 + self.app = app
  78 + self.relative_to = relative_to
  79 + if extra_environ is None:
  80 + extra_environ = {}
  81 + self.extra_environ = extra_environ
  82 + self.reset()
  83 +
  84 + def reset(self):
  85 + """
  86 + Resets the state of the application; currently just clears
  87 + saved cookies.
  88 + """
  89 + self.cookies = {}
  90 +
  91 + def _make_environ(self, extra_environ=None):
  92 + environ = self.extra_environ.copy()
  93 + environ['paste.throw_errors'] = True
  94 + if extra_environ:
  95 + environ.update(extra_environ)
  96 + return environ
  97 +
  98 + def get(self, url, params=None, headers=None, extra_environ=None,
  99 + status=None, expect_errors=False):
  100 + """
  101 + Get the given url (well, actually a path like
  102 + ``'/page.html'``).
  103 +
  104 + ``params``:
  105 + A query string, or a dictionary that will be encoded
  106 + into a query string. You may also include a query
  107 + string on the ``url``.
  108 +
  109 + ``headers``:
  110 + A dictionary of extra headers to send.
  111 +
  112 + ``extra_environ``:
  113 + A dictionary of environmental variables that should
  114 + be added to the request.
  115 +
  116 + ``status``:
  117 + The integer status code you expect (if not 200 or 3xx).
  118 + If you expect a 404 response, for instance, you must give
  119 + ``status=404`` or it will be an error. You can also give
  120 + a wildcard, like ``'3*'`` or ``'*'``.
  121 +
  122 + ``expect_errors``:
  123 + If this is not true, then if anything is written to
  124 + ``wsgi.errors`` it will be an error. If it is true, then
  125 + non-200/3xx responses are also okay.
  126 +
  127 + Returns a ``webob.Response`` object.
  128 + """
  129 + environ = self._make_environ(extra_environ)
  130 + # Hide from py.test:
  131 + __tracebackhide__ = True
  132 + if params:
  133 + if not isinstance(params, (str, unicode)):
  134 + params = urllib.urlencode(params, doseq=True)
  135 + if '?' in url:
  136 + url += '&'
  137 + else:
  138 + url += '?'
  139 + url += params
  140 + url = str(url)
  141 + if '?' in url:
  142 + url, environ['QUERY_STRING'] = url.split('?', 1)
  143 + else:
  144 + environ['QUERY_STRING'] = ''
  145 + req = TestRequest.blank(url, environ)
  146 + if headers:
  147 + req.headers.update(headers)
  148 + return self.do_request(req, status=status,
  149 + expect_errors=expect_errors)
  150 +
  151 + def _gen_request(self, method, url, params='', headers=None, extra_environ=None,
  152 + status=None, upload_files=None, expect_errors=False):
  153 + """
  154 + Do a generic request.
  155 + """
  156 + environ = self._make_environ(extra_environ)
  157 + # @@: Should this be all non-strings?
  158 + if isinstance(params, (list, tuple, dict)):
  159 + params = urllib.urlencode(params)
  160 + if hasattr(params, 'items'):
  161 + params = urllib.urlencode(params.items())
  162 + if upload_files:
  163 + params = cgi.parse_qsl(params, keep_blank_values=True)
  164 + content_type, params = self.encode_multipart(
  165 + params, upload_files)
  166 + environ['CONTENT_TYPE'] = content_type
  167 + elif params:
  168 + environ.setdefault('CONTENT_TYPE', 'application/x-www-form-urlencoded')
  169 + if '?' in url:
  170 + url, environ['QUERY_STRING'] = url.split('?', 1)
  171 + else:
  172 + environ['QUERY_STRING'] = ''
  173 + environ['CONTENT_LENGTH'] = str(len(params))
  174 + environ['REQUEST_METHOD'] = method
  175 + environ['wsgi.input'] = StringIO(params)
  176 + req = TestRequest.blank(url, environ)
  177 + if headers:
  178 + req.headers.update(headers)
  179 + return self.do_request(req, status=status,
  180 + expect_errors=expect_errors)
  181 +
  182 + def post(self, url, params='', headers=None, extra_environ=None,
  183 + status=None, upload_files=None, expect_errors=False):
  184 + """
  185 + Do a POST request. Very like the ``.get()`` method.
  186 + ``params`` are put in the body of the request.
  187 +
  188 + ``upload_files`` is for file uploads. It should be a list of
  189 + ``[(fieldname, filename, file_content)]``. You can also use
  190 + just ``[(fieldname, filename)]`` and the file content will be
  191 + read from disk.
  192 +
  193 + Returns a ``webob.Response`` object.
  194 + """
  195 + return self._gen_request('POST', url, params=params, headers=headers,
  196 + extra_environ=extra_environ,status=status,
  197 + upload_files=upload_files,
  198 + expect_errors=expect_errors)
  199 +
  200 + def put(self, url, params='', headers=None, extra_environ=None,
  201 + status=None, upload_files=None, expect_errors=False):
  202 + """
  203 + Do a PUT request. Very like the ``.get()`` method.
  204 + ``params`` are put in the body of the request.
  205 +
  206 + ``upload_files`` is for file uploads. It should be a list of
  207 + ``[(fieldname, filename, file_content)]``. You can also use
  208 + just ``[(fieldname, filename)]`` and the file content will be
  209 + read from disk.
  210 +
  211 + Returns a ``webob.Response`` object.
  212 + """
  213 + return self._gen_request('PUT', url, params=params, headers=headers,
  214 + extra_environ=extra_environ,status=status,
  215 + upload_files=upload_files,
  216 + expect_errors=expect_errors)
  217 +
  218 + def delete(self, url, headers=None, extra_environ=None,
  219 + status=None, expect_errors=False):
  220 + """
  221 + Do a DELETE request. Very like the ``.get()`` method.
  222 + ``params`` are put in the body of the request.
  223 +
  224 + Returns a ``webob.Response`` object.
  225 + """
  226 + return self._gen_request('DELETE', url, params=params, headers=headers,
  227 + extra_environ=extra_environ,status=status,
  228 + upload_files=None, expect_errors=expect_errors)
  229 +
  230 + def encode_multipart(self, params, files):
  231 + """
  232 + Encodes a set of parameters (typically a name/value list) and
  233 + a set of files (a list of (name, filename, file_body)) into a
  234 + typical POST body, returning the (content_type, body).
  235 + """
  236 + boundary = '----------a_BoUnDaRy%s$' % random.random()
  237 + lines = []
  238 + for key, value in params:
  239 + lines.append('--'+boundary)
  240 + lines.append('Content-Disposition: form-data; name="%s"' % key)
  241 + lines.append('')
  242 + lines.append(value)
  243 + for file_info in files:
  244 + key, filename, value = self._get_file_info(file_info)
  245 + lines.append('--'+boundary)
  246 + lines.append('Content-Disposition: form-data; name="%s"; filename="%s"'
  247 + % (key, filename))
  248 + fcontent = mimetypes.guess_type(filename)[0]
  249 + lines.append('Content-Type: %s' %
  250 + fcontent or 'application/octet-stream')
  251 + lines.append('')
  252 + lines.append(value)
  253 + lines.append('--' + boundary + '--')
  254 + lines.append('')
  255 + body = '\r\n'.join(lines)
  256 + content_type = 'multipart/form-data; boundary=%s' % boundary
  257 + return content_type, body
  258 +
  259 + def _get_file_info(self, file_info):
  260 + if len(file_info) == 2:
  261 + # It only has a filename
  262 + filename = file_info[1]
  263 + if self.relative_to:
  264 + filename = os.path.join(self.relative_to, filename)
  265 + f = open(filename, 'rb')
  266 + content = f.read()
  267 + f.close()
  268 + return (file_info[0], filename, content)
  269 + elif len(file_info) == 3:
  270 + return file_info
  271 + else:
  272 + raise ValueError(
  273 + "upload_files need to be a list of tuples of (fieldname, "
  274 + "filename, filecontent) or (fieldname, filename); "
  275 + "you gave: %r"
  276 + % repr(file_info)[:100])
  277 +
  278 + def do_request(self, req, status, expect_errors):
  279 + """
  280 + Executes the given request (``req``), with the expected
  281 + ``status``. Generally ``.get()`` and ``.post()`` are used
  282 + instead.
  283 + """
  284 + __tracebackhide__ = True
  285 + errors = StringIO()