--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iro/tests/jsonresource.py Thu Sep 27 14:25:41 2012 +0200
@@ -0,0 +1,354 @@
+# Copyright (c) 2012 netzguerilla.net <iro@netzguerilla.net>
+#
+# This file is part of Iro.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+# #Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from twisted.trial import unittest
+
+from twisted.web.client import Agent, ResponseDone
+from twisted.internet import reactor
+from twisted.web.http_headers import Headers
+from twisted.python.failure import Failure
+from twisted.internet.protocol import Protocol
+from twisted.internet import defer
+
+from zope.interface import implements
+
+from twisted.internet.defer import succeed
+from twisted.web.iweb import IBodyProducer
+
+import json
+
+from twisted.web import server
+from txjsonrpc.web.jsonrpc import Fault
+
+from datetime import datetime
+
+from iro.model.schema import User, Offer, Userright, Job, Message
+
+from iro.view import jsonresource
+import iro.error as IroError
+
+from iro.test_helpers.dbtestcase import DBTestCase
+
+from iro.model import setEngine, setPool
+from iro.controller.pool import startPool, dbPool
+
+
+class StringProducer(object):
+ implements(IBodyProducer)
+
+ def __init__(self, body):
+ self.body = json.dumps(body)
+ self.length = len(self.body)
+
+ def startProducing(self, consumer):
+ consumer.write(self.body)
+ return succeed(None)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+class SimpleReceiver(Protocol):
+ def __init__(self, d):
+ self.d = d
+ self.data = ""
+
+ def dataReceived(self, data):
+ self.data += data
+
+ def connectionLost(self, reason):
+ try:
+ if reason.check(ResponseDone):
+ self.d.callback(json.loads(self.data))
+ else:
+ self.d.errback(reason)
+ except:
+ self.d.errback(Failure())
+
+class JSONRPCTest(DBTestCase):
+ """tests for the jsonrpc interface"""
+ ContentType = 'application/json'
+ StringProducer = StringProducer
+ def setUp(self):
+ DBTestCase.setUp(self)
+
+ setEngine(self.engine)
+ startPool(reactor)
+ setPool(dbPool)
+ self.p = reactor.listenTCP(0, server.Site(jsonresource.JSONFactory()),
+ interface="127.0.0.1")
+ self.port = self.p.getHost().port
+ self.agent = Agent(reactor)
+
+ def tearDown(self):
+ DBTestCase.tearDown(self)
+ return self.p.stopListening()
+
+ def proxy(self, method, data=None, **kargs):
+ d = self.agent.request(
+ 'GET',
+ 'http://localhost:%d/%s'%(self.port, method),
+ Headers({'Content-Type':[self.ContentType]}),
+ self.StringProducer(data) if data else None,
+ )
+
+ def cbResponse(response):
+ if kargs.has_key('code'):
+ self.failUnlessEqual(response.code, kargs["code"])
+ if response.code not in [200,400,500]:
+ raise Fault(response.code,'')
+ self.failUnlessEqual(response.headers.getRawHeaders('Content-Type'), ['application/json'])
+ d = defer.Deferred()
+ response.deliverBody(SimpleReceiver(d))
+ def _(data):
+ if data["status"]:
+ return data["result"]
+ else:
+ raise Fault(data["error"]["code"],data["error"]["msg"])
+
+ d.addCallback(_)
+ return d
+ d.addCallback(cbResponse)
+ return d
+
+
+ def testListMethods(self):
+ '''list of all offical Methods, that can be executed'''
+
+ def cbMethods(ret):
+ ret.sort()
+ self.failUnlessEqual(ret, ['bill', 'defaultRoute', 'email', 'fax', 'listMethods', 'mail', 'routes', 'sms', 'status', 'telnumber'])
+
+ d=self.proxy("listMethods", code=200)
+ d.addCallback(cbMethods)
+ return d
+
+ def testApikey(self):
+ ''' test apikey'''
+ with self.session() as session:
+ session.add(User(name='test',apikey='abcdef123456789'))
+
+ d = self.proxy('status', ['abcdef123456789'], code=200)
+ d.addCallback(lambda ret: self.failUnlessEqual(ret,{}))
+
+ return d
+
+ def testStatus(self):
+ ''' test the status function'''
+ with self.session() as session:
+ u = User(name='test',apikey='abcdef123456789')
+ session.add(u)
+ j = Job(info='info', status="started")
+ j.user=u
+ session.add(j)
+ session.commit()
+ jid=j.id
+ status = {str(jid):{"status":"started"}}
+ args=[('abcdef123456789',jid),
+ ('abcdef123456789',),
+ ('abcdef123456789', '', 'false'),
+ ('abcdef123456789', '', 0),
+ ('abcdef123456789', '', 0),
+ {"id":jid, "user":'abcdef123456789'},
+ ]
+ dl=[]
+ for a in args:
+ d = self.proxy('status', a, code=200)
+ d.addCallback(lambda ret: self.failUnlessEqual(ret,status))
+ dl.append(d)
+
+ def f(exc):
+ jnf = IroError.JobNotFound()
+ self.failUnlessEqual(exc.faultCode, jnf.code)
+ self.failUnlessEqual(exc.faultString, jnf.msg)
+ d = self.proxy('status', ['abcdef123456789', jid+1], code=500)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ dl.append(d)
+
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def testNoSuchUser(self):
+ '''a unknown user should raise a UserNotNound Exception
+ bewcause xmlrpc only has a Fault exception this Exception has to be deliverd through a xmlrpclib.Fault Exception'''
+ def f(exc):
+ unf = IroError.UserNotFound()
+ self.failUnlessEqual(exc.faultCode, unf.code)
+ self.failUnlessEqual(exc.faultString, unf.msg)
+ d = self.proxy('status', ['abcdef123456789'], code=500)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ return d
+
+ def testNoSuchMethod(self):
+ '''a unknown mothod should raise a Exception '''
+ def f(exc):
+ self.failUnlessEqual(exc.faultCode, 404)
+ self.failUnlessEqual(exc.faultString, '')
+ d = self.proxy('nosuchmethod', code=404)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ return d
+
+ def testValidationFault(self):
+ '''a validate Exception should be translated to a xmlrpclib.Fault.'''
+ def f(exc):
+ self.failUnlessEqual(exc.faultCode, 700)
+ self.failUnlessEqual(exc.faultString, "Validation of 'apikey' failed.")
+ d = self.proxy('status',{'user':'xxx'}, code=400)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ return d
+
+ @defer.inlineCallbacks
+ def testRoutes(self):
+ '''test the route function'''
+ with self.session() as session:
+ u=User(name='test',apikey='abcdef123456789')
+ o=Offer(name="sipgate_basic", provider="sipgate", route="basic", typ="sms")
+ u.rights.append(Userright(o))
+ session.add(u)
+
+ x = yield self.proxy('routes',['abcdef123456789','sms'], code=200)
+ self.failUnlessEqual(x,['sipgate_basic'])
+
+ def f(exc):
+ self.failUnlessEqual(exc.faultCode, 700)
+ self.failUnlessEqual(exc.faultString, "Typ fax is not valid.")
+ d = self.proxy('routes',['abcdef123456789','fax'], code=400)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ yield d
+
+ with self.session() as session:
+ o=Offer(name="sipgate_plus", provider="sipgate", route="plus", typ="sms")
+ u = session.query(User).filter_by(name="test").first()
+ u.rights.append(Userright(o))
+ o=Offer(name="faxde", provider="faxde", route="", typ="fax")
+ session.add(o)
+ session.commit()
+
+ x = yield self.proxy('routes',['abcdef123456789','sms'], code=200)
+ self.failUnlessEqual(x, ['sipgate_basic','sipgate_plus'])
+ x = yield self.proxy('routes', ['abcdef123456789','fax'], code=200)
+ self.failUnlessEqual(x, [])
+
+ with self.session() as session:
+ u = session.query(User).filter_by(name="test").first()
+ u.rights.append(Userright(o))
+
+ x = yield self.proxy('routes',['abcdef123456789','sms'], code=200)
+ self.failUnlessEqual(x, ['sipgate_basic','sipgate_plus'])
+ x = yield self.proxy('routes', ['abcdef123456789','fax'], code=200)
+ self.failUnlessEqual(x, ['faxde'])
+
+ def testDefaultRoutes(self):
+ '''test the defaultRoute function'''
+ with self.session() as session:
+ u=User(name='test',apikey='abcdef123456789')
+ o=Offer(name="sipgate_basic", provider="sipgate", route="basic", typ="sms")
+ u.rights.append(Userright(o,True))
+ o=Offer(name="sipgate_plus", provider="sipgate", route="plus", typ="sms")
+ u.rights.append(Userright(o))
+ session.add(u)
+ d = self.proxy('defaultRoute', ['abcdef123456789','sms'], code=200)
+ d.addCallback(lambda x: self.failUnlessEqual(x,['sipgate_basic']))
+
+ return d
+
+ def testTelnumbers(self):
+ '''test the telefon validator'''
+ dl = []
+ d = self.proxy('telnumber',[["0123/456(78)","+4912346785433","00123435456-658"]], code=200)
+ d.addCallback(lambda x: self.failUnlessEqual(x,True))
+ dl.append(d)
+
+ invalid=['xa','+1','1-23',';:+0','0123']
+ def f(exc):
+ self.failUnlessEqual(exc.faultCode, 701)
+ self.failUnlessEqual(exc.faultString, "No valid telnumber: '%s'" % invalid[0])
+ d = self.proxy('telnumber',[['01234']+invalid], code=400)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ dl.append(d)
+
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def testVaildEmail(self):
+ '''test vaild email adresses (got from wikipedia)'''
+ validmails=["niceandsimple@example.com"]
+ d = self.proxy('email', {"recipients":validmails}, code=200)
+ d.addCallback(lambda x: self.failUnlessEqual(x,True))
+ return d
+
+ def testInvaildEmail(self):
+ '''test invaild email adresses (got from wikipedia)'''
+ invalid=["Abc.example.com",'foo@t.de']
+ def f(exc):
+ self.failUnlessEqual(exc.faultCode, 702)
+ self.failUnlessEqual(exc.faultString, "No valid email: '%s'" % invalid[0])
+ d = self.proxy('email', [invalid], code=400)
+ d = self.assertFailure(d, Fault)
+ d.addCallback(f)
+ return d
+
+ def testBill(self):
+ '''test bill function'''
+ apikey='abcdef123456789'
+ with self.session() as session:
+ u=User(name='test',apikey=apikey)
+ session.add(u)
+ d = self.proxy('bill', {"user":apikey}, code=200)
+ d.addCallback(lambda x: self.failUnlessEqual(x,{'total':{'price':0.0,'anz':0}}))
+
+ return d
+
+ def testBillWithPrice(self):
+ apikey='abcdef123456789'
+ with self.session() as session:
+ u=User(name='test',apikey=apikey)
+ session.add(u)
+ o = Offer(name='sipgate_basic',provider="sipgate",route="basic",typ="sms")
+ u.rights.append(Userright(o))
+ j = Job(info='i',status='sended')
+ j.messages.append(Message(recipient='0123456789', isBilled=False, date=datetime.now() , price=0.4, offer=o))
+ u.jobs.append(j)
+
+ j = Job(info='a',status='sended')
+ j.messages.append(Message(recipient='0123456789', isBilled=False, date=datetime.now(), price=0.4, offer=o))
+ u.jobs.append(j)
+
+ def f(ret):
+ self.failUnlessEqual(ret['total'],{'price':0.8,'anz':2})
+ self.failUnlessEqual(ret['sipgate_basic'],
+ {'price':0.8,'anz':2,
+ 'info':{'i':{'price':0.4,'anz':1},
+ 'a':{'price':0.4,'anz':1},
+ }
+ })
+
+ d = self.proxy('bill', [apikey], code=200)
+ d.addCallback(f)
+ return d
+
+if __name__ == '__main__':
+ unittest.main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iro/view/jsonresource.py Thu Sep 27 14:25:41 2012 +0200
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 netzguerilla.net <iro@netzguerilla.net>
+#
+# This file is part of Iro.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+# #Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from twisted.web import resource, server, http
+from twisted.python import log, failure
+from twisted.internet import defer
+
+from ..controller.viewinterface import Interface
+from ..error import ValidateException
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+class TwistedInterface(Interface):
+ """Class that addes needed function for JSON"""
+ def __init__(self):
+ Interface.__init__(self)
+
+ def listMethods(self):
+ """Since we override lookupProcedure, its suggested to override
+ listProcedures too.
+ """
+ return self.listProcedures()
+
+
+ def listProcedures(self):
+ """returns a list of all functions that are allowed to call via XML-RPC."""
+ return ['listMethods','status','sms','fax','mail','routes','defaultRoute','bill','telnumber','email']
+
+class MethodFactory(resource.Resource):
+ def __init__(self,method,twistedInterface):
+ self.method = method
+ self.twistedInterface = twistedInterface
+
+ def render(self,request):
+ try:
+ args = []
+ if request.getHeader('Content-Type') == 'application/x-www-form-urlencoded':
+ args = {}
+ for a in request.args:
+ value = request.args[a]
+ if a != "recipients" and len(value) == 1:
+ value = value[0]
+ args[a] = value
+ elif request.getHeader('Content-Type') == 'application/json':
+ content = request.content.read()
+ if content:
+ args = json.loads(content)
+ if args is None:
+ args = []
+ else:
+ request.setResponseCode(http.NOT_ACCEPTABLE)
+ return "Only application/x-www-form-urlencoded or application/json ist allowed for Content-Type"
+ if isinstance(args,list):
+ d = defer.maybeDeferred(getattr(self.twistedInterface,self.method),*args)
+ else:
+ d = defer.maybeDeferred(getattr(self.twistedInterface,self.method), **args)
+ d.addCallback(self._cbRender, request)
+ d.addErrback(self._ebRender, request)
+ d.addBoth(lambda _: request.finish())
+ return server.NOT_DONE_YET
+ except Exception:
+ log.err(failure.Failure())
+ request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ err= {
+ "code" : 999,
+ "msg" : "Unknown error.",
+ }
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({"status":False, "error":err})
+
+
+ def _cbRender(self,result,request):
+ request.setHeader('Content-Type', 'application/json')
+ request.write(json.dumps({"status":True, "result":result}))
+
+ def _ebRender(self, failure, request):
+ if isinstance(failure.value, ValidateException):
+ request.setResponseCode(http.BAD_REQUEST)
+ else:
+ request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+
+ err= {
+ "code" : 999,
+ "msg" : "Unknown error.",
+ }
+
+ try:
+ err["code"]=failure.value.code
+ err["msg"]=failure.value.msg
+ except Exception:
+ log.err(failure)
+ pass
+ request.setHeader('Content-Type', 'application/json')
+ request.write(json.dumps({"status":False, "error":err}))
+
+class JSONFactory(resource.Resource):
+ """JSON factory"""
+ def __init__(self):
+ resource.Resource.__init__(self)
+ self.twistedInterface = TwistedInterface()
+ for method in self.twistedInterface.listProcedures():
+ self.putChild(method, MethodFactory(method, self.twistedInterface))
+
+
+def appendResource(root):
+ """adding JSON to root."""
+ root.putChild('json', JSONFactory())
+
+if __name__ == '__main__':
+ from twisted.web import resource
+ from twisted.internet import reactor
+
+ root = resource.Resource()
+ root = appendResource(root)
+ reactor.listenTCP(7080, server.Site(root))
+ reactor.run()