diff --git a/mjacob2/LICENSE b/mjacob2/LICENSE new file mode 100644 index 0000000..3ec2114 --- /dev/null +++ b/mjacob2/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2010 Manuel Jacob + +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. diff --git a/mjacob2/bin/pypsyc b/mjacob2/bin/pypsyc new file mode 100755 index 0000000..9357005 --- /dev/null +++ b/mjacob2/bin/pypsyc @@ -0,0 +1,42 @@ +#!/usr/bin/env python +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import logging +logging.basicConfig() +from optparse import OptionParser +from os import path, mkdir + + +def main(): + parser = OptionParser() + parser.add_option('-c', '--config', dest='config_dir', + default=path.expanduser(path.join('~', '.pypsyc'))) + options, args = parser.parse_args() + + if not path.exists(options.config_dir): + mkdir(options.config_dir) + + accounts_file = path.join(options.config_dir, 'accounts') + if not path.exists(accounts_file): + with open(accounts_file, 'w') as f: + f.write('[]') + + from twisted.internet import gtk2reactor + gtk2reactor.install() + + from pypsyc.client.controller import MainController + from pypsyc.client.model import Client + from pypsyc.client.view import MainView + + model = Client(accounts_file=accounts_file) + view = MainView() + MainController(model, view) + model.load_accounts() + + +if __name__ == '__main__': + main() + from twisted.internet import reactor + reactor.run() diff --git a/mjacob2/bin/pypsycd b/mjacob2/bin/pypsycd new file mode 100755 index 0000000..3412347 --- /dev/null +++ b/mjacob2/bin/pypsycd @@ -0,0 +1,45 @@ +#!/usr/bin/env python +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import logging +logging.basicConfig() +from optparse import OptionParser +from os import path, mkdir +from socket import gethostname + +from twisted.internet import reactor + +from pypsyc.server import Server + + +def main(): + parser = OptionParser() + parser.add_option('-d', '--directory', dest='directory', + default=path.expanduser(path.join('~', '.pypsycd')), + help="directory where the database is stored " + "(defaults to ~/.pypsycd)") + parser.add_option('-H', '--hostname', dest='hostname', + help="hostname of the psyc server") + parser.add_option('-i', '--interface', dest='interface', default='', + help="interface to bind to (defaults to all)") + parser.add_option('-p', '--psyc-port', type='int', default=4404, + help="port to listen for incoming psyc connections " + "or 0 to disable them (defaults to 4404)") + parser.add_option('-w', '--webif-port', type='int', default=8080, + help="port of the web interface or 0 to disable it " + "(defaults to 8080)") + options = parser.parse_args()[0] + + if not path.exists(options.directory): + mkdir(options.directory) + Server(hostname=options.hostname or gethostname(), + interface=options.interface, + psyc_port=options.psyc_port, + webif_port=options.webif_port, + db_file=path.join(options.directory, 'database')) + reactor.run() + +if __name__ == '__main__': + main() diff --git a/mjacob2/pypsyc/__init__.py b/mjacob2/pypsyc/__init__.py new file mode 100644 index 0000000..0467527 --- /dev/null +++ b/mjacob2/pypsyc/__init__.py @@ -0,0 +1,8 @@ +""" + pypsyc + ~~~~~~ + + Pypsyc is a pythonic framework for PSYC servers and clients. +""" + +__version__ = '0.0dev' diff --git a/mjacob2/pypsyc/client/__init__.py b/mjacob2/pypsyc/client/__init__.py new file mode 100644 index 0000000..2954064 --- /dev/null +++ b/mjacob2/pypsyc/client/__init__.py @@ -0,0 +1,4 @@ +""" + pypsyc.client + ~~~~~~~~~~~~~ +""" diff --git a/mjacob2/pypsyc/client/controller.py b/mjacob2/pypsyc/client/controller.py new file mode 100644 index 0000000..0c5a7e4 --- /dev/null +++ b/mjacob2/pypsyc/client/controller.py @@ -0,0 +1,383 @@ +""" + pypsyc.client.controller + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Classes which connect the models with the views. They observe the models + and update the view. They are injected into the views and have methods + that can change the models after user input validation. + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from functools import partial + +from pypsyc.client.model import Account +from pypsyc.client.observable import ObsList +from pypsyc.util import schedule + + +def _observe_list(obs_list, insert, delitem, update, *args): + if insert: + for i, v in enumerate(obs_list): + insert(i, v, *args) + obs_list.insert_evt.add_observer(insert, *args) + if delitem is not None: + obs_list.delitem_evt.add_observer(delitem) + if update is not None: + obs_list.update_evt += update + +def _observe_dict(obs_dict, setitem, delitem, update, *args): + if setitem is not None: + for k, v in obs_dict.iteritems(): + setitem(k, None, v, *args) + obs_dict.setitem_evt.add_observer(setitem, *args) + if delitem is not None: + obs_dict.delitem_evt += delitem + if update is not None: + obs_dict.update_evt += update + +def _unobserve_list(obs_list, insert, delitem, update): + if insert is not None: + obs_list.insert_evt -= insert + if delitem is not None: + obs_list.delitem_evt -= delitem + for v in obs_list: + delitem(0, v) + if update is not None: + obs_list.update_evt -= update + +def _unobserve_dict(obs_dict, setitem, delitem, update): + if setitem is not None: + obs_dict.setitem_evt -= setitem + if delitem is not None: + obs_dict.delitem_evt -= delitem + for k, v in obs_dict.iteritems(): + delitem(k, v) + if update is not None: + obs_dict.update_evt -= update + + +class ListBinding(object): + def __init__(self, model_list, view_list, make_row): + self.model_list = model_list + self.view_list = view_list + self.make_row = make_row + _observe_list(model_list, self._add, self._del, self._update) + + def unbind(self): + _unobserve_list(self.model_list, self._add, self._del, self._update) + + def _add(self, index, obj): + self.view_list.append(self.make_row(obj)) + + def _del(self, index, obj): + del self.view_list[index] + + def _update(self, index, obj): + self.view_list[index] = self.make_row(obj) + + +class AccountsController(object): + def __init__(self, client, view): + self.client = client + self.view = view + view.controller = self + + self.binding = ListBinding(self.client.accounts, view.accounts, + lambda obj: (obj.uni, obj.active)) + + def add_account(self): + account = Account(self.client, "", "", "", False, False) + self.view.show_addedit_dialog(account.__dict__, True, + partial(self._saved, account, True)) + + def edit_account(self, pos): + account = self.client.accounts[pos] + self.view.show_addedit_dialog(account.__dict__, False, + partial(self._saved, account, False)) + + def _saved(self, account, add): + if add: + self.client.accounts.append(account) + else: + self.client.accounts.updated_item(account) + self.client.save_accounts() + + def remove_account(self, pos): + del self.client.accounts[pos] + self.client.save_accounts() + + def set_active(self, pos, active): + account = self.client.accounts[pos] + account.active = active + self.client.accounts.updated_item(account) + self.client.save_accounts() + + def closed(self): + self.binding.unbind() + + +class DumpController(object): + def __init__(self, client, view): + self.view = view + view.controller = self + self.accounts = client.accounts + _observe_list(self.accounts, self._add, self._del, None) + + def _add(self, index, account): + self._update(None, getattr(account, 'circuit', None), account.uni) + account.update_evt['circuit'].add_observer(self._update, account.uni) + + def _del(self, index, account): + account.update_evt['circuit'] -= self._update + if hasattr(account, 'circuit'): + account.circuit.dump_evt -= self.view.show_line + + def _update(self, old, value, account_uni): + if old is not None: + old.dump_evt -= self.view.show_line + if value is not None: + value.dump_evt.add_observer(self.view.show_line, account_uni) + + def closed(self): + _unobserve_list(self.accounts, self._add, self._del, None) + + +class ConversationController(object): + def __init__(self, tabs_controller, conversation, view): + self.tabs_controller = tabs_controller + self.conversation = conversation + self.view = view + view.controller = self + + conversation.unknown_target_evt += view.show_unknown_target + conversation.delivery_failed_evt += view.show_delivery_failed + _observe_list(conversation.messages, self.add_message, None, None) + + def add_message(self, index, message): + line = "(%s) <%s> %s" % (message.time.strftime("%H:%M:%S"), + message.source, message.message) + self.view.show_message(line) + + def enter(self, text): + if text.startswith('/'): + self.view.show_unknown_command() + else: + self.conversation.send_message(text) + + def closed(self): + _unobserve_list(self.conversation.messages, self.add_message, None, None) + self.conversation.unknown_target_evt -= self.view.show_unknown_target + self.conversation.delivery_failed_evt -= self.view.show_delivery_failed + + +class ConferenceController(ConversationController): + def __init__(self, tabs_controller, conference, view): + self.conference = conference + ConversationController.__init__(self, tabs_controller, conference, + view) + self.binding = ListBinding(conference.members, view.members, + lambda member: (member.uni, member.nick)) + + def open_conversation(self, pos): + account = self.conference.conferencing.account + member = self.conference.members[pos] + conversation = account.get_conversation(member.uni) + self.tabs_controller.focus_conversation(conversation) + + def closed(self): + self.binding.unbind() + ConversationController.closed(self) + + +class TabsController(object): + def __init__(self, client, view): + self.view = view + view.controller = self + + self.view_to_model = {} + self.model_to_controller = {} + _observe_list(client.accounts, self._add_account, self._del_account, None) + + def _add_account(self, index, account): + _observe_dict(account.conversations, self._add_conversation, + self._del_conversation, None, account.conversations, + self.view.show_conversation, ConversationController) + _observe_dict(account.conferences, self._add_conversation, + self._del_conversation, None, account.conferences, + self.view.show_conference, ConferenceController) + + def _del_account(self, index, account): + _unobserve_dict(account.conversations, self._add_conversation, + self._del_conversation, None) + _unobserve_dict(account.conferences, self._add_conversation, + self._del_conversation, None) + + def _add_conversation(self, uni, old, conversation, list, show, Class): + conv_view = show(conversation.uni) + controller = Class(self, conversation, conv_view) + self.view_to_model[conv_view] = list, conversation.uni + self.model_to_controller[conversation] = controller, conv_view + + def _del_conversation(self, uni, conversation): + controller, view = self.model_to_controller.pop(conversation) + del self.view_to_model[view] + self.view.remove_tab(view) + controller.closed() + + def focus_conversation(self, conversation): + self.view.focus_tab(self.model_to_controller[conversation][1]) + + def close_tab(self, view): + list, uni = self.view_to_model[view] + del list[uni] + + +class FriendListController(object): + def __init__(self, client, view, tabs_controller): + self.view = view + view.controller = self + self.tabs_controller = tabs_controller + self.friends = ObsList() + self.binding = ListBinding(self.friends, view.friends, self._make_row) + + _observe_list(client.accounts, self._add_account, self._del_account, None) + + def _add_account(self, index, account): + _observe_dict(account.friends, self._add_friend, self._del_friend, + self._update_friend) + + def _del_account(self, index, account): + _unobserve_dict(account.friends, self._add_friend, self._del_friend, + self._update_friend) + + def _add_friend(self, uni, old, friend): + self.friends.append(friend) + + def _del_friend(self, uni, friend): + self.friends.remove(friend) + + def _update_friend(self, uni, friend): + self.friends.updated_item(friend) + + def _make_row(self, friend): + uni = friend.uni + if uni.startswith('psyc://' + friend.account.server + '/'): + uni = uni.rpartition('/~')[2] + return (uni, friend.presence.availability > 1, friend.state) + + def open_conversation(self, pos): + friend = self.friends[pos] + model = friend.account.get_conversation(friend.uni) + self.tabs_controller.focus_conversation(model) + + def accept_friendship(self, pos): + friend = self.friends[pos] + schedule(friend.account.add_friend, friend.uni) + + def cancel_friendship(self, pos): + friend = self.friends[pos] + schedule(friend.account.remove_friend, friend.uni) + + +class MainController(object): + def __init__(self, client, view): + self.client = client + self.view = view + view.controller = self + self.active_accounts = set() + + self.tabs_controller = TabsController(client, view.tabs_view) + FriendListController(client, view.friends_view, self.tabs_controller) + + _observe_list(client.accounts, self._add_account, self._del_account, + self._update_account) + + def _add_account(self, index, account): + account.no_password_evt.add_observer(self._no_password, account) + account.connection_error_evt.add_observer(self._conn_error, account) + account.no_such_user_evt += self._no_such_user + account.auth_error_evt.add_observer(self._auth_error, account) + if account.active: + self._account_activated(account) + + def _del_account(self, index, account): + account.no_password_evt -= self._no_password + account.connection_error_evt -= self._conn_error + account.no_such_user_evt -= self._no_such_user + account.auth_error_evt -= self._auth_error + if account.active: + self._account_deactivated(account) + + def _update_account(self, index, account): + if account.active: + self._account_activated(account) + else: + self._account_deactivated(account) + + def _account_activated(self, account): + had_active_accounts = bool(self.active_accounts) + self.active_accounts.add(account) + if not had_active_accounts: + self.view.show_active_accounts(True) + + def _account_deactivated(self, account): + had_active_accounts = bool(self.active_accounts) + self.active_accounts.discard(account) + if had_active_accounts and not self.active_accounts: + self.view.show_active_accounts(False) + + def _no_password(self, account): + def callback(): + account.active = True + self.client.accounts.updated_item(account) + self.client.save_accounts() + self.view.show_password_dialog(account.uni, account.__dict__, callback) + + def _conn_error(self, error, account): + self.view.show_conn_error(account.uni, error.args[0]) + + def _no_such_user(self, error): + self.view.show_no_such_user(error.args[0]) + + def _auth_error(self, error, account): + self.view.show_auth_error(account.uni, error.args[0]) + + def open_accounts(self): + AccountsController(self.client, self.view.show_accounts()) + + def open_dump(self): + DumpController(self.client, self.view.show_dump_win()) + + def open_conversation(self): + accounts = [acc for acc in self.client.accounts if acc.active] + def callback(account, server, person): + uni = 'psyc://%s/~%s' % (server, person) + model = accounts[account].get_conversation(uni) + self.tabs_controller.focus_conversation(model) + + self.view.show_open_conv_dialog((account.uni for account in accounts), + callback) + + def open_conference(self): + accounts = [acc for acc in self.client.accounts if acc.active] + def callback(account, server, place): + uni = 'psyc://%s/@%s' % (server, place) + model = accounts[account].get_conference(uni, subscribe=True) + self.tabs_controller.focus_conversation(model) + + self.view.show_open_conf_dialog((account.uni for account in accounts), + callback) + + def add_friend(self): + accounts = [acc for acc in self.client.accounts if acc.active] + def callback(account, server, person): + uni = 'psyc://%s/~%s' % (server, person) + schedule(accounts[account].add_friend, uni) + + self.view.show_add_friend_dialog((account.uni for account in accounts), + callback) + def quit(self): + _unobserve_list(self.client.accounts, self._add_account, + self._del_account, self._update_account) + self.client.quit() diff --git a/mjacob2/pypsyc/client/model.py b/mjacob2/pypsyc/client/model.py new file mode 100644 index 0000000..19f2c0b --- /dev/null +++ b/mjacob2/pypsyc/client/model.py @@ -0,0 +1,349 @@ +""" + pypsyc.client.model + ~~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from collections import namedtuple, defaultdict +from datetime import datetime +from json import dump, load + +from twisted.internet import reactor +from twisted.internet.protocol import ClientFactory + +from pypsyc.core.mmp import Circuit, Uni +from pypsyc.core.psyc import PSYCObject +from pypsyc.client.observable import ObsObj, ObsAttr, ObsList, ObsDict +from pypsyc.protocol import (UnknownTargetError, DeliveryFailedError, + AuthenticationError, LinkingClient as LinkingProtocol, + RoutingErrorClient as RoutingErrorProtocol, + Messaging as MessagingProtocol, + ConferencingClient as ConferencingProtocol, + FriendshipClient as FriendshipProtocol, + ClientInterfaceClient as ClientInterfaceProtocol) +from pypsyc.util import (schedule, Event, resolve_hostname, connect, + key_intersection) + + +class Message(object): + def __init__(self, source, message): + self.source = source + self.message = message + self.time = datetime.now() + + +class Conversation(object): + def __init__(self, messaging, uni): + self.messaging = messaging + self.uni = uni + self.messages = ObsList() + self.unknown_target_evt = Event() + self.delivery_failed_evt = Event() + + def send_message(self, message): + self.messaging.protocol.send_private_message( + self.uni, message, relay=self.messaging.account.uni) + self.messages.append(Message(self.messaging.account.uni, message)) + + +Member = namedtuple('Member', ('uni', 'nick')) + + +class Conference(object): + def __init__(self, conferencing, uni): + self.conferencing = conferencing + self.uni = uni + self.members = ObsList() + self.messages = ObsList() + self.unknown_target_evt = Event() + self.delivery_failed_evt = Event() + + def send_message(self, message): + self.conferencing.protocol.send_public_message(self.uni, message) + + +class Messaging(object): + def __init__(self, account): + self.account = account + self.protocol = MessagingProtocol(self) + + def private_message(self, source, message): + conversation = self.account.get_conversation(source) + conversation.messages.append(Message(source, message)) + + def connected(self, circuit): + self.psyc = circuit.psyc + self.psyc.add_handler(self.protocol) + + def disconnected(self): + pass + + +class Conferencing(object): + def __init__(self, account): + self.account = account + self.protocol = ConferencingProtocol(self) + + def member_entered(self, place, uni, nick): + conference = self.account.get_conference(place) + conference.members.append(Member(uni, nick)) + + def member_left(self, place, uni): + conference = self.account.get_conference(place) + for i in conference.members: + if i.uni == uni: + conference.members.remove(i) + + def public_message(self, place, source, message): + conference = self.account.get_conference(place) + conference.messages.append(Message(source, message)) + + def connected(self, circuit): + self.psyc = circuit.psyc + self.psyc.add_handler(self.protocol) + circuit.add_pvar_handler(self.protocol) + + def disconnected(self): + pass + + +Presence = namedtuple('Presence', ('availability',)) + + +class Friend(object): + def __init__(self, account, uni): + self.account = account + self.uni = uni + self.presence = Presence(0) + + +class FriendList(object): + def __init__(self, account): + self.account = account + self.protocol = FriendshipProtocol(self) + + def friendship(self, uni, state): + friend = self.account.friends.get(uni) or Friend(self.account, uni) + friend.state = state + if uni in self.account.friends: + self.account.friends.updated_item(uni) + else: + self.account.friends[uni] = friend + + def friendship_removed(self, uni): + del self.account.friends[uni] + + def presence(self, context, value): + self.account.friends[context].presence = Presence(value) + self.account.friends.updated_item(context) + + def connected(self, circuit): + self.psyc = circuit.psyc + self.psyc.add_handler(self.protocol) + circuit.add_pvar_handler(self.protocol) + + def disconnected(self): + self.account.friends.clear() + + +class Account(ObsObj): + _active = False + circuit = ObsAttr('circuit') + + def __init__(self, client, server, person, password, save_password, + active): + ObsObj.__init__(self) + self.no_password_evt = Event() + self.connection_error_evt = Event() + self.no_such_user_evt = Event() + self.auth_error_evt = Event() + + self.client = client + self.server = server + self.person = person + self.resource = "*Resource" + self.password = password + self.save_password = save_password + schedule(setattr, self, 'active', active) + + self.conversations = ObsDict() + self.conferences = ObsDict() + self.conferences.delitem_evt += self._del_conference + self.friends = ObsDict() + self.messaging = Messaging(self) + self.conferencing = Conferencing(self) + self.friend_list = FriendList(self) + + @property + def uni(self): + return Uni.from_parts([self.server, '~' + self.person]) + + def get_conversation(self, uni): + if not uni in self.conversations: + self.conversations[uni] = Conversation(self.messaging, uni) + return self.conversations[uni] + + def get_conference(self, uni, subscribe=False): + if not uni in self.conferences: + self.conferences[uni] = Conference(self.conferencing, uni) + if subscribe: + schedule(self._subscribe, uni) + return self.conferences[uni] + + def _subscribe(self, uni): + try: + self.client_interface.subscribe(uni) + except UnknownTargetError: + self.conferences[uni].unknown_target_evt() + except DeliveryFailedError as e: + self.conferences[uni].delivery_failed_evt(e.args[0]) + + def _del_conference(self, uni, old): + schedule(self.client_interface.unsubscribe, uni) + + def add_friend(self, uni): + self.client_interface.add_friend(uni) + + def remove_friend(self, uni): + self.client_interface.remove_friend(uni) + + def unknown_target_error(self, uni): + conversation = self.conversations.get(uni) or self.conferences.get(uni) + if conversation: + conversation.unknown_target_evt() + + def delivery_failed_error(self, uni, message): + conversation = self.conversations.get(uni) or self.conferences.get(uni) + if conversation: + conversation.delivery_failed_evt(message) + + @property + def active(self): + return self._active + + @active.setter + def active(self, active): + if active != self._active: + self._active = active + if active: + schedule(self._activate) + elif hasattr(self, 'circuit'): + self._deactivate() + + def _activate(self): + if not self.save_password and not self.password: + self.active = False + self.no_password_evt() + return + try: + ip, port = resolve_hostname(self.server) + circuit = self.circuit = connect(ip, port, self.client) + except Exception as e: + return self._error(self.connection_error_evt, e) + try: + linking_protocol = LinkingProtocol(circuit) + linking_protocol.link(self.uni, self.resource, self.password) + self.psyc = circuit.psyc + self.psyc.uni = self.uni.chain(self.resource) + except UnknownTargetError as e: + return self._error(self.no_such_user_evt, e) + except AuthenticationError as e: + return self._error(self.auth_error_evt, e) + + circuit.psyc.add_handler(RoutingErrorProtocol(self)) + self.client_interface = ClientInterfaceProtocol(self) + self.messaging.connected(circuit) + self.conferencing.connected(circuit) + self.friend_list.connected(circuit) + + def _deactivate(self): + self.circuit.transport.loseConnection() + del self.circuit + + self.messaging.disconnected() + self.conferencing.disconnected() + self.friend_list.disconnected() + + def _error(self, event, error): + self.active = False + self.client.accounts.updated_item(self) + event(error) + + def connection_error(self, error): + self._error(self.connection_error_evt, error) + + +class ClientCircuit(Circuit): + def __init__(self): + self.dump_evt = Event() + Circuit.__init__(self) + self.psyc = PSYCObject(self.send) + self.pvar_handlers = defaultdict(dict) + + def connectionMade(self): + if hasattr(self.transport, 'write'): + self.orig_write = self.transport.write + self.transport.write = self.dump_write + Circuit.connectionMade(self) + + def dump_write(self, data): + for line in data.strip('\n').split('\n'): + self.dump_evt('o', line) + self.orig_write(data) + + def lineReceived(self, line): + self.dump_evt('i', line) + return Circuit.lineReceived(self, line) + + def packet_received(self, header, content): + if '_source_relay' in header: + assert header.source.is_ancestor_of(self.psyc.uni) + header.source = Uni(header['_source_relay']) + packet = self.psyc.handle_packet(header, content) + + for modifier in key_intersection(self.pvar_handlers, packet.modifiers): + handlers = self.pvar_handlers[modifier] + vars = packet.modifiers[modifier] + for var in key_intersection(handlers, vars): + handlers[var](packet.header.context, vars) + + def add_pvar_handler(self, handler): + for i in dir(handler): + if i.startswith('state_'): + _, operation, var = i.split('_', 2) + modifier = {'set': '=', 'add': '+', 'remove': '-'}[operation] + self.pvar_handlers[modifier]['_' + var] = getattr(handler, i) + + +class Client(ClientFactory, object): + protocol = ClientCircuit + + def __init__(self, accounts_file=None): + self.accounts = ObsList() + self.accounts_file = accounts_file + + def load_accounts(self): + with open(self.accounts_file, 'r') as f: + self.accounts.extend(Account(self, d['server'].encode('utf-8'), + d['person'].encode('utf-8'), + d['password'].encode('utf-8'), + d['save_password'], d['active']) + for d in load(f)) + + def save_accounts(self): + with open(self.accounts_file, 'w') as f: + dump(self.accounts, f, indent=4, default=lambda a: + {'server': a.server, 'person': a.person, + 'password': a.password if a.save_password else '', + 'save_password': a.save_password, 'active': a.active}) + + def connection_lost(self, circuit, error): + for account in self.accounts: + if getattr(account, 'circuit', None) is circuit: + account.connection_error(Exception(str(error))) + + def quit(self): + for account in self.accounts: + account.active = False + reactor.stop() diff --git a/mjacob2/pypsyc/client/observable.py b/mjacob2/pypsyc/client/observable.py new file mode 100644 index 0000000..fd6ac0b --- /dev/null +++ b/mjacob2/pypsyc/client/observable.py @@ -0,0 +1,108 @@ +""" + pypsyc.client.observable + ~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from collections import MutableSequence, MutableMapping + +from pypsyc.util import Event + + +class ObsAttr(object): + def __init__(self, name): + self.name = name + + def __get__(self, instance, owner): + try: + return instance.__dict__[self.name] + except KeyError: + raise AttributeError + + def __set__(self, instance, value): + old = instance.__dict__.get(self.name) + instance.__dict__[self.name] = value + if value != old: + instance.update_evt[self.name](old, value) + + def __delete__(self, instance): + old = instance.__dict__.get(self.name) + try: + del instance.__dict__[self.name] + except KeyError: + raise AttributeError + instance.update_evt[self.name](old, None) + + +class ObsObj(object): + def __init__(self): + self.update_evt = dict((obs_attr.name, Event()) for obs_attr in + self.__class__.__dict__.itervalues() + if isinstance(obs_attr, ObsAttr)) + + +class ObsList(list, MutableSequence): + def __init__(self, *args, **kwds): + list.__init__(self, *args, **kwds) + + self.setitem_evt = Event() + self.delitem_evt = Event() + self.insert_evt = Event() + self.update_evt = Event() + + def __setitem__(self, index, value): + old = self[index] + list.__setitem__(self, index, value) + if value != old: + self.setitem_evt(index, old, value) + + def __delitem__(self, index): + if index < 0: + index = len(self) + index + old = self[index] + list.__delitem__(self, index) + self.delitem_evt(index, old) + + def insert(self, index, value): + list.insert(self, index, value) + self.insert_evt(index, value) + + append = MutableSequence.append + reverse = MutableSequence.reverse + extend = MutableSequence.extend + pop = MutableSequence.pop + remove = MutableSequence.remove + __iadd__ = MutableSequence.__iadd__ + + def updated_item(self, obj): + self.update_evt(self.index(obj), obj) + + +class ObsDict(dict, MutableMapping): + def __init__(self, *args, **kwds): + dict.__init__(self, *args, **kwds) + + self.setitem_evt = Event() + self.delitem_evt = Event() + self.update_evt = Event() + + def __setitem__(self, key, value): + old = self.get(key) + dict.__setitem__(self, key, value) + if value != old: + self.setitem_evt(key, old, value) + + def __delitem__(self, key): + old = self.get(key) + dict.__delitem__(self, key) + self.delitem_evt(key, old) + + pop = MutableMapping.pop + popitem = MutableMapping.popitem + clear = MutableMapping.clear + update = MutableMapping.update + setdefault = MutableMapping.setdefault + + def updated_item(self, key): + self.update_evt(key, self[key]) diff --git a/mjacob2/pypsyc/client/psyc.ico b/mjacob2/pypsyc/client/psyc.ico new file mode 100644 index 0000000..e654174 Binary files /dev/null and b/mjacob2/pypsyc/client/psyc.ico differ diff --git a/mjacob2/pypsyc/client/view.py b/mjacob2/pypsyc/client/view.py new file mode 100644 index 0000000..70c5ef9 --- /dev/null +++ b/mjacob2/pypsyc/client/view.py @@ -0,0 +1,686 @@ +""" + pypsyc.client.view + ~~~~~~~~~~~~~~~~~~ + + pypsyc GTK views + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import pygtk +pygtk.require('2.0') +import gtk +import pango +from gobject import idle_add +from pkg_resources import resource_filename + + +def _(message): + return message + + +class Form(gtk.HBox): + def __init__(self, dict_, attrs): + gtk.HBox.__init__(self) + self.dict = dict_ + self.fields = [] + + left = gtk.VBox() + self.pack_start(left, False, False, 10) + + right = gtk.VBox() + self.pack_start(right) + + for l, a in attrs: + x = dict_[a] + le, ri, value_func = getattr(self, '_' + type(x).__name__)(l, x) + left.pack_start(le, padding=10) + right.pack_start(ri) + self.fields.append((a, value_func)) + + def _str(self, l, s): + label = gtk.Label(_(l) + ":") + label.set_alignment(0, 0.5) + entry = gtk.Entry() + entry.set_text(s) + if l.lower() == 'password': + entry.set_visibility(False) + return label, entry, entry.get_text + + def _bool(self, l, b): + check = gtk.CheckButton(_(l)) + check.set_active(b) + return gtk.Label(), check, check.get_active + + def _list(self, l, list_): + label = gtk.Label(_(l) + ":") + label.set_alignment(0, 0.5) + combobox = gtk.combo_box_new_text() + for i in list_: + combobox.append_text(i) + combobox.set_active(0) + return label, combobox, combobox.get_active + + _generator = _list + + def save(self): + self.dict.update((a, f()) for a, f in self.fields) + return self.dict + +def _open_form_dialog(title, window, dict_, attrs, buttons, func, markup=None): + dialog = gtk.Dialog(title, window, + (gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT | + gtk.DIALOG_NO_SEPARATOR), buttons) + if markup is not None: + label = gtk.Label() + label.set_markup(markup) + dialog.vbox.pack_start(label) + form = Form(dict_, attrs) + dialog.vbox.pack_start(form) + dialog.connect('response', _dialog_response, form, func) + dialog.show_all() + +def _dialog_response(w, response, form, func): + if response == gtk.RESPONSE_ACCEPT: + func(form.save()) + if response != gtk.RESPONSE_DELETE_EVENT: + w.destroy() + + +class AccountsView(object): + def __init__(self): + self.window = self.create_window() + self.window.show_all() + + def create_window(self): + win = gtk.Window() + win.set_title(_("Accounts")) + win.connect('destroy', lambda w: self.controller.closed()) + + vbox = gtk.VBox() + win.add(vbox) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.add(self.create_tree_view()) + vbox.pack_start(scroll) + + buttons = gtk.HButtonBox() + vbox.pack_start(buttons) + + add_button = gtk.Button(stock=gtk.STOCK_ADD) + buttons.pack_start(add_button) + + remove_button = self.remove_button = gtk.Button(stock=gtk.STOCK_REMOVE) + remove_button.set_sensitive(False) + buttons.pack_start(remove_button) + + edit_button = self.edit_button = gtk.Button(stock=gtk.STOCK_EDIT) + edit_button.set_sensitive(False) + buttons.pack_start(edit_button) + + add_button.connect('clicked', lambda w: self.controller.add_account()) + remove_button.connect('clicked', self.on_remove) + edit_button.connect('clicked', self.on_edit) + + return win + + def create_tree_view(self): + self.accounts = gtk.ListStore(str, bool) + tv = gtk.TreeView(self.accounts) + tv.set_size_request(400, 200) + self.selection = tv.get_selection() + self.selection.connect('changed', self.on_selection) + + renderer = gtk.CellRendererToggle() + renderer.connect('toggled', self.on_toggle) + column = gtk.TreeViewColumn(_("Active"), renderer, active=1) + tv.append_column(column) + + column = gtk.TreeViewColumn("UNI", gtk.CellRendererText(), text=0) + tv.append_column(column) + + return tv + + def on_selection(self, selection): + selected = selection.get_selected()[1] is not None + self.remove_button.set_sensitive(selected) + self.edit_button.set_sensitive(selected) + + def on_remove(self, w): + model, iter = self.selection.get_selected() + self.controller.remove_account(model.get_path(iter)[0]) + + def on_edit(self, w): + model, iter = self.selection.get_selected() + self.controller.edit_account(model.get_path(iter)[0]) + + def on_toggle(self, w, path): + self.controller.set_active(int(path), not self.accounts[path][1]) + + def show_addedit_dialog(self, account, add, callback): + attrs = (('Server', 'server'), + ('User', 'person'), + ('Password', 'password'), + ('Save password', 'save_password')) + _open_form_dialog(_("Add Account" if add else "Edit Account"), + self.window, account, attrs, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT), + lambda d: callback()) + + +class ConversationView(gtk.VBox): + def __init__(self, tabs_view): + self.tabs_view = tabs_view + gtk.VBox.__init__(self) + + self.pack_start(self.create_content()) + + self.entry = gtk.Entry() + self.entry.connect('activate', self._enter) + self.pack_start(self.entry, False) + + def create_content(self): + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.vadjustment = scroll.get_vadjustment() + + tv = gtk.TextView() + self.text_buffer = tv.get_buffer() + tv.set_editable(False) + tv.set_cursor_visible(False) + tv.set_wrap_mode(gtk.WRAP_WORD_CHAR) + scroll.add(tv) + + return scroll + + def _enter(self, w): + text = w.get_text() + if not text: + return + self.controller.enter(text) + w.set_text("") + + def show_message(self, message): + self._show_line(message) + self.tabs_view.showed_message(self) + + def show_unknown_command(self): + self._show_line(_("Unknown command")) + + def show_unknown_target(self): + self._show_line(_("Unknown user")) + + def show_delivery_failed(self, message): + self._show_line(_("Delivery failed") + ': ' + message) + + def _show_line(self, line): + va = self.vadjustment + must_scroll = (va.value == va.upper - va.page_size) + self.text_buffer.insert(self.text_buffer.get_end_iter(), '\n' + line) + if must_scroll: + idle_add(self._scroll) + + def _scroll(self): + va = self.vadjustment + va.set_value(va.upper - va.page_size) + + +class ConferenceView(ConversationView): + def create_content(self): + hpane = gtk.HPaned() + hpane.pack1(ConversationView.create_content(self), True) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.add(self.create_tree_view()) + hpane.pack2(scroll, True) + + hpane.set_position(450) + return hpane + + def create_tree_view(self): + self.members = gtk.ListStore(str, str) + tv = gtk.TreeView(self.members) + tv.set_headers_visible(False) + tv.set_tooltip_column(0) + + cell = gtk.CellRendererText() + cell.set_property('ellipsize', pango.ELLIPSIZE_END) + column = gtk.TreeViewColumn(None, cell, text=1) + tv.append_column(column) + + tv.connect('row-activated', self._double_click) + tv.connect('button-press-event', self._button_press) + tv.connect('popup-menu', self._popup_menu) + return tv + + def _double_click(self, tv, path, view_column): + self.controller.open_conversation(path[0]) + + def _button_press(self, widget, event): + if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: + model = widget.get_model() + path = widget.get_path_at_pos(int(event.x), int(event.y))[0] + menu = self.create_context_menu(model, model.get_iter(path)) + menu.popup(None, None, None, event.button, event.time) + + def _popup_menu(self, widget): + model, iter = widget.get_selection().get_selected() + menu = self.create_context_menu(model, iter) + menu.popup(None, None, None, 3, 0) + return True + + def create_context_menu(self, model, iter): + menu = gtk.Menu() + + conv = gtk.MenuItem(_("Open Conversation")) + menu.append(conv) + conv.connect('activate', lambda w: + self.controller.open_conversation(model.get_path(iter)[0])) + + if model.get_value(iter, 2) == 'offered': + accept = gtk.MenuItem(_("Accept Friendship Request")) + menu.append(accept) + accept.connect('activate', lambda w: + self.controller.accept_friendship(model.get_path(iter)[0])) + + menu.show_all() + return menu + + def show_unknown_target(self): + self._show_line(_("Unknown place")) + + +class TabsView(object): + def __init__(self, status_icon): + self.status_icon = status_icon + self.window = self.create_window() + self.window.set_default_size(600, 400) + self.unread_tabs = [] + + settings = gtk.Entry().get_settings() + settings.set_property('gtk-entry-select-on-focus', False) + + def create_window(self): + win = gtk.Window() + win.set_title("pypsyc") + win.connect("delete_event", self._delete_event) + win.connect('focus-in-event', self._window_focus) + + self.notebook = gtk.Notebook() + self.notebook.set_scrollable(True) + self.notebook.connect('switch-page', self._switched_page) + win.add(self.notebook) + self.notebook.show() + + return win + + def _delete_event(self, w, e): + self.window.hide() + for tab in self.notebook: + self.controller.close_tab(tab) + return True + + def _window_focus(self, w, e): + current_page = self.notebook.get_current_page() + if current_page != -1: + self._switched_page(None, None, current_page) + + def _switched_page(self, w, page_ptr, page_num): + tab = self.notebook.get_nth_page(page_num) + if tab in self.unread_tabs: + self._change_tab_color(tab, 'black') + self.unread_tabs.remove(tab) + if not self.unread_tabs: + self.status_icon.set_blinking(False) + idle_add(tab.entry.grab_focus) + + def showed_message(self, tab): + if (not self.window.is_active() or + self.notebook.get_current_page() != self.notebook.page_num(tab)): + self._change_tab_color(tab, 'red') + if tab not in self.unread_tabs: + if not self.unread_tabs: + self.status_icon.set_blinking(True) + self.unread_tabs.append(tab) + + def _change_tab_color(self, tab, color_spec): + tab_label = self.notebook.get_tab_label(tab).get_children()[0] + color = gtk.gdk.color_parse(color_spec) + tab_label.modify_fg(gtk.STATE_NORMAL, color) + tab_label.modify_fg(gtk.STATE_ACTIVE, color) + + def on_status_icon_click(self): + if not self.unread_tabs: + return True + self.focus_tab(self.unread_tabs[0]) + + def show_conversation(self, label): + view = ConversationView(self) + self._add_tab(view, label) + return view + + def show_conference(self, label): + view = ConferenceView(self) + self._add_tab(view, label) + return view + + def _add_tab(self, tab, label): + tab_label = gtk.HBox() + tab_label.pack_start(gtk.Label(label)) + + image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + button = gtk.Button() + button.set_image(image) + button.set_relief(gtk.RELIEF_NONE) + button.connect('clicked', lambda w: self.controller.close_tab(tab)) + tab_label.pack_start(button) + + tab.show_all() + tab_label.show_all() + self.notebook.append_page(tab, tab_label) + + def focus_tab(self, tab): + self.notebook.set_current_page(self.notebook.page_num(tab)) + self.window.present() + + def remove_tab(self, tab): + self.notebook.remove_page(self.notebook.page_num(tab)) + if self.notebook.get_n_pages() == 0: + self.window.hide() + if tab in self.unread_tabs: + self.unread_tabs.remove(tab) + if not self.unread_tabs: + self.status_icon.set_blinking(False) + + +class FriendsView(object): + def __init__(self): + self.list_view = self.create_tree_view() + + def create_tree_view(self): + self.friends = gtk.ListStore(str, bool, str) + tv = gtk.TreeView(self.friends) + tv.set_headers_visible(False) + tv.set_size_request(200, 400) + tv.set_property('has-tooltip', True) + + cell = gtk.CellRendererText() + cell.set_property('foreground-set', True) + column = gtk.TreeViewColumn(None, cell, text=0) + column.set_cell_data_func(cell, self._foreground) + tv.append_column(column) + + tv.connect('row-activated', self._double_click) + tv.connect('query-tooltip', self._tooltip) + tv.connect('button-press-event', self._button_press) + tv.connect('popup-menu', self._popup_menu) + return tv + + def _foreground(self, column, cell, model, iter): + if model.get_value(iter, 1): + cell.set_property('foreground', 'black') + else: + cell.set_property('foreground', 'grey') + + def _double_click(self, tv, path, view_column): + self.controller.open_conversation(path[0]) + + def _tooltip(self, widget, x, y, keyboard_mode, tooltip): + tooltip_context = widget.get_tooltip_context(x, y, keyboard_mode) + if tooltip_context is None: + return False + model, path, iter = tooltip_context + tooltip.set_text(_("Friendship state") + ": %s" % model.get(iter, 2)) + widget.set_tooltip_row(tooltip, path) + return True + + def _button_press(self, widget, event): + if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: + model = widget.get_model() + path = widget.get_path_at_pos(int(event.x), int(event.y))[0] + menu = self.create_context_menu(model, model.get_iter(path)) + menu.popup(None, None, None, event.button, event.time) + + def _popup_menu(self, widget): + model, iter = widget.get_selection().get_selected() + menu = self.create_context_menu(model, iter) + menu.popup(None, None, None, 3, 0) + return True + + def create_context_menu(self, model, iter): + menu = gtk.Menu() + + conv = gtk.MenuItem(_("Open Conversation")) + menu.append(conv) + conv.connect('activate', lambda w: + self.controller.open_conversation(model.get_path(iter)[0])) + + if model.get_value(iter, 2) == 'offered': + accept = gtk.MenuItem(_("Accept Friendship Request")) + menu.append(accept) + accept.connect('activate', lambda w: + self.controller.accept_friendship(model.get_path(iter)[0])) + + deny = gtk.MenuItem(_("Deny Friendship Request")) + menu.append(deny) + deny.connect('activate', lambda w: + self.controller.cancel_friendship(model.get_path(iter)[0])) + + elif model.get_value(iter, 2) == 'pending': + cancel = gtk.MenuItem(_("Cancel Friendship Request")) + menu.append(cancel) + cancel.connect('activate', lambda w: + self.controller.cancel_friendship(model.get_path(iter)[0])) + else: + cancel = gtk.MenuItem(_("Cancel Friendship")) + menu.append(cancel) + cancel.connect('activate', lambda w: + self.controller.cancel_friendship(model.get_path(iter)[0])) + + menu.show_all() + return menu + + +class DumpView(object): + def __init__(self): + self.buffers = {} + self.window = self.create_window() + self.window.show_all() + + def create_window(self): + win = gtk.Window() + win.set_title(_("pypsyc protocol dump")) + win.set_default_size(500, 300) + win.connect('destroy', lambda w: self.controller.closed()) + + self.notebook = gtk.Notebook() + win.add(self.notebook) + + return win + + def show_line(self, direction, line, account): + if account not in self.buffers: + scroll = gtk.ScrolledWindow() + tv = gtk.TextView() + tv.set_editable(False) + scroll.add(tv) + + scroll.show_all() + self.notebook.append_page(scroll, gtk.Label(account)) + self.buffers[account] = tv.get_buffer(), scroll.get_vadjustment() + buffer, va = self.buffers[account] + + must_scroll = (va.value == va.upper - va.page_size) + line = repr(line)[1:-1] + buffer.insert(buffer.get_end_iter(), + ('\n< ' if direction == 'o' else '\n> ') + line) + if must_scroll: + idle_add(self.scroll, va) + + def scroll(self, va): + va.set_value(va.upper - va.page_size) + + +class MainView(object): + def __init__(self): + self.window = self.create_window() + self.window.show_all() + self.position = self.window.get_position() + + self.status_icon = self.create_status_icon() + self.tabs_view = TabsView(self.status_icon) + + def create_window(self): + win = gtk.Window() + win.set_title("pypsyc") + win.connect("delete_event", lambda w, e: win.hide() or True) + + vbox = gtk.VBox() + win.add(vbox) + + menu_bar = gtk.MenuBar() + vbox.pack_start(menu_bar, False) + menu_bar.append(self.create_file_menu()) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + vbox.pack_start(scroll) + + self.friends_view = FriendsView() + scroll.add(self.friends_view.list_view) + + return win + + def create_file_menu(self): + menu = gtk.Menu() + + accounts = gtk.MenuItem(_("Accounts")) + menu.append(accounts) + accounts.connect('activate', lambda w: self.controller.open_accounts()) + + conv = gtk.MenuItem(_("Open Conversation")) + menu.append(conv) + conv.connect('activate', lambda w: self.controller.open_conversation()) + + conf = gtk.MenuItem(_("Open Conference")) + menu.append(conf) + conf.connect('activate', lambda w: self.controller.open_conference()) + + add_friend = gtk.MenuItem(_("Add Friend")) + menu.append(add_friend) + add_friend.connect('activate', lambda w: self.controller.add_friend()) + + self.account_buttons = (conv, conf, add_friend) + self.show_active_accounts(False) + + dump = gtk.MenuItem(_("Protocol Dump")) + menu.append(dump) + dump.connect('activate', lambda w: self.controller.open_dump()) + + menu.append(gtk.MenuItem()) + + quit = gtk.MenuItem(_("Quit")) + menu.append(quit) + quit.connect('activate', lambda w: self.controller.quit()) + + item = gtk.MenuItem(_("File")) + item.set_submenu(menu) + return item + + def create_status_icon(self): + status_icon = gtk.StatusIcon() + status_icon.set_from_file(resource_filename(__name__, 'psyc.ico')) + status_icon.connect('activate', self._activated_status_icon) + return status_icon + + def _activated_status_icon(self, w): + if self.tabs_view.on_status_icon_click(): + if self.window.is_active(): + self.position = self.window.get_position() + self.window.hide() + else: + self.window.move(*self.position) + self.window.present() + + def show_active_accounts(self, active): + for i in self.account_buttons: + i.set_sensitive(active) + + def show_password_dialog(self, uni, account, callback): + title = _("Enter Password") + attrs = (('Password', 'password'), + ('Save password', 'save_password')) + text = "Please enter password for account %s." % uni + _open_form_dialog(title, self.window, account, attrs, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT), + lambda d: callback(), markup=text) + + def show_conn_error(self, account, message): + text = "%s - %s" % (_("Connection error"), account) + dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT), + gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text) + dialog.format_secondary_text(message) + dialog.connect('response', lambda w, r: dialog.destroy()) + dialog.show() + + def show_no_such_user(self, account): + text = "No such user" + dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT), + gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text) + dialog.format_secondary_text(account) + dialog.connect('response', lambda w, r: dialog.destroy()) + dialog.show() + + def show_auth_error(self, account, message): + text = "%s - %s" % (_("Authentication error"), account) + dialog = gtk.MessageDialog(self.window, (gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT), + gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text) + dialog.format_secondary_text(message) + dialog.connect('response', lambda w, r: dialog.destroy()) + dialog.show() + + def show_accounts(self): + return AccountsView() + + def show_open_conv_dialog(self, accounts, callback): + data = {'account': accounts, 'server': "", 'person': ""} + attrs = (('Account', 'account'), + ('Server', 'server'), + ('Person', 'person')) + _open_form_dialog(_("Open Conversation"), + self.window, data, attrs, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT), + lambda d: callback(**d)) + + def show_open_conf_dialog(self, accounts, callback): + data = {'account': accounts, 'server': "", 'place': ""} + attrs = (('Account', 'account'), + ('Server', 'server'), + ('Place', 'place')) + _open_form_dialog(_("Open Conference"), + self.window, data, attrs, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT), + lambda d: callback(**d)) + + def show_add_friend_dialog(self, accounts, callback): + data = {'account': accounts, 'server': "", 'person': ""} + attrs = (('Account', 'account'), + ('Server', 'server'), + ('Person', 'person')) + _open_form_dialog(_("Add Friend"), + self.window, data, attrs, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT), + lambda d: callback(**d)) + + def show_dump_win(self): + return DumpView() diff --git a/mjacob2/pypsyc/core/__init__.py b/mjacob2/pypsyc/core/__init__.py new file mode 100644 index 0000000..20a288a --- /dev/null +++ b/mjacob2/pypsyc/core/__init__.py @@ -0,0 +1,4 @@ +""" + pypsyc.core + ~~~~~~~~~~~ +""" diff --git a/mjacob2/pypsyc/core/mmp.py b/mjacob2/pypsyc/core/mmp.py new file mode 100644 index 0000000..cef6f63 --- /dev/null +++ b/mjacob2/pypsyc/core/mmp.py @@ -0,0 +1,158 @@ +""" + pypsyc.core.mmp + ~~~~~~~~~~~~~~~ + + Classes which are related to the routing layer (called MMP). + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from cStringIO import StringIO + +from twisted.internet.tcp import BaseClient +from twisted.protocols.basic import LineReceiver + + +class Uni(str): + """ + This class provides some helpers for working with PSYC Uniforms. + It can be initialized with a string or a list (:meth:`from_parts`). + """ + + def __init__(self, uni): + str.__init__(uni) + self.prefix = uni.split(':', 1)[0] + + def into_parts(self): + tmp = self.split(':', 1)[1] + return tmp.strip('/').split('/') + + @classmethod + def from_parts(cls, parts, prefix='psyc'): + """ + Construct a new UNI object from a list of parts. A prefix other than + 'psyc' can be given optionally. + + :param parts: the parts which represent the UNI's path + :type parts: list of strings + :param prefix: prefix of the UNI + :type prefix: string + """ + uni = str.__new__(cls, '%s://%s' % (prefix, '/'.join(parts))) + uni.prefix = prefix + return uni + + def chain(self, child): + return Uni('/'.join((self.strip('/'), child))) + + def is_descendant_of(self, other): + """Return whether this uni is a descendant of another.""" + other = other.strip('/') + l = len(other) + return self[0:l] == other and self[l] == '/' + + def is_ancestor_of(self, other): + """Return whether this uni is an ancestor of another.""" + self_ = self.strip('/') + l = len(self_) + return other[0:l] == self_ and other[l] == '/' + + +class Header(dict): + """ + A class which provides simple access to the header of a PSYC Packet. + + .. attribute:: source + + '_source' variable of the packet header as :class:`Uni` + + .. attribute:: target + + '_target' variable of the packet header as :class:`Uni` + + .. attribute:: context + + '_context' variable of the packet header as :class:`Uni` + """ + + def _init(self): + """Initialize the header with the set values.""" + source = self.get('_source') + if source: self.source = Uni(source) + else: self.source = None + + target = self.get('_target') + if target: self.target = Uni(target) + else: self.target = None + + context = self.get('_context') + if context: self.context = Uni(context) + else: self.context = None + + +class Circuit(LineReceiver, object): + """Base class for all PSYC (MMP) Circuits.""" + delimiter = '\n' + + def __init__(self): + # states: + # 0 = uninitialized + # 1 = header + # 2 = content + self.state = 0 + self.vars = {'=': {}} + self.inited = None + + def connectionMade(self): + if not hasattr(self, 'initiator'): + self.initiator = isinstance(self.transport, BaseClient) + + if self.initiator: + self._init() + + def _init(self): + self.send({}, '') + if callable(self.inited): + self.inited() + del self.inited + del self.initiator + + def lineReceived(self, line): + if line == '|': # end of packet + if self.state != 0: # the connection is inited already + header = Header(self.vars['=']) + header.update(self.vars[':']) + header._init() + self.packet_received(header, self.content) + elif not getattr(self, 'initiator', True): + self._init() + + # reset parser state + self.state = 1 + self.vars[':'] = {} + self.content = [] + + elif self.state == 1: + if line == '': + self.state = 2 + else: + self.vars[line[0]].__setitem__(*line[1:].split('\t', 1)) + + elif self.state == 2: + self.content.append(line) + + def packet_received(self, header, content): + """Overide this in subclass.""" + raise NotImplementedError + + def send(self, header, content): + """Send a packet over this circuit.""" + out = StringIO() + out.writelines(':%s\t%s\n' % x for x in header.iteritems()) + content = '\n'.join(content) + if content: + out.write('\n') + out.write(content) + out.write('\n') + out.write('|\n') + self.transport.write(out.getvalue()) diff --git a/mjacob2/pypsyc/core/psyc.py b/mjacob2/pypsyc/core/psyc.py new file mode 100644 index 0000000..25ee4d5 --- /dev/null +++ b/mjacob2/pypsyc/core/psyc.py @@ -0,0 +1,254 @@ +""" + pypsyc.core.psyc + ~~~~~~~~~~~~~~~~ + + Classes which are related to the entity layer. + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import logging +from collections import defaultdict +from os import urandom + +from greenlet import greenlet + +from pypsyc.core.mmp import Header +from pypsyc.util import Waiter + + +log = logging.getLogger(__name__) + + +def _get_binary_arg(it, must_len, first_fragment): + fragments = [first_fragment] + cur_len = len(first_fragment) + while cur_len < must_len: + f = it.next() + fragments.append(f) + cur_len += len(f) + 1 + return '\n'.join(fragments) + +class PSYCPacket(object): + """ + Initialize a new PSYCPacket with the given arguments. + + :param modifiers: entity modifiers + :type modifiers: dict + :param mc: method + :type mc: string or None + :param data: data + :type data: string + + Attributes: + + .. attribute:: modifiers + + entity modifiers as described `here `_ + + .. attribute:: cvars + + same as :attr:`modifiers`\[':'\] + + .. attribute:: mc + + method as described `here `_ + + .. attribute:: data + + data as described `here `_ + """ + + def __init__(self, modifiers=None, mc=None, data=''): + self.modifiers = modifiers or {} + self.cvars = self.modifiers.setdefault(':', {}) + self.mc = mc + self.data = data + + @classmethod + def from_kwds(cls, mc=None, data='', **kwds): + """ + Return a new PSYCPacket which is initialized with mc and data given by + arguments and cvars given by keyword arguments. + + :: + + packet1 = PSYCPacket.from_kwds(mc='_message_public', + data="hello", _somevar='foo') + + packet2 = PSYCPacket(mc='_message_public', data="hello", + modifiers={':': {'_somevar': 'foo'}}) + + packet1 == packet2 # True + + :param mc: method + :type mc: string or None + :param data: data + :type data: string + :rtype: PSYCPacket + """ + return cls({':': kwds}, mc, data) + + def render(self): + """Return a generator of lines which represent this packet.""" + for glyph, vars in self.modifiers.iteritems(): + for x, y in vars.iteritems(): + if '\n' in y: + yield '%s%s %s\t%s' % (glyph, x, len(y), y) + else: + yield '%s%s\t%s' % (glyph, x, y) + if self.mc: + yield self.mc + if self.data: + yield self.data + else: + assert not self.data + + @classmethod + def parse(cls, content): + """Parse a list of lines to a packet.""" + modifiers = defaultdict(dict) + it = iter(content) + for line in it: + if line.startswith('_'): # body + return cls(modifiers, line, '\n'.join(it)) + else: # entity-modifier + glyph = line[0] + x, y = line[1:].split('\t', 1) + if ' ' in x: # binary-arg + var, l = x.split(' ') + modifiers[glyph][var] = _get_binary_arg(it, int(l), y) + else: # simple-arg + modifiers[glyph][x] = y + return cls(modifiers, None, '') + + def __repr__(self): + return '\n'.join(( + "PSYCPacket(", + " modifiers={", + ",\n".join(' '*8 + "'%s': %s" % i for i in self.modifiers.items()), + " },", + " mc=%r,", + " data=%r", + ")" + )) % (self.mc, self.data) + + def __eq__(self, other): + return (self.modifiers == other.modifiers and + self.mc == other.mc and + self.data == other.data) + + def __ne__(self, other): + return not self.__eq__(other) + + +class PSYCObject(object): + """ + This is the base class for all classes that provide PSYC functionality, + such as receiving and sending :class:`PSYCPacket`\s. + + Subclassed by :class:`pypsyc.server.Entity`. Used by + :class:`pypsyc.server.routing.ServerCircuit`. + """ + + def __init__(self, sendfunc, uni=None): + self._send = sendfunc + self.uni = uni + self.handlers = {} + self.tags = {} + + def handle_packet(self, header, content): + packet = PSYCPacket.parse(content) + packet.header = header + + tag_relay = header.get('_tag_relay') + if tag_relay: + self.tags.pop(tag_relay).callback(packet) + return packet + + mc = packet.mc + while mc: + f = self.handlers.get(mc) + if f is not None: + break + mc = mc.rpartition('_')[0] + else: + return packet + + tag = header.get('_tag') + if tag is None: + try: + f(packet) + except Exception: + log.exception("error calling handler %s of %s", mc, self) + else: + def respond(): + try: + response = f(packet) + except Exception: + log.exception("error calling handler %s of %s", mc, self) + response = PSYCPacket(mc='_error_internal') + self.sendmsg(header.source, response, {'_tag_relay': tag}) + greenlet(respond).switch() + return packet + + def sendmsg(self, target, packet=None, header=None, **kwds): + """ + Send a packet. + + 1. + Simple example usage:: + + packet = PSYCPacket(mc='_message_public', + modifiers={':': {'_somevar': 'foo'}}) + psyc.sendmsg(target, packet) + + This simply sends the packet to the target. + + + 2. + The following code block is equal to the previous:: + + psyc.sendmsg(target, mc='_message_public', _somevar='foo') + + See also :meth:`PSYCPacket.from_kwds`. + + :param target: target of the packet + :type target: :class:`pypsyc.core.mmp.Uni` or \ + :class:`pypsyc.core.mmp.Circuit` + :param packet: packet to send, use keyword arguments if None + :type packet: :class:`PSYCPacket` or None + :param header: additional header variables + :type header: dict or None + :rtype: PSYCPacket or None + """ + if packet is None: + packet = PSYCPacket.from_kwds(**kwds) + + header = Header() if header is None else Header(header) + if self.uni is not None: + header['_source'] = self.uni + + if packet.mc and packet.mc.startswith('_request'): + tag = urandom(20).replace('\n', '') + header['_tag'] = tag + waiter = self.tags[tag] = Waiter() + else: + waiter = None + + if isinstance(target, str): + header['_target'] = target + header._init() + self._send(header, packet.render()) + else: # the target is a circuit + target.send(header, packet.render()) + + if waiter is not None: + return waiter.get() + + def add_handler(self, handler): + self.handlers.update((i[6:], getattr(handler, i)) for i in + dir(handler) if i.startswith('handle_')) + + def __repr__(self): + return '' % self.uni diff --git a/mjacob2/pypsyc/protocol.py b/mjacob2/pypsyc/protocol.py new file mode 100644 index 0000000..c1ca118 --- /dev/null +++ b/mjacob2/pypsyc/protocol.py @@ -0,0 +1,333 @@ +""" + pypsyc.protocol + ~~~~~~~~~~~~~~~ + + Protocol classes for PSYC :class:`~pypsyc.server.Entity`\s. + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import hmac +from hashlib import sha256 +from itertools import izip +from os import urandom + +from pypsyc.core.psyc import PSYCPacket + + +class Error(Exception): + pass + +class ServerProtocol(object): + def __init__(self, package): + self.package = package + + def sendmsg(self, *args, **kwds): + return self.package.entity.sendmsg(*args, **kwds) + + def castmsg(self, *args, **kwds): + return self.package.entity.castmsg(*args, **kwds) + + def state_set(self, *args, **kwds): + return self.package.entity.context_master.state_set(*args, **kwds) + + def state_add(self, *args, **kwds): + return self.package.entity.context_master.state_add(*args, **kwds) + + def state_remove(self, *args, **kwds): + return self.package.entity.context_master.state_remove(*args, **kwds) + +class ClientProtocol(object): + def __init__(self, package): + self.package = package + + def sendmsg(self, *args, **kwds): + return self.package.psyc.sendmsg(*args, **kwds) + + +class UnknownTargetError(Error): + pass + +class DeliveryFailedError(Error): + pass + +def check_response(packet, default_mc): + if packet.mc == '_error_unknown_target': + raise UnknownTargetError(packet.cvars['_uni']) + if packet.mc == '_failure_unsuccessful_delivery': + raise DeliveryFailedError(packet.data, packet.cvars['_uni']) + assert packet.mc == default_mc, "response mc is %s" % packet.mc + + +class RoutingErrorRoot(ServerProtocol): + def send_unknown_target_error(self, target, uni): + self.sendmsg(target, mc='_error_unknown_target', _uni=uni) + + def send_delivery_failed_error(self, target, uni, message): + self.sendmsg(target, mc='_failure_unsuccessful_delivery', data=message, + _uni=uni) + + +class RoutingErrorRelaying(ServerProtocol): + def handle_error_unknown_target(self, packet): + self.package.unknown_target_error(packet.cvars['_uni']) + + def handle_failure_unsuccessful_delivery(self, packet): + self.package.delivery_failed_error(packet.cvars['_uni'], packet.data) + + def relay_unknown_target_error(self, target, uni): + self.sendmsg(target, mc='_error_unknown_target', _uni=uni) + + def relay_delivery_failed_error(self, target, uni, message): + self.sendmsg(target, mc='_failure_unsuccessful_delivery', data=message, + _uni=uni) + + +class RoutingErrorClient(ClientProtocol): + def handle_error_unknown_target(self, packet): + self.package.unknown_target_error(packet.cvars['_uni']) + + def handle_failure_unsuccessful_delivery(self, packet): + self.package.delivery_failed_error(packet.cvars['_uni'], packet.data) + + +class AuthenticationError(Error): + pass + +class LinkingServer(ServerProtocol): + def __init__(self, package): + ServerProtocol.__init__(self, package) + self.attempts = {} + + def handle_request_link(self, p): + password = p.cvars.get('_password') + if not password: + nonce = urandom(20) + self.attempts[p.header.source] = (nonce, p.cvars['_resource']) + return PSYCPacket.from_kwds(mc='_query_password', _nonce=nonce) + + else: + nonce, resource = self.attempts[p.header.source] + try: + self.package.authenticate(('hmac', password, nonce), + p.header.source, resource) + except AuthenticationError as e: + return PSYCPacket(mc='_error_authentication', data=e.args[0]) + return PSYCPacket(mc='_echo_link') + +class LinkingClient(ClientProtocol): + def link(self, target, resource, password): + r = self.sendmsg(target, mc='_request_link', _resource=resource) + check_response(r, '_query_password') + + digest = hmac.new(password, r.cvars['_nonce'], sha256).digest() + r = self.sendmsg(target, mc='_request_link', _password=digest) + if r.mc == '_error_authentication': + raise AuthenticationError(r.data) + check_response(r, '_echo_link') + + +class Messaging(ClientProtocol): + def send_private_message(self, target, message, relay=None): + packet = PSYCPacket(mc='_message_private', data=message) + if relay: + self.sendmsg(relay, packet, {'_target_relay': target}) + else: + self.sendmsg(target, packet) + + def handle_message_private(self, packet): + self.package.private_message(packet.header.source, packet.data) + +class MessageRelaying(ServerProtocol): + def handle_message_private(self, packet): + target_relay = packet.header.get('_target_relay') + if target_relay is not None: + self.package.private_message_relay(packet.header.source, + target_relay, + packet.data) + else: + self.package.private_message(packet.header.source, packet.data) + + def send_private_message(self, target, message): + self.sendmsg(target, mc='_message_private', data=message) + + def relay_private_message(self, source, target, message): + self.sendmsg(target, header={'_source_relay': source}, + mc='_message_private', data=message) + + +class ConferencingServer(ServerProtocol): + def cast_member_entered(self, member, nick): + self.state_add(_list_members=member, _list_members_nick=nick) + + def cast_member_left(self, member): + self.state_remove(_list_members=member) + + def handle_message_public(self, packet): + self.package.public_message(packet.header.source, packet.data) + + def cast_public_message(self, source, message): + self.castmsg(mc='_message_public', data=message, _member=source) + +class ConferencingClient(ClientProtocol): + def state_set_list_members(self, context, vars): + for args in izip(vars['_list_members'].split('|'), + vars['_list_members_nick'].split('|')): + self.package.member_entered(context, *args) + + def state_add_list_members(self, context, vars): + self.package.member_entered(context, vars['_list_members'], + vars['_list_members_nick']) + + def state_remove_list_members(self, context, vars): + self.package.member_left(context, vars['_list_members']) + + def send_public_message(self, target, message): + self.sendmsg(target, mc='_message_public', data=message) + + def handle_message_public(self, packet): + self.package.public_message( + packet.header.context, packet.cvars['_member'], packet.data) + + +class EntryDeniedError(Error): + pass + +class ContextMaster(ServerProtocol): + def handle_request_context_enter(self, packet): + try: + state = self.package.enter_request(packet.header.source) + except EntryDeniedError as e: + return PSYCPacket(mc='_error_illegal_entry', data=e.args[0]) + return PSYCPacket({'=': state}, '_echo_context_enter') + + def handle_request_context_leave(self, packet): + self.package.leave_context(packet.header.source) + return PSYCPacket(mc='_echo_context_leave') + +class ContextSlave(ServerProtocol): + def enter(self, target): + r = self.sendmsg(target, mc='_request_context_enter') + if r.mc == '_error_illegal_entry': + raise EntryDeniedError(r.data) + check_response(r, '_echo_context_enter') + return r.modifiers.get('=') + + def leave(self, target): + r = self.sendmsg(target, mc='_request_context_leave') + check_response(r, '_echo_context_leave') + + +class UnauthorizedError(Error): + pass + +class ClientInterfaceServer(ServerProtocol): + client_interface = { + 'subscribe': lambda p: (p.cvars['_group'], p.header.source), + 'unsubscribe': lambda p: (p.cvars['_group'], p.header.source), + 'presence': lambda p: (int(p.cvars['_degree_availability']),), + 'add_friend': lambda p: (p.cvars['_person'],), + 'remove_friend': lambda p: (p.cvars['_person'],) + } + + def handle_request_do(self, packet): + if not packet.header.source.is_descendant_of(self.package.entity.uni): + return PSYCPacket(mc='_error_illegal_source') + action = packet.mc[12:] + method = self.client_interface.get(action, None) + try: + getattr(self.package, 'client_' + action)(*method(packet)) + except UnknownTargetError as e: + return PSYCPacket.from_kwds(mc='_error_unknown_target', + _uni=e.args[0]) + except DeliveryFailedError as e: + return PSYCPacket.from_kwds(mc='_failure_unsuccessful_delivery', + data=e.args[0], _uni=e.args[1]) + return PSYCPacket(mc='_echo_do_' + action) + +class ClientInterfaceClient(ClientProtocol): + def subscribe(self, uni): + self._request_do('subscribe', _group=uni) + + def unsubscribe(self, uni): + self._request_do('unsubscribe', _group=uni) + + def cast_presence(self, availability): + self._request_do('presence', _degree_availability=str(availability)) + + def add_friend(self, uni): + self._request_do('add_friend', _person=uni) + + def remove_friend(self, uni): + self._request_do('remove_friend', _person=uni) + + def _request_do(self, command, **kwds): + r = self.sendmsg(self.package.uni, mc='_request_do_' + command, **kwds) + if r.mc == '_error_illegal_source': + raise UnauthorizedError + check_response(r, '_echo_do_' + command) + + +class FriendshipPendingError(Error): + pass + +class FriendshipEstablishedError(Error): + pass + +class FriendshipServer(ServerProtocol): + def handle_request_friendship(self, packet): + state = self.package.friendship_request(packet.header.source) + return PSYCPacket(mc='_echo_friendship_' + state) + + def establish(self, uni): + r = self.sendmsg(uni, mc='_request_friendship') + if r.mc == '_echo_friendship_pending': + return 'pending' + if r.mc == '_echo_friendship_established': + return 'established' + else: + raise Exception("unknown mc: %s" % r.mc) + + def handle_request_friendship_remove(self, packet): + self.package.friendship_cancel(packet.header.source) + return PSYCPacket(mc='_echo_friendship_removed') + + def remove(self, uni): + r = self.sendmsg(uni, mc='_request_friendship_remove') + check_response(r, '_echo_friendship_removed') + + def send_friendships(self, target, friendships): + unis = friendships.iterkeys() + states = friendships.itervalues() + self.sendmsg(target, mc='_notice_list_friendships', + _list_friendships='|'.join(unis), + _list_friendships_state='|'.join(i['state'] for i + in states)) + + def send_updated_friendship(self, target, uni, fs): + self.sendmsg(target, mc='_notice_friendship_updated', + _person=uni, _state=fs['state']) + + def send_removed_friendship(self, target, uni): + self.sendmsg(target, mc='_notice_friendship_removed', _person=uni) + + def cast_presence(self, availability): + self.state_set(_degree_availability=str(availability)) + +class FriendshipClient(ClientProtocol): + def handle_notice_list_friendships(self, packet): + if packet.cvars['_list_friendships'] == '': + return + for args in izip(packet.cvars['_list_friendships'].split('|'), + packet.cvars['_list_friendships_state'].split('|')): + self.package.friendship(*args) + + def handle_notice_friendship_updated(self, packet): + self.package.friendship(packet.cvars['_person'], + packet.cvars['_state']) + + def handle_notice_friendship_removed(self, packet): + self.package.friendship_removed(packet.cvars['_person']) + + def state_set_degree_availability(self, context, vars): + self.package.presence(context, int(vars['_degree_availability'])) diff --git a/mjacob2/pypsyc/server/__init__.py b/mjacob2/pypsyc/server/__init__.py new file mode 100644 index 0000000..7ab38fd --- /dev/null +++ b/mjacob2/pypsyc/server/__init__.py @@ -0,0 +1,105 @@ +""" + pypsyc.server + ~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import signal + +from pkg_resources import iter_entry_points +from twisted.internet import reactor + +from pypsyc.core.mmp import Uni, Header +from pypsyc.core.psyc import PSYCObject, PSYCPacket +from pypsyc.server.db import Database +from pypsyc.server.routing import _TreeNode, Routing +from pypsyc.server.multicast import ContextMaster +from pypsyc.server.webif import run_webif +from pypsyc.util import schedule + + +class Entity(PSYCObject, _TreeNode): + def __init__(self, parent=None, name='', server=None): + _TreeNode.__init__(self, parent, name) + self.server = (server or self._root.server) + if parent is None: + uni = Uni('psyc://%s/' % self.server.hostname) + else: + uni = parent.uni.chain(name) + PSYCObject.__init__(self, self.server.routing.route_singlecast, uni) + self.context_master = ContextMaster(self) + self.packages = {} + + def castmsg(self, packet=None, header=None, **kwds): + if packet is None: + packet = PSYCPacket.from_kwds(**kwds) + header = Header() if header is None else Header(header) + header['_context'] = self.uni + header._init() + self.server.routing.route_multicast(header, packet.render()) + + +class Server(object): + def __init__(self, hostname, interface, psyc_port, webif_port, db_file): + self.hostname = hostname + self.routing = Routing(hostname, interface) + self.root = Entity(server=self) + self.routing.init(self.root) + if psyc_port: + self.routing.listen(psyc_port) + + self.database = Database(db_file) + schedule(self._load_entities) + + if webif_port: + schedule(run_webif, self, interface, webif_port, None) + + signal.signal(signal.SIGINT, self.shutdown) + signal.signal(signal.SIGTERM, self.shutdown) + try: + signal.signal(signal.SIGBREAK, self.shutdown) + except AttributeError: + pass + + def _load_entities(self): + self.database.execute( + 'CREATE TABLE IF NOT EXISTS packages (' + 'entity TEXT, package TEXT, PRIMARY KEY (entity, package))') + entities = self.database.fetch('SELECT entity, package FROM packages') + for entity_name, package_name in entities: + self._load_package(entity_name, package_name) + + def _load_package(self, entity_name, package_name): + if entity_name is None: + entity = self.root + else: + try: + entity = self.root.children[entity_name] + except KeyError: + entity = Entity(self.root, entity_name) + + assert package_name not in entity.packages + l = list(iter_entry_points('pypsyc.server.packages', package_name)) + assert len(l) == 1 + package = l[0].load()(entity) + entity.packages[package_name] = package + return package + + def add_package(self, entity_name, package_name): + package = self._load_package(entity_name, package_name) + self.database.execute('INSERT INTO packages VALUES (?, ?)', + entity_name, package_name) + return package + + def add_place(self, name): + return self.add_package('@' + name, 'place') + + def register_person(self, name, password): + person = self.add_package('~' + name, 'person') + person.register(password) + return person + + def shutdown(self, signum=None, frame=None): + self.database.stop() + reactor.stop() diff --git a/mjacob2/pypsyc/server/db.py b/mjacob2/pypsyc/server/db.py new file mode 100644 index 0000000..a979748 --- /dev/null +++ b/mjacob2/pypsyc/server/db.py @@ -0,0 +1,105 @@ +""" + pypsyc.server.db + ~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import sqlite3 +from threading import Thread +from Queue import Queue + +from greenlet import getcurrent +from twisted.internet import reactor + + +class Database(object): + """ + Open a sqlite database that queues the database operations and executes + them in a thread. + """ + + def __init__(self, filename): + self.filename = filename + self.sync = (filename == ':memory:') + if self.sync: + self._connect() + else: + self.queue = Queue() + self.thread = Thread(target=self.run_async) + self.thread.start() + self.main_greenlet = getcurrent() + + def execute(self, *args): + """ + Execute a query. + + :param query: the sqlite query + :type query: string + :param \*params: additional query parameters + :type \*params: one or more strings + """ + if self.sync: + return self._execute(*args) + self.queue.put(('execute', args, getcurrent())) + assert getcurrent() is not self.main_greenlet + return self.main_greenlet.switch() + + def fetch(self, *args): + """ + Fetch a result from the database asynchronously. + + :param query: the sqlite query + :type query: string + :param \*params: additional query parameters + :type \*params: one or more strings + :rtype: result rows + """ + if self.sync: + return self._fetch(*args) + self.queue.put(('fetch', args, getcurrent())) + assert getcurrent() is not self.main_greenlet + return self.main_greenlet.switch() + + def stop(self): + """Close the database and stop the worker thread.""" + if self.sync: + return self._close() + del self.sync # make database unusable + self.queue.put(('stop', (), None)) + self.thread.join() + + def run_async(self): + self._connect() + while 1: + cmd, args, gl = self.queue.get() + if cmd == 'stop': + self._close() + break + try: + if cmd == 'execute': + ret = self._execute(*args) + elif cmd == 'fetch': + ret = self._fetch(*args) + except Exception, e: + reactor.callFromThread(gl.throw, e) + else: + reactor.callFromThread(gl.switch, ret) + + def _connect(self): + self.conn = sqlite3.connect(self.filename) + # return bytestrings instead of unicode strings + self.conn.text_factory = str + + def _close(self): + self.conn.close() + + def _execute(self, query, *params): + cur = self.conn.cursor() + cur.execute(query, params) + self.conn.commit() + + def _fetch(self, query, *params): + cur = self.conn.cursor() + cur.execute(query, params) + return cur.fetchall() diff --git a/mjacob2/pypsyc/server/multicast.py b/mjacob2/pypsyc/server/multicast.py new file mode 100644 index 0000000..613c158 --- /dev/null +++ b/mjacob2/pypsyc/server/multicast.py @@ -0,0 +1,100 @@ +""" + pypsyc.server.multicast + ~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from collections import defaultdict + +from pypsyc.core.mmp import Header +from pypsyc.core.psyc import PSYCPacket +from pypsyc.protocol import ContextSlave as ContextSlaveProtocol + + +class ContextMaster(object): + def __init__(self, entity): + self.state = {} + self.list_state = defaultdict(list) + self.entity = entity + if hasattr(entity.server.routing.mrouting_table, '__setitem__'): + entity.server.routing.mrouting_table[entity.uni] = set() + + def add_member(self, uni): + if not uni.is_descendant_of(self.entity._root.uni): + routing = self.entity.server.routing + circuit = routing.srouting_table[uni.into_parts()[0]] + routing.mrouting_table[self.entity.uni].add(circuit) + state = self.state.copy() + state.update((k, '|'.join(v)) for k, v in self.list_state.iteritems()) + return state + + def remove_member(self, uni): + if not uni.is_descendant_of(self.entity._root.uni): + routing = self.entity.server.routing + circuit = routing.srouting_table[uni.into_parts()[0]] + routing.mrouting_table[self.entity.uni].remove(circuit) + + def state_set(self, **kwds): + self.state.update(kwds) + self.entity.castmsg(PSYCPacket({'=': kwds})) + + def state_add(self, **kwds): + for key, value in kwds.iteritems(): + self.list_state[key].append(value) + self.entity.castmsg(PSYCPacket({'+': kwds})) + + def state_remove(self, **kwds): + for key, value in kwds.iteritems(): + idx = self.list_state[key].index(value) + for i, j in self.list_state.items(): + if i.startswith(key): + del j[idx] + if not j: + del self.list_state[i] + self.entity.castmsg(PSYCPacket({'-': kwds})) + + +class ContextSlave(object): + def __init__(self, person): + self.protocol = ContextSlaveProtocol(person) + self.person = person + self.subscribed_contexts = set() + + def enter(self, context, resource=None): + if context not in self.subscribed_contexts: + state = self.protocol.enter(context) + self.subscribed_contexts.add(context) + else: + state = None # TODO: get state from context master + entity = self.person.entity + table = entity.server.routing.mrouting_table.setdefault(context, set()) + resources = ([entity.children[resource.into_parts()[-1]]] if resource + else entity.children.values()) + + table.update(resource.circuit for resource in resources) + if state: + header = Header({'_context': context}) + content = list(PSYCPacket({'=': state}).render()) + for resource in resources: + resource.circuit.send(header, content) + + def leave(self, context, resource=None): + if context not in self.subscribed_contexts: + return + entity = self.person.entity + table = entity.server.routing.mrouting_table[context] + circuits = (r.circuit for r in entity.children.itervalues()) + if resource: + table.remove(entity.children[resource.into_parts()[-1]].circuit) + if table.intersection(circuits): return + else: + table.difference_update(circuits) + self.protocol.leave(context) + self.subscribed_contexts.remove(context) + if not (table or entity._root.uni.is_ancestor_of(context)): + del entity.server.routing.mrouting_table[context] + + def leave_all(self): + for i in list(self.subscribed_contexts): + self.leave(i) diff --git a/mjacob2/pypsyc/server/person.py b/mjacob2/pypsyc/server/person.py new file mode 100644 index 0000000..881cbab --- /dev/null +++ b/mjacob2/pypsyc/server/person.py @@ -0,0 +1,188 @@ +""" + pypsyc.server.person + ~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import hmac +from hashlib import sha256 + +from pypsyc.server.multicast import ContextSlave +from pypsyc.protocol import (RoutingErrorRelaying, LinkingServer, + MessageRelaying, AuthenticationError, EntryDeniedError, + ContextMaster as ContextMasterProtocol, FriendshipPendingError, + FriendshipEstablishedError, FriendshipServer as FriendshipProtocol, + ClientInterfaceServer as ClientInterfaceProtocol) +from pypsyc.server import _TreeNode +from pypsyc.util import schedule + + +class Resource(_TreeNode): + def __init__(self, parent, resource, circuit): + _TreeNode.__init__(self, parent=parent, name=resource) + self.circuit = circuit + if hasattr(circuit, 'allowed_sources'): + circuit.allowed_sources.append(parent.uni.chain(resource)) + + def handle_packet(self, header, content): + self.circuit.send(header, content) + + +class Person(object): + def __init__(self, entity): + self.entity = entity + self.uni = entity.uni + self.routing_error_relaying = RoutingErrorRelaying(self) + entity.add_handler(self.routing_error_relaying) + entity.add_handler(LinkingServer(self)) + self.message_relaying = MessageRelaying(self) + entity.add_handler(self.message_relaying) + entity.add_handler(ContextMasterProtocol(self)) + self.context_slave = ContextSlave(self) + self.friendship_protocol = FriendshipProtocol(self) + entity.add_handler(self.friendship_protocol) + entity.add_handler(ClientInterfaceProtocol(self)) + + def register(self, password): + """ + Register this person entity with the given password. This method has to + be called after the entity was created. + """ + sql = ('CREATE TABLE IF NOT EXISTS passwords (' + 'person TEXT PRIMARY KEY, password BLOB)') + self.entity.server.database.execute(sql) + sql = ('CREATE TABLE IF NOT EXISTS friendships (' + 'person TEXT, uni TEXT, state TEXT, PRIMARY KEY (person, uni))') + self.entity.server.database.execute(sql) + sql = 'INSERT INTO passwords VALUES (?, ?)' + self.entity.server.database.execute(sql, self.uni, password) + + def unknown_target_error(self, uni): + for i in self.entity.children: + self.routing_error_relaying.relay_unknown_target_error( + self.uni.chain(i), uni) + + def delivery_failed_error(self, uni, message): + for i in self.entity.children: + self.routing_error_relaying.relay_delivery_failed_error( + self.uni.chain(i), uni, message) + + def authenticate(self, pw, circuit, resource): + sql = 'SELECT password FROM passwords WHERE person = ?' + db_pass = self.entity.server.database.fetch(sql, self.uni)[0][0] + type_, req_pass, nonce = pw + assert type_ == 'hmac' + if req_pass != hmac.new(db_pass, nonce, sha256).digest(): + raise AuthenticationError("Incorrect password.") + Resource(self.entity, resource, circuit) + self.friendship_protocol.cast_presence(7) + schedule(self._after_authentication, self.uni.chain(resource)) + + def _after_authentication(self, uni): + sql = 'SELECT uni, state FROM friendships WHERE person = ?' + rows = self.entity.server.database.fetch(sql, self.uni) + friendships = dict((i[0], {'state': i[1]}) for i in rows) + self.friendship_protocol.send_friendships(uni, friendships) + for context, state in rows: + if state == 'established': + self.context_slave.enter(context, uni) + + def unlink(self, resource): + del self.entity.children[resource] + self.friendship_protocol.cast_presence(1) + self.context_slave.leave_all() + + def private_message_relay(self, source, target, message): + if source.is_descendant_of(self.uni): + self.message_relaying.send_private_message(target, message) + + def private_message(self, source, message): + for i in self.entity.children: + self.message_relaying.relay_private_message( + source, self.uni.chain(i), message) + + def friendship_request(self, uni): + state = self._get_friendship_state(uni) + if state == 'none': + self._update_friendship(uni, 'offered', True) + return 'pending' + elif state == 'pending': + self._update_friendship(uni, 'established', False) + self.context_slave.enter(uni) + return 'established' + elif state == 'offered': + return 'pending' + elif state == 'established': + return 'established' + else: + raise Exception("unknown friendship state: " + state) + + def friendship_cancel(self, uni): + self.context_slave.leave(uni) + self._update_friendship(uni, None) + + def client_add_friend(self, uni): + state = self._get_friendship_state(uni) + if state == 'none': + self.friendship_protocol.establish(uni) + self._update_friendship(uni, 'pending', True) + elif state == 'pending': + raise FriendshipPendingError + elif state == 'offered': + self.friendship_protocol.establish(uni) + self._update_friendship(uni, 'established', False) + self.context_slave.enter(uni) + elif state == 'established': + raise FriendshipEstablishedError + else: + raise Exception("unknown friendship state: " + state) + + def client_remove_friend(self, uni): + self.context_slave.leave(uni) + self.friendship_protocol.remove(uni) + self._update_friendship(uni, None) + + def _get_friendship_state(self, uni): + sql = 'SELECT state FROM friendships WHERE person = ? AND uni = ?' + ret = self.entity.server.database.fetch(sql, self.uni, uni) + if ret: + return ret[0][0] + return 'none' + + def _update_friendship(self, uni, state, insert=False): + if state is None: + sql = 'DELETE FROM friendships WHERE person = ? AND uni = ?' + self.entity.server.database.execute(sql, self.uni, uni) + elif insert: + sql = 'INSERT INTO friendships VALUES(?, ?, ?)' + self.entity.server.database.execute(sql, self.uni, uni, state) + else: + sql = ('UPDATE friendships SET state = ? ' + 'WHERE person = ? AND uni = ?') + self.entity.server.database.execute(sql, state, self.uni, uni) + for resource in self.entity.children: + if state: + self.friendship_protocol.send_updated_friendship( + self.uni.chain(resource), uni, {'state': state}) + else: + self.friendship_protocol.send_removed_friendship( + self.uni.chain(resource), uni) + + def enter_request(self, uni): + if self._get_friendship_state(uni) == 'established': + return self.entity.context_master.add_member(uni) + else: + raise EntryDeniedError("You are not my friend!") + + def leave_context(self, uni): + self.entity.context_master.remove_member(uni) + + def client_presence(self, availability): + self.friendship_protocol.cast_presence(availability) + + def client_subscribe(self, uni, resource_uni): + self.context_slave.enter(uni, resource_uni) + + def client_unsubscribe(self, uni, resource_uni): + self.context_slave.leave(uni, resource_uni) diff --git a/mjacob2/pypsyc/server/place.py b/mjacob2/pypsyc/server/place.py new file mode 100644 index 0000000..792bb6d --- /dev/null +++ b/mjacob2/pypsyc/server/place.py @@ -0,0 +1,34 @@ +""" + pypsyc.server.place + ~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from pypsyc.protocol import (ContextMaster as ContextProtocol, + ConferencingServer as ConferencingProtocol) + + +class Place(object): + def __init__(self, entity): + self.entity = entity + entity.add_handler(ContextProtocol(self)) + self.conferencing_protocol = ConferencingProtocol(self) + entity.add_handler(self.conferencing_protocol) + self.members = set() + + def enter_request(self, uni): + nick = uni.rsplit('/', 1)[1].lstrip('~') + self.conferencing_protocol.cast_member_entered(uni, nick) + self.members.add(uni) + return self.entity.context_master.add_member(uni) + + def leave_context(self, uni): + self.entity.context_master.remove_member(uni) + self.conferencing_protocol.cast_member_left(uni) + self.members.remove(uni) + + def public_message(self, source, message): + member = source.rsplit('/', 1)[0] + if member in self.members: + self.conferencing_protocol.cast_public_message(member, message) diff --git a/mjacob2/pypsyc/server/root.py b/mjacob2/pypsyc/server/root.py new file mode 100644 index 0000000..ebd2e46 --- /dev/null +++ b/mjacob2/pypsyc/server/root.py @@ -0,0 +1,13 @@ +""" + pypsyc.server.root + ~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" + + +class Root(object): + def __init__(self, entity): + self.entity = entity + self.server = entity.server diff --git a/mjacob2/pypsyc/server/routing.py b/mjacob2/pypsyc/server/routing.py new file mode 100644 index 0000000..7abd24b --- /dev/null +++ b/mjacob2/pypsyc/server/routing.py @@ -0,0 +1,209 @@ +""" + pypsyc.server.routing + ~~~~~~~~~~~~~~~~~~~~~ + + This module defines the following classes: + - `ServerCircuit` + - `_TreeNode` + - `Routing` + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import logging + +from twisted.internet import reactor +from twisted.internet.protocol import Factory + +from pypsyc.core.mmp import Circuit, Uni +from pypsyc.core.psyc import PSYCPacket, PSYCObject +from pypsyc.protocol import Error, check_response +from pypsyc.util import schedule, resolve_hostname, DNSError, connect + + +log = logging.getLogger(__name__) + + +class InvalidTargetError(Error): + pass + +class InvalidSourceError(Error): + pass + +class ServerCircuit(Circuit): + def __init__(self): + Circuit.__init__(self) + self.allowed_sources = [] + self.psyc = PSYCObject(self.send) + self.psyc.add_handler(self) + + def packet_received(self, header, content): + """Handle a packet that was received.""" + if header.context: + if header.source: + raise NotImplementedError + if header.target: + self.factory.route_singlecast(header, content) + else: + self.factory.route_multicast(header, content) + + else: + if header.source: + if not any((header.source == i or + header.source.is_descendant_of(i)) + for i in self.allowed_sources): + return + else: + header.source = self + + if header.target: + self.factory.route_singlecast(header, content) + else: + self.psyc.handle_packet(header, content) + + def handle_request_verification(self, packet): + source_uni = Uni(packet.cvars['_uni_source']) + target_uni = packet.cvars['_uni_target'] + try: + self.factory.verify_address(self, source_uni, target_uni) + except InvalidSourceError: + return PSYCPacket(mc='_error_invalid_source') + except InvalidTargetError: + return PSYCPacket(mc='_error_invalid_target') + self.allowed_sources.append(source_uni) + return PSYCPacket(mc='_echo_verification') + + def request_verification(self, source_uni, target_uni): + r = self.psyc.sendmsg(self, mc='_request_verification', + _uni_source=source_uni, _uni_target=target_uni) + if r.mc == '_error_invalid_source': + raise InvalidSourceError + if r.mc == '_error_invalid_target': + raise InvalidTargetError + check_response(r, '_echo_verification') + self.allowed_sources.append(target_uni) + + def connectionLost(self, reason): + for uni in self.allowed_sources: + parts = uni.into_parts() + if len(parts) == 1: + del self.factory.srouting_table[parts[0]] + else: + entity = self.factory.root.children[parts[1]] + entity.packages['person'].unlink(parts[2]) + + +class _TreeNode(object): + def __init__(self, parent=None, name=''): + self._parent = parent + self.children = {} + if parent: + self._root = parent._root + parent.children[name] = self + else: + self._root = self + + +class Routing(Factory, object): + """This class handles routing and circuit managment.""" + protocol = ServerCircuit + + def __init__(self, hostname, interface): + self.hostname = hostname + self.interface = interface + + self.circuits = {} # ip -> circuit + self.queues = {} # hostname -> list + self.srouting_table = {} # hostname -> circuit + self.mrouting_table = {} # context -> list of circuits + + def init(self, root): + self.root = root + + def route_singlecast(self, header, content): + """Route the packet to the right target.""" + parts = header.target.into_parts() + host = parts[0] + if host == self.hostname: + node = self.root + try: + for i in parts[1:]: + node = node.children[i] + except KeyError: + self._error(header, '_error_unknown_target') + else: + node.handle_packet(header, content) + elif header.get('_source'): + try: + self.srouting_table[host].send(header, content) + except KeyError: + if host not in self.queues: + self.queues[host] = [] + schedule(self._add_route, host) + self.queues[host].append((header, content)) + else: + client = header.source.transport.client + log.error("Dropped packet without _source from %s", client) + + def _error(self, header, error_mc, message=None): + tag = header.get('_tag') + self.root.sendmsg(header.source, None, tag and {'_tag_relay': tag}, + mc=error_mc, _uni=header.target, data=message) + + def route_multicast(self, header, content): + content = list(content) + for circuit in self.mrouting_table[header.context]: + circuit.send(header, content) + + def listen(self, port): + reactor.listenTCP(port, self, interface=self.interface) + + def _add_route(self, host): + """ + Add a route to host. + + First check if we have an open connection to the target host. + If, do address verification. + If not, try to connect to the server and then do address verification. + """ + try: + addr = resolve_hostname(host) + except DNSError: + self._unsuccessful_delivery(host, "Can't resolve '%s'" % host) + return + try: + circuit = self.circuits[addr] + except KeyError: + try: + ip, p = addr + circuit = connect(ip, p, self, bindAddress=(self.interface, 0)) + except Exception: + self._unsuccessful_delivery(host, "Can't connect '%s'" % host) + return + self.circuits[addr] = circuit + + try: + target_uni = Uni('psyc://%s/' % host) + circuit.request_verification(self.root.uni, target_uni) + except Error: + self._unsuccessful_delivery(host, "Can't verify %s" % target_uni) + else: + self.srouting_table[host] = circuit + for header, content in self.queues.pop(host): + circuit.send(header, content) + + def _unsuccessful_delivery(self, host, message): + log.exception(message) + for header, content in self.queues.pop(host): + self._error(header, '_failure_unsuccessful_delivery', message) + + def verify_address(self, circuit, source_uni, target_uni): + if target_uni != self.root.uni: + raise InvalidTargetError + source_host = source_uni.into_parts()[0] + if resolve_hostname(source_host)[0] != circuit.transport.client[0]: + raise InvalidSourceError + self.srouting_table[source_host] = circuit + + def connection_lost(self, circuit, error): + pass diff --git a/mjacob2/pypsyc/server/webif/__init__.py b/mjacob2/pypsyc/server/webif/__init__.py new file mode 100644 index 0000000..4796b40 --- /dev/null +++ b/mjacob2/pypsyc/server/webif/__init__.py @@ -0,0 +1,117 @@ +""" + pypsyc.server.webif + ~~~~~~~~~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from collections import MutableMapping +from logging import getLogger +from os import urandom +from sqlite3 import IntegrityError + +from flask import Flask, request, flash, url_for, redirect, render_template +from greenlet import greenlet +from twisted.internet import reactor +from twisted.web.server import Site +from twisted.web.wsgi import WSGIResource + + +log = getLogger(__name__) + +app = Flask(__name__) +app.jinja_env.trim_blocks = True + + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/register', methods=['GET', 'POST']) +def register(): + error = None + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + password2 = request.form.get('password2') + if not username: + error = 'please specify a valid username' + elif '~' + username in server.root.children: + error = 'username already in use' + elif not password or len(password) < 6: + error = 'your password must have at least 6 characters' + elif password != password2: + error = 'the two passwords do not match' + else: + server.register_person(username, password) + flash("you were registered") + return redirect(url_for('index')) + return render_template('register.html', error=error) + + +class PersistentStore(MutableMapping): + def __init__(self): + server.database.execute('CREATE TABLE IF NOT EXISTS webif_store (' + 'key TEXT, value BLOB, PRIMARY KEY (key))') + + def __getitem__(self, key): + sql = 'SELECT value FROM webif_store WHERE key = ?' + ret = server.database.fetch(sql, key) + if not ret: + raise KeyError + return ret[0][0] + + def __setitem__(self, key, value): + try: + sql = 'INSERT INTO webif_store VALUES (?, ?)' + server.database.execute(sql, key, value) + except IntegrityError: + sql = 'UPDATE webif_store SET value = ? WHERE key = ?' + server.database.execute(sql, value, key) + + def __delitem__(self, key): + server.database.execute('DELETE FROM webif_store WHERE key = ?', key) + + def __iter__(self): + sql = 'SELECT key FROM webif_store' + return (ret[0] for ret in server.database.fetch(sql)) + + def __len__(self): + return server.database.fetch('SELECT count(*) FROM webif_store')[0][0] + + +class _Reactor(object): + def callFromThread(self, f, *args, **kwds): + f(*args, **kwds) + +class _Threadpool(object): + def callInThread(self, f, *args, **kwds): + greenlet(f).switch(*args, **kwds) + +def run_webif(server_, interface, port, context_factory): + global server + server = server_ + global store + store = PersistentStore() + try: + app.secret_key = store['secret_key'] + except: + app.secret_key = store['secret_key'] = urandom(20) + assert len(app.secret_key) == 20 + + site = Site(WSGIResource(_Reactor(), _Threadpool(), app.wsgi_app)) + if context_factory is None: + log.warning("listening on localhost because ssl is disabled") + reactor.listenTCP(port, site, interface='localhost') + else: + reactor.listenSSL(port, site, context_factory, interface=interface) + + +if __name__ == '__main__': # pragma: no cover + from minimock import Mock + server = Mock('server') + server.root.children = {} + + app.debug = True + app.secret_key='debug key' + app.run(port=8080) diff --git a/mjacob2/pypsyc/server/webif/templates/index.html b/mjacob2/pypsyc/server/webif/templates/index.html new file mode 100644 index 0000000..ad99e7e --- /dev/null +++ b/mjacob2/pypsyc/server/webif/templates/index.html @@ -0,0 +1,6 @@ +{% extends "layout.html" %} +{% block title %}welcome{% endblock %} +{% block content %} +

pypsyc

+register +{% endblock %} diff --git a/mjacob2/pypsyc/server/webif/templates/layout.html b/mjacob2/pypsyc/server/webif/templates/layout.html new file mode 100644 index 0000000..b851aaa --- /dev/null +++ b/mjacob2/pypsyc/server/webif/templates/layout.html @@ -0,0 +1,20 @@ + + + + {% block title %}{% endblock %} - pypsyc + + +{% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} +{% endwith %} +
+{% block content %}{% endblock %} +
+ + diff --git a/mjacob2/pypsyc/server/webif/templates/register.html b/mjacob2/pypsyc/server/webif/templates/register.html new file mode 100644 index 0000000..3fba4ff --- /dev/null +++ b/mjacob2/pypsyc/server/webif/templates/register.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} +{% block title %}register{% endblock %} +{% macro input(name, type='text', label=None) %} + + {{ label or name|capitalize }}: + + +{%- endmacro %} +{% block content %} +

register your identity

+{% if error %}

Error: {{ error }}{% endif %} +

+ + {{ input('username') }} + {{ input('password', type='password') }} + {{ input('password2', label='Confirm password', type='password') }} + + + + +
 
+
+{% endblock %} diff --git a/mjacob2/pypsyc/util.py b/mjacob2/pypsyc/util.py new file mode 100644 index 0000000..8d01e9b --- /dev/null +++ b/mjacob2/pypsyc/util.py @@ -0,0 +1,157 @@ +""" + pypsyc.util + ~~~~~~~~~~~ + + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import logging + +from greenlet import greenlet, getcurrent +from twisted.internet import reactor, error +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.tcp import Connector + + +scheduler = getcurrent() + +def schedule(f, *args, **kwds): + g = greenlet(f, scheduler) + if getcurrent() is scheduler: + g.switch(*args, **kwds) + else: + reactor.callLater(0, g.switch, *args, **kwds) + + +class Waiter(object): + def __init__(self): + self.greenlet = None + self.value = None + self.exception = None + + def callback(self, *args, **kwds): + if self.greenlet is None: + self.value, = args + else: + assert getcurrent() is not self.greenlet + self.greenlet.switch(*args, **kwds) + + def errback(self, exception): + if self.greenlet is None: + self.exception = exception + else: + self.greenlet.throw(exception) + + def get(self): + if self.value is not None: + return self.value + if self.exception is not None: + raise self.exception + self.greenlet = getcurrent() + return self.greenlet.parent.switch() + + +class DNSError(Exception): + pass + +@inlineCallbacks +def _resolve_hostname(host): + try: + name = '_psyc._tcp.%s.' % host + answers, auth, add = yield lookupService(name) + + srv_rr = answers[0] + assert srv_rr.name.name == name + host = srv_rr.payload.target.name + port = srv_rr.payload.port + + a_rr = [rr for rr in add if rr.name.name == host][0] + ip = a_rr.payload.dottedQuad() + + except (DNSNameError, IndexError): + try: + ip = yield getHostByName(host) + except DNSNameError: + raise DNSError("Unknown host %s." % host) + port = 4404 + + returnValue((ip, port)) + +try: + from twisted.names.client import lookupService, getHostByName + from twisted.names.error import DNSNameError +except ImportError: + log = logging.getLogger(__name__) + log.warn("twisted names isn't installed -- DNS SRV is disabled") + def resolve_hostname(host): + return host, 4404 +else: + def resolve_hostname(host): + waiter = Waiter() + _resolve_hostname(host).addCallbacks(waiter.callback, + lambda f: waiter.errback(f.value)) + return waiter.get() + + +class _PSYCConnector(Connector): + def __init__(self, *args): + Connector.__init__(self, *args, reactor=reactor) + self.waiter = Waiter() + + def connect(self): + assert self.state == 'disconnected', "can't connect in this state" + self.state = 'connecting' + + self.transport = transport = self._makeTransport() + self.timeoutID = self.reactor.callLater(self.timeout, + transport.failIfNotConnected, + error.TimeoutError()) + self.waiter.get() + return self.circuit + + def buildProtocol(self, addr): + self.circuit = Connector.buildProtocol(self, addr) + self.circuit.inited = self.waiter.callback + return self.circuit + + def connectionFailed(self, reason): + self.cancelTimeout() + self.transport = None + self.state = 'disconnected' + self.waiter.errback(reason.value) + + def connectionLost(self, reason): + self.state = 'disconnected' + self.factory.connection_lost(self.circuit, reason.value) + +def connect(host, port, factory, timeout=30, bindAddress=None): + return _PSYCConnector(host, port, factory, timeout, bindAddress).connect() + + +class Event(object): + def __init__(self): + self.observers = [] + + def add_observer(self, observer, *args, **kwds): + self.observers.append((observer, args, kwds)) + + def __iadd__(self, observer): + self.observers.append((observer, (), {})) + return self + + def __isub__(self, observer): + observers = [i for i in self.observers if i[0] == observer] + assert len(observers) == 1, observers + self.observers.remove(observers[0]) + return self + + def __call__(self, *args, **kwds): + for observer, args2, kwds2 in self.observers: + kwds.update(kwds2) + observer(*(args + args2), **kwds) + + +def key_intersection(a, b): + for k in b: + if k in a: + yield k diff --git a/mjacob2/setup.py b/mjacob2/setup.py new file mode 100644 index 0000000..30fc64b --- /dev/null +++ b/mjacob2/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup, find_packages + +import pypsyc + +setup( + name = "pypsyc", + version = pypsyc.__version__, + packages = find_packages(exclude='tests'), + package_data = { + 'pypsyc.client': ['psyc.ico'], + 'pypsyc.server.webif': ['templates/*'] + }, + zip_safe = False, + scripts = ['bin/pypsyc', 'bin/pypsycd'], + entry_points = ''' + [pypsyc.server.packages] + root = pypsyc.server.root:Root + person = pypsyc.server.person:Person + place = pypsyc.server.place:Place + ''' +) diff --git a/mjacob2/tests/__init__.py b/mjacob2/tests/__init__.py new file mode 100644 index 0000000..8733ea0 --- /dev/null +++ b/mjacob2/tests/__init__.py @@ -0,0 +1,4 @@ +import tests.helpers +from twisted.python.log import defaultObserver, PythonLoggingObserver +defaultObserver.stop() +PythonLoggingObserver().start() diff --git a/mjacob2/tests/constants.py b/mjacob2/tests/constants.py new file mode 100644 index 0000000..40091b2 --- /dev/null +++ b/mjacob2/tests/constants.py @@ -0,0 +1,41 @@ +from pypsyc.client.model import Presence +from pypsyc.core.mmp import Uni + + +SERVER1 = 'server1' +SERVER1_UNI = Uni('psyc://%s/' % SERVER1) +USER1 = '~user1' +USER2 = '~user2' +USER1_UNI = SERVER1_UNI.chain(USER1) +USER2_UNI = SERVER1_UNI.chain(USER2) +USER1_NICK = 'user1' +USER2_NICK = 'user2' +RESOURCE = '*Resource' +RESOURCE1_UNI = USER1_UNI.chain(RESOURCE) +RESOURCE2_UNI = USER2_UNI.chain(RESOURCE) +PLACE = '@place' +PLACE_UNI = SERVER1_UNI.chain(PLACE) +SERVER2 = 'server2' +SERVER2_UNI = Uni('psyc://%s/' % SERVER2) +SERVER3 = 'server3' +SERVER3_UNI = Uni('psyc://%s/' % SERVER3) + +INTERFACE = 'interface' +IP = '10.0.0.1' +PORT = 4404 +VARIABLES = {'_key': 'value'} +CONTENT = [''] + +PASSWORD = 'password' +MESSAGE = 'message' +UNKNOWN = 0 +OFFLINE = 1 +ONLINE = 7 +PRESENCE_UNKNOWN = Presence(UNKNOWN) +PRESENCE_OFFLINE = Presence(OFFLINE) +PRESENCE_ONLINE = Presence(ONLINE) +PENDING = 'pending' +OFFERED = 'offered' +ESTABLISHED = 'established' +ERROR = 'error' +EXCEPTION = Exception(ERROR) diff --git a/mjacob2/tests/helpers.py b/mjacob2/tests/helpers.py new file mode 100644 index 0000000..874b74e --- /dev/null +++ b/mjacob2/tests/helpers.py @@ -0,0 +1,133 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from functools import wraps +from traceback import format_exc +import sys + +from greenlet import getcurrent +from mock import Mock +import twisted.internet + +from pypsyc.core.mmp import Header + + +# keep reactor from being installed + +assert not 'twisted.internet.reactor' in sys.modules +twisted.internet.reactor = sys.modules['twisted.internet.reactor'] = None + + +# random helpers + +def inited_header(*args, **kwds): + header = Header(*args, **kwds) + header._init() + return header + +def _make_send_function(other_circuit): + def f(header, content): + try: + other_circuit.packet_received(inited_header(header), content) + except: + raise Exception("%s.packet_received raised exception:\n%s" % + (other_circuit, format_exc())) + return f + +def connect_circuits(c1, c2): + """Make a virtual connection between two curcuits.""" + c1.send = c1.psyc._send = _make_send_function(c2) + c2.send = c2.psyc._send = _make_send_function(c1) + return c1, c2 + + +class iter_(object): + def __init__(self, *iter_): + self.iter_ = iter_ + + def __eq__(self, iter_): + return tuple(iter_) == self.iter_ + +def rendered(packet): + return iter_(*packet.render()) + + +def check_success(f): + """ + Wrap `f` so that it checks if :attr:`TestCase.success` is ``True``. This is + useful to check if an asynchronous test has passed completely. + """ + @wraps(f) + def _wrapper(self): + self.success = False + f(self) + assert self.success + del self.success + return _wrapper + + +class AsyncMethod(object): + def __call__(self, *args, **kwds): + self.child = getcurrent() + return self.child.parent.switch() + + def callback(self, *args, **kwds): + self.child.switch(*args, **kwds) + + def errback(self, *args, **kwds): + self.child.throw(*args, **kwds) + + +def mockify(obj, attrs): + """Replace attrs of obj with mocks. Return the original attributes.""" + originals = {} + for attr in attrs: + orig = originals[attr] = getattr(obj, attr) + mock = Mock(spec=orig) + setattr(obj, attr, mock) + return originals + +class mockified(object): + """ + Wrap a function so that it 'mockifies' and resets `obj`. If `obj` is a + string, mockify the module named like it. + Can also be used as a context manager. + """ + def __init__(self, obj, attrs): + if isinstance(obj, basestring): + self.obj = __import__(obj, globals(), locals(), attrs) + else: + self.obj = obj + self.attrs = attrs + + def __enter__(self): + self.originals = mockify(self.obj, self.attrs) + return (getattr(self.obj, attr) for attr in self.attrs) + + def __exit__(self, type, value, traceback): + for attr, original in self.originals.iteritems(): + setattr(self.obj, attr, original) + + def __call__(self, f): + @wraps(f) + def _wrapper(*args, **kwds): + with self: + kwds.update((attr, getattr(self.obj, attr)) for attr in self.attrs) + return f(*args, **kwds) + return _wrapper + + +class PlaceHolder(object): + """ + An object that be used as a placeholder for comparisons. The object on the + other side of the comparison is saved as attribute `obj`. + """ + + def __eq__(self, other): + self.obj = other + return True + + +class StubException(Exception): + pass diff --git a/mjacob2/tests/test_client/test_controller.py b/mjacob2/tests/test_client/test_controller.py new file mode 100644 index 0000000..fdf1f8e --- /dev/null +++ b/mjacob2/tests/test_client/test_controller.py @@ -0,0 +1,680 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock, MagicMock +from nose.tools import assert_raises +from tests.constants import (SERVER1, USER1_UNI, USER2_UNI, USER1_NICK, + USER2_NICK, PLACE_UNI, SERVER2_UNI, MESSAGE, PRESENCE_OFFLINE, + PRESENCE_ONLINE, ERROR, EXCEPTION) +from tests.helpers import mockified, AsyncMethod, PlaceHolder, iter_ + +from pypsyc.client.controller import (ListBinding, AccountsController, + ConversationController, ConferenceController, TabsController, + FriendListController, DumpController, MainController) +from pypsyc.client.model import Message +from pypsyc.client.observable import ObsList, ObsDict, ObsObj, ObsAttr +from pypsyc.util import Event + + +def _check_obs_list(obs_list): + assert obs_list.setitem_evt.observers == [] + assert obs_list.delitem_evt.observers == [] + assert obs_list.insert_evt.observers == [] + assert obs_list.update_evt.observers == [] + return True + + +def _check_obs_dict(obs_dict): + assert obs_dict.setitem_evt.observers == [] + assert obs_dict.delitem_evt.observers == [] + assert obs_dict.update_evt.observers == [] + return True + + +def _check_obs_obj(obs_obj): + assert obs_obj.update_evt != {} + for evt in obs_obj.update_evt.itervalues(): + assert evt.observers == [] + return True + + +class TestListBinding(object): + def setup(self): + self.model = Mock() + self.model_list = ObsList([self.model]) + self.view_list = [] + self.make_row = Mock() + self.binding = ListBinding(self.model_list, self.view_list, + self.make_row) + + def test_bind(self): + assert self.make_row.call_args_list == [((self.model,),)] + assert self.view_list == [self.make_row.return_value] + + def test_add(self): + assert self.make_row.call_args_list == [((self.model,),)] + self.make_row.reset_mock() + + model2 = Mock() + self.model_list.append(model2) + assert self.make_row.call_args_list == [((model2,),)] + assert self.view_list == [self.make_row.return_value] * 2 + + def test_update(self): + assert self.make_row.call_args_list == [((self.model,),)] + self.make_row.reset_mock() + + self.model_list.updated_item(self.model) + assert self.make_row.call_args_list == [((self.model,),)] + assert self.view_list == [self.make_row.return_value] + + def test_delete(self): + del self.model_list[0] + assert self.view_list == [] + + def test_unbind(self): + self.binding.unbind() + assert _check_obs_list(self.model_list) + assert self.view_list == [] + + +class _List(list): + def __init__(self, *args, **kwds): + list.__init__(self, *args, **kwds) + self.updated_item = Mock() + +class TestAccountsController(object): + @mockified('pypsyc.client.controller', ['ListBinding']) + def setup(self, ListBinding): + self.ListBinding = ListBinding + self.account = Mock() + self.client = Mock() + self.client.accounts = _List([self.account]) + self.view = Mock() + self.controller = AccountsController(self.client, self.view) + + def test_binding(self): + make_row_ph = PlaceHolder() + assert self.ListBinding.call_args_list == [ + ((self.client.accounts, self.view.accounts, make_row_ph),)] + + account = Mock() + row = make_row_ph.obj(account) + assert row == (account.uni, account.active) + + @mockified('pypsyc.client.controller', ['Account']) + def test_add_account(self, Account): + account = Account.return_value + callback_ph = PlaceHolder() + + self.controller.add_account() + assert Account.call_args_list == [ + ((self.client, '', '', '', False, False),)] + assert self.view.method_calls == [ + ('show_addedit_dialog', (account.__dict__, True, callback_ph))] + + callback_ph.obj() + assert self.client.accounts == [self.account, account] + assert self.client.method_calls == [('save_accounts',)] + + def test_edit_account(self): + callback_ph = PlaceHolder() + + self.controller.edit_account(0) + assert self.view.method_calls == [ + ('show_addedit_dialog', (self.account.__dict__, False, + callback_ph))] + + callback_ph.obj() + assert self.client.accounts.updated_item.call_args_list == [ + ((self.account,),)] + assert self.client.method_calls == [('save_accounts',)] + + def test_remove_account(self): + assert_raises(IndexError, self.controller.remove_account, 1) + self.controller.remove_account(0) + assert self.client.accounts == [] + assert self.client.method_calls == [('save_accounts',)] + + def test_set_active(self): + self.controller.set_active(0, True) + assert self.account.active == True + assert self.client.accounts.updated_item.call_args_list == [ + ((self.account,),)] + assert self.client.method_calls == [('save_accounts',)] + + def test_closed(self): + self.controller.closed() + assert self.ListBinding.return_value.method_calls == [('unbind',)] + + +IN = 'i' +OUT = 'o' +LINE = 'line' + +class _StubAccount(ObsObj): + circuit = ObsAttr('circuit') + +class TestDumpController: + def setup(self): + self.circuit = Mock() + self.circuit.dump_evt = Event() + self.account = _StubAccount() + self.account.uni = USER1_UNI + self.client = Mock() + self.client.accounts = ObsList() + self.view = Mock() + + def test_connected(self): + self.account.circuit = self.circuit + self.client.accounts.append(self.account) + DumpController(self.client, self.view) + + self.circuit.dump_evt(IN, LINE) + assert self.view.method_calls == [('show_line', (IN, LINE, USER1_UNI))] + + def test_connect(self): + DumpController(self.client, self.view) + self.client.accounts.append(self.account) + self.account.circuit = self.circuit + + self.circuit.dump_evt(IN, LINE) + assert self.view.method_calls == [('show_line', (IN, LINE, USER1_UNI))] + + def test_disconnect(self): + self.account.circuit = self.circuit + self.client.accounts.append(self.account) + DumpController(self.client, self.view) + + del self.account.circuit + assert self.circuit.dump_evt.observers == [] + + def test_reconnect(self): + DumpController(self.client, self.view) + self.account.circuit = self.circuit + self.client.accounts.append(self.account) + + circuit2 = Mock() + circuit2.dump_evt = Event() + self.account.circuit = circuit2 + assert self.circuit.dump_evt.observers == [] + + circuit2.dump_evt(OUT, LINE) + assert self.view.method_calls == [ + ('show_line', (OUT, LINE, USER1_UNI))] + + def test_remove(self): + self.client.accounts.append(self.account) + self.account.circuit = self.circuit + DumpController(self.client, self.view) + + self.client.accounts.remove(self.account) + assert _check_obs_obj(self.account) + assert self.circuit.dump_evt.observers == [] + + def test_remove_nocircuit(self): + self.client.accounts.append(self.account) + DumpController(self.client, self.view) + + self.client.accounts.remove(self.account) + assert _check_obs_obj(self.account) + + def test_closed(self): + self.client.accounts.append(self.account) + self.account.circuit = self.circuit + dump_controller = DumpController(self.client, self.view) + + dump_controller.closed() + assert _check_obs_list(self.client.accounts) + assert _check_obs_obj(self.account) + assert self.circuit.dump_evt.observers == [] + + +class TestConversationController(object): + def setup(self): + self.tabs_controller = Mock() + self.conversation = Mock() + self.conversation.messages = ObsList() + self.conversation.unknown_target_evt = Event() + self.conversation.delivery_failed_evt = Event() + self.view = Mock() + self.controller = ConversationController(self.tabs_controller, + self.conversation, self.view) + + def test_message(self): + MESSAGE_OBJ = Message(USER2_UNI, MESSAGE) + MESSAGE_LINE = "(%s) <%s> %s" % ( + MESSAGE_OBJ.time.strftime("%H:%M:%S"), USER2_UNI, MESSAGE) + + self.conversation.messages.append(MESSAGE_OBJ) + assert self.view.method_calls == [('show_message', (MESSAGE_LINE,))] + + def test_unknown_target(self): + self.conversation.unknown_target_evt() + assert self.view.method_calls == [('show_unknown_target',)] + + def test_delivery_failed(self): + self.conversation.delivery_failed_evt(ERROR) + assert self.view.method_calls == [('show_delivery_failed', (ERROR,))] + + def test_enter_message(self): + self.controller.enter(MESSAGE) + assert self.conversation.method_calls == [('send_message', (MESSAGE,))] + + def test_enter_command(self): + self.controller.enter("/command") + assert self.view.method_calls == [('show_unknown_command',)] + assert self.conversation.method_calls == [] + + def test_closed(self): + self.conversation.messages.append(Message(None, None)) + self.controller.closed() + assert _check_obs_list(self.conversation.messages) + assert self.conversation.unknown_target_evt.observers == [] + assert self.conversation.delivery_failed_evt.observers == [] + + +class TestConferenceController(object): + @mockified('pypsyc.client.controller', ['ListBinding']) + def setup(self, ListBinding): + self.ListBinding = ListBinding + self.tabs_controller = Mock() + self.conference = Mock() + self.conference.members = [] + self.conference.messages = ObsList() + self.conference.unknown_target_evt = Event() + self.conference.delivery_failed_evt = Event() + self.view = Mock() + self.controller = ConferenceController(self.tabs_controller, + self.conference, self.view) + + def test_binding(self): + makerow_ph = PlaceHolder() + assert self.ListBinding.call_args_list == [ + ((self.conference.members, self.view.members, makerow_ph),)] + + member = Mock() + row = makerow_ph.obj(member) + assert row == (member.uni, member.nick) + + def test_open_conversation(self): + member = Mock() + self.conference.members.append(member) + account = self.conference.conferencing.account + conversation = account.get_conversation.return_value + + self.controller.open_conversation(0) + assert self.conference.conferencing.account.method_calls == [ + ('get_conversation', (member.uni,))] + assert self.tabs_controller.method_calls == [ + ('focus_conversation', (conversation,))] + + def test_closed(self): + self.controller.closed() + assert self.ListBinding.return_value.method_calls == [('unbind',)] + assert _check_obs_list(self.conference.messages) + assert self.conference.unknown_target_evt.observers == [] + assert self.conference.delivery_failed_evt.observers == [] + + +class TestTabsController(object): + def setup(self): + self.client = Mock() + self.client.accounts = ObsList() + self.view = Mock() + + self.account = Mock() + self.account.conversations = ObsDict() + self.account.conferences = ObsDict() + self.client.accounts.append(self.account) + + @mockified('pypsyc.client.controller', ['ConversationController', + 'ConferenceController']) + def test_deleted_account(self, ConversationController, + ConferenceController): + self.account.conversations[USER2_UNI] = Mock() + self.account.conferences[PLACE_UNI] = Mock() + conv_view = self.view.show_conversation.return_value + conv_controller = ConversationController.return_value + conf_view = self.view.show_conference.return_value + conf_controller = ConferenceController.return_value + TabsController(self.client, self.view) + self.view.reset_mock() + + del self.client.accounts[0] + assert _check_obs_dict(self.account.conversations) + assert _check_obs_dict(self.account.conferences) + assert self.view.method_calls == [('remove_tab', (conv_view,)), + ('remove_tab', (conf_view,))] + assert conv_controller.method_calls == [('closed',)] + assert conf_controller.method_calls == [('closed',)] + + @mockified('pypsyc.client.controller', ['ConversationController']) + def test_conversation(self, ConversationController): + conversation = Mock() + conversation.uni = USER2_UNI + conv_view = self.view.show_conversation.return_value + conv_controller = ConversationController.return_value + controller = TabsController(self.client, self.view) + + self.account.conversations[USER2_UNI] = conversation + assert self.view.method_calls == [('show_conversation', (USER2_UNI,))] + assert ConversationController.call_args_list == [ + ((controller, conversation, conv_view),)] + self.view.reset_mock() + + controller.focus_conversation(conversation) + assert self.view.method_calls == [('focus_tab', (conv_view,))] + self.view.reset_mock() + + controller.close_tab(conv_view) + assert self.account.conversations == {} + assert self.view.method_calls == [('remove_tab', (conv_view,))] + assert conv_controller.method_calls == [('closed',)] + + @mockified('pypsyc.client.controller', ['ConferenceController']) + def test_conference(self, ConferenceController): + conference = Mock() + conference.uni = PLACE_UNI + conf_view = self.view.show_conference.return_value + conf_controller = ConferenceController.return_value + self.account.conferences[PLACE_UNI] = conference + + controller = TabsController(self.client, self.view) + assert self.view.method_calls == [ + ('show_conference', (PLACE_UNI,),)] + assert ConferenceController.call_args_list == [ + ((controller, conference, conf_view),)] + self.view.reset_mock() + + controller.focus_conversation(conference) + assert self.view.method_calls == [('focus_tab', (conf_view,))] + self.view.reset_mock() + + controller.close_tab(conf_view) + assert self.account.conferences == {} + assert self.view.method_calls == [('remove_tab', (conf_view,))] + assert conf_controller.method_calls == [('closed',)] + + + +class TestFriendListController(object): + @mockified('pypsyc.client.controller', ['ListBinding']) + def setup(self, ListBinding): + self.ListBinding = ListBinding + model_list_ph = PlaceHolder() + make_row_ph = PlaceHolder() + self.client = Mock() + self.client.accounts = ObsList() + self.view = Mock() + self.tabs_controller = Mock() + + self.controller = FriendListController(self.client, self.view, + self.tabs_controller) + assert self.ListBinding.call_args_list == [ + ((model_list_ph, self.view.friends, make_row_ph),)] + self.model_list = model_list_ph.obj + self.make_row = make_row_ph.obj + + def test_binding(self): + EXTERN_FRIEND_UNI = SERVER2_UNI.chain('~friend') + + friend = Mock() + friend.account.server = SERVER1 + friend.uni = USER1_UNI + friend.presence = PRESENCE_OFFLINE + row = self.make_row(friend) + assert row == (USER1_NICK, False, friend.state) + + friend.uni = EXTERN_FRIEND_UNI + friend.presence = PRESENCE_ONLINE + row = self.make_row(friend) + assert row == (EXTERN_FRIEND_UNI, True, friend.state) + + def test_account(self): + update_evt = Mock() + friend1 = Mock() + friend2 = Mock() + account = Mock() + account.friends = ObsDict({USER1_UNI: friend1}) + + self.model_list.update_evt += update_evt + assert self.model_list == [] + + self.client.accounts.append(account) + assert self.model_list == [friend1] + + account.friends[USER2_UNI] = friend2 + assert self.model_list == [friend1, friend2] + + account.friends.updated_item(USER2_UNI) + assert update_evt.call_args_list == [((1, friend2),)] + + del account.friends[USER1_UNI] + assert self.model_list == [friend2] + + del self.client.accounts[0] + assert _check_obs_dict(account.friends) + assert self.model_list == [] + + def test_open_conversation(self): + friend = Mock() + friend.uni = USER2_UNI + self.model_list.append(friend) + conversation = friend.account.get_conversation.return_value + + self.controller.open_conversation(0) + assert friend.account.method_calls == [ + ('get_conversation', (USER2_UNI,))] + assert self.tabs_controller.method_calls == [ + ('focus_conversation', (conversation,))] + + def test_accept_friendship(self): + friend = Mock() + friend.uni = USER2_UNI + friend.account.add_friend.side_effect = AsyncMethod() + self.model_list.append(friend) + + self.controller.accept_friendship(0) + assert friend.account.method_calls == [('add_friend', (USER2_UNI,))] + + def test_cancel_friendship(self): + friend = Mock() + friend.uni = USER2_UNI + friend.account.remove_friend.side_effect = AsyncMethod() + self.model_list.append(friend) + + self.controller.cancel_friendship(0) + assert friend.account.method_calls == [('remove_friend', (USER2_UNI,))] + + +class TestMainController(object): + @mockified('pypsyc.client.controller', ['TabsController', + 'FriendListController']) + def setup(self, TabsController, FriendListController): + self.client = Mock() + self.client.accounts = ObsList() + self.view = Mock() + self.controller = MainController(self.client, self.view) + self.TabsController = TabsController + self.FriendListController = FriendListController + + def test_main_controller(self): + tabs_controller = self.TabsController.return_value + assert self.TabsController.call_args_list == [ + ((self.client, self.view.tabs_view),)] + assert self.FriendListController.call_args_list == [ + ((self.client, self.view.friends_view, tabs_controller),)] + + def test_no_password(self): + account = self._make_account() + self.client.accounts.append(account) + callback_ph = PlaceHolder() + update_evt = Mock() + self.client.accounts.update_evt += update_evt + + account.no_password_evt() + assert self.view.method_calls == [ + ('show_password_dialog', (account.uni, account.__dict__, + callback_ph))] + callback_ph.obj() + assert account.active == True + assert update_evt.call_args_list == [((0, account),)] + assert self.client.method_calls == [('save_accounts',)] + + del self.client.accounts[0] + assert account.no_password_evt.observers == [] + + def test_connection_error(self): + account = self._make_account() + self.client.accounts.append(account) + + account.connection_error_evt(EXCEPTION) + assert self.view.method_calls == [ + ('show_conn_error', (account.uni, ERROR))] + + del self.client.accounts[0] + assert account.connection_error_evt.observers == [] + + def test_no_such_user(self): + account = self._make_account() + self.client.accounts.append(account) + + account.no_such_user_evt(EXCEPTION) + assert self.view.method_calls == [('show_no_such_user', (ERROR,))] + + del self.client.accounts[0] + assert account.no_such_user_evt.observers == [] + + def test_auth_error(self): + account = self._make_account() + self.client.accounts.append(account) + + account.auth_error_evt(EXCEPTION) + assert self.view.method_calls == [ + ('show_auth_error', (account.uni, ERROR))] + + del self.client.accounts[0] + assert account.auth_error_evt.observers == [] + + def _make_account(self, active=False): + account = Mock() + account.active = active + account.no_password_evt = Event() + account.connection_error_evt = Event() + account.no_such_user_evt = Event() + account.auth_error_evt = Event() + return account + + @mockified('pypsyc.client.controller', ['AccountsController']) + def test_open_accounts(self, AccountsController): + self.controller.open_accounts() + assert AccountsController.call_args_list == [ + ((self.client, self.view.show_accounts.return_value),)] + + @mockified('pypsyc.client.controller', ['DumpController']) + def test_open_dump(self, DumpController): + self.controller.open_dump() + assert DumpController.call_args_list == [ + ((self.client, self.view.show_dump_win.return_value),)] + + def test_open_conversation(self): + account1 = Mock() + account1.active = False + account2 = Mock() + account2.active = True + self.client.accounts = [account1, account2] + callback_ph = PlaceHolder() + conversation = account2.get_conversation.return_value + + self.controller.open_conversation() + assert self.view.method_calls == [ + ('show_open_conv_dialog', (iter_(account2.uni), callback_ph))] + + callback_ph.obj(0, SERVER1, USER2_NICK) + assert account2.method_calls == [('get_conversation', (USER2_UNI,))] + assert self.TabsController.return_value.method_calls == [ + ('focus_conversation', (conversation,))] + + def test_open_conference(self): + account1 = Mock() + account1.active = False + account2 = Mock() + account2.active = True + self.client.accounts = [account1, account2] + callback_ph = PlaceHolder() + conference = account2.get_conference.return_value + + self.controller.open_conference() + assert self.view.method_calls == [ + ('show_open_conf_dialog', (iter_(account2.uni), callback_ph))] + + callback_ph.obj(0, SERVER1, 'place') + assert account2.method_calls == [ + ('get_conference', (PLACE_UNI,), {'subscribe': True})] + assert self.TabsController.return_value.method_calls == [ + ('focus_conversation', (conference,))] + + def test_add_friend(self): + account1 = Mock() + account1.active = False + account2 = Mock() + account2.active = True + account2.add_friend.side_effect = AsyncMethod() + callback_ph = PlaceHolder() + self.client.accounts = [account1, account2] + + self.controller.add_friend() + assert self.view.method_calls == [ + ('show_add_friend_dialog', (iter_(account2.uni), callback_ph))] + + callback_ph.obj(0, SERVER1, USER2_NICK) + assert account2.method_calls == [('add_friend', (USER2_UNI,))] + + def test_show_active_accounts(self): + account1 = self._make_account(active=True) + account2 = self._make_account(active=True) + + self.client.accounts.append(account1) + assert self.view.method_calls == [('show_active_accounts', (True,))] + self.view.reset_mock() + + self.client.accounts.append(account2) + assert self.view.method_calls == [] + + self.client.accounts.remove(account1) + assert self.view.method_calls == [] + + self.client.accounts.remove(account2) + assert self.view.method_calls == [('show_active_accounts', (False,))] + + def test_show_active_accounts_updated(self): + account = self._make_account(active=False) + + self.client.accounts.append(account) + assert self.view.method_calls == [] + + account.active = True + self.client.accounts.updated_item(account) + assert self.view.method_calls == [('show_active_accounts', (True,))] + self.view.reset_mock() + + self.client.accounts.updated_item(account) + assert self.view.method_calls == [] + + account.active = False + self.client.accounts.updated_item(account) + assert self.view.method_calls == [('show_active_accounts', (False,))] + self.view.reset_mock() + + self.client.accounts.updated_item(account) + assert self.view.method_calls == [] + + del self.client.accounts[0] + assert self.view.method_calls == [] + + def test_quit(self): + self.controller.quit() + assert _check_obs_list(self.client.accounts) + assert self.client.method_calls == [('quit',)] diff --git a/mjacob2/tests/test_client/test_model.py b/mjacob2/tests/test_client/test_model.py new file mode 100644 index 0000000..5b04f44 --- /dev/null +++ b/mjacob2/tests/test_client/test_model.py @@ -0,0 +1,563 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from os import remove +from tempfile import NamedTemporaryFile + +from mock import Mock, MagicMock, sentinel +from nose.tools import assert_raises +from tests.constants import (SERVER1, SERVER2, USER1_UNI, USER2_UNI, + USER1_NICK, USER2_NICK, RESOURCE, RESOURCE1_UNI, PLACE_UNI, VARIABLES, + CONTENT, PASSWORD, MESSAGE, ONLINE, PRESENCE_UNKNOWN, PRESENCE_ONLINE, + OFFERED, ESTABLISHED, ERROR, EXCEPTION) +from tests.helpers import mockified, PlaceHolder, inited_header, AsyncMethod + +from pypsyc.client.model import (Conversation, Member, Conference, Messaging, + Conferencing, Account, FriendList, ClientCircuit, Circuit, Client) +from pypsyc.core.psyc import PSYCPacket +from pypsyc.protocol import (UnknownTargetError, DeliveryFailedError, + AuthenticationError) + + +class TestConversation(object): + def test_send_message(self): + message_ph = PlaceHolder() + messaging = Mock() + conversation = Conversation(messaging, USER2_UNI) + + conversation.send_message(MESSAGE) + assert messaging.protocol.method_calls == [ + ('send_private_message', (USER2_UNI, MESSAGE), + {'relay': messaging.account.uni})] + assert conversation.messages == [message_ph] + assert message_ph.obj.source == messaging.account.uni + + +class TestConference(object): + def test_send_message(self): + conferencing = Mock() + conference = Conference(conferencing, PLACE_UNI) + + conference.send_message(MESSAGE) + assert conferencing.protocol.method_calls == [ + ('send_public_message', (PLACE_UNI, MESSAGE))] + + +class TestMessaging(object): + def test_recv_message(self): + account = Mock() + conversation = account.get_conversation.return_value + conversation.messages = [] + message_ph = PlaceHolder() + messaging = Messaging(account) + + messaging.private_message(USER2_UNI, MESSAGE) + assert account.get_conversation.call_args_list == [((USER2_UNI,),)] + assert conversation.messages == [message_ph] + assert message_ph.obj.source == USER2_UNI + assert message_ph.obj.message == MESSAGE + + @mockified('pypsyc.client.model', ['MessagingProtocol']) + def test_connected(self, MessagingProtocol): + circuit = Mock() + messaging = Messaging(Mock()) + + messaging.connected(circuit) + assert circuit.psyc.method_calls == [ + ('add_handler', (MessagingProtocol.return_value,))] + + def test_disconnected(self): + Messaging(Mock).disconnected() + + +class TestConferencing(object): + def test_member_entered(self): + account = Mock() + conference = account.get_conference.return_value + conference.members = [] + conferencing = Conferencing(account) + + conferencing.member_entered(PLACE_UNI, USER2_UNI, USER2_NICK) + assert account.get_conference.call_args_list == [((PLACE_UNI,),)] + assert conference.members == [Member(USER2_UNI, USER2_NICK)] + + def test_member_left(self): + account = Mock() + conference = account.get_conference.return_value + conference.members = [Member(USER1_UNI, USER1_NICK), + Member(USER2_UNI, USER2_NICK)] + conferencing = Conferencing(account) + + conferencing.member_left(PLACE_UNI, USER2_UNI) + assert conference.members == [Member(USER1_UNI, USER1_NICK)] + + def test_recv_message(self): + account = Mock() + conference = account.get_conference.return_value + conference.messages = [] + message_ph = PlaceHolder() + conferencing = Conferencing(account) + + conferencing.public_message(PLACE_UNI, USER2_UNI, MESSAGE) + assert account.get_conference.call_args_list == [((PLACE_UNI,),)] + assert conference.messages == [message_ph] + assert message_ph.obj.source == USER2_UNI + assert message_ph.obj.message == MESSAGE + + @mockified('pypsyc.client.model', ['ConferencingProtocol']) + def test_connected(self, ConferencingProtocol): + circuit = Mock() + conferencing = Conferencing(Mock()) + + conferencing.connected(circuit) + assert circuit.method_calls == [ + ('psyc.add_handler', (ConferencingProtocol.return_value,)), + ('add_pvar_handler', (ConferencingProtocol.return_value,))] + + def test_disconnected(self): + Conferencing(Mock()).disconnected() + + +class _Dict(dict): + def __init__(self, *args, **kwds): + dict.__init__(self, *args, **kwds) + self.updated_item = Mock() + +class TestFriendList(object): + def test_friendship(self): + friend_ph = PlaceHolder() + account = Mock() + account.friends = _Dict() + friend_list = FriendList(account) + + friend_list.friendship(USER1_UNI, OFFERED) + assert account.friends == {USER1_UNI: friend_ph} + friend = friend_ph.obj + assert friend.account == account + assert friend.uni == USER1_UNI + assert friend.presence == PRESENCE_UNKNOWN + assert friend.state == OFFERED + + friend_list.friendship(USER1_UNI, ESTABLISHED) + assert friend.state == ESTABLISHED + assert account.friends.updated_item.call_args_list == [((USER1_UNI,),)] + + friend_list.friendship_removed(USER1_UNI) + assert account.friends == {} + + def test_presence(self): + account = Mock() + account.friends = _Dict({USER2_UNI: Mock()}) + friend_list = FriendList(account) + + friend_list.presence(USER2_UNI, ONLINE) + assert account.friends[USER2_UNI].presence == PRESENCE_ONLINE + assert account.friends.updated_item.call_args_list == [((USER2_UNI,),)] + + @mockified('pypsyc.client.model', ['FriendshipProtocol']) + def test_connected(self, FriendshipProtocol): + circuit = Mock() + friend_list = FriendList(Mock()) + + friend_list.connected(circuit) + assert circuit.method_calls == [ + ('psyc.add_handler', (FriendshipProtocol.return_value,)), + ('add_pvar_handler', (FriendshipProtocol.return_value,))] + + def test_disconnected(self): + account = Mock() + account.friends = _Dict({USER2_UNI: Mock()}) + friend_list = FriendList(account) + + friend_list.disconnected() + assert account.friends == {} + + +class TestAccount(object): + @mockified('pypsyc.client.model', ['ObsDict']) + def test_account(self, ObsDict): + obs_dict = ObsDict.return_value = MagicMock() + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + assert account.uni == USER1_UNI + assert ObsDict.call_args_list == [(), (), ()] + assert account.conversations == obs_dict + assert account.conferences == obs_dict + assert account.friends == obs_dict + + @mockified('pypsyc.client.model', ['Messaging', 'Conversation']) + def test_get_conversation(self, Messaging, Conversation): + messaging = Messaging.return_value + conversation = Conversation.return_value + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + assert Messaging.call_args_list == [((account,),)] + + assert account.get_conversation(USER2_UNI) == conversation + assert account.get_conversation(USER2_UNI) == conversation + assert Conversation.call_args_list == [((messaging, USER2_UNI),)] + assert account.conversations == {USER2_UNI: conversation} + + @mockified('pypsyc.client.model', ['Conferencing', 'Conference']) + def test_get_conference(self, Conferencing, Conference): + conferencing = Conferencing.return_value + conference = Conference.return_value + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + assert Conferencing.call_args_list == [((account,),)] + + assert account.get_conference(PLACE_UNI) == conference + assert account.get_conference(PLACE_UNI) == conference + assert Conference.call_args_list == [((conferencing, PLACE_UNI),)] + assert account.conferences == {PLACE_UNI: conference} + + @mockified('pypsyc.client.model', ['Conferencing', 'Conference']) + def test_get_conference_subscribe(self, Conferencing, Conference): + conferencing = Conferencing.return_value + conference = Conference.return_value + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + client_interface = account.client_interface = Mock() + client_interface.subscribe.side_effect = AsyncMethod() + + assert account.get_conference(PLACE_UNI, subscribe=True) == conference + assert account.get_conference(PLACE_UNI, subscribe=True) == conference + assert Conference.call_args_list == [((conferencing, PLACE_UNI),)] + assert account.conferences == {PLACE_UNI: conference} + assert client_interface.method_calls == [('subscribe', (PLACE_UNI,))] + + @mockified('pypsyc.client.model', ['Conference']) + def test_get_conference_subscribe_unknown_target(self, Conference): + conference = Conference.return_value + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + client_interface = account.client_interface = Mock() + client_interface.subscribe.side_effect = UnknownTargetError + + account.get_conference(PLACE_UNI, subscribe=True) + assert conference.method_calls == [('unknown_target_evt',)] + + @mockified('pypsyc.client.model', ['Conference']) + def test_get_conference_subscribe_delivery_failed(self, Conference): + conference = Conference.return_value + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + client_interface = account.client_interface = Mock() + client_interface.subscribe.side_effect = DeliveryFailedError(ERROR) + + account.get_conference(PLACE_UNI, subscribe=True) + assert conference.method_calls == [('delivery_failed_evt', (ERROR,))] + + def test_remove_conference(self): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + account.conferences[PLACE_UNI] = Mock() + client_interface = account.client_interface = Mock() + client_interface.unsubscribe.side_effect = AsyncMethod() + + del account.conferences[PLACE_UNI] + assert client_interface.method_calls == [('unsubscribe', (PLACE_UNI,))] + + def test_add_friend(self): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + client_interface = account.client_interface = Mock() + + account.add_friend(USER2_UNI) + assert client_interface.method_calls == [('add_friend', (USER2_UNI,))] + + def test_remove_friend(self): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + client_interface = account.client_interface = Mock() + + account.remove_friend(USER2_UNI) + assert client_interface.method_calls == [ + ('remove_friend', (USER2_UNI,))] + + @mockified('pypsyc.client.model', ['FriendList']) + def test_friendlist(self, FriendList): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + assert FriendList.call_args_list == [((account,),)] + + def test_unknown_target_error(self): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + account.conversations[USER1_UNI] = conversation = Mock() + account.conferences[PLACE_UNI] = conference = Mock() + + account.unknown_target_error(USER1_UNI) + account.unknown_target_error(USER2_UNI) + account.unknown_target_error(PLACE_UNI) + assert conversation.method_calls == [('unknown_target_evt',)] + assert conference.method_calls == [('unknown_target_evt',)] + + def test_delivery_failed_error(self): + account = Account(None, SERVER1, USER1_NICK, PASSWORD, True, False) + account.conversations[USER1_UNI] = conversation = Mock() + account.conferences[PLACE_UNI] = conference = Mock() + + account.delivery_failed_error(USER1_UNI, ERROR) + account.delivery_failed_error(USER2_UNI, ERROR) + account.delivery_failed_error(PLACE_UNI, ERROR) + assert conversation.method_calls == [('delivery_failed_evt', (ERROR,))] + assert conference.method_calls == [('delivery_failed_evt', (ERROR,))] + + @mockified('pypsyc.client.model', ['resolve_hostname', 'connect', + 'LinkingProtocol', + 'RoutingErrorProtocol', + 'ClientInterfaceProtocol', 'Messaging', + 'Conferencing', 'FriendList']) + def test_active(self, resolve_hostname, connect, LinkingProtocol, + RoutingErrorProtocol, ClientInterfaceProtocol, Messaging, + Conferencing, FriendList): + resolve_hostname.return_value = (sentinel.ip, sentinel.port) + circuit = connect.return_value + linking_protocol = LinkingProtocol.return_value + linked = linking_protocol.link.side_effect = AsyncMethod() + messaging = Messaging.return_value + conferencing = Conferencing.return_value + friend_list = FriendList.return_value + client = Mock() + account = Account(client, SERVER1, USER1_NICK, PASSWORD, True, False) + + account.active = True + account.active = True + assert resolve_hostname.call_args_list == [((account.server,),)] + assert connect.call_args_list == [ + ((sentinel.ip, sentinel.port, client),)] + assert LinkingProtocol.call_args_list == [((circuit,),)] + assert linking_protocol.method_calls == [ + ('link', (USER1_UNI, RESOURCE, PASSWORD))] + + linked.callback() + assert circuit.psyc.uni == RESOURCE1_UNI + assert RoutingErrorProtocol.call_args_list == [((account,),)] + assert circuit.psyc.method_calls == [ + ('add_handler', (RoutingErrorProtocol.return_value,))] + assert ClientInterfaceProtocol.call_args_list == [((account,),)] + assert messaging.method_calls == [('connected', (circuit,))] + assert conferencing.method_calls == [('connected', (circuit,))] + assert friend_list.method_calls == [('connected', (circuit,))] + messaging.reset_mock() + conferencing.reset_mock() + friend_list.reset_mock() + + account.active = False + account.active = False + assert circuit.transport.method_calls == [('loseConnection',)] + assert not hasattr(account, 'circuit') + assert messaging.method_calls == [('disconnected',)] + assert conferencing.method_calls == [('disconnected',)] + assert friend_list.method_calls == [('disconnected',)] + + @mockified('pypsyc.client.model', ['resolve_hostname']) + def test_active_no_password(self, resolve_hostname): + no_password_evt = Mock() + account = Account(None, SERVER1, USER1_NICK, '', False, False) + account.no_password_evt += no_password_evt + + account.active = True + assert account.active == False + assert no_password_evt.call_args_list == [()] + assert resolve_hostname.call_args_list == [] + + @mockified('pypsyc.client.model', ['resolve_hostname', 'connect']) + def test_active_dns_error(self, resolve_hostname, connect): + resolve_hostname.side_effect = EXCEPTION + client = Mock() + connection_error_evt = Mock() + account = Account(client, SERVER1, USER1_NICK, PASSWORD, False, False) + account.connection_error_evt += connection_error_evt + + account.active = True + assert account.active == False + assert client.accounts.updated_item.call_args_list == [((account,),)] + assert connection_error_evt.call_args_list == [((EXCEPTION,),)] + assert connect.call_args_list == [] + + @mockified('pypsyc.client.model', ['resolve_hostname', 'connect', + 'LinkingProtocol']) + def test_active_connect_error(self, resolve_hostname, connect, + LinkingProtocol): + client = Mock() + resolve_hostname.return_value = (sentinel.ip, sentinel.port) + connect.side_effect = EXCEPTION + connection_error_evt = Mock() + account = Account(client, SERVER1, USER1_NICK, PASSWORD, False, False) + account.connection_error_evt += connection_error_evt + + account.active = True + assert account.active == False + assert client.accounts.updated_item.call_args_list == [((account,),)] + assert connection_error_evt.call_args_list == [((EXCEPTION,),)] + assert LinkingProtocol.call_args_list == [] + + @mockified('pypsyc.client.model', ['resolve_hostname', 'connect', + 'LinkingProtocol']) + def test_active_no_such_user(self, resolve_hostname, connect, + LinkingProtocol): + ERROR = UnknownTargetError() + client = Mock() + resolve_hostname.return_value = (sentinel.ip, sentinel.port) + circuit = connect.return_value + LinkingProtocol.return_value.link.side_effect = ERROR + no_such_user_evt = Mock() + account = Account(client, SERVER1, USER1_NICK, PASSWORD, False, False) + account.no_such_user_evt += no_such_user_evt + + account.active = True + assert account.active == False + assert client.accounts.updated_item.call_args_list == [((account,),)] + assert no_such_user_evt.call_args_list == [((ERROR,),)] + assert circuit.psyc.uni != account.uni.chain(account.resource) + + @mockified('pypsyc.client.model', ['resolve_hostname', 'connect', + 'LinkingProtocol']) + def test_active_incorrect_password(self, resolve_hostname, connect, + LinkingProtocol): + ERROR = AuthenticationError() + client = Mock() + resolve_hostname.return_value = (sentinel.ip, sentinel.port) + circuit = connect.return_value + LinkingProtocol.return_value.link.side_effect = ERROR + auth_error_evt = Mock() + account = Account(client, SERVER1, USER1_NICK, 'wrong', False, False) + account.auth_error_evt += auth_error_evt + + account.active = True + assert account.active == False + assert client.accounts.updated_item.call_args_list == [((account,),)] + assert auth_error_evt.call_args_list == [((ERROR,),)] + assert circuit.psyc.uni != account.uni.chain(account.resource) + + def test_connection_error(self): + client = Mock() + connection_error_evt = Mock() + account = Account(client, SERVER1, USER1_NICK, PASSWORD, False, False) + account.connection_error_evt += connection_error_evt + + account.connection_error(EXCEPTION) + assert account.active == False + assert client.accounts.updated_item.call_args_list == [((account,),)] + assert connection_error_evt.call_args_list == [((EXCEPTION,),)] + + +class TestClientCircuit(object): + @mockified(Circuit, ['connectionMade']) + def test_dump_out(self, connectionMade): + LINE1 = ':_target\t' + USER2_UNI + LINE2 = '|' + + cc = ClientCircuit() + dump_evt = Mock() + cc.dump_evt += dump_evt + cc.transport = Mock() + orig_write = cc.transport.write + + cc.connectionMade() + assert connectionMade.call_args_list == [((cc,),)] + + cc.send({'_target': USER2_UNI}, '') + assert dump_evt.call_args_list == [(('o', LINE1),), (('o', LINE2),)] + assert orig_write.call_args_list == [((LINE1+'\n'+LINE2+'\n',),)] + + @mockified(Circuit, ['lineReceived']) + def test_dump_in(self, lineReceived): + lineReceived.return_value = None + cc = ClientCircuit() + dump_evt = Mock() + cc.dump_evt += dump_evt + + cc.dataReceived('a\nb\n') + assert dump_evt.call_args_list == [(('i', 'a'),), (('i', 'b'),)] + assert lineReceived.call_args_list == [((cc, 'a'),), ((cc, 'b'),)] + + @mockified('pypsyc.client.model', ['PSYCObject']) + def test_packet_received(self, PSYCObject): + psyc = PSYCObject.return_value + psyc.handle_packet.return_value = PSYCPacket() + + cc = ClientCircuit() + assert PSYCObject.call_args_list == [((cc.send,),)] + + cc.packet_received({}, CONTENT) + assert psyc.method_calls == [('handle_packet', ({}, CONTENT))] + + @mockified('pypsyc.client.model', ['PSYCObject']) + def test_relay(self, PSYCObject): + psyc = PSYCObject.return_value + psyc.uni = RESOURCE1_UNI + psyc.handle_packet.return_value = PSYCPacket() + cc = ClientCircuit() + + header = inited_header(_source=USER1_UNI, _source_relay=USER2_UNI) + cc.packet_received(header, CONTENT) + assert header.source == USER2_UNI + + header2 = inited_header(_source=USER2_UNI, _source_relay=USER2_UNI) + assert_raises(AssertionError, cc.packet_received, header2, CONTENT) + + @mockified('pypsyc.client.model', ['PSYCObject']) + def test_persistent(self, PSYCObject): + HEADER = {'_context': USER2_UNI} + packet = PSYCPacket({'=': VARIABLES, '+': VARIABLES, '-': VARIABLES}) + packet.header = inited_header(HEADER) + psyc = PSYCObject.return_value + psyc.handle_packet.return_value = packet + cc = ClientCircuit() + + handler = Mock() + state_set_key = handler.state_set_key = Mock() + state_add_key = handler.state_add_key = Mock() + state_remove_key = handler.state_remove_key = Mock() + cc.add_pvar_handler(handler) + + cc.packet_received(HEADER, CONTENT) + assert psyc.method_calls == [('handle_packet', (HEADER, CONTENT))] + assert state_set_key.call_args_list == [((USER2_UNI, VARIABLES),)] + assert state_add_key.call_args_list == [((USER2_UNI, VARIABLES),)] + assert state_remove_key.call_args_list == [((USER2_UNI, VARIABLES),)] + + +class TestClient(object): + @mockified('pypsyc.client.model', ['Account']) + def test_accounts(self, Account): + account = Account.return_value + error_ph = PlaceHolder() + + account1 = Mock() + account1.server = SERVER1 + account1.person = USER1_NICK + account1.password = PASSWORD + account1.save_password = True + account1.active = False + account2 = Mock() + account2.server = SERVER2 + account2.person = USER2_NICK + account2.password = PASSWORD + account2.save_password = False + account2.active = True + + try: + with NamedTemporaryFile(delete=False) as f: + f.write('[]') + + client = Client(accounts_file=f.name) + assert client.accounts == [] + client.accounts = [account1, account2] + client.save_accounts() + + client = Client(accounts_file=f.name) + client.load_accounts() + assert client.accounts == [Account.return_value] * 2 + assert Account.call_args_list == [ + ((client, SERVER1, USER1_NICK, PASSWORD, True, False),), + ((client, SERVER2, USER2_NICK, '', False, True),)] + finally: + remove(f.name) + + client.connection_lost(account.circuit, sentinel.error) + assert account.method_calls == [('connection_error', (error_ph,))] * 2 + assert error_ph.obj.args[0] == str(sentinel.error) + + @mockified('pypsyc.client.model', ['reactor']) + def test_quit(self, reactor): + account = Mock() + client = Client() + client.accounts = [account] + + client.quit() + assert account.active == False + assert reactor.method_calls == [('stop',)] diff --git a/mjacob2/tests/test_client/test_observable.py b/mjacob2/tests/test_client/test_observable.py new file mode 100644 index 0000000..4d2f3bd --- /dev/null +++ b/mjacob2/tests/test_client/test_observable.py @@ -0,0 +1,173 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from nose.tools import assert_raises + +from pypsyc.client.observable import ObsAttr, ObsObj, ObsList, ObsDict + + +class StubObsObj(ObsObj): + foo = ObsAttr('foo') + + +def test_obs_obj(): + obs_obj = StubObsObj() + update_evt = Mock() + obs_obj.update_evt['foo'] += update_evt + + obs_obj.ignore = 'foo' + assert not hasattr(obs_obj, 'foo') + obs_obj.foo = 'foo' + obs_obj.foo = 'foo' + obs_obj.foo = 'bar' + assert obs_obj.foo == 'bar' + + del obs_obj.foo + assert not hasattr(obs_obj, 'foo') + assert_raises(AttributeError, obs_obj.__delattr__, 'foo') + + assert update_evt.call_args_list == [ + ((None, 'foo'),), (('foo', 'bar'),), (('bar', None),)] + + +class TestObsList(object): + def test_basic(self): + obs_list = ObsList(xrange(10)) + setitem_evt = Mock() + obs_list.setitem_evt += setitem_evt + delitem_evt = Mock() + obs_list.delitem_evt += delitem_evt + insert_evt = Mock() + obs_list.insert_evt += insert_evt + + obs_list[3] = 3 + obs_list[3] = 333 + del obs_list[3] + obs_list.insert(3, 3) + + assert obs_list == list(xrange(10)) + assert setitem_evt.call_args_list == [((3, 3, 333),)] + assert delitem_evt.call_args_list == [((3, 333),)] + assert insert_evt.call_args_list == [((3, 3),)] + + def test_ext_add(self): + obs_list = ObsList() + insert_evt = Mock() + obs_list.insert_evt += insert_evt + + obs_list.append(42) + obs_list.extend([42, 1337]) + obs_list += [42, 1337] + + assert obs_list == [42, 42, 1337, 42, 1337] + assert insert_evt.call_args_list == [ + ((0, 42),), ((1, 42),), ((2, 1337),), ((3, 42),), ((4, 1337),)] + + def test_ext_del(self): + obs_list = ObsList(xrange(10)) + delitem_evt = Mock() + obs_list.delitem_evt += delitem_evt + + assert obs_list.pop() == 9 + assert obs_list.pop(8) == 8 + obs_list.remove(7) + + assert delitem_evt.call_args_list == [((9, 9),), ((8, 8),), ((7, 7),)] + assert obs_list == list(xrange(7)) + + def test_reverse(self): + obs_list = ObsList(xrange(10)) + obs_list.reverse() + assert obs_list == list(reversed(xrange(10))) + + def test_updated_item(self): + obs_list = ObsList(['a', 'b']) + update_evt = Mock() + obs_list.update_evt += update_evt + + obs_list.updated_item('b') + assert update_evt.call_args_list == [((1, 'b'),)] + + +class TestObsDict(object): + def test_basic(self): + obs_dict = ObsDict(a='A') + setitem_evt = Mock() + obs_dict.setitem_evt += setitem_evt + delitem_evt = Mock() + obs_dict.delitem_evt += delitem_evt + + obs_dict['a'] = 'A' + obs_dict['a'] = 'X' + del obs_dict['a'] + obs_dict['b'] = 'B' + + assert obs_dict == dict(b='B') + assert setitem_evt.call_args_list == [(('a', 'A', 'X'),), + (('b', None, 'B'),)] + assert delitem_evt.call_args_list == [(('a', 'X'),)] + + def test_pop(self): + obs_dict = ObsDict(a='A', b='B') + delitem_evt = Mock() + obs_dict.delitem_evt += delitem_evt + + assert obs_dict.pop('a') == 'A' + assert obs_dict.pop('a', 'X') == 'X' + assert obs_dict.pop('b', 'X') == 'B' + + assert obs_dict == {} + assert delitem_evt.call_args_list == [(('a', 'A'),), (('b', 'B'),)] + + def test_popitem(self): + obs_dict = ObsDict(a='A', b='B') + delitem_evt = Mock() + obs_dict.delitem_evt += delitem_evt + + k, v = obs_dict.popitem() + assert v == k.upper() + + assert len(obs_dict) == 1 + assert delitem_evt.call_args_list == [((k, v),)] + + def test_clear(self): + obs_dict = ObsDict(a='A', b='B') + delitem_evt = Mock() + obs_dict.delitem_evt += delitem_evt + + obs_dict.clear() + + assert delitem_evt.call_args_list == [(('a', 'A'),), (('b', 'B'),)] + + def test_update(self): + obs_dict = ObsDict() + setitem_evt = Mock() + obs_dict.setitem_evt += setitem_evt + + obs_dict.update({'a': 'A'}, b='B') + obs_dict.update((('c', 'C'),)) + + assert obs_dict == dict(a='A', b='B', c='C') + assert setitem_evt.call_args_list == [ + (('a', None, 'A'),), (('b', None, 'B'),), (('c', None, 'C'),)] + + def test_setdefault(self): + obs_dict = ObsDict(a='A') + setitem_evt = Mock() + obs_dict.setitem_evt += setitem_evt + + assert obs_dict.setdefault('a') == 'A' + assert obs_dict.setdefault('b', 'B') == 'B' + + assert obs_dict == dict(a='A', b='B') + assert setitem_evt.call_args_list == [(('b', None, 'B'),)] + + def test_updated_item(self): + obs_dict = ObsDict({'a': 'A', 'b': 'B'}) + update_evt = Mock() + obs_dict.update_evt += update_evt + + obs_dict.updated_item('b') + assert update_evt.call_args_list == [(('b', 'B'),)] diff --git a/mjacob2/tests/test_client/test_view.py b/mjacob2/tests/test_client/test_view.py new file mode 100644 index 0000000..bfc1bee --- /dev/null +++ b/mjacob2/tests/test_client/test_view.py @@ -0,0 +1,111 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" + + +def _callback(*args, **kwds): # pragma: no cover + print ('callback', args, kwds) + +def _test_accounts_view(): # pragma: no cover + accounts_view = AccountsView() + Controller(controller.AccountsController, accounts_view) + accounts_view.window.connect('destroy', lambda w: gtk.main_quit()) + for i in xrange(10): + accounts_view.accounts.append(("psyc://server/~account%i" % i, True)) + accounts_view.accounts[4] = ("psyc://server/~updatedaccount4", False) + #return + + from pypsyc.client.model import Account + account = Account(None, "", "", "", False, False) + accounts_view.show_addedit_dialog(account.__dict__, True, _callback) + +def _test_tabs_view(): # pragma: no cover + status_icon = gtk.StatusIcon() + status_icon.set_from_file('pypsyc/client/psyc.ico') + status_icon.connect('activate', lambda w: tabs_view.on_status_icon_click()) + tabs_view = TabsView(status_icon) + tabs_view.window.connect('destroy', lambda w: gtk.main_quit()) + Controller(controller.TabsController, tabs_view) + + conversation_view = tabs_view.show_conversation("Conversation 1") + Controller(controller.ConversationController, conversation_view) + tabs_view.focus_tab(conversation_view) + + conference_view = tabs_view.show_conference("Conversation 2") + Controller(controller.ConferenceController, conference_view) + + def show(): + for i in xrange(40): + conversation_view.show_message("Message %02i" % i) + for i in xrange(40): + conference_view.members.append(("psyc://server/~person%02i" % i, + "person%02i" % i)) + conference_view.show_message("Message %02i" % i) + conference_view.members[20] = ("psyc://server/~longpersonname20", + "longpersonname20") + idle_add(show) + +def _test_dump_view(): # pragma: no cover + dump_view = DumpView() + Controller(controller.DumpController, dump_view) + dump_view.show_line('o', 'a', 'psyc://server/~account') + dump_view.show_line('i', ':_tag_relay\t\xc8\xe6', 'psyc://server/~account') + +def _test_main_view(): # pragma: no cover + main_view = MainView() + Controller(controller.MainController, main_view) + Controller(controller.FriendListController, main_view.friends_view) + for i in xrange(40): + main_view.friends_view.friends.append(("Friend %02i" % i, False, + 'pending')) + for i in xrange(0, 40, 2): + main_view.friends_view.friends[i] = ("Friend %02i updated" % i, False, + 'offered') + for i in xrange(0, 40, 3): + main_view.friends_view.friends[i] = ("Friend %02i updated 2" % i, True, + 'established') + #return + + account_dict = {'password': '', 'save_password': False} + main_view.show_password_dialog("psyc://server/~account", account_dict, + _callback) + main_view.show_no_such_user("psyc://server/~account") + main_view.show_auth_error("psyc://server/~account", "Error description") + main_view.show_open_conv_dialog(["psyc://server/~person1", + "psyc://server/~person2"], _callback) + main_view.show_open_conf_dialog(["psyc://server/~person1", + "psyc://server/~person2"], _callback) + main_view.show_add_friend_dialog(["psyc://server/~person1", + "psyc://server/~person2"], _callback) + + +if __name__ == '__main__': # pragma: no cover + class Controller(object): + def __init__(self, controller, view): + self.controller = controller + self.c_name = controller.__name__ + view.controller = self + def __getattr__(self, attr): + m = getattr(self.controller, attr, None) + assert m, "%s has no method %s" % (self.c_name, attr) + def f(*args, **kwds): + m_argcount = m.__code__.co_argcount - 1 # minus self + f_argcount = len(args) + len(kwds) + assert m_argcount == f_argcount, \ + "%s.%s has %s arguments, called with %s arguments" % ( + self.c_name, attr, m_argcount, f_argcount) + print "%s.%s called: %s, %s" % (self.c_name, attr, args, kwds) + return f + + from gobject import idle_add + import gtk + + from pypsyc.client.view import AccountsView, TabsView, DumpView, MainView + from pypsyc.client import controller + +# _test_accounts_view() +# _test_tabs_view() +# _test_dump_view() +# _test_main_view() + gtk.main() diff --git a/mjacob2/tests/test_core/test_mmp.py b/mjacob2/tests/test_core/test_mmp.py new file mode 100644 index 0000000..facbba3 --- /dev/null +++ b/mjacob2/tests/test_core/test_mmp.py @@ -0,0 +1,97 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from nose.tools import assert_raises +from twisted.protocols.loopback import loopbackAsync as loopback_async + +from pypsyc.core.mmp import Uni, Header, Circuit + + +UNI = 'psyc://server/foo/bar' + +class TestUni(object): + def test_uni(self): + uni = Uni(UNI) + assert uni == UNI + assert uni.prefix == 'psyc' + + def test_parts(self): + uni = Uni(UNI) + assert uni.into_parts() == ['server', 'foo', 'bar'] + assert Uni.from_parts(uni.into_parts()) == uni + + def test_descendant_ancestor(self): + uni = Uni(UNI) + assert uni.is_descendant_of('psyc://server/foo') + assert not uni.is_descendant_of('psyc://server/spam') + assert uni.is_ancestor_of('psyc://server/foo/bar/child') + assert not uni.is_ancestor_of('psyc://server/spam/eggs/child') + + assert uni.is_descendant_of('psyc://server/') + assert Uni('psyc://server/').is_ancestor_of('psyc://server/foo/bar') + + def test_chain(self): + assert Uni(UNI).chain('foobar') == 'psyc://server/foo/bar/foobar' + assert Uni('psyc://server/').chain('foo') == 'psyc://server/foo' + + +class TestHeader(object): + def test_empty(self): + header = Header() + assert header == {} + header._init() + assert header.source is None + assert header.target is None + assert header.context is None + + def test_header(self): + header_dict = { + '_source': 'psyc://server/uni1', + '_target': 'psyc://server/uni2', + '_context': 'psyc://server/@place' + } + header = Header(header_dict) + assert header == header_dict + header._init() + assert header.source == Uni('psyc://server/uni1') + assert header.target == Uni('psyc://server/uni2') + assert header.context == Uni('psyc://server/@place') + + +class TestCircuit(object): + def setup(self): + self.success = False + + def test_render_empty(self): + self._test({}, []) + + def test_onlyvars(self): + self._test({'_target': 'psyc://example.org/'}, []) + + def test_onlycontent(self): + self._test({}, ['content']) + + def test_packet(self): + self._test({'_target': 'psyc://example.org/'}, ['content']) + + def _test(self, header, content): + def received(header_, content_): + assert header_ == header + assert content_ == content + self.success = True + + circuit1 = Circuit() + circuit1.packet_received = received + + circuit2 = Circuit() + circuit2.initiator = True + circuit2.inited = lambda: circuit2.send(header, iter(content)) + + loopback_async(circuit1, circuit2) + assert self.success + + def test_subclass(self): + circuit = Circuit() + circuit.lineReceived('|') + assert_raises(NotImplementedError, circuit.lineReceived, '|') diff --git a/mjacob2/tests/test_core/test_psyc.py b/mjacob2/tests/test_core/test_psyc.py new file mode 100644 index 0000000..be544c0 --- /dev/null +++ b/mjacob2/tests/test_core/test_psyc.py @@ -0,0 +1,151 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from nose.tools import assert_raises +from tests.constants import USER1_UNI, USER2_UNI +from tests.helpers import check_success, rendered + +from pypsyc.core.psyc import PSYCPacket, PSYCObject +from pypsyc.util import schedule + + +class TestPSYCPacket(object): + def test_vars(self): + packet = PSYCPacket( + modifiers={ + ':': {'_foo': 'bar'}, + '=': {'_spam': 'eggs'} + } + ) + self._test(packet, [':_foo\tbar', '=_spam\teggs']) + + def test_method(self): + packet = PSYCPacket(mc='_notice_foo') + self._test(packet, ['_notice_foo']) + + def test_packet(self): + packet = PSYCPacket( + modifiers={ + ':': {'_foo': 'bar'} + }, + mc='_message_foo', + data='this is\ndata' + ) + self._test(packet, [':_foo\tbar', '_message_foo', 'this is\ndata']) + + def test_binary1(self): + packet = PSYCPacket({':': {'_foo': 'foo\nbar'}}) + self._test(packet, [':_foo 7\tfoo\nbar']) + assert PSYCPacket.parse([':_foo 7\tfoo', 'bar']) == packet + + def test_binary2(self): + packet = PSYCPacket({':': {'_foo': 'foo\n\nbar'}}) + self._test(packet, [':_foo 8\tfoo\n\nbar']) + assert PSYCPacket.parse([':_foo 8\tfoo', '', 'bar']) == packet + + def _test(self, packet, rendered_): + assert PSYCPacket.parse(packet.render()) == packet + assert rendered(packet) == rendered_ + + def test_nomethod(self): + packet = PSYCPacket(data='foobar') + assert_raises(AssertionError, list, packet.render()) + + def test_repr(self): + s = '\n'.join(( + r"PSYCPacket(", + r" modifiers={", + r" ':': {'_foo': 'bar'},", + r" '=': {'_foo': 'bar'}", + r" },", + r" mc='_message_foo',", + r" data='this is\ndata'", + r")" + )) + assert repr(eval(s)) == s + + def test_equal(self): + assert PSYCPacket() == PSYCPacket() + assert not PSYCPacket() != PSYCPacket() + + def test_empty(self): + packet = PSYCPacket() + self._test(packet, []) + assert packet.modifiers == {':': {}} + assert packet.mc is None + assert packet.data == '' + + def test_from_kwds(self): + packet1 = PSYCPacket.from_kwds(mc='_message_public', + data="hello", _somevar='foo') + packet2 = PSYCPacket(mc='_message_public', data="hello", + modifiers={':': {'_somevar': 'foo'}}) + assert packet1 == packet2 + + +class TestPSYCObject(object): + def test_handle_packet(self): + psyc = PSYCObject(None) + psyc.handlers['_message'] = message = Mock() + psyc.handlers['_message_public'] = message_public = Mock() + + p1 = PSYCPacket() # psyc_packet should return + p2 = PSYCPacket(mc='_message_public') + p3 = PSYCPacket(mc='_message_private') + + assert psyc.handle_packet({}, p1.render()) == p1 + assert psyc.handle_packet({}, p2.render()) == p2 + assert psyc.handle_packet({}, p3.render()) == p3 + assert message_public.call_args_list == [((p2,),)] + assert message.call_args_list == [((p3,),)] + + def test_handle_packet_internal_error(self): + psyc = PSYCObject(None) + psyc.handlers['_message_public'] = Mock(side_effect=Exception) + + packet = PSYCPacket(mc='_message_public') + assert psyc.handle_packet({}, packet.render()) == packet + + def test_sendmsg_packet(self): + packet = PSYCPacket() + sendfunc = Mock() + psyc = PSYCObject(sendfunc, USER1_UNI) + + psyc.sendmsg(USER2_UNI, packet) + header = {'_source': USER1_UNI, '_target': USER2_UNI} + assert sendfunc.call_args_list == [((header, rendered(packet)),)] + + def test_sendmsg_kwds(self): + packet = PSYCPacket({':': {'_somevar': 'foo'}}, mc='_message_private') + sendfunc = Mock() + psyc = PSYCObject(sendfunc, USER1_UNI) + + psyc.sendmsg(USER2_UNI, mc='_message_private', _somevar='foo') + header = {'_source': USER1_UNI, '_target': USER2_UNI} + assert sendfunc.call_args_list == [((header, rendered(packet)),)] + + @check_success + def test_async(self): + MC = '_request_foo' + ECHO = PSYCPacket(mc='_echo_foo') + o1 = PSYCObject(lambda *args: o2.handle_packet(*args), USER1_UNI) + o2 = PSYCObject(lambda *args: o1.handle_packet(*args), USER2_UNI) + o2.handlers[MC] = lambda _: ECHO + + def _test(): + ret = o1.sendmsg(USER2_UNI, mc=MC) + assert ret == ECHO + self.success = True + schedule(_test) + + def test_async_internal_error(self): + MC = '_request_foo' + ERROR = PSYCPacket(mc='_error_internal') + o1 = PSYCObject(lambda *args: o2.handle_packet(*args), USER1_UNI) + o2 = PSYCObject(lambda *args: o1.handle_packet(*args), USER2_UNI) + o2.handlers[MC] = Mock(side_effect=Exception) + + ret = o1.sendmsg(USER2_UNI, mc=MC) + assert ret == ERROR diff --git a/mjacob2/tests/test_protocol.py b/mjacob2/tests/test_protocol.py new file mode 100644 index 0000000..d2e346a --- /dev/null +++ b/mjacob2/tests/test_protocol.py @@ -0,0 +1,396 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import hmac +from hashlib import sha256 + +from mock import Mock +from nose.tools import assert_raises +from tests.constants import (SERVER1, USER1, USER2, USER1_UNI, USER2_UNI, + USER1_NICK, RESOURCE, RESOURCE1_UNI, RESOURCE2_UNI, PLACE, PLACE_UNI, + INTERFACE, VARIABLES, PASSWORD, MESSAGE, ONLINE, PENDING, OFFERED, + ESTABLISHED, ERROR) +from tests.helpers import connect_circuits, PlaceHolder + +from pypsyc.client.model import ClientCircuit +from pypsyc.core.psyc import PSYCPacket +from pypsyc.protocol import (UnknownTargetError, DeliveryFailedError, + RoutingErrorRoot, RoutingErrorRelaying, RoutingErrorClient, + AuthenticationError, LinkingServer, LinkingClient, Messaging, + MessageRelaying, ConferencingServer, ConferencingClient, EntryDeniedError, + ContextMaster, ContextSlave, UnauthorizedError, ClientInterfaceServer, + ClientInterfaceClient, FriendshipServer, FriendshipClient) +from pypsyc.server import Entity +from pypsyc.server.person import Resource +from pypsyc.server.routing import Routing + + +def _setup(): + server = Mock() + server.hostname = SERVER1 + routing = Routing(server.hostname, INTERFACE) + server.routing = routing + routing.init(_entity(server=server)) + return routing + +def _entity(*args, **kwds): + entity = Entity(*args, **kwds) + entity.package = Mock() + entity.package.entity = entity + return entity + +def _c2s_circuit(routing, link_to=None): + sc, cc = connect_circuits(routing.buildProtocol(None), ClientCircuit()) + cc.package = Mock() + cc.package.psyc = cc.psyc + if link_to is not None: + Resource(link_to, RESOURCE, sc) + cc.psyc.uni = link_to.uni.chain(RESOURCE) + return sc, cc + +def _enter(routing, entity, sc): + routing.mrouting_table[entity.uni].add(sc) + state = entity.context_master.add_member(USER1_UNI) + sc.send({'_context': entity.uni}, PSYCPacket({'=': state}).render()) + + +class TestRoutingErrorProtocol(object): + def setup(self): + routing = _setup() + self.root = routing.root + self.user1 = _entity(routing.root, USER1) + self.sc, self.client = _c2s_circuit(routing, link_to=self.user1) + + def test_unknown_target_error(self): + rer = RoutingErrorRoot(self.root.package) + self.client.psyc.add_handler(RoutingErrorClient(self.client.package)) + + rer.send_unknown_target_error(RESOURCE1_UNI, USER2_UNI) + assert self.client.package.method_calls == [ + ('unknown_target_error', (USER2_UNI,))] + + def test_relay_unknown_target_error1(self): + rer = RoutingErrorRoot(self.root.package) + self.user1.add_handler(RoutingErrorRelaying(self.user1.package)) + + rer.send_unknown_target_error(USER1_UNI, USER2_UNI) + assert self.user1.package.method_calls == [ + ('unknown_target_error', (USER2_UNI,))] + + def test_relay_unknown_target_error2(self): + rer = RoutingErrorRelaying(self.root.package) + self.client.psyc.add_handler(RoutingErrorClient(self.client.package)) + + rer.relay_unknown_target_error(RESOURCE1_UNI, USER2_UNI) + assert self.client.package.method_calls == [ + ('unknown_target_error', (USER2_UNI,))] + + def test_delivery_failed_error(self): + rer = RoutingErrorRoot(self.root.package) + self.client.psyc.add_handler(RoutingErrorClient(self.client.package)) + + rer.send_delivery_failed_error(RESOURCE1_UNI, USER2_UNI, ERROR) + assert self.client.package.method_calls == [ + ('delivery_failed_error', (USER2_UNI, ERROR))] + + def test_relay_delivery_failed_error1(self): + rer = RoutingErrorRoot(self.root.package) + self.user1.add_handler(RoutingErrorRelaying(self.user1.package)) + + rer.send_delivery_failed_error(USER1_UNI, USER2_UNI, ERROR) + assert self.user1.package.method_calls == [ + ('delivery_failed_error', (USER2_UNI, ERROR))] + + def test_relay_delivery_failed_error2(self): + rer = RoutingErrorRelaying(self.root.package) + self.client.psyc.add_handler(RoutingErrorClient(self.client.package)) + + rer.relay_delivery_failed_error(RESOURCE1_UNI, USER2_UNI, ERROR) + assert self.client.package.method_calls == [ + ('delivery_failed_error', (USER2_UNI, ERROR))] + + +class TestLinkingProtocol(object): + def setup(self): + routing = _setup() + self.user1 = _entity(routing.root, USER1) + self.sc, self.client = _c2s_circuit(routing) + + def test_linking(self): + lc = LinkingClient(self.client.package) + self.user1.add_handler(LinkingServer(self.user1.package)) + pw = PlaceHolder() + + lc.link(USER1_UNI, RESOURCE, PASSWORD) + assert self.user1.package.method_calls == [ + ('authenticate', (pw, self.sc, RESOURCE))] + assert pw.obj[0] == 'hmac' + assert pw.obj[1] == hmac.new(PASSWORD, pw.obj[2], sha256).digest() + + def test_incorrect_password(self): + lc = LinkingClient(self.client.package) + self.user1.add_handler(LinkingServer(self.user1.package)) + self.user1.package.authenticate.side_effect = AuthenticationError('xy') + + with assert_raises(AuthenticationError) as cm: + lc.link(USER1_UNI, RESOURCE, 'pw') + assert cm.exception.args == ('xy',) + + +class TestMessagingProtocol(object): + def setup(self): + routing = _setup() + self.user1 = _entity(routing.root, USER1) + self.user2 = _entity(routing.root, USER2) + + sc1, self.client1 = _c2s_circuit(routing, link_to=self.user1) + sc2, self.client2 = _c2s_circuit(routing, link_to=self.user2) + + def test_private_message(self): + msg1 = Messaging(self.client1.package) + self.client2.psyc.add_handler(Messaging(self.client2.package)) + + msg1.send_private_message(RESOURCE2_UNI, MESSAGE) + assert self.client2.package.method_calls == [ + ('private_message', (RESOURCE1_UNI, MESSAGE))] + + def test_message_relaying1(self): + msg1 = Messaging(self.client1.package) + self.user1.add_handler(MessageRelaying(self.user1.package)) + + msg1.send_private_message(USER2_UNI, MESSAGE, relay=USER1_UNI) + assert self.user1.package.method_calls == [ + ('private_message_relay', (RESOURCE1_UNI, USER2_UNI, MESSAGE))] + + def test_message_relaying2(self): + msg_relaying1 = MessageRelaying(self.user1.package) + self.user2.add_handler(MessageRelaying(self.user2.package)) + + msg_relaying1.send_private_message(USER2_UNI, MESSAGE) + assert self.user2.package.method_calls == [ + ('private_message', (USER1_UNI, MESSAGE))] + + def test_message_relaying3(self): + msg_relaying2 = MessageRelaying(self.user2.package) + self.client2.psyc.add_handler(Messaging(self.client2.package)) + + msg_relaying2.relay_private_message(USER1_UNI, RESOURCE2_UNI, MESSAGE) + assert self.client2.package.method_calls == [ + ('private_message', (USER1_UNI, MESSAGE))] + + +class TestConferencingProtocol(object): + def setup(self): + routing = self.routing = _setup() + self.place = _entity(routing.root, PLACE) + self.user1 = _entity(routing.root, USER1) + self.user2 = _entity(routing.root, USER2) + + self.sc1, self.client1 = _c2s_circuit(routing, link_to=self.user1) + self.sc2, self.client2 = _c2s_circuit(routing, link_to=self.user2) + _enter(routing, self.place, self.sc1) + + def test_cast_member(self): + con_server = ConferencingServer(self.place.package) + self.client1.add_pvar_handler(ConferencingClient(self.client1.package)) + self.client2.add_pvar_handler(ConferencingClient(self.client2.package)) + + con_server.cast_member_entered(USER1_UNI, USER1_NICK) + assert self.client1.package.method_calls == [ + ('member_entered', (PLACE_UNI, USER1_UNI, USER1_NICK))] + + _enter(self.routing, self.place, self.sc2) + assert self.client2.package.method_calls == [ + ('member_entered', (PLACE_UNI, USER1_UNI, USER1_NICK))] + self.client1.package.reset_mock() + + con_server.cast_member_left(USER1_UNI) + assert self.client1.package.method_calls == [ + ('member_left', (PLACE_UNI, USER1_UNI))] + + def test_public_message1(self): + con_client = ConferencingClient(self.client1.package) + self.place.add_handler(ConferencingServer(self.place.package)) + + con_client.send_public_message(PLACE_UNI, MESSAGE) + assert self.place.package.method_calls == [ + ('public_message', (RESOURCE1_UNI, MESSAGE))] + + def test_public_message2(self): + con_server = ConferencingServer(self.place.package) + self.client1.psyc.add_handler(ConferencingClient(self.client1.package)) + self.client2.psyc.add_handler(ConferencingClient(self.client2.package)) + + con_server.cast_public_message(USER1_UNI, MESSAGE) + assert self.client1.package.method_calls == [ + ('public_message', (PLACE_UNI, USER1_UNI, MESSAGE))] + + +class TestContextProtocol(object): + def setup(self): + routing = _setup() + self.place = _entity(routing.root, PLACE) + self.user1 = _entity(routing.root, USER1) + + def test_enter(self): + cs = ContextSlave(self.user1.package) + self.place.add_handler(ContextMaster(self.place.package)) + self.place.package.enter_request.return_value = VARIABLES + + state = cs.enter(PLACE_UNI) + assert state == VARIABLES + assert self.place.package.method_calls == [ + ('enter_request', (USER1_UNI,))] + + def test_enter_denied(self): + cs = ContextSlave(self.user1.package) + self.place.add_handler(ContextMaster(self.place.package)) + self.place.package.enter_request.side_effect = EntryDeniedError('xy') + + with assert_raises(EntryDeniedError) as cm: + cs.enter(PLACE_UNI) + assert cm.exception.args == ('xy',) + + def test_leave(self): + cs = ContextSlave(self.user1.package) + self.place.add_handler(ContextMaster(self.place.package)) + + cs.leave(PLACE_UNI) + assert self.place.package.method_calls == [ + ('leave_context', (USER1_UNI,))] + + +class TestClientInterface(object): + def setup(self): + routing = self.routing = _setup() + self.user1 = _entity(routing.root, USER1) + _, self.client = _c2s_circuit(routing, link_to=self.user1) + self.client.package.uni = USER1_UNI + + self.ci_client = ClientInterfaceClient(self.client.package) + self.user1.add_handler(ClientInterfaceServer(self.user1.package)) + + def test_unauthorized(self): + user2 = _entity(self.routing.root, USER2) + _, client2 = _c2s_circuit(self.routing, link_to=user2) + client2.package.uni = USER1_UNI + ci_client2 = ClientInterfaceClient(client2.package) + + assert_raises(UnauthorizedError, ci_client2.subscribe, PLACE_UNI) + assert self.user1.package.method_calls == [] + + def test_unknown_target_error(self): + error = UnknownTargetError('xy') + self.user1.package.client_subscribe.side_effect = error + with assert_raises(UnknownTargetError) as cm: + self.ci_client.subscribe(PLACE_UNI) + assert cm.exception.args == ('xy',) + + def test_delivery_failed_error(self): + error = DeliveryFailedError('x', 'y') + self.user1.package.client_subscribe.side_effect = error + with assert_raises(DeliveryFailedError) as cm: + self.ci_client.subscribe(PLACE_UNI) + assert cm.exception.args == ('x', 'y') + + def test_subscribe(self): + self.ci_client.subscribe(PLACE_UNI) + assert self.user1.package.method_calls == [ + ('client_subscribe', (PLACE_UNI, RESOURCE1_UNI))] + + def test_unsubscribe(self): + self.ci_client.unsubscribe(PLACE_UNI) + assert self.user1.package.method_calls == [ + ('client_unsubscribe', (PLACE_UNI, RESOURCE1_UNI))] + + def test_presence(self): + self.ci_client.cast_presence(ONLINE) + assert self.user1.package.method_calls == [ + ('client_presence', (ONLINE,))] + + def test_add_friend(self): + self.ci_client.add_friend(USER2_UNI) + assert self.user1.package.method_calls == [ + ('client_add_friend', (USER2_UNI,))] + + def test_remove_friend(self): + self.ci_client.remove_friend(USER2_UNI) + assert self.user1.package.method_calls == [ + ('client_remove_friend', (USER2_UNI,))] + + +class TestFriendshipProtocol(object): + def setup(self): + routing = _setup() + self.user1 = _entity(routing.root, USER1) + self.user2 = _entity(routing.root, USER2) + + sc1, self.client1 = _c2s_circuit(routing, link_to=self.user1) + sc2, self.client2 = _c2s_circuit(routing, link_to=self.user2) + routing.mrouting_table[USER1_UNI] = [sc2] + + def test_establish_pending(self): + friendship_server1 = FriendshipServer(self.user1.package) + self.user2.add_handler(FriendshipServer(self.user2.package)) + self.user2.package.friendship_request.return_value = PENDING + + state = friendship_server1.establish(USER2_UNI) + assert state == PENDING + assert self.user2.package.method_calls == [ + ('friendship_request', (USER1_UNI,))] + + def test_establish_established(self): + friendship_server1 = FriendshipServer(self.user1.package) + self.user2.add_handler(FriendshipServer(self.user2.package)) + self.user2.package.friendship_request.return_value = ESTABLISHED + + state = friendship_server1.establish(USER2_UNI) + assert state == ESTABLISHED + assert self.user2.package.method_calls == [ + ('friendship_request', (USER1_UNI,))] + + def test_remove(self): + friendship_server1 = FriendshipServer(self.user1.package) + self.user2.add_handler(FriendshipServer(self.user2.package)) + + friendship_server1.remove(USER2_UNI) + assert self.user2.package.method_calls == [ + ('friendship_cancel', (USER1_UNI,))] + + def test_friendships(self): + friendship_server = FriendshipServer(self.user1.package) + self.client1.psyc.add_handler(FriendshipClient(self.client1.package)) + + friendship_server.send_friendships(RESOURCE1_UNI, { + USER1_UNI: {'state': ESTABLISHED}, + USER2_UNI: {'state': OFFERED}}) + friendship_server.send_friendships(RESOURCE1_UNI, {}) + assert self.client1.package.method_calls == [ + ('friendship', (USER1_UNI, ESTABLISHED)), + ('friendship', (USER2_UNI, OFFERED))] + + def test_updated_friendship(self): + friendship_server = FriendshipServer(self.user1.package) + self.client1.psyc.add_handler(FriendshipClient(self.client1.package)) + + friendship_server.send_updated_friendship(RESOURCE1_UNI, USER1_UNI, + {'state': 'established'}) + assert self.client1.package.method_calls == [ + ('friendship', (USER1_UNI, ESTABLISHED))] + + def test_removed_friendship(self): + friendship_server = FriendshipServer(self.user1.package) + self.client1.psyc.add_handler(FriendshipClient(self.client1.package)) + + friendship_server.send_removed_friendship(RESOURCE1_UNI, USER1_UNI) + assert self.client1.package.method_calls == [ + ('friendship_removed', (USER1_UNI,))] + + def test_presence(self): + friendship_server = FriendshipServer(self.user1.package) + self.client2.add_pvar_handler(FriendshipClient(self.client2.package)) + + friendship_server.cast_presence(ONLINE) + assert self.client2.package.method_calls == [ + ('presence', (USER1_UNI, ONLINE))] diff --git a/mjacob2/tests/test_server/test_db.py b/mjacob2/tests/test_server/test_db.py new file mode 100644 index 0000000..23c7f7a --- /dev/null +++ b/mjacob2/tests/test_server/test_db.py @@ -0,0 +1,50 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from greenlet import greenlet +from nose.tools import assert_raises +from tests.helpers import mockify, mockified, check_success + +from pypsyc.server.db import Database + + +class TestDatabase: + def test_sync(self): + db = Database(':memory:') + db.execute('CREATE TABLE test(test TEXT)') + db.execute("INSERT INTO test VALUES ('test')") + + result = db.fetch('SELECT test FROM test') + assert result == [('test',)] + assert isinstance(result[0][0], str) + + db.stop() + + @check_success + @mockified('pypsyc.server.db',['sqlite3', 'Thread', 'reactor']) + def test_async(self, sqlite3, Thread, reactor): + db = Database('') + mockify(db, ['_execute', '_fetch']) + e = Exception() + db._execute.side_effect = e + + def f(): + assert_raises(Exception, db.execute) + assert db.fetch() == db._fetch.return_value + self.success = True + gl = greenlet(f) + gl.switch() + + # emulate reactor calling this methods + gl.throw(e) + gl.switch(db._fetch.return_value) + + db.stop() + assert db.thread.method_calls == [('start',), ('join',)] + + db.run_async() + assert sqlite3.method_calls == [('connect', ('',))] + assert reactor.method_calls == [ + ('callFromThread', (gl.throw, e)), + ('callFromThread', (gl.switch, db._fetch.return_value))] diff --git a/mjacob2/tests/test_server/test_multicast.py b/mjacob2/tests/test_server/test_multicast.py new file mode 100644 index 0000000..199bd94 --- /dev/null +++ b/mjacob2/tests/test_server/test_multicast.py @@ -0,0 +1,162 @@ +""" + ::copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from tests.constants import (SERVER1, SERVER2, SERVER1_UNI, SERVER2_UNI, USER1, USER2, + USER1_UNI, USER2_UNI, RESOURCE, PLACE_UNI, VARIABLES) +from tests.helpers import mockified, rendered + +from pypsyc.core.psyc import PSYCPacket +from pypsyc.server import Entity +from pypsyc.server.multicast import ContextSlave +from pypsyc.server.person import Resource + + +EXTERN_USER_UNI = SERVER2_UNI.chain(USER1) +SET_PACKET = PSYCPacket({'=': VARIABLES}) +ADD_PACKET = PSYCPacket({'+': VARIABLES}) +REMOVE_PACKET = PSYCPacket({'-': VARIABLES}) + +class TestContextMaster(object): + def setup(self): + self.server = Mock() + self.server.hostname = SERVER1 + self.server.routing.mrouting_table = self.mrouting_table = {} + root = Entity(server=self.server) + self.user1 = Entity(root, USER1) + self.user1.castmsg = Mock() + user2 = Entity(root, USER2) + Resource(user2, RESOURCE, Mock()) + + def test_extern_member(self): + circuit = Mock() + self.server.routing.srouting_table = {SERVER2: circuit} + + self.user1.context_master.add_member(EXTERN_USER_UNI) + assert self.mrouting_table[USER1_UNI] == set([circuit]) + + self.user1.context_master.remove_member(EXTERN_USER_UNI) + assert self.mrouting_table[USER1_UNI] == set() + + def test_set_persistent(self): + self.user1.context_master.state_set(**VARIABLES) + assert self.user1.castmsg.call_args_list == [((SET_PACKET,),),] + + state = self.user1.context_master.add_member(USER2_UNI) + assert state == VARIABLES + + def test_persistent_list(self): + self.user1.context_master.state_add(**VARIABLES) + assert self.user1.castmsg.call_args_list == [((ADD_PACKET,),)] + self.user1.castmsg.reset_mock() + + state = self.user1.context_master.add_member(USER2_UNI) + assert state == VARIABLES + + self.user1.context_master.state_remove(**VARIABLES) + assert self.user1.castmsg.call_args_list == [((REMOVE_PACKET,),)] + + state = self.user1.context_master.add_member(USER2_UNI) + assert state == {} + + def test_remove_inheritance(self): + self.user1.context_master.state_add(_a='a', _a_b='b') + self.user1.context_master.state_remove(_a='a') + + state = self.user1.context_master.add_member(USER2_UNI) + assert state == {} + + +RESOURCE1 = '*resource1' +RESOURCE2 = '*resource2' +USER1_RESOURCE1_UNI = USER1_UNI.chain(RESOURCE1) +USER1_RESOURCE2_UNI = USER1_UNI.chain(RESOURCE2) + +class TestContextSlave(object): + @mockified('pypsyc.server.multicast', ['ContextSlaveProtocol']) + def setup(self, ContextSlaveProtocol): + person = Mock() + self.entity = person.entity + self.entity._root.uni = SERVER1_UNI + self.entity.uni = USER1_UNI + self.entity.server.routing.mrouting_table = self.mrouting_table = {} + self.entity.children = {} + self.entity.children[RESOURCE1] = self.resource1 = Mock() + self.entity.children[RESOURCE2] = self.resource2 = Mock() + self.protocol = ContextSlaveProtocol.return_value + self.protocol.enter.return_value = VARIABLES + self.context_slave = ContextSlave(person) + assert ContextSlaveProtocol.call_args_list == [((person,),)] + + def test_enter_leave(self): + self.context_slave.enter(PLACE_UNI, USER1_RESOURCE1_UNI) + assert self.protocol.method_calls == [('enter', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set([self.resource1.circuit]) + assert self.resource1.circuit.method_calls == [ + ('send', ({'_context': PLACE_UNI}, rendered(SET_PACKET)))] + self.protocol.reset_mock() + + self.context_slave.leave(PLACE_UNI, USER1_RESOURCE1_UNI) + assert self.protocol.method_calls == [('leave', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set() + + def test_enter_leave_all_resources(self): + self.context_slave.enter(PLACE_UNI) + assert self.protocol.method_calls == [('enter', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set([self.resource1.circuit, + self.resource2.circuit]) + assert self.resource1.circuit.method_calls == [ + ('send', ({'_context': PLACE_UNI}, rendered(SET_PACKET)),)] + assert self.resource2.circuit.method_calls == [ + ('send', ({'_context': PLACE_UNI}, rendered(SET_PACKET)),)] + self.protocol.reset_mock() + + self.context_slave.leave(PLACE_UNI) + assert self.protocol.method_calls == [('leave', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set() + + def test_enter_leave_two_resources(self): + extern_circuit = Mock() + self.mrouting_table[PLACE_UNI] = set([extern_circuit]) + + self.context_slave.enter(PLACE_UNI, USER1_RESOURCE1_UNI) + self.context_slave.enter(PLACE_UNI, USER1_RESOURCE2_UNI) + assert self.protocol.method_calls == [('enter', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set([extern_circuit, + self.resource1.circuit, + self.resource2.circuit]) + assert self.resource1.circuit.method_calls == [ + ('send', ({'_context': PLACE_UNI}, rendered(SET_PACKET)))] + self.protocol.reset_mock() + + self.context_slave.leave(PLACE_UNI, USER1_RESOURCE1_UNI) + assert self.protocol.method_calls == [] + self.context_slave.leave(PLACE_UNI, USER1_RESOURCE2_UNI) + assert self.protocol.method_calls == [('leave', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set([extern_circuit]) + + def test_enter_leave_extern(self): + self.context_slave.enter(EXTERN_USER_UNI, USER1_RESOURCE1_UNI) + assert self.protocol.method_calls == [('enter', (EXTERN_USER_UNI,))] + assert self.mrouting_table[EXTERN_USER_UNI] == \ + set([self.resource1.circuit]) + assert self.resource1.circuit.method_calls == [ + ('send', ({'_context':EXTERN_USER_UNI}, rendered(SET_PACKET)))] + self.protocol.reset_mock() + + self.context_slave.leave(EXTERN_USER_UNI, USER1_RESOURCE1_UNI) + assert self.protocol.method_calls == [('leave', (EXTERN_USER_UNI,))] + assert EXTERN_USER_UNI not in self.mrouting_table + + def test_leave_all(self): + self.context_slave.enter(PLACE_UNI, USER1_RESOURCE1_UNI) + self.protocol.reset_mock() + + self.context_slave.leave_all() + assert self.protocol.method_calls == [('leave', (PLACE_UNI,))] + assert self.mrouting_table[PLACE_UNI] == set() + + def test_not_leave(self): + self.context_slave.leave(PLACE_UNI) + assert self.protocol.method_calls == [] diff --git a/mjacob2/tests/test_server/test_person.py b/mjacob2/tests/test_server/test_person.py new file mode 100644 index 0000000..fc8b802 --- /dev/null +++ b/mjacob2/tests/test_server/test_person.py @@ -0,0 +1,268 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +import hmac +from hashlib import sha256 + +from mock import Mock +from nose.tools import assert_raises +from tests.constants import (SERVER1, USER1, USER1_UNI, USER2_UNI, RESOURCE, + RESOURCE1_UNI, RESOURCE2_UNI, PLACE_UNI, VARIABLES, PASSWORD, MESSAGE, + ONLINE, ERROR) +from tests.helpers import mockified + +from pypsyc.protocol import (AuthenticationError, FriendshipPendingError, + FriendshipEstablishedError, EntryDeniedError) +from pypsyc.server import Entity +from pypsyc.server.db import Database +from pypsyc.server.person import Person + + +class TestPerson(object): + def setup(self): + self.server = Mock() + self.server.hostname = SERVER1 + self.server.database = Database(':memory:') + + root = Entity(server=self.server) + self.entity = Entity(root, USER1) + + @mockified('pypsyc.server.person', ['RoutingErrorRelaying']) + def test_unknown_target_error(self, RoutingErrorRelaying): + RESOURCE1 = USER1_UNI.chain('*resource1') + RESOURCE2 = USER1_UNI.chain('*resource2') + + Entity(self.entity, '*resource1') + Entity(self.entity, '*resource2') + routing_error_relaying = RoutingErrorRelaying.return_value + person = Person(self.entity) + assert RoutingErrorRelaying.call_args_list == [((person,),)] + + person.unknown_target_error(USER2_UNI) + assert routing_error_relaying.method_calls == [ + ('relay_unknown_target_error', (RESOURCE1, USER2_UNI)), + ('relay_unknown_target_error', (RESOURCE2, USER2_UNI))] + + @mockified('pypsyc.server.person', ['RoutingErrorRelaying']) + def test_unknown_target_error(self, RoutingErrorRelaying): + RESOURCE1 = USER1_UNI.chain('*resource1') + RESOURCE2 = USER1_UNI.chain('*resource2') + + Entity(self.entity, '*resource1') + Entity(self.entity, '*resource2') + routing_error_relaying = RoutingErrorRelaying.return_value + person = Person(self.entity) + assert RoutingErrorRelaying.call_args_list == [((person,),)] + + person.delivery_failed_error(USER2_UNI, ERROR) + assert routing_error_relaying.method_calls == [ + ('relay_delivery_failed_error', (RESOURCE1, USER2_UNI, ERROR)), + ('relay_delivery_failed_error', (RESOURCE2, USER2_UNI, ERROR))] + + @mockified('pypsyc.server.person', ['LinkingServer', 'FriendshipProtocol', + 'ContextSlave']) + def test_authenticate(self, LinkingServer, FriendshipProtocol, + ContextSlave): + NONCE = 'random_nonce' + DIGEST = hmac.new(PASSWORD, NONCE, sha256).digest() + + circuit = Mock() + circuit.allowed_sources = [] + friendship_protocol = FriendshipProtocol.return_value + context_slave = ContextSlave.return_value + + person = Person(self.entity) + person.register(PASSWORD) + assert LinkingServer.call_args_list == [((person,),)] + + assert_raises(AuthenticationError, + person.authenticate, ('hmac', 'wrong', NONCE), None, RESOURCE) + + sql = "INSERT INTO friendships VALUES(?, ?, 'established')" + self.server.database.execute(sql, USER1_UNI, USER2_UNI) + + person.authenticate(('hmac', DIGEST, NONCE), circuit, RESOURCE) + assert self.entity.children[RESOURCE].circuit == circuit + assert circuit.allowed_sources == [RESOURCE1_UNI] + friendships = {USER2_UNI: {'state': 'established'}} + assert friendship_protocol.method_calls == [ + ('cast_presence', (7,)), + ('send_friendships', (RESOURCE1_UNI, friendships))] + assert context_slave.method_calls == [ + ('enter', (USER2_UNI, RESOURCE1_UNI))] + friendship_protocol.reset_mock() + context_slave.reset_mock() + + person.unlink(RESOURCE) + assert self.entity.children == {} + assert friendship_protocol.method_calls == [('cast_presence', (1,))] + assert context_slave.method_calls == [('leave_all',)] + + @mockified('pypsyc.server.person', ['MessageRelaying']) + def test_message_relay(self, MessageRelaying): + message_relaying = MessageRelaying.return_value + person = Person(self.entity) + assert MessageRelaying.call_args_list == [((person,),)] + + person.private_message_relay(RESOURCE1_UNI, USER2_UNI, MESSAGE) + person.private_message_relay(RESOURCE2_UNI, USER2_UNI, MESSAGE) + assert message_relaying.method_calls == [ + ('send_private_message', (USER2_UNI, MESSAGE))] + + @mockified('pypsyc.server.person', ['MessageRelaying']) + def test_message(self, MessageRelaying): + RESOURCE1 = USER1_UNI.chain('*resource1') + RESOURCE2 = USER1_UNI.chain('*resource2') + + Entity(self.entity, '*resource1') + Entity(self.entity, '*resource2') + message_relaying = MessageRelaying.return_value + person = Person(self.entity) + + person.private_message(USER2_UNI, MESSAGE) + assert message_relaying.method_calls == [ + ('relay_private_message', (USER2_UNI, RESOURCE1, MESSAGE)), + ('relay_private_message', (USER2_UNI, RESOURCE2, MESSAGE))] + + @mockified('pypsyc.server.person', ['FriendshipProtocol', 'ContextSlave']) + def test_outgoing_friendship(self, FriendshipProtocol, ContextSlave): + friendship_protocol = FriendshipProtocol.return_value + context_slave = ContextSlave.return_value + person = Person(self.entity) + person.register('') + Entity(self.entity, RESOURCE) + + person.client_add_friend(USER2_UNI) + fs = {'state': 'pending'} + assert friendship_protocol.method_calls == [ + ('establish', (USER2_UNI,)), + ('send_updated_friendship', (RESOURCE1_UNI, USER2_UNI, fs))] + friendship_protocol.reset_mock() + assert_raises(FriendshipPendingError, person.client_add_friend, + USER2_UNI) + + state = person.friendship_request(USER2_UNI) + assert state == 'established' + state = person.friendship_request(USER2_UNI) + assert state == 'established' + fs = {'state': 'established'} + assert friendship_protocol.method_calls == [ + ('send_updated_friendship', (RESOURCE1_UNI, USER2_UNI, fs))] + assert context_slave.method_calls == [('enter', (USER2_UNI,))] + assert_raises(FriendshipEstablishedError, person.client_add_friend, + USER2_UNI) + + @mockified('pypsyc.server.person', ['FriendshipProtocol', 'ContextSlave']) + def test_incoming_friendship(self, FriendshipProtocol, ContextSlave): + friendship_protocol = FriendshipProtocol.return_value + context_slave = ContextSlave.return_value + person = Person(self.entity) + person.register('') + Entity(self.entity, RESOURCE) + + state = person.friendship_request(USER2_UNI) + assert state == 'pending' + state = person.friendship_request(USER2_UNI) + assert state == 'pending' + fs = {'state': 'offered'} + assert friendship_protocol.method_calls == [ + ('send_updated_friendship', (RESOURCE1_UNI, USER2_UNI, fs))] + friendship_protocol.reset_mock() + + person.client_add_friend(USER2_UNI) + fs = {'state': 'established'} + assert friendship_protocol.method_calls == [ + ('establish', (USER2_UNI,)), + ('send_updated_friendship', (RESOURCE1_UNI, USER2_UNI, fs))] + assert context_slave.method_calls == [('enter', (USER2_UNI,))] + assert_raises(FriendshipEstablishedError, person.client_add_friend, + USER2_UNI) + + @mockified('pypsyc.server.person', ['FriendshipProtocol', 'ContextSlave']) + def test_cancel_friendship(self, FriendshipProtocol, ContextSlave): + friendship_protocol = FriendshipProtocol.return_value + context_slave = ContextSlave.return_value + person = Person(self.entity) + person.register('') + Entity(self.entity, RESOURCE) + + person.friendship_cancel(USER2_UNI) + assert context_slave.method_calls == [('leave', (USER2_UNI,))] + assert friendship_protocol.method_calls == [ + ('send_removed_friendship', (RESOURCE1_UNI, USER2_UNI))] + + @mockified('pypsyc.server.person', ['FriendshipProtocol', 'ContextSlave']) + def test_remove_friend(self, FriendshipProtocol, ContextSlave): + friendship_protocol = FriendshipProtocol.return_value + context_slave = ContextSlave.return_value + person = Person(self.entity) + person.register('') + Entity(self.entity, RESOURCE) + + person.client_remove_friend(USER2_UNI) + assert context_slave.method_calls == [('leave', (USER2_UNI,))] + assert friendship_protocol.method_calls == [ + ('remove', (USER2_UNI,)), + ('send_removed_friendship', (RESOURCE1_UNI, USER2_UNI))] + + @mockified('pypsyc.server.person', ['FriendshipProtocol', 'ContextSlave', + 'ContextMasterProtocol']) + def test_enter_context(self, FriendshipProtocol, ContextSlave, + ContextMasterProtocol): + cm = self.entity.context_master = Mock() + cm.add_member.return_value = VARIABLES + person = Person(self.entity) + person.register('') + person.friendship_request(USER2_UNI) + person.client_add_friend(USER2_UNI) + assert ContextMasterProtocol.call_args_list == [((person,),)] + + state = person.enter_request(USER2_UNI) + assert state == VARIABLES + assert cm.method_calls == [('add_member', (USER2_UNI,))] + + def test_enter_context_entry_denied(self): + person = Person(self.entity) + person.register('') + + assert_raises(EntryDeniedError, person.enter_request, USER2_UNI) + + def test_leave_context(self): + cm = self.entity.context_master = Mock() + person = Person(self.entity) + + person.leave_context(USER2_UNI) + assert cm.method_calls == [('remove_member', (USER2_UNI,))] + + @mockified('pypsyc.server.person', ['FriendshipProtocol']) + def test_client_presence(self, FriendshipProtocol): + friendship_protocol = FriendshipProtocol.return_value + + person = Person(self.entity) + assert FriendshipProtocol.call_args_list == [((person,),)] + + person.client_presence(ONLINE) + assert friendship_protocol.method_calls == [ + ('cast_presence', (ONLINE,))] + + @mockified('pypsyc.server.person', ['ContextSlave', + 'ClientInterfaceProtocol']) + def test_client_subscribe(self, ContextSlave, ClientInterfaceProtocol): + context_slave = ContextSlave.return_value + person = Person(self.entity) + assert ContextSlave.call_args_list == [((person,),)] + assert ClientInterfaceProtocol.call_args_list == [((person,),)] + + person.client_subscribe(PLACE_UNI, RESOURCE1_UNI) + assert context_slave.method_calls == [ + ('enter', (PLACE_UNI, RESOURCE1_UNI))] + + @mockified('pypsyc.server.person', ['ContextSlave']) + def test_client_unsubscribe(self, ContextSlave): + context_slave = ContextSlave.return_value + person = Person(self.entity) + + person.client_unsubscribe(PLACE_UNI, RESOURCE1_UNI) + assert context_slave.method_calls == [ + ('leave', (PLACE_UNI, RESOURCE1_UNI))] diff --git a/mjacob2/tests/test_server/test_place.py b/mjacob2/tests/test_server/test_place.py new file mode 100644 index 0000000..5acfd4a --- /dev/null +++ b/mjacob2/tests/test_server/test_place.py @@ -0,0 +1,68 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from tests.constants import (SERVER1, USER1_UNI, USER2_UNI, USER1_NICK, + RESOURCE1_UNI, RESOURCE2_UNI, PLACE, VARIABLES, MESSAGE) +from tests.helpers import mockified + +from pypsyc.server import Entity +from pypsyc.server.place import Place + + +class TestPlace(object): + def setup(self): + self.server = Mock() + self.server.hostname = SERVER1 + + root = Entity(server=self.server) + self.entity = Entity(root, PLACE) + self.entity.context_master = Mock() + + @mockified('pypsyc.server.place', ['ContextProtocol', + 'ConferencingProtocol']) + def test_subscription(self, ContextProtocol, ConferencingProtocol): + conferencing_protocol = ConferencingProtocol.return_value + self.entity.context_master.add_member.return_value = VARIABLES + + place = Place(self.entity) + assert ContextProtocol.call_args_list == [((place,),)] + + state = place.enter_request(USER1_UNI) + assert state == VARIABLES + assert conferencing_protocol.method_calls == [ + ('cast_member_entered', (USER1_UNI, USER1_NICK))] + assert self.entity.context_master.method_calls == [ + ('add_member', (USER1_UNI,))] + + @mockified('pypsyc.server.place', ['ConferencingProtocol']) + def test_leave(self, ConferencingProtocol): + conferencing_protocol = ConferencingProtocol.return_value + place = Place(self.entity) + + place.enter_request(USER1_UNI) + self.entity.context_master.reset_mock() + conferencing_protocol.reset_mock() + + place.leave_context(USER1_UNI) + assert self.entity.context_master.method_calls == [ + ('remove_member', (USER1_UNI,))] + assert conferencing_protocol.method_calls == [ + ('cast_member_left', (USER1_UNI,))] + + @mockified('pypsyc.server.place', ['ConferencingProtocol']) + def test_public_message(self, ConferencingProtocol): + conferencing_protocol = ConferencingProtocol.return_value + + place = Place(self.entity) + assert ConferencingProtocol.call_args_list == [((place,),)] + place.enter_request(USER1_UNI) + place.enter_request(USER2_UNI) + place.leave_context(USER2_UNI) + conferencing_protocol.reset_mock() + + place.public_message(RESOURCE2_UNI, MESSAGE) + place.public_message(RESOURCE1_UNI, MESSAGE) + assert conferencing_protocol.method_calls == [ + ('cast_public_message', (USER1_UNI, MESSAGE))] diff --git a/mjacob2/tests/test_server/test_root.py b/mjacob2/tests/test_server/test_root.py new file mode 100644 index 0000000..0840af4 --- /dev/null +++ b/mjacob2/tests/test_server/test_root.py @@ -0,0 +1,21 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from tests.constants import SERVER1 + +from pypsyc.server import Entity +from pypsyc.server.root import Root + + +class TestRoot(object): + def setup(self): + server = Mock() + server.hostname = SERVER1 + + root_entity = Entity(server=server) + self.root = Root(root_entity) + + def test_root(self): + pass diff --git a/mjacob2/tests/test_server/test_routing.py b/mjacob2/tests/test_server/test_routing.py new file mode 100644 index 0000000..6efc57e --- /dev/null +++ b/mjacob2/tests/test_server/test_routing.py @@ -0,0 +1,353 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock +from nose.tools import assert_raises +from tests.constants import (SERVER1, SERVER2, SERVER1_UNI, SERVER2_UNI, + SERVER3_UNI, USER1, USER2, USER1_UNI, USER2_UNI, RESOURCE, RESOURCE1_UNI, + RESOURCE2_UNI, PLACE_UNI, INTERFACE, IP, PORT, CONTENT) +from tests.helpers import (inited_header, connect_circuits, mockified, + AsyncMethod, PlaceHolder) + +from pypsyc.core.mmp import Uni +from pypsyc.protocol import Error +from pypsyc.server.routing import (InvalidTargetError, InvalidSourceError, + ServerCircuit, _TreeNode, Routing) +from pypsyc.util import DNSError + + +class TestServerCircuit(object): + def setup(self): + self.sc = ServerCircuit() + self.sc.factory = self.routing = Mock() + self.sc.psyc = Mock() + self.sc.allowed_sources.append(SERVER2_UNI) + + def _send(self, header): + self.sc.packet_received(inited_header(header), CONTENT) + + def test_withtarget(self): + HEADER1 = {'_target': SERVER1_UNI} + HEADER2 = {'_target': SERVER1_UNI, '_source': SERVER2_UNI} + HEADER3 = {'_target': SERVER1_UNI, '_source': SERVER3_UNI} + + self._send(HEADER1) + assert self.routing.method_calls == [ + ('route_singlecast', (HEADER1, CONTENT))] + + self._send(HEADER2) + assert self.routing.method_calls == [ + ('route_singlecast', (HEADER1, CONTENT)), + ('route_singlecast', (HEADER2, CONTENT))] + + self._send(HEADER3) + assert self.routing.method_calls == [ + ('route_singlecast', (HEADER1, CONTENT)), + ('route_singlecast', (HEADER2, CONTENT))] + + def test_withouttarget(self): + HEADER1 = {} + HEADER2 = {'_source': SERVER2_UNI} + HEADER3 = {'_source': SERVER3_UNI} + + self._send(HEADER1) + assert self.routing.method_calls == [] + assert self.sc.psyc.method_calls == [ + ('handle_packet', (HEADER1, CONTENT))] + + self._send(HEADER2) + assert self.routing.method_calls == [] + assert self.sc.psyc.method_calls == [ + ('handle_packet', (HEADER1, CONTENT)), + ('handle_packet', (HEADER2, CONTENT))] + + self._send(HEADER3) + assert self.routing.method_calls == [] + assert self.sc.psyc.method_calls == [ + ('handle_packet', (HEADER1, CONTENT)), + ('handle_packet', (HEADER2, CONTENT))] + + def test_context(self): + HEADER1 = {'_context': PLACE_UNI} + HEADER2 = {'_context': PLACE_UNI, '_target': USER1_UNI} + + self._send(HEADER1) + assert self.routing.method_calls == [ + ('route_multicast', (HEADER1, CONTENT))] + + self._send(HEADER2) + assert self.routing.method_calls == [ + ('route_multicast', (HEADER1, CONTENT)), + ('route_singlecast', (HEADER2, CONTENT))] + + def test_context_withsource(self): + HEADER1 = {'_context': PLACE_UNI, '_source': USER1_UNI} + HEADER2 = {'_context': PLACE_UNI, '_source': USER2_UNI, + '_target': USER1_UNI} + + assert_raises(NotImplementedError, self._send, HEADER1) + assert self.routing.method_calls == [] + + assert_raises(NotImplementedError, self._send, HEADER2) + assert self.routing.method_calls == [] + + def test_verification(self): + sc1, sc2 = connect_circuits(ServerCircuit(), ServerCircuit()) + sc2.factory = Mock() + + sc1.request_verification(SERVER1_UNI, SERVER2_UNI) + assert sc2.factory.method_calls == [ + ('verify_address', (sc2, SERVER1_UNI, SERVER2_UNI))] + assert type(sc2.factory.method_calls[0][1][1]) is Uni + assert sc1.allowed_sources == [SERVER2_UNI] + assert sc2.allowed_sources == [SERVER1_UNI] + assert type(sc2.allowed_sources[0]) is Uni + + def test_verification_invalid_source(self): + sc1, sc2 = connect_circuits(ServerCircuit(), ServerCircuit()) + sc2.factory = Mock() + sc2.factory.verify_address.side_effect = InvalidSourceError + + assert_raises(InvalidSourceError, sc1.request_verification, + SERVER1_UNI, SERVER2_UNI) + assert sc1.allowed_sources == [] + assert sc2.allowed_sources == [] + + def test_verification_invalid_target(self): + sc1, sc2 = connect_circuits(ServerCircuit(), ServerCircuit()) + sc2.factory = Mock() + sc2.factory.verify_address.side_effect = InvalidTargetError + + assert_raises(InvalidTargetError, sc1.request_verification, + SERVER1_UNI, SERVER2_UNI) + assert sc1.allowed_sources == [] + assert sc2.allowed_sources == [] + + def test_connection_lost(self): + root = self.routing.root = _TreeNode() + person = Mock() + person_entity = _TreeNode(root, USER1) + person_entity.packages = {'person': person} + self.sc.allowed_sources.append(RESOURCE1_UNI) + + self.routing.srouting_table = {SERVER2: Mock()} + self.sc.connectionLost(None) + assert self.routing.srouting_table == {} + assert person.method_calls == [('unlink', (RESOURCE,))] + + +def test_treenode(): + root = _TreeNode() + assert root._root == root + + n1 = _TreeNode(root, 'n1') + assert root.children == {'n1': n1} + assert n1._parent == root + assert n1._root == root + + n2 = _TreeNode(n1, 'n2') + assert n1.children == {'n2': n2} + assert n2._parent == n1 + assert n2._root == root + + +class StubEntity(_TreeNode): + def __init__(self, *args, **kwds): + _TreeNode.__init__(self, *args, **kwds) + self.headers = [] + + def handle_packet(self, header, contents): + self.headers.append(header) + +EXTERN_HEADER = {'_source': USER1_UNI, '_target': SERVER2_UNI} + +class TestRouting(object): + def test_sroute_local(self): + HEADER1 = {'_target': SERVER1_UNI} + HEADER2 = {'_target': USER1_UNI} + HEADER3 = {'_target': RESOURCE2_UNI} + + root = StubEntity() + user1 = StubEntity(root, USER1) + user2 = StubEntity(root, USER2) + home = StubEntity(user2, RESOURCE) + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(inited_header(HEADER1), CONTENT) + routing.route_singlecast(inited_header(HEADER2), CONTENT) + routing.route_singlecast(inited_header(HEADER3), CONTENT) + assert root.headers == [HEADER1] + assert user1.headers == [HEADER2] + assert user2.headers == [] + assert home.headers == [HEADER3] + + def test_sroute_unkown_target(self): + HEADER1 = inited_header({'_source': USER1_UNI, '_target': USER2_UNI}) + HEADER2 = inited_header({'_target': USER2_UNI, '_tag': 'tag'}) + KWDS = {'mc': '_error_unknown_target', '_uni': USER2_UNI, 'data': None} + + root = StubEntity() + root.sendmsg = Mock() + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(HEADER1, CONTENT) + routing.route_singlecast(HEADER2, CONTENT) + assert root.sendmsg.call_args_list == [ + ((USER1_UNI, None, None), KWDS), + ((None, None, {'_tag_relay': 'tag'}), KWDS)] + assert root.headers == [] + + def test_sroute_extern(self): + routing = Routing(SERVER1, INTERFACE) + circuit = routing.srouting_table[SERVER2] = Mock() + + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + assert circuit.method_calls == [(('send'), (EXTERN_HEADER, CONTENT))] + + @mockified('pypsyc.server.routing', ['resolve_hostname', 'connect']) + def test_sroute_extern_queued(self, resolve_hostname, connect): + resolve_hostname.return_value = IP, PORT + connected = connect.side_effect = AsyncMethod() + root = Mock() + root.uni = SERVER1_UNI + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + assert connect.call_args_list == [ + ((IP, PORT, routing), {'bindAddress': (INTERFACE, 0)})] + + circuit = Mock() + connected.callback(circuit) + assert circuit.method_calls == [ + ('request_verification', (SERVER1_UNI, SERVER2_UNI)), + ('send', (EXTERN_HEADER, CONTENT)), + ('send', (EXTERN_HEADER, CONTENT))] + assert type(circuit.method_calls[0][1][1]) is Uni + assert routing.srouting_table == {SERVER2: circuit} + assert routing.queues == {} + + @mockified('pypsyc.server.routing', ['resolve_hostname']) + def test_sroute_extern_resolution_fail(self, resolve_hostname): + KWDS = {'mc': '_failure_unsuccessful_delivery', '_uni': SERVER2_UNI} + resolved = resolve_hostname.side_effect = AsyncMethod() + data_ph = KWDS['data'] = PlaceHolder() + root = Mock() + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + resolved.errback(DNSError) + assert root.method_calls == [ + ('sendmsg', (USER1_UNI, None, None), KWDS)] * 2 + assert 'resolve' in data_ph.obj + assert routing.queues == {} + + @mockified('pypsyc.server.routing', ['resolve_hostname', 'connect']) + def test_sroute_extern_connection_fail(self, resolve_hostname, connect): + KWDS = {'mc': '_failure_unsuccessful_delivery', '_uni': SERVER2_UNI} + resolve_hostname.return_value = None, None + connected = connect.side_effect = AsyncMethod() + data_ph = KWDS['data'] = PlaceHolder() + root = Mock() + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + connected.errback(Exception('error')) + assert root.method_calls == [ + ('sendmsg', (USER1_UNI, None, None), KWDS)] + assert 'connect' in data_ph.obj + assert routing.queues == {} + + @mockified('pypsyc.server.routing', ['resolve_hostname', 'connect']) + def test_sroute_extern_verification_fail(self, resolve_hostname, connect): + KWDS = {'mc': '_failure_unsuccessful_delivery', '_uni': SERVER2_UNI} + resolve_hostname.return_value = None, None + circuit = connect.return_value + verified = circuit.request_verification.side_effect = AsyncMethod() + data_ph = KWDS['data'] = PlaceHolder() + root = Mock() + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + + routing.route_singlecast(inited_header(EXTERN_HEADER), CONTENT) + verified.errback(Error) + assert root.method_calls == [ + ('sendmsg', (USER1_UNI, None, None), KWDS)] + assert 'verify' in data_ph.obj + assert routing.queues == {} + + @mockified('pypsyc.server.routing', ['resolve_hostname', 'connect']) + def test_sroute_extern_no_source(self, resolve_hostname, connect): + header = inited_header({'_target': SERVER2_UNI}) + header.source = Mock() + routing = Routing(SERVER1, INTERFACE) + + routing.route_singlecast(header, CONTENT) + header['_source'] = '' + routing.route_singlecast(header, CONTENT) + assert resolve_hostname.call_args_list == [] + assert connect.call_args_list == [] + assert routing.srouting_table == {} + assert routing.queues == {} + + def test_mroute(self): + HEADER = {'_context': PLACE_UNI} + circuit1 = Mock() + circuit2 = Mock() + routing = Routing(SERVER1, INTERFACE) + routing.mrouting_table[PLACE_UNI] = [circuit1, circuit2] + + routing.route_multicast(inited_header(HEADER), iter(CONTENT)) + assert circuit1.method_calls == [('send', (HEADER, CONTENT))] + assert circuit2.method_calls == [('send', (HEADER, CONTENT))] + + @mockified('pypsyc.server.routing', ['reactor']) + def test_listen(self, reactor): + routing = Routing(SERVER1, INTERFACE) + routing.listen(PORT) + assert reactor.method_calls == [ + ('listenTCP', (PORT, routing), {'interface': INTERFACE})] + + def _setup_verification(self): + root = Mock() + root.uni = SERVER1_UNI + routing = Routing(SERVER1, INTERFACE) + routing.init(root) + circuit = Mock() + circuit.transport.client = IP, 35771 + return routing, circuit + + @mockified('pypsyc.server.routing', ['resolve_hostname']) + def test_verification(self, resolve_hostname): + resolve_hostname.return_value = IP, PORT + routing, circuit = self._setup_verification() + + routing.verify_address(circuit, SERVER2_UNI, SERVER1_UNI) + assert resolve_hostname.call_args_list == [((SERVER2,),)] + assert routing.srouting_table == {SERVER2: circuit} + + @mockified('pypsyc.server.routing', ['resolve_hostname']) + def test_verification_invalid_target(self, resolve_hostname): + routing, circuit = self._setup_verification() + + assert_raises(InvalidTargetError, routing.verify_address, + circuit, SERVER2_UNI, SERVER3_UNI) + assert resolve_hostname.call_args_list == [] + assert routing.srouting_table == {} + + @mockified('pypsyc.server.routing', ['resolve_hostname']) + def test_verification_invalid_source(self, resolve_hostname): + resolve_hostname.return_value = '10.0.0.2', PORT + routing, circuit = self._setup_verification() + + assert_raises(InvalidSourceError, routing.verify_address, + circuit, SERVER2_UNI, SERVER1_UNI) + assert resolve_hostname.call_args_list == [((SERVER2,),)] + assert routing.srouting_table == {} diff --git a/mjacob2/tests/test_server/test_server.py b/mjacob2/tests/test_server/test_server.py new file mode 100644 index 0000000..3c82e41 --- /dev/null +++ b/mjacob2/tests/test_server/test_server.py @@ -0,0 +1,134 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from contextlib import contextmanager + +from mock import Mock +from nose.tools import assert_raises +from tests.constants import (SERVER1, SERVER1_UNI, USER1, USER1_UNI, PLACE, + INTERFACE, PORT, PASSWORD) +from tests.helpers import mockified, rendered, AsyncMethod + +from pypsyc.core.psyc import PSYCPacket +from pypsyc.server import Entity, Server +from pypsyc.server.routing import _TreeNode + + +class TestEntity(object): + def setup(self): + self.server = Mock() + self.server.hostname = SERVER1 + self.entity = Entity(server=self.server) + + def test_uni(self): + assert self.entity.uni == SERVER1_UNI + assert Entity(self.entity, USER1).uni == USER1_UNI + + def test_castmsg(self): + HEADER = {'_context': self.entity.uni} + PACKET = PSYCPacket(mc='_message_public', data="Hello") + + self.entity.castmsg(PACKET) + assert self.server.routing.method_calls == [ + ('route_multicast', (HEADER, rendered(PACKET)))] + + def test_castmsg_kwds(self): + HEADER = {'_context': self.entity.uni} + PACKET = PSYCPacket(mc='_message_public', data="Hello") + + self.entity.castmsg(mc='_message_public', data="Hello") + assert self.server.routing.method_calls == [ + ('route_multicast', (HEADER, rendered(PACKET)))] + + +def StubEntity(*args, **kwds): + entity = _TreeNode(*args, **kwds) + entity.packages = {} + return entity + +class TestServer(object): + @mockified('pypsyc.server', ['Routing', 'Entity', 'run_webif', 'signal']) + def test_server1(self, Routing, Entity, run_webif, signal): + WEBIF_PORT = 8080 + routing = Routing.return_value + root = Entity.return_value = StubEntity() + run_webif.side_effect = AsyncMethod() + + server = Server(SERVER1, INTERFACE, PORT, WEBIF_PORT, ':memory:') + assert Routing.call_args_list == [((SERVER1, INTERFACE),)] + assert Entity.call_args_list == [({'server': server},)] + assert routing.method_calls == [('init', (root,)), ('listen', (PORT,))] + assert run_webif.call_args_list == [ + ((server, INTERFACE, WEBIF_PORT, None),)] + assert signal.signal.called + + with mockified('pypsyc.server', ['iter_entry_points', 'Entity']) as x: + with self._test_load_package(root, *x) as packages: + assert server.add_package(None, 'package') == packages[0] + assert server.add_place('place') == packages[1] + assert server.add_package(PLACE, 'package') == packages[2] + assert server.add_package(USER1, 'package') == packages[3] + person = server.register_person('user1', PASSWORD) + assert person == packages[4] + assert person.method_calls == [('register', (PASSWORD,))] + assert_raises(AssertionError, server.add_package, None, 'package') + + self._test_server2(server.database) + + @mockified('pypsyc.server', ['Routing', 'Entity', 'Database', 'run_webif']) + def _test_server2(self, database, Routing, Entity, Database, run_webif): + routing = Routing.return_value + root = Entity.return_value = StubEntity() + Database.return_value = database + executed = database.execute = AsyncMethod() + + server = Server(SERVER1, INTERFACE, 0, 0, ':memory:') + assert routing.method_calls == [('init', (root,))] + assert run_webif.call_args_list == [] + + with mockified('pypsyc.server', ['iter_entry_points', 'Entity']) as x: + with self._test_load_package(root, *x): + executed.callback() + assert_raises(AssertionError, server.add_package, None, 'package1') + + @contextmanager + def _test_load_package(self, root, iter_entry_points, Entity): + package_classes = [Mock(), Mock(), Mock(), Mock(), Mock()] + entrypoint = Mock() + entrypoint.load.side_effect = iter(package_classes).next + iter_entry_points.return_value = [entrypoint] + Entity.side_effect = StubEntity + + packages = [package.return_value for package in package_classes] + yield packages + assert Entity.call_args_list == [((root, PLACE),), ((root, USER1),)] + assert iter_entry_points.call_args_list == [ + (('pypsyc.server.packages', 'package'),), + (('pypsyc.server.packages', 'place'),), + (('pypsyc.server.packages', 'package'),), + (('pypsyc.server.packages', 'package'),), + (('pypsyc.server.packages', 'person'),)] + place = root.children[PLACE] + person = root.children[USER1] + assert package_classes[0].call_args_list == [((root,),)] + assert package_classes[1].call_args_list == [((place,),)] + assert package_classes[2].call_args_list == [((place,),)] + assert package_classes[3].call_args_list == [((person,),)] + assert package_classes[4].call_args_list == [((person,),)] + assert root.packages == {'package': packages[0]} + assert place.packages == {'place': packages[1], 'package': packages[2]} + assert person.packages == {'package': packages[3], + 'person': packages[4]} + + @mockified('pypsyc.server', ['Routing', 'Entity', 'Database', 'signal', + 'reactor']) + def test_shutdown(self, Routing, Entity, Database, signal, reactor): + database = Database.return_value + database.fetch.return_value = () + server = Server(SERVER1, INTERFACE, 0, 0, ':memory') + database.reset_mock() + + server.shutdown() + assert database.method_calls == [('stop',)] + assert reactor.method_calls == [('stop',)] diff --git a/mjacob2/tests/test_server/test_webif/test_webif.py b/mjacob2/tests/test_server/test_webif/test_webif.py new file mode 100644 index 0000000..3305db0 --- /dev/null +++ b/mjacob2/tests/test_server/test_webif/test_webif.py @@ -0,0 +1,139 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from mock import Mock, sentinel +from nose.tools import assert_raises +from tests.constants import USER1, USER1_NICK, PASSWORD +from tests.helpers import mockified, PlaceHolder + +from pypsyc.server import webif +from pypsyc.server.db import Database +from pypsyc.server.webif import app, PersistentStore, run_webif + + +class TestViews(object): + @classmethod + def setup_class(cls): + app.secret_key = 'testing key' + + def setup(self): + self.server = webif.server = Mock() + self.server.root.children = {} + self.client = app.test_client() + + def test_index(self): + rv = self.client.get('/') + assert 'register' in rv.data + + def _register(self, username, password, password2): + return self.client.post('/register', data=dict( + username=username, password=password, password2=password2 + ), follow_redirects=True) + + def test_register_get(self): + rv = self.client.get('/register') + assert 'Username' in rv.data + assert 'Register' in rv.data + + def test_register_post(self): + rv = self._register(USER1_NICK, PASSWORD, PASSWORD) + assert 'you were registered' in rv.data + assert self.server.method_calls == [ + ('register_person', (USER1_NICK, PASSWORD))] + + def test_register_nousername(self): + rv = self._register('', '', '') + assert 'please specify a valid username' in rv.data + + def test_register_username_inuse(self): + self.server.root.children = {USER1: True} + + rv = self._register(USER1_NICK, '', '') + assert 'username already in use' in rv.data + + def test_register_nopassword(self): + rv = self._register(USER1_NICK, '', '') + assert 'your password must have at least 6 characters' in rv.data + + def test_register_shortpassword(self): + rv = self._register(USER1_NICK, 'passw', 'passw') + assert 'your password must have at least 6 characters' in rv.data + + def test_register_unmatching_passwords(self): + rv = self._register(USER1_NICK, PASSWORD, 'password2') + assert 'the two passwords do not match' in rv.data + + +def test_persistent_store(): + webif.server = Mock() + webif.server.database = Database(':memory:') + + store = PersistentStore() + assert_raises(KeyError, store.__getitem__, 'key') + assert dict(store) == {} + assert len(store) == 0 + + store['key'] = 'value' + assert store['key'] == 'value' + assert len(store) == 1 + + store['key'] = 'value2' + assert dict(store) == {'key': 'value2'} + assert len(store) == 1 + + del store['key'] + assert dict(store) == {} + assert len(store) == 0 + + +@mockified('pypsyc.server.webif', ['PersistentStore', 'urandom', + 'WSGIResource', 'Site', 'reactor', + 'greenlet']) +def test_run_webif_ssl(PersistentStore, urandom, WSGIResource, Site, reactor, + greenlet): + store = PersistentStore.return_value = {'secret_key': 'key1'*5} + reactor_ph = PlaceHolder() + threadpool_ph = PlaceHolder() + wsgi_resource = WSGIResource.return_value + site = Site.return_value + + run_webif(sentinel.server, sentinel.interface, sentinel.port, + sentinel.context_factory) + assert webif.server == sentinel.server + assert PersistentStore.call_args_list == [()] + assert webif.store == store + assert app.secret_key == 'key1'*5 + assert not urandom.called + assert WSGIResource.call_args_list == [ + ((reactor_ph, threadpool_ph, app.wsgi_app),)] + assert Site.call_args_list == [((wsgi_resource,),)] + assert reactor.method_calls == [ + ('listenSSL', (sentinel.port, site, sentinel.context_factory), + {'interface': sentinel.interface})] + + func = Mock() + reactor_ph.obj.callFromThread(func, sentinel.a, b=sentinel.b) + assert func.call_args_list == [((sentinel.a,), {'b': sentinel.b})] + + threadpool_ph.obj.callInThread(sentinel.func, sentinel.a, b=sentinel.b) + assert greenlet.call_args_list == [((sentinel.func,),)] + assert greenlet.return_value.method_calls == [ + ('switch', (sentinel.a,), {'b': sentinel.b})] + + +@mockified('pypsyc.server.webif', ['PersistentStore', 'urandom', + 'WSGIResource', 'Site', 'reactor']) +def test_run_webif_tcp(PersistentStore, urandom, WSGIResource, Site, reactor): + store = PersistentStore.return_value = {} + rand = urandom.return_value = 'key2'*5 + wsgi_resource = WSGIResource.return_value + site = Site.return_value + + run_webif(sentinel.server, sentinel.interface, sentinel.port, None) + assert urandom.call_args_list == [((20,),)] + assert app.secret_key == rand + assert store['secret_key'] == rand + assert Site.call_args_list == [((wsgi_resource,),)] + assert reactor.method_calls == [ + ('listenTCP', (sentinel.port, site), {'interface': 'localhost'})] diff --git a/mjacob2/tests/test_util.py b/mjacob2/tests/test_util.py new file mode 100644 index 0000000..0aeaf7d --- /dev/null +++ b/mjacob2/tests/test_util.py @@ -0,0 +1,206 @@ +""" + :copyright: 2010 by Manuel Jacob + :license: MIT +""" +from greenlet import greenlet +from mock import Mock, sentinel +from nose.tools import assert_raises +from tests.constants import SERVER1, IP, PORT +from tests.helpers import mockified, check_success, StubException +from twisted.internet.defer import Deferred, fail +from twisted.names.dns import RRHeader, Record_A, Record_SRV +from twisted.names.error import DNSNameError +from twisted.python.failure import Failure + +from pypsyc.util import (scheduler, schedule, Waiter, DNSError, + resolve_hostname, _PSYCConnector, connect, Event) + + +@mockified('pypsyc.util', ['greenlet']) +def test_schedule_scheduler(greenlet): + func = Mock() + + schedule(func) + assert greenlet.call_args_list == [((func, scheduler),)] + assert greenlet.return_value.method_calls == [('switch',)] + +orig_greenlet = greenlet +@mockified('pypsyc.util', ['greenlet', 'reactor']) +def test_schedule_nonscheduler(greenlet, reactor): + func = Mock() + + orig_greenlet(schedule).switch(func) + assert greenlet.call_args_list == [((func, scheduler),)] + assert reactor.method_calls == [ + ('callLater', (0, greenlet.return_value.switch))] + + +class TestWaiter(object): + def test_callback_sync(self): + waiter = Waiter() + + waiter.callback(sentinel.value) + ret = waiter.get() + assert ret == sentinel.value + + def test_errback_sync(self): + e = StubException() + waiter = Waiter() + + waiter.errback(e) + assert_raises(StubException, waiter.get) + + @check_success + def test_callback_async(self): + waiter = Waiter() + + def _test(): + ret = waiter.get() + assert ret == sentinel.value + self.success = True + greenlet(_test).switch() + waiter.callback(sentinel.value) + + @check_success + def test_errback_async(self): + e = StubException() + waiter = Waiter() + + def _test(): + assert_raises(StubException, waiter.get) + self.success = True + greenlet(_test).switch() + waiter.errback(e) + + +class TestResolveHostname(object): + @check_success + @mockified('pypsyc.util', ['lookupService', 'getHostByName']) + def test_resolve_hostname_srv(self, lookupService, getHostByName): + SERVICE = '_psyc._tcp.%s.' % SERVER1 + srv_rr = RRHeader(name=SERVICE, type=Record_SRV.TYPE, + payload=Record_SRV(target=SERVER1, port=PORT)) + a_rr = RRHeader(name=SERVER1, type=Record_A.TYPE, + payload=Record_A(IP)) + looked_up = lookupService.return_value = Deferred() + + def _test(): + ip, port = resolve_hostname(SERVER1) + assert ip == IP + assert port == PORT + assert lookupService.call_args_list == [((SERVICE,),)] + self.success = True + schedule(_test) + looked_up.callback(([srv_rr], [], [a_rr])) + + @check_success + @mockified('pypsyc.util', ['lookupService', 'getHostByName']) + def test_resolve_hostname_a(self, lookupService, getHostByName): + lookupService.side_effect = DNSNameError + looked_up = getHostByName.return_value = Deferred() + + def _test(): + ip, port = resolve_hostname(SERVER1) + assert ip == IP + assert port == PORT + assert getHostByName.call_args_list == [((SERVER1,),)] + self.success = True + schedule(_test) + looked_up.callback(IP) + + @check_success + @mockified('pypsyc.util', ['lookupService', 'getHostByName']) + def test_resolve_hostname_a_error(self, lookupService, getHostByName): + lookupService.side_effect = DNSNameError + getHostByName.return_value = fail(DNSNameError()) + + def _test(): + assert_raises(DNSError, resolve_hostname, SERVER1) + self.success = True + schedule(_test) + + +class TestConnector(object): + @mockified('pypsyc.util', ['reactor']) + def setup(self, reactor): + self.reactor = reactor + self.factory = Mock() + self.circuit = self.factory.buildProtocol.return_value + self.connector = _PSYCConnector(SERVER1, PORT, self.factory, 30, None) + self.transport = Mock() + self.connector._makeTransport = lambda: self.transport + self.connector.cancelTimeout = Mock() + + @check_success + def test_connect(self): + def _test(): + circuit = self.connector.connect() + assert circuit == self.circuit + self.success = True + schedule(_test) + assert self.connector.state == 'connecting' + assert self.connector.transport == self.transport + assert self.connector.timeoutID == self.reactor.callLater.return_value + + self.connector.buildProtocol(None) + assert self.connector.state == 'connected' + assert self.connector.cancelTimeout.called + assert self.factory.method_calls == [('buildProtocol', (None,))] + + self.circuit.inited() + + @check_success + def test_connection_failed(self): + def _test(): + assert_raises(StubException, self.connector.connect) + self.success = True + schedule(_test) + self.connector.connectionFailed(Failure(StubException())) + assert self.connector.cancelTimeout.called + assert self.connector.transport is None + assert self.connector.state == 'disconnected' + + def test_connection_lost(self): + ERROR = StubException() + self.connector.buildProtocol(None) + self.connector.connectionLost(Failure(ERROR)) + assert self.connector.state == 'disconnected' + assert self.factory.method_calls == [ + ('buildProtocol', (None,)), + ('connection_lost', (self.circuit, ERROR))] + +@mockified('pypsyc.util', ['_PSYCConnector']) +def test_connect1(_PSYCConnector): + connect(sentinel.host, sentinel.port, sentinel.factory) + assert _PSYCConnector.call_args_list == [ + ((sentinel.host, sentinel.port, sentinel.factory, 30, None),)] + assert _PSYCConnector.return_value.method_calls == [('connect',)] + +@mockified('pypsyc.util', ['_PSYCConnector']) +def test_connect2(_PSYCConnector): + connect(sentinel.host, sentinel.port, sentinel.factory, + timeout=sentinel.timeout, bindAddress=sentinel.bindAddress) + assert _PSYCConnector.call_args_list == [ + ((sentinel.host, sentinel.port, sentinel.factory, sentinel.timeout, + sentinel.bindAddress),)] + assert _PSYCConnector.return_value.method_calls == [('connect',)] + + +def test_event(): + event = Event() + f1 = Mock() + f2 = Mock() + event += f1 + event.add_observer(f2, 1, i=2) + + x = Mock() + y = Mock() + + event(x, y=y) + assert f1.call_args_list == [((x,), {'y': y})] + assert f2.call_args_list == [((x, 1), {'y': y, 'i': 2})] + + event -= f2 + event(x) + assert f1.call_args_list == [((x,), {'y': y}), ((x,),)] + assert f2.call_args_list == [((x, 1), {'y': y, 'i': 2})]