diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py
index a9991e9c94..0cee196851 100644
--- a/synapse/api/events/__init__.py
+++ b/synapse/api/events/__init__.py
@@ -156,7 +156,8 @@ class SynapseEvent(JsonEncodedObject):
return "Missing %s key" % key
if type(content[key]) != type(template[key]):
- return "Key %s is of the wrong type." % key
+ return "Key %s is of the wrong type (got %s, want %s)" % (
+ key, type(content[key]), type(template[key]))
if type(content[key]) == dict:
# we must go deeper
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index d675d8c8f9..2f1b954902 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from synapse.storage import read_schema
+from synapse.storage import prepare_database
from synapse.server import HomeServer
@@ -36,30 +36,14 @@ from daemonize import Daemonize
import twisted.manhole.telnet
import logging
-import sqlite3
import os
import re
import sys
+import sqlite3
logger = logging.getLogger(__name__)
-SCHEMAS = [
- "transactions",
- "pdu",
- "users",
- "profiles",
- "presence",
- "im",
- "room_aliases",
-]
-
-
-# Remember to update this number every time an incompatible change is made to
-# database schema files, so the users will be informed on server restarts.
-SCHEMA_VERSION = 3
-
-
class SynapseHomeServer(HomeServer):
def build_http_client(self):
@@ -80,52 +64,12 @@ class SynapseHomeServer(HomeServer):
)
def build_db_pool(self):
- """ Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
- don't have to worry about overwriting existing content.
- """
- logging.info("Preparing database: %s...", self.db_name)
-
- with sqlite3.connect(self.db_name) as db_conn:
- c = db_conn.cursor()
- c.execute("PRAGMA user_version")
- row = c.fetchone()
-
- if row and row[0]:
- user_version = row[0]
-
- if user_version > SCHEMA_VERSION:
- raise ValueError("Cannot use this database as it is too " +
- "new for the server to understand"
- )
- elif user_version < SCHEMA_VERSION:
- logging.info("Upgrading database from version %d",
- user_version
- )
-
- # Run every version since after the current version.
- for v in range(user_version + 1, SCHEMA_VERSION + 1):
- sql_script = read_schema("delta/v%d" % (v))
- c.executescript(sql_script)
-
- db_conn.commit()
-
- else:
- for sql_loc in SCHEMAS:
- sql_script = read_schema(sql_loc)
-
- c.executescript(sql_script)
- db_conn.commit()
- c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
-
- c.close()
-
- logging.info("Database prepared in %s.", self.db_name)
-
- pool = adbapi.ConnectionPool(
- 'sqlite3', self.db_name, check_same_thread=False,
- cp_min=1, cp_max=1)
-
- return pool
+ return adbapi.ConnectionPool(
+ "sqlite3", self.get_db_name(),
+ check_same_thread=False,
+ cp_min=1,
+ cp_max=1
+ )
def create_resource_tree(self, web_client, redirect_root_to_web_client):
"""Create the resource tree for this Home Server.
@@ -230,10 +174,6 @@ class SynapseHomeServer(HomeServer):
logger.info("Synapse now listening on port %d", unsecure_port)
-def run():
- reactor.run()
-
-
def setup():
config = HomeServerConfig.load_config(
"Synapse Homeserver",
@@ -268,7 +208,15 @@ def setup():
web_client=config.webclient,
redirect_root_to_web_client=True,
)
- hs.start_listening(config.bind_port, config.unsecure_port)
+
+ db_name = hs.get_db_name()
+
+ logging.info("Preparing database: %s...", db_name)
+
+ with sqlite3.connect(db_name) as db_conn:
+ prepare_database(db_conn)
+
+ logging.info("Database prepared in %s.", db_name)
hs.get_db_pool()
@@ -279,12 +227,14 @@ def setup():
f.namespace['hs'] = hs
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
+ hs.start_listening(config.bind_port, config.unsecure_port)
+
if config.daemonize:
print config.pid_file
daemon = Daemonize(
app="synapse-homeserver",
pid=config.pid_file,
- action=run,
+ action=reactor.run,
auto_close_fds=False,
verbose=True,
logger=logger,
@@ -292,7 +242,7 @@ def setup():
daemon.start()
else:
- run()
+ reactor.run()
if __name__ == '__main__':
diff --git a/synapse/server.py b/synapse/server.py
index 7c185537aa..cdea49e6ab 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -58,6 +58,7 @@ class BaseHomeServer(object):
DEPENDENCIES = [
'clock',
'http_client',
+ 'db_name',
'db_pool',
'persistence_service',
'replication_layer',
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 1cede2809d..66658f6721 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -47,6 +47,23 @@ import os
logger = logging.getLogger(__name__)
+
+SCHEMAS = [
+ "transactions",
+ "pdu",
+ "users",
+ "profiles",
+ "presence",
+ "im",
+ "room_aliases",
+]
+
+
+# Remember to update this number every time an incompatible change is made to
+# database schema files, so the users will be informed on server restarts.
+SCHEMA_VERSION = 3
+
+
class _RollbackButIsFineException(Exception):
""" This exception is used to rollback a transaction without implying
something went wrong.
@@ -78,7 +95,7 @@ class DataStore(RoomMemberStore, RoomStore,
stream_ordering = self.min_token
try:
- yield self._db_pool.runInteraction(
+ yield self.runInteraction(
self._persist_pdu_event_txn,
pdu=pdu,
event=event,
@@ -291,7 +308,7 @@ class DataStore(RoomMemberStore, RoomStore,
prev_state_pdu=prev_state_pdu,
)
- return self._db_pool.runInteraction(_snapshot)
+ return self.runInteraction(_snapshot)
class Snapshot(object):
@@ -361,3 +378,42 @@ def read_schema(schema):
"""
with open(schema_path(schema)) as schema_file:
return schema_file.read()
+
+
+def prepare_database(db_conn):
+ """ Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
+ don't have to worry about overwriting existing content.
+ """
+ c = db_conn.cursor()
+ c.execute("PRAGMA user_version")
+ row = c.fetchone()
+
+ if row and row[0]:
+ user_version = row[0]
+
+ if user_version > SCHEMA_VERSION:
+ raise ValueError("Cannot use this database as it is too " +
+ "new for the server to understand"
+ )
+ elif user_version < SCHEMA_VERSION:
+ logging.info("Upgrading database from version %d",
+ user_version
+ )
+
+ # Run every version since after the current version.
+ for v in range(user_version + 1, SCHEMA_VERSION + 1):
+ sql_script = read_schema("delta/v%d" % (v))
+ c.executescript(sql_script)
+
+ db_conn.commit()
+
+ else:
+ for sql_loc in SCHEMAS:
+ sql_script = read_schema(sql_loc)
+
+ c.executescript(sql_script)
+ db_conn.commit()
+ c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
+
+ c.close()
+
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index cf88bfc22b..76ed7d06fb 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -26,6 +26,44 @@ import json
logger = logging.getLogger(__name__)
+sql_logger = logging.getLogger("synapse.storage.SQL")
+
+
+class LoggingTransaction(object):
+ """An object that almost-transparently proxies for the 'txn' object
+ passed to the constructor. Adds logging to the .execute() method."""
+ __slots__ = ["txn"]
+
+ def __init__(self, txn):
+ object.__setattr__(self, "txn", txn)
+
+ def __getattribute__(self, name):
+ if name == "execute":
+ return object.__getattribute__(self, "execute")
+
+ return getattr(object.__getattribute__(self, "txn"), name)
+
+ def __setattr__(self, name, value):
+ setattr(object.__getattribute__(self, "txn"), name, value)
+
+ def execute(self, sql, *args, **kwargs):
+ # TODO(paul): Maybe use 'info' and 'debug' for values?
+ sql_logger.debug("[SQL] %s", sql)
+ try:
+ if args and args[0]:
+ values = args[0]
+ sql_logger.debug("[SQL values] " +
+ ", ".join(("<%s>",) * len(values)), *values)
+ except:
+ # Don't let logging failures stop SQL from working
+ pass
+
+ # TODO(paul): Here would be an excellent place to put some timing
+ # measurements, and log (warning?) slow queries.
+ return object.__getattribute__(self, "txn").execute(
+ sql, *args, **kwargs
+ )
+
class SQLBaseStore(object):
@@ -35,6 +73,13 @@ class SQLBaseStore(object):
self.event_factory = hs.get_event_factory()
self._clock = hs.get_clock()
+ def runInteraction(self, func, *args, **kwargs):
+ """Wraps the .runInteraction() method on the underlying db_pool."""
+ def inner_func(txn, *args, **kwargs):
+ return func(LoggingTransaction(txn), *args, **kwargs)
+
+ return self._db_pool.runInteraction(inner_func, *args, **kwargs)
+
def cursor_to_dict(self, cursor):
"""Converts a SQL cursor into an list of dicts.
@@ -60,11 +105,6 @@ class SQLBaseStore(object):
Returns:
The result of decoder(results)
"""
- logger.debug(
- "[SQL] %s Args=%s Func=%s",
- query, args, decoder.__name__ if decoder else None
- )
-
def interaction(txn):
cursor = txn.execute(query, args)
if decoder:
@@ -72,7 +112,7 @@ class SQLBaseStore(object):
else:
return cursor.fetchall()
- return self._db_pool.runInteraction(interaction)
+ return self.runInteraction(interaction)
def _execute_and_decode(self, query, *args):
return self._execute(self.cursor_to_dict, query, *args)
@@ -88,7 +128,7 @@ class SQLBaseStore(object):
values : dict of new column names and values for them
or_replace : bool; if True performs an INSERT OR REPLACE
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._simple_insert_txn, table, values, or_replace=or_replace
)
@@ -172,7 +212,7 @@ class SQLBaseStore(object):
txn.execute(sql, keyvalues.values())
return txn.fetchall()
- res = yield self._db_pool.runInteraction(func)
+ res = yield self.runInteraction(func)
defer.returnValue([r[0] for r in res])
@@ -195,7 +235,7 @@ class SQLBaseStore(object):
txn.execute(sql, keyvalues.values())
return self.cursor_to_dict(txn)
- return self._db_pool.runInteraction(func)
+ return self.runInteraction(func)
def _simple_update_one(self, table, keyvalues, updatevalues,
retcols=None):
@@ -263,7 +303,7 @@ class SQLBaseStore(object):
raise StoreError(500, "More than one row matched")
return ret
- return self._db_pool.runInteraction(func)
+ return self.runInteraction(func)
def _simple_delete_one(self, table, keyvalues):
"""Executes a DELETE query on the named table, expecting to delete a
@@ -284,7 +324,7 @@ class SQLBaseStore(object):
raise StoreError(404, "No row found")
if txn.rowcount > 1:
raise StoreError(500, "more than one row matched")
- return self._db_pool.runInteraction(func)
+ return self.runInteraction(func)
def _simple_max_id(self, table):
"""Executes a SELECT query on the named table, expecting to return the
@@ -302,7 +342,7 @@ class SQLBaseStore(object):
return 0
return max_id
- return self._db_pool.runInteraction(func)
+ return self.runInteraction(func)
def _parse_event_from_row(self, row_dict):
d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
@@ -325,7 +365,7 @@ class SQLBaseStore(object):
)
def _parse_events(self, rows):
- return self._db_pool.runInteraction(self._parse_events_txn, rows)
+ return self.runInteraction(self._parse_events_txn, rows)
def _parse_events_txn(self, txn, rows):
events = [self._parse_event_from_row(r) for r in rows]
diff --git a/synapse/storage/pdu.py b/synapse/storage/pdu.py
index 3c859fdeac..d70467dcd6 100644
--- a/synapse/storage/pdu.py
+++ b/synapse/storage/pdu.py
@@ -43,7 +43,7 @@ class PduStore(SQLBaseStore):
PduTuple: If the pdu does not exist in the database, returns None
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_pdu_tuple, pdu_id, origin
)
@@ -95,7 +95,7 @@ class PduStore(SQLBaseStore):
list: A list of PduTuples
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_current_state_for_context,
context
)
@@ -143,7 +143,7 @@ class PduStore(SQLBaseStore):
pdu_origin (str)
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._mark_as_processed, pdu_id, pdu_origin
)
@@ -152,7 +152,7 @@ class PduStore(SQLBaseStore):
def get_all_pdus_from_context(self, context):
"""Get a list of all PDUs for a given context."""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_all_pdus_from_context, context,
)
@@ -179,7 +179,7 @@ class PduStore(SQLBaseStore):
Return:
list: A list of PduTuples
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_backfill, context, pdu_list, limit
)
@@ -240,7 +240,7 @@ class PduStore(SQLBaseStore):
txn
context (str)
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_min_depth_for_context, context
)
@@ -346,7 +346,7 @@ class PduStore(SQLBaseStore):
bool
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._is_pdu_new,
pdu_id=pdu_id,
origin=origin,
@@ -499,7 +499,7 @@ class StatePduStore(SQLBaseStore):
)
def get_unresolved_state_tree(self, new_state_pdu):
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_unresolved_state_tree, new_state_pdu
)
@@ -538,7 +538,7 @@ class StatePduStore(SQLBaseStore):
def update_current_state(self, pdu_id, origin, context, pdu_type,
state_key):
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._update_current_state,
pdu_id, origin, context, pdu_type, state_key
)
@@ -577,7 +577,7 @@ class StatePduStore(SQLBaseStore):
PduEntry
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_current_state_pdu, context, pdu_type, state_key
)
@@ -636,7 +636,7 @@ class StatePduStore(SQLBaseStore):
Returns:
bool: True if the new_pdu clobbered the current state, False if not
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._handle_new_state, new_pdu
)
diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py
index fd762bc643..db20b1daa0 100644
--- a/synapse/storage/registration.py
+++ b/synapse/storage/registration.py
@@ -62,7 +62,7 @@ class RegistrationStore(SQLBaseStore):
Raises:
StoreError if the user_id could not be registered.
"""
- yield self._db_pool.runInteraction(self._register, user_id, token,
+ yield self.runInteraction(self._register, user_id, token,
password_hash)
def _register(self, txn, user_id, token, password_hash):
@@ -99,7 +99,7 @@ class RegistrationStore(SQLBaseStore):
Raises:
StoreError if no user was found.
"""
- user_id = yield self._db_pool.runInteraction(self._query_for_auth,
+ user_id = yield self.runInteraction(self._query_for_auth,
token)
defer.returnValue(user_id)
diff --git a/synapse/storage/room.py b/synapse/storage/room.py
index 017169ce00..5adf8cdf1b 100644
--- a/synapse/storage/room.py
+++ b/synapse/storage/room.py
@@ -149,7 +149,7 @@ class RoomStore(SQLBaseStore):
defer.returnValue(None)
def get_power_level(self, room_id, user_id):
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_power_level,
room_id, user_id,
)
@@ -182,7 +182,7 @@ class RoomStore(SQLBaseStore):
return None
def get_ops_levels(self, room_id):
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_ops_levels,
room_id,
)
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index 676b2f2653..04b4067d03 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -149,7 +149,7 @@ class RoomMemberStore(SQLBaseStore):
membership_list (list): A list of synapse.api.constants.Membership
values which the user must be in.
Returns:
- A list of dicts with "room_id" and "membership" keys.
+ A list of RoomMemberEvent objects
"""
if not membership_list:
return defer.succeed(None)
@@ -198,10 +198,11 @@ class RoomMemberStore(SQLBaseStore):
return results
@defer.inlineCallbacks
- def user_rooms_intersect(self, user_list):
- """ Checks whether a list of users share a room.
+ def user_rooms_intersect(self, user_id_list):
+ """ Checks whether all the users whose IDs are given in a list share a
+ room.
"""
- user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
+ user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_id_list))
sql = (
"SELECT m.room_id FROM room_memberships as m "
"INNER JOIN current_state_events as c "
@@ -211,8 +212,8 @@ class RoomMemberStore(SQLBaseStore):
"GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
) % {"clause": user_list_clause}
- args = user_list
- args.append(len(user_list))
+ args = list(user_id_list)
+ args.append(len(user_id_list))
rows = yield self._execute(None, sql, *args)
diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py
index aff6dc9855..8c766b8a00 100644
--- a/synapse/storage/stream.py
+++ b/synapse/storage/stream.py
@@ -286,7 +286,7 @@ class StreamStore(SQLBaseStore):
defer.returnValue(ret)
def get_room_events_max_id(self):
- return self._db_pool.runInteraction(self._get_room_events_max_id_txn)
+ return self.runInteraction(self._get_room_events_max_id_txn)
def _get_room_events_max_id_txn(self, txn):
txn.execute(
diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py
index 7467e1035b..ab4599b468 100644
--- a/synapse/storage/transactions.py
+++ b/synapse/storage/transactions.py
@@ -41,7 +41,7 @@ class TransactionStore(SQLBaseStore):
this transaction or a 2-tuple of (int, dict)
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_received_txn_response, transaction_id, origin
)
@@ -72,7 +72,7 @@ class TransactionStore(SQLBaseStore):
response_json (str)
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._set_received_txn_response,
transaction_id, origin, code, response_dict
)
@@ -104,7 +104,7 @@ class TransactionStore(SQLBaseStore):
list: A list of previous transaction ids.
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._prep_send_transaction,
transaction_id, destination, ts, pdu_list
)
@@ -159,7 +159,7 @@ class TransactionStore(SQLBaseStore):
code (int)
response_json (str)
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._delivered_txn,
transaction_id, destination, code, response_dict
)
@@ -184,7 +184,7 @@ class TransactionStore(SQLBaseStore):
Returns:
list: A list of `ReceivedTransactionsTable.EntryType`
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_transactions_after, transaction_id, destination
)
@@ -214,7 +214,7 @@ class TransactionStore(SQLBaseStore):
Returns
list: A list of PduTuple
"""
- return self._db_pool.runInteraction(
+ return self.runInteraction(
self._get_pdus_after_transaction,
transaction_id, destination
)
diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py
index 54d6e51f97..dd5d85dde6 100644
--- a/tests/handlers/test_directory.py
+++ b/tests/handlers/test_directory.py
@@ -24,6 +24,8 @@ from synapse.http.client import HttpClient
from synapse.handlers.directory import DirectoryHandler
from synapse.storage.directory import RoomAliasMapping
+from tests.utils import SQLiteMemoryDbPool
+
class DirectoryHandlers(object):
def __init__(self, hs):
@@ -33,6 +35,7 @@ class DirectoryHandlers(object):
class DirectoryTestCase(unittest.TestCase):
""" Tests the directory service. """
+ @defer.inlineCallbacks
def setUp(self):
self.mock_federation = Mock(spec=[
"make_query",
@@ -43,11 +46,11 @@ class DirectoryTestCase(unittest.TestCase):
self.query_handlers[query_type] = handler
self.mock_federation.register_query_handler = register_query_handler
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
hs = HomeServer("test",
- datastore=Mock(spec=[
- "get_association_from_room_alias",
- "get_joined_hosts_for_room",
- ]),
+ db_pool=db_pool,
http_client=None,
resource_for_federation=Mock(),
replication_layer=self.mock_federation,
@@ -56,20 +59,16 @@ class DirectoryTestCase(unittest.TestCase):
self.handler = hs.get_handlers().directory_handler
- self.datastore = hs.get_datastore()
-
- def hosts(room_id):
- return defer.succeed([])
- self.datastore.get_joined_hosts_for_room.side_effect = hosts
+ self.store = hs.get_datastore()
self.my_room = hs.parse_roomalias("#my-room:test")
+ self.your_room = hs.parse_roomalias("#your-room:test")
self.remote_room = hs.parse_roomalias("#another:remote")
@defer.inlineCallbacks
def test_get_local_association(self):
- mocked_get = self.datastore.get_association_from_room_alias
- mocked_get.return_value = defer.succeed(
- RoomAliasMapping("!8765qwer:test", "#my-room:test", ["test"])
+ yield self.store.create_room_alias_association(
+ self.my_room, "!8765qwer:test", ["test"]
)
result = yield self.handler.get_association(self.my_room)
@@ -102,9 +101,8 @@ class DirectoryTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_incoming_fed_query(self):
- mocked_get = self.datastore.get_association_from_room_alias
- mocked_get.return_value = defer.succeed(
- RoomAliasMapping("!8765asdf:test", "#your-room:test", ["test"])
+ yield self.store.create_room_alias_association(
+ self.your_room, "!8765asdf:test", ["test"]
)
response = yield self.query_handlers["directory"](
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 0cb4dfba39..765929d204 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -20,7 +20,9 @@ from twisted.internet import defer, reactor
from mock import Mock, call, ANY
import json
-from ..utils import MockHttpResource, MockClock, DeferredMockCallable
+from tests.utils import (
+ MockHttpResource, MockClock, DeferredMockCallable, SQLiteMemoryDbPool
+)
from synapse.server import HomeServer
from synapse.api.constants import PresenceState
@@ -60,30 +62,21 @@ class JustPresenceHandlers(object):
class PresenceStateTestCase(unittest.TestCase):
""" Tests presence management. """
+ @defer.inlineCallbacks
def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
hs = HomeServer("test",
- clock=MockClock(),
- db_pool=None,
- datastore=Mock(spec=[
- "get_presence_state",
- "set_presence_state",
- "add_presence_list_pending",
- "set_presence_list_accepted",
- ]),
- handlers=None,
- resource_for_federation=Mock(),
- http_client=None,
- )
+ clock=MockClock(),
+ db_pool=db_pool,
+ handlers=None,
+ resource_for_federation=Mock(),
+ http_client=None,
+ )
hs.handlers = JustPresenceHandlers(hs)
- self.datastore = hs.get_datastore()
-
- def is_presence_visible(observed_localpart, observer_userid):
- allow = (observed_localpart == "apple" and
- observer_userid == "@banana:test"
- )
- return defer.succeed(allow)
- self.datastore.is_presence_visible = is_presence_visible
+ self.store = hs.get_datastore()
# Mock the RoomMemberHandler
room_member_handler = Mock(spec=[])
@@ -94,6 +87,11 @@ class PresenceStateTestCase(unittest.TestCase):
self.u_banana = hs.parse_userid("@banana:test")
self.u_clementine = hs.parse_userid("@clementine:test")
+ yield self.store.create_presence(self.u_apple.localpart)
+ yield self.store.set_presence_state(
+ self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
+ )
+
self.handler = hs.get_handlers().presence_handler
self.room_members = []
@@ -117,7 +115,7 @@ class PresenceStateTestCase(unittest.TestCase):
shared = all(map(lambda i: i in room_member_ids, userlist))
return defer.succeed(shared)
- self.datastore.user_rooms_intersect = user_rooms_intersect
+ self.store.user_rooms_intersect = user_rooms_intersect
self.mock_start = Mock()
self.mock_stop = Mock()
@@ -127,11 +125,6 @@ class PresenceStateTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_my_state(self):
- mocked_get = self.datastore.get_presence_state
- mocked_get.return_value = defer.succeed(
- {"state": ONLINE, "status_msg": "Online"}
- )
-
state = yield self.handler.get_state(
target_user=self.u_apple, auth_user=self.u_apple
)
@@ -140,13 +133,12 @@ class PresenceStateTestCase(unittest.TestCase):
{"presence": ONLINE, "status_msg": "Online"},
state
)
- mocked_get.assert_called_with("apple")
@defer.inlineCallbacks
def test_get_allowed_state(self):
- mocked_get = self.datastore.get_presence_state
- mocked_get.return_value = defer.succeed(
- {"state": ONLINE, "status_msg": "Online"}
+ yield self.store.allow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
)
state = yield self.handler.get_state(
@@ -157,15 +149,9 @@ class PresenceStateTestCase(unittest.TestCase):
{"presence": ONLINE, "status_msg": "Online"},
state
)
- mocked_get.assert_called_with("apple")
@defer.inlineCallbacks
def test_get_same_room_state(self):
- mocked_get = self.datastore.get_presence_state
- mocked_get.return_value = defer.succeed(
- {"state": ONLINE, "status_msg": "Online"}
- )
-
self.room_members = [self.u_apple, self.u_clementine]
state = yield self.handler.get_state(
@@ -179,11 +165,6 @@ class PresenceStateTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_get_disallowed_state(self):
- mocked_get = self.datastore.get_presence_state
- mocked_get.return_value = defer.succeed(
- {"state": ONLINE, "status_msg": "Online"}
- )
-
self.room_members = []
yield self.assertFailure(
@@ -195,16 +176,17 @@ class PresenceStateTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_set_my_state(self):
- mocked_set = self.datastore.set_presence_state
- mocked_set.return_value = defer.succeed({"state": OFFLINE})
-
yield self.handler.set_state(
target_user=self.u_apple, auth_user=self.u_apple,
state={"presence": UNAVAILABLE, "status_msg": "Away"})
- mocked_set.assert_called_with("apple",
- {"state": UNAVAILABLE, "status_msg": "Away"}
+ self.assertEquals(
+ {"state": UNAVAILABLE,
+ "status_msg": "Away",
+ "mtime": 1000000},
+ (yield self.store.get_presence_state(self.u_apple.localpart))
)
+
self.mock_start.assert_called_with(self.u_apple,
state={
"presence": UNAVAILABLE,
@@ -222,50 +204,34 @@ class PresenceStateTestCase(unittest.TestCase):
class PresenceInvitesTestCase(unittest.TestCase):
""" Tests presence management. """
+ @defer.inlineCallbacks
def setUp(self):
self.mock_http_client = Mock(spec=[])
self.mock_http_client.put_json = DeferredMockCallable()
self.mock_federation_resource = MockHttpResource()
- hs = HomeServer("test",
- clock=MockClock(),
- db_pool=None,
- datastore=Mock(spec=[
- "has_presence_state",
- "allow_presence_visible",
- "add_presence_list_pending",
- "set_presence_list_accepted",
- "get_presence_list",
- "del_presence_list",
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
- # Bits that Federation needs
- "prep_send_transaction",
- "delivered_txn",
- "get_received_txn_response",
- "set_received_txn_response",
- ]),
- handlers=None,
- resource_for_client=Mock(),
- resource_for_federation=self.mock_federation_resource,
- http_client=self.mock_http_client,
- )
+ hs = HomeServer("test",
+ clock=MockClock(),
+ db_pool=db_pool,
+ handlers=None,
+ resource_for_client=Mock(),
+ resource_for_federation=self.mock_federation_resource,
+ http_client=self.mock_http_client,
+ )
hs.handlers = JustPresenceHandlers(hs)
- self.datastore = hs.get_datastore()
-
- def has_presence_state(user_localpart):
- return defer.succeed(
- user_localpart in ("apple", "banana"))
- self.datastore.has_presence_state = has_presence_state
-
- def get_received_txn_response(*args):
- return defer.succeed(None)
- self.datastore.get_received_txn_response = get_received_txn_response
+ self.store = hs.get_datastore()
# Some local users to test with
self.u_apple = hs.parse_userid("@apple:test")
self.u_banana = hs.parse_userid("@banana:test")
+ yield self.store.create_presence(self.u_apple.localpart)
+ yield self.store.create_presence(self.u_banana.localpart)
+
# ID of a local user that does not exist
self.u_durian = hs.parse_userid("@durian:test")
@@ -288,12 +254,16 @@ class PresenceInvitesTestCase(unittest.TestCase):
yield self.handler.send_invite(
observer_user=self.u_apple, observed_user=self.u_banana)
- self.datastore.add_presence_list_pending.assert_called_with(
- "apple", "@banana:test")
- self.datastore.allow_presence_visible.assert_called_with(
- "banana", "@apple:test")
- self.datastore.set_presence_list_accepted.assert_called_with(
- "apple", "@banana:test")
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
+ self.assertTrue(
+ (yield self.store.is_presence_visible(
+ observed_localpart=self.u_banana.localpart,
+ observer_userid=self.u_apple.to_string(),
+ ))
+ )
self.mock_start.assert_called_with(
self.u_apple, target_user=self.u_banana)
@@ -303,10 +273,10 @@ class PresenceInvitesTestCase(unittest.TestCase):
yield self.handler.send_invite(
observer_user=self.u_apple, observed_user=self.u_durian)
- self.datastore.add_presence_list_pending.assert_called_with(
- "apple", "@durian:test")
- self.datastore.del_presence_list.assert_called_with(
- "apple", "@durian:test")
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
@defer.inlineCallbacks
def test_invite_remote(self):
@@ -328,8 +298,10 @@ class PresenceInvitesTestCase(unittest.TestCase):
yield self.handler.send_invite(
observer_user=self.u_apple, observed_user=self.u_cabbage)
- self.datastore.add_presence_list_pending.assert_called_with(
- "apple", "@cabbage:elsewhere")
+ self.assertEquals(
+ [{"observed_user_id": "@cabbage:elsewhere", "accepted": 0}],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
yield put_json.await_calls()
@@ -362,8 +334,12 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
)
- self.datastore.allow_presence_visible.assert_called_with(
- "apple", "@cabbage:elsewhere")
+ self.assertTrue(
+ (yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_cabbage.to_string(),
+ ))
+ )
yield put_json.await_calls()
@@ -398,6 +374,11 @@ class PresenceInvitesTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_accepted_remote(self):
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_accept",
@@ -408,14 +389,21 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
)
- self.datastore.set_presence_list_accepted.assert_called_with(
- "apple", "@cabbage:elsewhere")
+ self.assertEquals(
+ [{"observed_user_id": "@cabbage:elsewhere", "accepted": 1}],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
self.mock_start.assert_called_with(
self.u_apple, target_user=self.u_cabbage)
@defer.inlineCallbacks
def test_denied_remote(self):
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid="@eggplant:elsewhere",
+ )
+
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence_deny",
@@ -426,62 +414,76 @@ class PresenceInvitesTestCase(unittest.TestCase):
)
)
- self.datastore.del_presence_list.assert_called_with(
- "apple", "@eggplant:elsewhere")
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
@defer.inlineCallbacks
def test_drop_local(self):
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+ yield self.store.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
yield self.handler.drop(
- observer_user=self.u_apple, observed_user=self.u_banana)
+ observer_user=self.u_apple,
+ observed_user=self.u_banana,
+ )
- self.datastore.del_presence_list.assert_called_with(
- "apple", "@banana:test")
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
self.mock_stop.assert_called_with(
self.u_apple, target_user=self.u_banana)
@defer.inlineCallbacks
def test_drop_remote(self):
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+ yield self.store.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_cabbage.to_string(),
+ )
+
yield self.handler.drop(
- observer_user=self.u_apple, observed_user=self.u_cabbage)
+ observer_user=self.u_apple,
+ observed_user=self.u_cabbage,
+ )
- self.datastore.del_presence_list.assert_called_with(
- "apple", "@cabbage:elsewhere")
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(self.u_apple.localpart))
+ )
@defer.inlineCallbacks
def test_get_presence_list(self):
- self.datastore.get_presence_list.return_value = defer.succeed(
- [{"observed_user_id": "@banana:test"}]
- )
-
- presence = yield self.handler.get_presence_list(
- observer_user=self.u_apple)
-
- self.assertEquals([
- {"observed_user": self.u_banana,
- "presence": OFFLINE},
- ], presence)
-
- self.datastore.get_presence_list.assert_called_with("apple",
- accepted=None
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
)
-
- self.datastore.get_presence_list.return_value = defer.succeed(
- [{"observed_user_id": "@banana:test"}]
+ yield self.store.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
)
presence = yield self.handler.get_presence_list(
- observer_user=self.u_apple, accepted=True
- )
+ observer_user=self.u_apple)
self.assertEquals([
{"observed_user": self.u_banana,
- "presence": OFFLINE},
+ "presence": OFFLINE,
+ "accepted": 1},
], presence)
- self.datastore.get_presence_list.assert_called_with("apple",
- accepted=True)
-
class PresencePushTestCase(unittest.TestCase):
""" Tests steady-state presence status updates.
diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py
index ee2be9b6d5..5dc9b456e1 100644
--- a/tests/handlers/test_profile.py
+++ b/tests/handlers/test_profile.py
@@ -24,6 +24,8 @@ from synapse.server import HomeServer
from synapse.handlers.profile import ProfileHandler
from synapse.api.constants import Membership
+from tests.utils import SQLiteMemoryDbPool
+
class ProfileHandlers(object):
def __init__(self, hs):
@@ -33,6 +35,7 @@ class ProfileHandlers(object):
class ProfileTestCase(unittest.TestCase):
""" Tests profile management. """
+ @defer.inlineCallbacks
def setUp(self):
self.mock_federation = Mock(spec=[
"make_query",
@@ -43,63 +46,50 @@ class ProfileTestCase(unittest.TestCase):
self.query_handlers[query_type] = handler
self.mock_federation.register_query_handler = register_query_handler
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
hs = HomeServer("test",
- db_pool=None,
+ db_pool=db_pool,
http_client=None,
- datastore=Mock(spec=[
- "get_profile_displayname",
- "set_profile_displayname",
- "get_profile_avatar_url",
- "set_profile_avatar_url",
- "get_rooms_for_user_where_membership_is",
- ]),
handlers=None,
resource_for_federation=Mock(),
replication_layer=self.mock_federation,
)
hs.handlers = ProfileHandlers(hs)
- self.datastore = hs.get_datastore()
+ self.store = hs.get_datastore()
self.frank = hs.parse_userid("@1234ABCD:test")
self.bob = hs.parse_userid("@4567:test")
self.alice = hs.parse_userid("@alice:remote")
- self.handler = hs.get_handlers().profile_handler
+ yield self.store.create_profile(self.frank.localpart)
- self.mock_get_joined = (
- self.datastore.get_rooms_for_user_where_membership_is
- )
+ self.handler = hs.get_handlers().profile_handler
# TODO(paul): Icky signal declarings.. booo
hs.get_distributor().declare("changed_presencelike_data")
@defer.inlineCallbacks
def test_get_my_name(self):
- mocked_get = self.datastore.get_profile_displayname
- mocked_get.return_value = defer.succeed("Frank")
+ yield self.store.set_profile_displayname(
+ self.frank.localpart, "Frank"
+ )
displayname = yield self.handler.get_displayname(self.frank)
self.assertEquals("Frank", displayname)
- mocked_get.assert_called_with("1234ABCD")
@defer.inlineCallbacks
def test_set_my_name(self):
- mocked_set = self.datastore.set_profile_displayname
- mocked_set.return_value = defer.succeed(())
-
- self.mock_get_joined.return_value = defer.succeed([])
-
yield self.handler.set_displayname(self.frank, self.frank, "Frank Jr.")
- self.mock_get_joined.assert_called_once_with(
- self.frank.to_string(),
- [Membership.JOIN]
+ self.assertEquals(
+ (yield self.store.get_profile_displayname(self.frank.localpart)),
+ "Frank Jr."
)
- mocked_set.assert_called_with("1234ABCD", "Frank Jr.")
-
@defer.inlineCallbacks
def test_set_my_name_noauth(self):
d = self.handler.set_displayname(self.frank, self.bob, "Frank Jr.")
@@ -123,40 +113,31 @@ class ProfileTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_incoming_fed_query(self):
- mocked_get = self.datastore.get_profile_displayname
- mocked_get.return_value = defer.succeed("Caroline")
+ yield self.store.create_profile("caroline")
+ yield self.store.set_profile_displayname("caroline", "Caroline")
response = yield self.query_handlers["profile"](
{"user_id": "@caroline:test", "field": "displayname"}
)
self.assertEquals({"displayname": "Caroline"}, response)
- mocked_get.assert_called_with("caroline")
@defer.inlineCallbacks
def test_get_my_avatar(self):
- mocked_get = self.datastore.get_profile_avatar_url
- mocked_get.return_value = defer.succeed("http://my.server/me.png")
+ yield self.store.set_profile_avatar_url(
+ self.frank.localpart, "http://my.server/me.png"
+ )
avatar_url = yield self.handler.get_avatar_url(self.frank)
self.assertEquals("http://my.server/me.png", avatar_url)
- mocked_get.assert_called_with("1234ABCD")
@defer.inlineCallbacks
def test_set_my_avatar(self):
- mocked_set = self.datastore.set_profile_avatar_url
- mocked_set.return_value = defer.succeed(())
-
- self.mock_get_joined.return_value = defer.succeed([])
-
yield self.handler.set_avatar_url(self.frank, self.frank,
"http://my.server/pic.gif")
- self.mock_get_joined.assert_called_once_with(
- self.frank.to_string(),
- [Membership.JOIN]
+ self.assertEquals(
+ (yield self.store.get_profile_avatar_url(self.frank.localpart)),
+ "http://my.server/pic.gif"
)
-
-
- mocked_set.assert_called_with("1234ABCD", "http://my.server/pic.gif")
diff --git a/tests/storage/TESTS_NEEDED_FOR b/tests/storage/TESTS_NEEDED_FOR
new file mode 100644
index 0000000000..8e5d0cbdc4
--- /dev/null
+++ b/tests/storage/TESTS_NEEDED_FOR
@@ -0,0 +1,5 @@
+synapse/storage/feedback.py
+synapse/storage/keys.py
+synapse/storage/pdu.py
+synapse/storage/stream.py
+synapse/storage/transactions.py
diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py
new file mode 100644
index 0000000000..7e8e7e1e83
--- /dev/null
+++ b/tests/storage/test_directory.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.storage.directory import DirectoryStore
+
+from tests.utils import SQLiteMemoryDbPool
+
+
+class DirectoryStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ self.store = DirectoryStore(hs)
+
+ self.room = hs.parse_roomid("!abcde:test")
+ self.alias = hs.parse_roomalias("#my-room:test")
+
+ @defer.inlineCallbacks
+ def test_room_to_alias(self):
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"],
+ )
+
+ self.assertEquals(
+ ["#my-room:test"],
+ (yield self.store.get_aliases_for_room(self.room.to_string()))
+ )
+
+ @defer.inlineCallbacks
+ def test_alias_to_room(self):
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"],
+ )
+
+
+ self.assertObjectHasAttributes(
+ {"room_id": self.room.to_string(),
+ "servers": ["test"]},
+ (yield self.store.get_association_from_room_alias(self.alias))
+ )
diff --git a/tests/storage/test_presence.py b/tests/storage/test_presence.py
new file mode 100644
index 0000000000..9655d3cf42
--- /dev/null
+++ b/tests/storage/test_presence.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.storage.presence import PresenceStore
+
+from tests.utils import SQLiteMemoryDbPool, MockClock
+
+
+class PresenceStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ clock=MockClock(),
+ db_pool=db_pool,
+ )
+
+ self.store = PresenceStore(hs)
+
+ self.u_apple = hs.parse_userid("@apple:test")
+ self.u_banana = hs.parse_userid("@banana:test")
+
+ @defer.inlineCallbacks
+ def test_state(self):
+ yield self.store.create_presence(
+ self.u_apple.localpart
+ )
+
+ state = yield self.store.get_presence_state(
+ self.u_apple.localpart
+ )
+
+ self.assertEquals(
+ {"state": None, "status_msg": None, "mtime": None}, state
+ )
+
+ yield self.store.set_presence_state(
+ self.u_apple.localpart, {"state": "online", "status_msg": "Here"}
+ )
+
+ state = yield self.store.get_presence_state(
+ self.u_apple.localpart
+ )
+
+ self.assertEquals(
+ {"state": "online", "status_msg": "Here", "mtime": 1000000}, state
+ )
+
+ @defer.inlineCallbacks
+ def test_visibility(self):
+ self.assertFalse((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ yield self.store.allow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )
+
+ self.assertTrue((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ yield self.store.disallow_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )
+
+ self.assertFalse((yield self.store.is_presence_visible(
+ observed_localpart=self.u_apple.localpart,
+ observer_userid=self.u_banana.to_string(),
+ )))
+
+ @defer.inlineCallbacks
+ def test_presence_list(self):
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.add_presence_list_pending(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 0}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.set_presence_list_accepted(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [{"observed_user_id": "@banana:test", "accepted": 1}],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
+
+ yield self.store.del_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ observed_userid=self.u_banana.to_string(),
+ )
+
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ ))
+ )
+ self.assertEquals(
+ [],
+ (yield self.store.get_presence_list(
+ observer_localpart=self.u_apple.localpart,
+ accepted=True,
+ ))
+ )
diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py
new file mode 100644
index 0000000000..5d36723c28
--- /dev/null
+++ b/tests/storage/test_profile.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.storage.profile import ProfileStore
+
+from tests.utils import SQLiteMemoryDbPool
+
+
+class ProfileStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ self.store = ProfileStore(hs)
+
+ self.u_frank = hs.parse_userid("@frank:test")
+
+ @defer.inlineCallbacks
+ def test_displayname(self):
+ yield self.store.create_profile(
+ self.u_frank.localpart
+ )
+
+ yield self.store.set_profile_displayname(
+ self.u_frank.localpart, "Frank"
+ )
+
+ self.assertEquals(
+ "Frank",
+ (yield self.store.get_profile_displayname(self.u_frank.localpart))
+ )
+
+ @defer.inlineCallbacks
+ def test_avatar_url(self):
+ yield self.store.create_profile(
+ self.u_frank.localpart
+ )
+
+ yield self.store.set_profile_avatar_url(
+ self.u_frank.localpart, "http://my.site/here"
+ )
+
+ self.assertEquals(
+ "http://my.site/here",
+ (yield self.store.get_profile_avatar_url(self.u_frank.localpart))
+ )
diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py
new file mode 100644
index 0000000000..91e221d53e
--- /dev/null
+++ b/tests/storage/test_registration.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.storage.registration import RegistrationStore
+
+from tests.utils import SQLiteMemoryDbPool
+
+
+class RegistrationStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ self.store = RegistrationStore(hs)
+
+ self.user_id = "@my-user:test"
+ self.tokens = ["AbCdEfGhIjKlMnOpQrStUvWxYz",
+ "BcDeFgHiJkLmNoPqRsTuVwXyZa"]
+ self.pwhash = "{xx1}123456789"
+
+ @defer.inlineCallbacks
+ def test_register(self):
+ yield self.store.register(self.user_id, self.tokens[0], self.pwhash)
+
+ self.assertEquals(
+ # TODO(paul): Surely this field should be 'user_id', not 'name'
+ # Additionally surely it shouldn't come in a 1-element list
+ [{"name": self.user_id, "password_hash": self.pwhash}],
+ (yield self.store.get_user_by_id(self.user_id))
+ )
+
+ self.assertEquals(
+ self.user_id,
+ (yield self.store.get_user_by_token(self.tokens[0]))
+ )
+
+ @defer.inlineCallbacks
+ def test_add_tokens(self):
+ yield self.store.register(self.user_id, self.tokens[0], self.pwhash)
+ yield self.store.add_access_token_to_user(self.user_id, self.tokens[1])
+
+ self.assertEquals(
+ self.user_id,
+ (yield self.store.get_user_by_token(self.tokens[1]))
+ )
+
diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py
new file mode 100644
index 0000000000..369a73d917
--- /dev/null
+++ b/tests/storage/test_room.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.api.events.room import (
+ RoomNameEvent, RoomTopicEvent
+)
+
+from tests.utils import SQLiteMemoryDbPool
+
+
+class RoomStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ # We can't test RoomStore on its own without the DirectoryStore, for
+ # management of the 'room_aliases' table
+ self.store = hs.get_datastore()
+
+ self.room = hs.parse_roomid("!abcde:test")
+ self.alias = hs.parse_roomalias("#a-room-name:test")
+ self.u_creator = hs.parse_userid("@creator:test")
+
+ yield self.store.store_room(self.room.to_string(),
+ room_creator_user_id=self.u_creator.to_string(),
+ is_public=True
+ )
+
+ @defer.inlineCallbacks
+ def test_get_room(self):
+ self.assertObjectHasAttributes(
+ {"room_id": self.room.to_string(),
+ "creator": self.u_creator.to_string(),
+ "is_public": True},
+ (yield self.store.get_room(self.room.to_string()))
+ )
+
+ @defer.inlineCallbacks
+ def test_store_room_config(self):
+ yield self.store.store_room_config(self.room.to_string(),
+ visibility=False
+ )
+
+ self.assertObjectHasAttributes(
+ {"is_public": False},
+ (yield self.store.get_room(self.room.to_string()))
+ )
+
+ @defer.inlineCallbacks
+ def test_get_rooms(self):
+ # get_rooms does an INNER JOIN on the room_aliases table :(
+
+ rooms = yield self.store.get_rooms(is_public=True)
+ # Should be empty before we add the alias
+ self.assertEquals([], rooms)
+
+ yield self.store.create_room_alias_association(
+ room_alias=self.alias,
+ room_id=self.room.to_string(),
+ servers=["test"]
+ )
+
+ rooms = yield self.store.get_rooms(is_public=True)
+
+ self.assertEquals(1, len(rooms))
+ self.assertEquals({
+ "name": None,
+ "room_id": self.room.to_string(),
+ "topic": None,
+ "aliases": [self.alias.to_string()],
+ }, rooms[0])
+
+
+class RoomEventsStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ # Room events need the full datastore, for persist_event() and
+ # get_room_state()
+ self.store = hs.get_datastore()
+ self.event_factory = hs.get_event_factory();
+
+ self.room = hs.parse_roomid("!abcde:test")
+
+ yield self.store.store_room(self.room.to_string(),
+ room_creator_user_id="@creator:text",
+ is_public=True
+ )
+
+ @defer.inlineCallbacks
+ def inject_room_event(self, **kwargs):
+ yield self.store.persist_event(
+ self.event_factory.create_event(
+ room_id=self.room.to_string(),
+ **kwargs
+ )
+ )
+
+ @defer.inlineCallbacks
+ def test_room_name(self):
+ name = u"A-Room-Name"
+
+ yield self.inject_room_event(
+ etype=RoomNameEvent.TYPE,
+ name=name,
+ content={"name": name},
+ depth=1,
+ )
+
+ state = yield self.store.get_current_state(
+ room_id=self.room.to_string()
+ )
+
+ self.assertEquals(1, len(state))
+ self.assertObjectHasAttributes(
+ {"type": "m.room.name",
+ "room_id": self.room.to_string(),
+ "name": name},
+ state[0]
+ )
+
+ @defer.inlineCallbacks
+ def test_room_name(self):
+ topic = u"A place for things"
+
+ yield self.inject_room_event(
+ etype=RoomTopicEvent.TYPE,
+ topic=topic,
+ content={"topic": topic},
+ depth=1,
+ )
+
+ state = yield self.store.get_current_state(
+ room_id=self.room.to_string()
+ )
+
+ self.assertEquals(1, len(state))
+ self.assertObjectHasAttributes(
+ {"type": "m.room.topic",
+ "room_id": self.room.to_string(),
+ "topic": topic},
+ state[0]
+ )
+
+ # Not testing the various 'level' methods for now because there's lots
+ # of them and need coalescing; see JIRA SPEC-11
diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py
new file mode 100644
index 0000000000..eae278ee8d
--- /dev/null
+++ b/tests/storage/test_roommember.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from tests import unittest
+from twisted.internet import defer
+
+from synapse.server import HomeServer
+from synapse.api.constants import Membership
+from synapse.api.events.room import RoomMemberEvent
+
+from tests.utils import SQLiteMemoryDbPool
+
+
+class RoomMemberStoreTestCase(unittest.TestCase):
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ db_pool = SQLiteMemoryDbPool()
+ yield db_pool.prepare()
+
+ hs = HomeServer("test",
+ db_pool=db_pool,
+ )
+
+ # We can't test the RoomMemberStore on its own without the other event
+ # storage logic
+ self.store = hs.get_datastore()
+ self.event_factory = hs.get_event_factory()
+
+ self.u_alice = hs.parse_userid("@alice:test")
+ self.u_bob = hs.parse_userid("@bob:test")
+
+ # User elsewhere on another host
+ self.u_charlie = hs.parse_userid("@charlie:elsewhere")
+
+ self.room = hs.parse_roomid("!abc123:test")
+
+ @defer.inlineCallbacks
+ def inject_room_member(self, room, user, membership):
+ # Have to create a join event using the eventfactory
+ yield self.store.persist_event(
+ self.event_factory.create_event(
+ etype=RoomMemberEvent.TYPE,
+ user_id=user.to_string(),
+ state_key=user.to_string(),
+ room_id=room.to_string(),
+ membership=membership,
+ content={"membership": membership},
+ depth=1,
+ )
+ )
+
+ @defer.inlineCallbacks
+ def test_one_member(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+
+ self.assertEquals(
+ Membership.JOIN,
+ (yield self.store.get_room_member(
+ user_id=self.u_alice.to_string(),
+ room_id=self.room.to_string(),
+ )).membership
+ )
+ self.assertEquals(
+ [self.u_alice.to_string()],
+ [m.user_id for m in (
+ yield self.store.get_room_members(self.room.to_string())
+ )]
+ )
+ self.assertEquals(
+ [self.room.to_string()],
+ [m.room_id for m in (
+ yield self.store.get_rooms_for_user_where_membership_is(
+ self.u_alice.to_string(), [Membership.JOIN]
+ ))
+ ]
+ )
+ self.assertFalse(
+ (yield self.store.user_rooms_intersect(
+ [self.u_alice.to_string(), self.u_bob.to_string()]
+ ))
+ )
+
+ @defer.inlineCallbacks
+ def test_two_members(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+ yield self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
+
+ self.assertEquals(
+ {self.u_alice.to_string(), self.u_bob.to_string()},
+ {m.user_id for m in (
+ yield self.store.get_room_members(self.room.to_string())
+ )}
+ )
+ self.assertTrue(
+ (yield self.store.user_rooms_intersect(
+ [self.u_alice.to_string(), self.u_bob.to_string()]
+ ))
+ )
+
+ @defer.inlineCallbacks
+ def test_room_hosts(self):
+ yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
+
+ self.assertEquals(
+ ["test"],
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
+
+ # Should still have just one host after second join from it
+ yield self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
+
+ self.assertEquals(
+ ["test"],
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
+
+ # Should now have two hosts after join from other host
+ yield self.inject_room_member(self.room, self.u_charlie, Membership.JOIN)
+
+ self.assertEquals(
+ {"test", "elsewhere"},
+ set((yield
+ self.store.get_joined_hosts_for_room(self.room.to_string())
+ ))
+ )
+
+ # Should still have both hosts
+ yield self.inject_room_member(self.room, self.u_alice, Membership.LEAVE)
+
+ self.assertEquals(
+ {"test", "elsewhere"},
+ set((yield
+ self.store.get_joined_hosts_for_room(self.room.to_string())
+ ))
+ )
+
+ # Should have only one host after other leaves
+ yield self.inject_room_member(self.room, self.u_charlie, Membership.LEAVE)
+
+ self.assertEquals(
+ ["test"],
+ (yield self.store.get_joined_hosts_for_room(self.room.to_string()))
+ )
diff --git a/tests/unittest.py b/tests/unittest.py
index fb97fb1148..a9c0e05541 100644
--- a/tests/unittest.py
+++ b/tests/unittest.py
@@ -71,6 +71,17 @@ class TestCase(unittest.TestCase):
logging.getLogger().setLevel(level)
return orig()
+ def assertObjectHasAttributes(self, attrs, obj):
+ """Asserts that the given object has each of the attributes given, and
+ that the value of each matches according to assertEquals."""
+ for (key, value) in attrs.items():
+ if not hasattr(obj, key):
+ raise AssertionError("Expected obj to have a '.%s'" % key)
+ try:
+ self.assertEquals(attrs[key], getattr(obj, key))
+ except AssertionError as e:
+ raise (type(e))(e.message + " for '.%s'" % key)
+
def DEBUG(target):
"""A decorator to set the .loglevel attribute to logging.DEBUG.
diff --git a/tests/utils.py b/tests/utils.py
index d90214e418..bc5d35e56b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,12 +16,14 @@
from synapse.http.server import HttpServer
from synapse.api.errors import cs_error, CodeMessageException, StoreError
from synapse.api.constants import Membership
+from synapse.storage import prepare_database
from synapse.api.events.room import (
RoomMemberEvent, MessageEvent
)
from twisted.internet import defer, reactor
+from twisted.enterprise.adbapi import ConnectionPool
from collections import namedtuple
from mock import patch, Mock
@@ -120,6 +122,18 @@ class MockClock(object):
self.now += secs
+class SQLiteMemoryDbPool(ConnectionPool, object):
+ def __init__(self):
+ super(SQLiteMemoryDbPool, self).__init__(
+ "sqlite3", ":memory:",
+ cp_min=1,
+ cp_max=1,
+ )
+
+ def prepare(self):
+ return self.runWithConnection(prepare_database)
+
+
class MemoryDataStore(object):
Room = namedtuple(
diff --git a/webclient/README b/webclient/README
index 13224c3d07..ef79b25708 100644
--- a/webclient/README
+++ b/webclient/README
@@ -1,8 +1,8 @@
Basic Usage
-----------
-The web client should automatically run when running the home server. Alternatively, you can run
-it stand-alone:
+The web client should automatically run when running the home server.
+Alternatively, you can run it stand-alone:
$ python -m SimpleHTTPServer
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index c7be65720a..5e95f34f4e 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -99,7 +99,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
};
reset();
- var initRoom = function(room_id) {
+ var initRoom = function(room_id, room) {
if (!(room_id in $rootScope.events.rooms)) {
console.log("Creating new handler entry for " + room_id);
$rootScope.events.rooms[room_id] = {
@@ -112,6 +112,16 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
}
};
}
+
+ if (room) {
+ // Report all other metadata of the room object (membership, inviter, visibility, ...)
+ for (var field in room) {
+ if (-1 === ["room_id", "messages", "state"].indexOf(field)) {
+ $rootScope.events.rooms[room_id][field] = room[field];
+ }
+ }
+ $rootScope.events.rooms[room_id].membership = room.membership;
+ }
};
var resetRoomMessages = function(room_id) {
@@ -327,6 +337,10 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
reset();
$rootScope.$broadcast(RESET_EVENT);
},
+
+ initRoom: function(room) {
+ initRoom(room.room_id, room);
+ },
handleEvent: function(event, isLiveEvent, isStateEvent) {
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 5af1ab2911..05469a3ded 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -112,6 +112,8 @@ angular.module('eventStreamService', [])
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
+
+ eventHandlerService.initRoom(room);
if ("messages" in room) {
eventHandlerService.handleRoomMessages(room.room_id, room.messages, false);
@@ -120,8 +122,6 @@ angular.module('eventStreamService', [])
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false, true);
}
-
- eventHandlerService.setRoomVisibility(room.room_id, room.visibility);
}
var presence = response.data.presence;
diff --git a/webclient/js/angular-mocks.js b/webclient/js/angular-mocks.js
new file mode 100755
index 0000000000..48c0b5decb
--- /dev/null
+++ b/webclient/js/angular-mocks.js
@@ -0,0 +1,2173 @@
+/**
+ * @license AngularJS v1.2.22
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {
+
+'use strict';
+
+/**
+ * @ngdoc object
+ * @name angular.mock
+ * @description
+ *
+ * Namespace from 'angular-mocks.js' which contains testing related code.
+ */
+angular.mock = {};
+
+/**
+ * ! This is a private undocumented service !
+ *
+ * @name $browser
+ *
+ * @description
+ * This service is a mock implementation of {@link ng.$browser}. It provides fake
+ * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr,
+ * cookies, etc...
+ *
+ * The api of this service is the same as that of the real {@link ng.$browser $browser}, except
+ * that there are several helper methods available which can be used in tests.
+ */
+angular.mock.$BrowserProvider = function() {
+ this.$get = function() {
+ return new angular.mock.$Browser();
+ };
+};
+
+angular.mock.$Browser = function() {
+ var self = this;
+
+ this.isMock = true;
+ self.$$url = "http://server/";
+ self.$$lastUrl = self.$$url; // used by url polling fn
+ self.pollFns = [];
+
+ // TODO(vojta): remove this temporary api
+ self.$$completeOutstandingRequest = angular.noop;
+ self.$$incOutstandingRequestCount = angular.noop;
+
+
+ // register url polling fn
+
+ self.onUrlChange = function(listener) {
+ self.pollFns.push(
+ function() {
+ if (self.$$lastUrl != self.$$url) {
+ self.$$lastUrl = self.$$url;
+ listener(self.$$url);
+ }
+ }
+ );
+
+ return listener;
+ };
+
+ self.cookieHash = {};
+ self.lastCookieHash = {};
+ self.deferredFns = [];
+ self.deferredNextId = 0;
+
+ self.defer = function(fn, delay) {
+ delay = delay || 0;
+ self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
+ self.deferredFns.sort(function(a,b){ return a.time - b.time;});
+ return self.deferredNextId++;
+ };
+
+
+ /**
+ * @name $browser#defer.now
+ *
+ * @description
+ * Current milliseconds mock time.
+ */
+ self.defer.now = 0;
+
+
+ self.defer.cancel = function(deferId) {
+ var fnIndex;
+
+ angular.forEach(self.deferredFns, function(fn, index) {
+ if (fn.id === deferId) fnIndex = index;
+ });
+
+ if (fnIndex !== undefined) {
+ self.deferredFns.splice(fnIndex, 1);
+ return true;
+ }
+
+ return false;
+ };
+
+
+ /**
+ * @name $browser#defer.flush
+ *
+ * @description
+ * Flushes all pending requests and executes the defer callbacks.
+ *
+ * @param {number=} number of milliseconds to flush. See {@link #defer.now}
+ */
+ self.defer.flush = function(delay) {
+ if (angular.isDefined(delay)) {
+ self.defer.now += delay;
+ } else {
+ if (self.deferredFns.length) {
+ self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
+ } else {
+ throw new Error('No deferred tasks to be flushed');
+ }
+ }
+
+ while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
+ self.deferredFns.shift().fn();
+ }
+ };
+
+ self.$$baseHref = '';
+ self.baseHref = function() {
+ return this.$$baseHref;
+ };
+};
+angular.mock.$Browser.prototype = {
+
+/**
+ * @name $browser#poll
+ *
+ * @description
+ * run all fns in pollFns
+ */
+ poll: function poll() {
+ angular.forEach(this.pollFns, function(pollFn){
+ pollFn();
+ });
+ },
+
+ addPollFn: function(pollFn) {
+ this.pollFns.push(pollFn);
+ return pollFn;
+ },
+
+ url: function(url, replace) {
+ if (url) {
+ this.$$url = url;
+ return this;
+ }
+
+ return this.$$url;
+ },
+
+ cookies: function(name, value) {
+ if (name) {
+ if (angular.isUndefined(value)) {
+ delete this.cookieHash[name];
+ } else {
+ if (angular.isString(value) && //strings only
+ value.length <= 4096) { //strict cookie storage limits
+ this.cookieHash[name] = value;
+ }
+ }
+ } else {
+ if (!angular.equals(this.cookieHash, this.lastCookieHash)) {
+ this.lastCookieHash = angular.copy(this.cookieHash);
+ this.cookieHash = angular.copy(this.cookieHash);
+ }
+ return this.cookieHash;
+ }
+ },
+
+ notifyWhenNoOutstandingRequests: function(fn) {
+ fn();
+ }
+};
+
+
+/**
+ * @ngdoc provider
+ * @name $exceptionHandlerProvider
+ *
+ * @description
+ * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
+ * passed into the `$exceptionHandler`.
+ */
+
+/**
+ * @ngdoc service
+ * @name $exceptionHandler
+ *
+ * @description
+ * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
+ * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
+ * information.
+ *
+ *
+ * ```js
+ * describe('$exceptionHandlerProvider', function() {
+ *
+ * it('should capture log messages and exceptions', function() {
+ *
+ * module(function($exceptionHandlerProvider) {
+ * $exceptionHandlerProvider.mode('log');
+ * });
+ *
+ * inject(function($log, $exceptionHandler, $timeout) {
+ * $timeout(function() { $log.log(1); });
+ * $timeout(function() { $log.log(2); throw 'banana peel'; });
+ * $timeout(function() { $log.log(3); });
+ * expect($exceptionHandler.errors).toEqual([]);
+ * expect($log.assertEmpty());
+ * $timeout.flush();
+ * expect($exceptionHandler.errors).toEqual(['banana peel']);
+ * expect($log.log.logs).toEqual([[1], [2], [3]]);
+ * });
+ * });
+ * });
+ * ```
+ */
+
+angular.mock.$ExceptionHandlerProvider = function() {
+ var handler;
+
+ /**
+ * @ngdoc method
+ * @name $exceptionHandlerProvider#mode
+ *
+ * @description
+ * Sets the logging mode.
+ *
+ * @param {string} mode Mode of operation, defaults to `rethrow`.
+ *
+ * - `rethrow`: If any errors are passed into the handler in tests, it typically
+ * means that there is a bug in the application or test, so this mock will
+ * make these tests fail.
+ * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
+ * mode stores an array of errors in `$exceptionHandler.errors`, to allow later
+ * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
+ * {@link ngMock.$log#reset reset()}
+ */
+ this.mode = function(mode) {
+ switch(mode) {
+ case 'rethrow':
+ handler = function(e) {
+ throw e;
+ };
+ break;
+ case 'log':
+ var errors = [];
+
+ handler = function(e) {
+ if (arguments.length == 1) {
+ errors.push(e);
+ } else {
+ errors.push([].slice.call(arguments, 0));
+ }
+ };
+
+ handler.errors = errors;
+ break;
+ default:
+ throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
+ }
+ };
+
+ this.$get = function() {
+ return handler;
+ };
+
+ this.mode('rethrow');
+};
+
+
+/**
+ * @ngdoc service
+ * @name $log
+ *
+ * @description
+ * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays
+ * (one array per logging level). These arrays are exposed as `logs` property of each of the
+ * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`.
+ *
+ */
+angular.mock.$LogProvider = function() {
+ var debug = true;
+
+ function concat(array1, array2, index) {
+ return array1.concat(Array.prototype.slice.call(array2, index));
+ }
+
+ this.debugEnabled = function(flag) {
+ if (angular.isDefined(flag)) {
+ debug = flag;
+ return this;
+ } else {
+ return debug;
+ }
+ };
+
+ this.$get = function () {
+ var $log = {
+ log: function() { $log.log.logs.push(concat([], arguments, 0)); },
+ warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
+ info: function() { $log.info.logs.push(concat([], arguments, 0)); },
+ error: function() { $log.error.logs.push(concat([], arguments, 0)); },
+ debug: function() {
+ if (debug) {
+ $log.debug.logs.push(concat([], arguments, 0));
+ }
+ }
+ };
+
+ /**
+ * @ngdoc method
+ * @name $log#reset
+ *
+ * @description
+ * Reset all of the logging arrays to empty.
+ */
+ $log.reset = function () {
+ /**
+ * @ngdoc property
+ * @name $log#log.logs
+ *
+ * @description
+ * Array of messages logged using {@link ngMock.$log#log}.
+ *
+ * @example
+ * ```js
+ * $log.log('Some Log');
+ * var first = $log.log.logs.unshift();
+ * ```
+ */
+ $log.log.logs = [];
+ /**
+ * @ngdoc property
+ * @name $log#info.logs
+ *
+ * @description
+ * Array of messages logged using {@link ngMock.$log#info}.
+ *
+ * @example
+ * ```js
+ * $log.info('Some Info');
+ * var first = $log.info.logs.unshift();
+ * ```
+ */
+ $log.info.logs = [];
+ /**
+ * @ngdoc property
+ * @name $log#warn.logs
+ *
+ * @description
+ * Array of messages logged using {@link ngMock.$log#warn}.
+ *
+ * @example
+ * ```js
+ * $log.warn('Some Warning');
+ * var first = $log.warn.logs.unshift();
+ * ```
+ */
+ $log.warn.logs = [];
+ /**
+ * @ngdoc property
+ * @name $log#error.logs
+ *
+ * @description
+ * Array of messages logged using {@link ngMock.$log#error}.
+ *
+ * @example
+ * ```js
+ * $log.error('Some Error');
+ * var first = $log.error.logs.unshift();
+ * ```
+ */
+ $log.error.logs = [];
+ /**
+ * @ngdoc property
+ * @name $log#debug.logs
+ *
+ * @description
+ * Array of messages logged using {@link ngMock.$log#debug}.
+ *
+ * @example
+ * ```js
+ * $log.debug('Some Error');
+ * var first = $log.debug.logs.unshift();
+ * ```
+ */
+ $log.debug.logs = [];
+ };
+
+ /**
+ * @ngdoc method
+ * @name $log#assertEmpty
+ *
+ * @description
+ * Assert that the all of the logging methods have no logged messages. If messages present, an
+ * exception is thrown.
+ */
+ $log.assertEmpty = function() {
+ var errors = [];
+ angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
+ angular.forEach($log[logLevel].logs, function(log) {
+ angular.forEach(log, function (logItem) {
+ errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
+ (logItem.stack || ''));
+ });
+ });
+ });
+ if (errors.length) {
+ errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
+ "an expected log message was not checked and removed:");
+ errors.push('');
+ throw new Error(errors.join('\n---------\n'));
+ }
+ };
+
+ $log.reset();
+ return $log;
+ };
+};
+
+
+/**
+ * @ngdoc service
+ * @name $interval
+ *
+ * @description
+ * Mock implementation of the $interval service.
+ *
+ * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
+ * move forward by `millis` milliseconds and trigger any functions scheduled to run in that
+ * time.
+ *
+ * @param {function()} fn A function that should be called repeatedly.
+ * @param {number} delay Number of milliseconds between each function call.
+ * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
+ * indefinitely.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @returns {promise} A promise which will be notified on each iteration.
+ */
+angular.mock.$IntervalProvider = function() {
+ this.$get = ['$rootScope', '$q',
+ function($rootScope, $q) {
+ var repeatFns = [],
+ nextRepeatId = 0,
+ now = 0;
+
+ var $interval = function(fn, delay, count, invokeApply) {
+ var deferred = $q.defer(),
+ promise = deferred.promise,
+ iteration = 0,
+ skipApply = (angular.isDefined(invokeApply) && !invokeApply);
+
+ count = (angular.isDefined(count)) ? count : 0;
+ promise.then(null, null, fn);
+
+ promise.$$intervalId = nextRepeatId;
+
+ function tick() {
+ deferred.notify(iteration++);
+
+ if (count > 0 && iteration >= count) {
+ var fnIndex;
+ deferred.resolve(iteration);
+
+ angular.forEach(repeatFns, function(fn, index) {
+ if (fn.id === promise.$$intervalId) fnIndex = index;
+ });
+
+ if (fnIndex !== undefined) {
+ repeatFns.splice(fnIndex, 1);
+ }
+ }
+
+ if (!skipApply) $rootScope.$apply();
+ }
+
+ repeatFns.push({
+ nextTime:(now + delay),
+ delay: delay,
+ fn: tick,
+ id: nextRepeatId,
+ deferred: deferred
+ });
+ repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
+
+ nextRepeatId++;
+ return promise;
+ };
+ /**
+ * @ngdoc method
+ * @name $interval#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`.
+ *
+ * @param {promise} promise A promise from calling the `$interval` function.
+ * @returns {boolean} Returns `true` if the task was successfully cancelled.
+ */
+ $interval.cancel = function(promise) {
+ if(!promise) return false;
+ var fnIndex;
+
+ angular.forEach(repeatFns, function(fn, index) {
+ if (fn.id === promise.$$intervalId) fnIndex = index;
+ });
+
+ if (fnIndex !== undefined) {
+ repeatFns[fnIndex].deferred.reject('canceled');
+ repeatFns.splice(fnIndex, 1);
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * @ngdoc method
+ * @name $interval#flush
+ * @description
+ *
+ * Runs interval tasks scheduled to be run in the next `millis` milliseconds.
+ *
+ * @param {number=} millis maximum timeout amount to flush up until.
+ *
+ * @return {number} The amount of time moved forward.
+ */
+ $interval.flush = function(millis) {
+ now += millis;
+ while (repeatFns.length && repeatFns[0].nextTime <= now) {
+ var task = repeatFns[0];
+ task.fn();
+ task.nextTime += task.delay;
+ repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
+ }
+ return millis;
+ };
+
+ return $interval;
+ }];
+};
+
+
+/* jshint -W101 */
+/* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
+ * This directive should go inside the anonymous function but a bug in JSHint means that it would
+ * not be enacted early enough to prevent the warning.
+ */
+var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
+
+function jsonStringToDate(string) {
+ var match;
+ if (match = string.match(R_ISO8061_STR)) {
+ var date = new Date(0),
+ tzHour = 0,
+ tzMin = 0;
+ if (match[9]) {
+ tzHour = int(match[9] + match[10]);
+ tzMin = int(match[9] + match[11]);
+ }
+ date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
+ date.setUTCHours(int(match[4]||0) - tzHour,
+ int(match[5]||0) - tzMin,
+ int(match[6]||0),
+ int(match[7]||0));
+ return date;
+ }
+ return string;
+}
+
+function int(str) {
+ return parseInt(str, 10);
+}
+
+function padNumber(num, digits, trim) {
+ var neg = '';
+ if (num < 0) {
+ neg = '-';
+ num = -num;
+ }
+ num = '' + num;
+ while(num.length < digits) num = '0' + num;
+ if (trim)
+ num = num.substr(num.length - digits);
+ return neg + num;
+}
+
+
+/**
+ * @ngdoc type
+ * @name angular.mock.TzDate
+ * @description
+ *
+ * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
+ *
+ * Mock of the Date type which has its timezone specified via constructor arg.
+ *
+ * The main purpose is to create Date-like instances with timezone fixed to the specified timezone
+ * offset, so that we can test code that depends on local timezone settings without dependency on
+ * the time zone settings of the machine where the code is running.
+ *
+ * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
+ * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
+ *
+ * @example
+ * !!!! WARNING !!!!!
+ * This is not a complete Date object so only methods that were implemented can be called safely.
+ * To make matters worse, TzDate instances inherit stuff from Date via a prototype.
+ *
+ * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
+ * incomplete we might be missing some non-standard methods. This can result in errors like:
+ * "Date.prototype.foo called on incompatible Object".
+ *
+ * ```js
+ * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
+ * newYearInBratislava.getTimezoneOffset() => -60;
+ * newYearInBratislava.getFullYear() => 2010;
+ * newYearInBratislava.getMonth() => 0;
+ * newYearInBratislava.getDate() => 1;
+ * newYearInBratislava.getHours() => 0;
+ * newYearInBratislava.getMinutes() => 0;
+ * newYearInBratislava.getSeconds() => 0;
+ * ```
+ *
+ */
+angular.mock.TzDate = function (offset, timestamp) {
+ var self = new Date(0);
+ if (angular.isString(timestamp)) {
+ var tsStr = timestamp;
+
+ self.origDate = jsonStringToDate(timestamp);
+
+ timestamp = self.origDate.getTime();
+ if (isNaN(timestamp))
+ throw {
+ name: "Illegal Argument",
+ message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
+ };
+ } else {
+ self.origDate = new Date(timestamp);
+ }
+
+ var localOffset = new Date(timestamp).getTimezoneOffset();
+ self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
+ self.date = new Date(timestamp + self.offsetDiff);
+
+ self.getTime = function() {
+ return self.date.getTime() - self.offsetDiff;
+ };
+
+ self.toLocaleDateString = function() {
+ return self.date.toLocaleDateString();
+ };
+
+ self.getFullYear = function() {
+ return self.date.getFullYear();
+ };
+
+ self.getMonth = function() {
+ return self.date.getMonth();
+ };
+
+ self.getDate = function() {
+ return self.date.getDate();
+ };
+
+ self.getHours = function() {
+ return self.date.getHours();
+ };
+
+ self.getMinutes = function() {
+ return self.date.getMinutes();
+ };
+
+ self.getSeconds = function() {
+ return self.date.getSeconds();
+ };
+
+ self.getMilliseconds = function() {
+ return self.date.getMilliseconds();
+ };
+
+ self.getTimezoneOffset = function() {
+ return offset * 60;
+ };
+
+ self.getUTCFullYear = function() {
+ return self.origDate.getUTCFullYear();
+ };
+
+ self.getUTCMonth = function() {
+ return self.origDate.getUTCMonth();
+ };
+
+ self.getUTCDate = function() {
+ return self.origDate.getUTCDate();
+ };
+
+ self.getUTCHours = function() {
+ return self.origDate.getUTCHours();
+ };
+
+ self.getUTCMinutes = function() {
+ return self.origDate.getUTCMinutes();
+ };
+
+ self.getUTCSeconds = function() {
+ return self.origDate.getUTCSeconds();
+ };
+
+ self.getUTCMilliseconds = function() {
+ return self.origDate.getUTCMilliseconds();
+ };
+
+ self.getDay = function() {
+ return self.date.getDay();
+ };
+
+ // provide this method only on browsers that already have it
+ if (self.toISOString) {
+ self.toISOString = function() {
+ return padNumber(self.origDate.getUTCFullYear(), 4) + '-' +
+ padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' +
+ padNumber(self.origDate.getUTCDate(), 2) + 'T' +
+ padNumber(self.origDate.getUTCHours(), 2) + ':' +
+ padNumber(self.origDate.getUTCMinutes(), 2) + ':' +
+ padNumber(self.origDate.getUTCSeconds(), 2) + '.' +
+ padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z';
+ };
+ }
+
+ //hide all methods not implemented in this mock that the Date prototype exposes
+ var unimplementedMethods = ['getUTCDay',
+ 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
+ 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
+ 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
+ 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString',
+ 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
+
+ angular.forEach(unimplementedMethods, function(methodName) {
+ self[methodName] = function() {
+ throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
+ };
+ });
+
+ return self;
+};
+
+//make "tzDateInstance instanceof Date" return true
+angular.mock.TzDate.prototype = Date.prototype;
+/* jshint +W101 */
+
+angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
+
+ .config(['$provide', function($provide) {
+
+ var reflowQueue = [];
+ $provide.value('$$animateReflow', function(fn) {
+ var index = reflowQueue.length;
+ reflowQueue.push(fn);
+ return function cancel() {
+ reflowQueue.splice(index, 1);
+ };
+ });
+
+ $provide.decorator('$animate', function($delegate, $$asyncCallback) {
+ var animate = {
+ queue : [],
+ enabled : $delegate.enabled,
+ triggerCallbacks : function() {
+ $$asyncCallback.flush();
+ },
+ triggerReflow : function() {
+ angular.forEach(reflowQueue, function(fn) {
+ fn();
+ });
+ reflowQueue = [];
+ }
+ };
+
+ angular.forEach(
+ ['enter','leave','move','addClass','removeClass','setClass'], function(method) {
+ animate[method] = function() {
+ animate.queue.push({
+ event : method,
+ element : arguments[0],
+ args : arguments
+ });
+ $delegate[method].apply($delegate, arguments);
+ };
+ });
+
+ return animate;
+ });
+
+ }]);
+
+
+/**
+ * @ngdoc function
+ * @name angular.mock.dump
+ * @description
+ *
+ * *NOTE*: this is not an injectable instance, just a globally available function.
+ *
+ * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for
+ * debugging.
+ *
+ * This method is also available on window, where it can be used to display objects on debug
+ * console.
+ *
+ * @param {*} object - any object to turn into string.
+ * @return {string} a serialized string of the argument
+ */
+angular.mock.dump = function(object) {
+ return serialize(object);
+
+ function serialize(object) {
+ var out;
+
+ if (angular.isElement(object)) {
+ object = angular.element(object);
+ out = angular.element('<div></div>');
+ angular.forEach(object, function(element) {
+ out.append(angular.element(element).clone());
+ });
+ out = out.html();
+ } else if (angular.isArray(object)) {
+ out = [];
+ angular.forEach(object, function(o) {
+ out.push(serialize(o));
+ });
+ out = '[ ' + out.join(', ') + ' ]';
+ } else if (angular.isObject(object)) {
+ if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
+ out = serializeScope(object);
+ } else if (object instanceof Error) {
+ out = object.stack || ('' + object.name + ': ' + object.message);
+ } else {
+ // TODO(i): this prevents methods being logged,
+ // we should have a better way to serialize objects
+ out = angular.toJson(object, true);
+ }
+ } else {
+ out = String(object);
+ }
+
+ return out;
+ }
+
+ function serializeScope(scope, offset) {
+ offset = offset || ' ';
+ var log = [offset + 'Scope(' + scope.$id + '): {'];
+ for ( var key in scope ) {
+ if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
+ log.push(' ' + key + ': ' + angular.toJson(scope[key]));
+ }
+ }
+ var child = scope.$$childHead;
+ while(child) {
+ log.push(serializeScope(child, offset + ' '));
+ child = child.$$nextSibling;
+ }
+ log.push('}');
+ return log.join('\n' + offset);
+ }
+};
+
+/**
+ * @ngdoc service
+ * @name $httpBackend
+ * @description
+ * Fake HTTP backend implementation suitable for unit testing applications that use the
+ * {@link ng.$http $http service}.
+ *
+ * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less
+ * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
+ *
+ * During unit testing, we want our unit tests to run quickly and have no external dependencies so
+ * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
+ * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is
+ * to verify whether a certain request has been sent or not, or alternatively just let the
+ * application make requests, respond with pre-trained responses and assert that the end result is
+ * what we expect it to be.
+ *
+ * This mock implementation can be used to respond with static or dynamic responses via the
+ * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
+ *
+ * When an Angular application needs some data from a server, it calls the $http service, which
+ * sends the request to a real server using $httpBackend service. With dependency injection, it is
+ * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
+ * the requests and respond with some testing data without sending a request to a real server.
+ *
+ * There are two ways to specify what test data should be returned as http responses by the mock
+ * backend when the code under test makes http requests:
+ *
+ * - `$httpBackend.expect` - specifies a request expectation
+ * - `$httpBackend.when` - specifies a backend definition
+ *
+ *
+ * # Request Expectations vs Backend Definitions
+ *
+ * Request expectations provide a way to make assertions about requests made by the application and
+ * to define responses for those requests. The test will fail if the expected requests are not made
+ * or they are made in the wrong order.
+ *
+ * Backend definitions allow you to define a fake backend for your application which doesn't assert
+ * if a particular request was made or not, it just returns a trained response if a request is made.
+ * The test will pass whether or not the request gets made during testing.
+ *
+ *
+ * <table class="table">
+ * <tr><th width="220px"></th><th>Request expectations</th><th>Backend definitions</th></tr>
+ * <tr>
+ * <th>Syntax</th>
+ * <td>.expect(...).respond(...)</td>
+ * <td>.when(...).respond(...)</td>
+ * </tr>
+ * <tr>
+ * <th>Typical usage</th>
+ * <td>strict unit tests</td>
+ * <td>loose (black-box) unit testing</td>
+ * </tr>
+ * <tr>
+ * <th>Fulfills multiple requests</th>
+ * <td>NO</td>
+ * <td>YES</td>
+ * </tr>
+ * <tr>
+ * <th>Order of requests matters</th>
+ * <td>YES</td>
+ * <td>NO</td>
+ * </tr>
+ * <tr>
+ * <th>Request required</th>
+ * <td>YES</td>
+ * <td>NO</td>
+ * </tr>
+ * <tr>
+ * <th>Response required</th>
+ * <td>optional (see below)</td>
+ * <td>YES</td>
+ * </tr>
+ * </table>
+ *
+ * In cases where both backend definitions and request expectations are specified during unit
+ * testing, the request expectations are evaluated first.
+ *
+ * If a request expectation has no response specified, the algorithm will search your backend
+ * definitions for an appropriate response.
+ *
+ * If a request didn't match any expectation or if the expectation doesn't have the response
+ * defined, the backend definitions are evaluated in sequential order to see if any of them match
+ * the request. The response from the first matched definition is returned.
+ *
+ *
+ * # Flushing HTTP requests
+ *
+ * The $httpBackend used in production always responds to requests asynchronously. If we preserved
+ * this behavior in unit testing, we'd have to create async unit tests, which are hard to write,
+ * to follow and to maintain. But neither can the testing mock respond synchronously; that would
+ * change the execution of the code under test. For this reason, the mock $httpBackend has a
+ * `flush()` method, which allows the test to explicitly flush pending requests. This preserves
+ * the async api of the backend, while allowing the test to execute synchronously.
+ *
+ *
+ * # Unit testing with mock $httpBackend
+ * The following code shows how to setup and use the mock backend when unit testing a controller.
+ * First we create the controller under test:
+ *
+ ```js
+ // The controller code
+ function MyController($scope, $http) {
+ var authToken;
+
+ $http.get('/auth.py').success(function(data, status, headers) {
+ authToken = headers('A-Token');
+ $scope.user = data;
+ });
+
+ $scope.saveMessage = function(message) {
+ var headers = { 'Authorization': authToken };
+ $scope.status = 'Saving...';
+
+ $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
+ $scope.status = '';
+ }).error(function() {
+ $scope.status = 'ERROR!';
+ });
+ };
+ }
+ ```
+ *
+ * Now we setup the mock backend and create the test specs:
+ *
+ ```js
+ // testing controller
+ describe('MyController', function() {
+ var $httpBackend, $rootScope, createController;
+
+ beforeEach(inject(function($injector) {
+ // Set up the mock http service responses
+ $httpBackend = $injector.get('$httpBackend');
+ // backend definition common for all tests
+ $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
+
+ // Get hold of a scope (i.e. the root scope)
+ $rootScope = $injector.get('$rootScope');
+ // The $controller service is used to create instances of controllers
+ var $controller = $injector.get('$controller');
+
+ createController = function() {
+ return $controller('MyController', {'$scope' : $rootScope });
+ };
+ }));
+
+
+ afterEach(function() {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+
+ it('should fetch authentication token', function() {
+ $httpBackend.expectGET('/auth.py');
+ var controller = createController();
+ $httpBackend.flush();
+ });
+
+
+ it('should send msg to server', function() {
+ var controller = createController();
+ $httpBackend.flush();
+
+ // now you don’t care about the authentication, but
+ // the controller will still send the request and
+ // $httpBackend will respond without you having to
+ // specify the expectation and response for this request
+
+ $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
+ $rootScope.saveMessage('message content');
+ expect($rootScope.status).toBe('Saving...');
+ $httpBackend.flush();
+ expect($rootScope.status).toBe('');
+ });
+
+
+ it('should send auth header', function() {
+ var controller = createController();
+ $httpBackend.flush();
+
+ $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
+ // check if the header was send, if it wasn't the expectation won't
+ // match the request and the test will fail
+ return headers['Authorization'] == 'xxx';
+ }).respond(201, '');
+
+ $rootScope.saveMessage('whatever');
+ $httpBackend.flush();
+ });
+ });
+ ```
+ */
+angular.mock.$HttpBackendProvider = function() {
+ this.$get = ['$rootScope', createHttpBackendMock];
+};
+
+/**
+ * General factory function for $httpBackend mock.
+ * Returns instance for unit testing (when no arguments specified):
+ * - passing through is disabled
+ * - auto flushing is disabled
+ *
+ * Returns instance for e2e testing (when `$delegate` and `$browser` specified):
+ * - passing through (delegating request to real backend) is enabled
+ * - auto flushing is enabled
+ *
+ * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified)
+ * @param {Object=} $browser Auto-flushing enabled if specified
+ * @return {Object} Instance of $httpBackend mock
+ */
+function createHttpBackendMock($rootScope, $delegate, $browser) {
+ var definitions = [],
+ expectations = [],
+ responses = [],
+ responsesPush = angular.bind(responses, responses.push),
+ copy = angular.copy;
+
+ function createResponse(status, data, headers, statusText) {
+ if (angular.isFunction(status)) return status;
+
+ return function() {
+ return angular.isNumber(status)
+ ? [status, data, headers, statusText]
+ : [200, status, data];
+ };
+ }
+
+ // TODO(vojta): change params to: method, url, data, headers, callback
+ function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) {
+ var xhr = new MockXhr(),
+ expectation = expectations[0],
+ wasExpected = false;
+
+ function prettyPrint(data) {
+ return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
+ ? data
+ : angular.toJson(data);
+ }
+
+ function wrapResponse(wrapped) {
+ if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
+
+ return handleResponse;
+
+ function handleResponse() {
+ var response = wrapped.response(method, url, data, headers);
+ xhr.$$respHeaders = response[2];
+ callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
+ copy(response[3] || ''));
+ }
+
+ function handleTimeout() {
+ for (var i = 0, ii = responses.length; i < ii; i++) {
+ if (responses[i] === handleResponse) {
+ responses.splice(i, 1);
+ callback(-1, undefined, '');
+ break;
+ }
+ }
+ }
+ }
+
+ if (expectation && expectation.match(method, url)) {
+ if (!expectation.matchData(data))
+ throw new Error('Expected ' + expectation + ' with different data\n' +
+ 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
+
+ if (!expectation.matchHeaders(headers))
+ throw new Error('Expected ' + expectation + ' with different headers\n' +
+ 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
+ prettyPrint(headers));
+
+ expectations.shift();
+
+ if (expectation.response) {
+ responses.push(wrapResponse(expectation));
+ return;
+ }
+ wasExpected = true;
+ }
+
+ var i = -1, definition;
+ while ((definition = definitions[++i])) {
+ if (definition.match(method, url, data, headers || {})) {
+ if (definition.response) {
+ // if $browser specified, we do auto flush all requests
+ ($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
+ } else if (definition.passThrough) {
+ $delegate(method, url, data, callback, headers, timeout, withCredentials);
+ } else throw new Error('No response defined !');
+ return;
+ }
+ }
+ throw wasExpected ?
+ new Error('No response defined !') :
+ new Error('Unexpected request: ' + method + ' ' + url + '\n' +
+ (expectation ? 'Expected ' + expectation : 'No more request expected'));
+ }
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#when
+ * @description
+ * Creates a new backend definition.
+ *
+ * @param {string} method HTTP method.
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled.
+ *
+ * - respond –
+ * `{function([status,] data[, headers, statusText])
+ * | function(function(method, url, data, headers)}`
+ * – The respond method takes a set of static data to be returned or a function that can
+ * return an array containing response status (number), response data (string), response
+ * headers (Object), and the text for the status (string).
+ */
+ $httpBackend.when = function(method, url, data, headers) {
+ var definition = new MockHttpExpectation(method, url, data, headers),
+ chain = {
+ respond: function(status, data, headers, statusText) {
+ definition.response = createResponse(status, data, headers, statusText);
+ }
+ };
+
+ if ($browser) {
+ chain.passThrough = function() {
+ definition.passThrough = true;
+ };
+ }
+
+ definitions.push(definition);
+ return chain;
+ };
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenGET
+ * @description
+ * Creates a new backend definition for GET requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenHEAD
+ * @description
+ * Creates a new backend definition for HEAD requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenDELETE
+ * @description
+ * Creates a new backend definition for DELETE requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenPOST
+ * @description
+ * Creates a new backend definition for POST requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenPUT
+ * @description
+ * Creates a new backend definition for PUT requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
+ * data string and returns true if the data is as expected.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenJSONP
+ * @description
+ * Creates a new backend definition for JSONP requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+ createShortMethods('when');
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expect
+ * @description
+ * Creates a new request expectation.
+ *
+ * @param {string} method HTTP method.
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
+ * receives data string and returns true if the data is as expected, or Object if request body
+ * is in JSON format.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current expectation.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ *
+ * - respond –
+ * `{function([status,] data[, headers, statusText])
+ * | function(function(method, url, data, headers)}`
+ * – The respond method takes a set of static data to be returned or a function that can
+ * return an array containing response status (number), response data (string), response
+ * headers (Object), and the text for the status (string).
+ */
+ $httpBackend.expect = function(method, url, data, headers) {
+ var expectation = new MockHttpExpectation(method, url, data, headers);
+ expectations.push(expectation);
+ return {
+ respond: function (status, data, headers, statusText) {
+ expectation.response = createResponse(status, data, headers, statusText);
+ }
+ };
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectGET
+ * @description
+ * Creates a new request expectation for GET requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled. See #expect for more info.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectHEAD
+ * @description
+ * Creates a new request expectation for HEAD requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectDELETE
+ * @description
+ * Creates a new request expectation for DELETE requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectPOST
+ * @description
+ * Creates a new request expectation for POST requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
+ * receives data string and returns true if the data is as expected, or Object if request body
+ * is in JSON format.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectPUT
+ * @description
+ * Creates a new request expectation for PUT requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
+ * receives data string and returns true if the data is as expected, or Object if request body
+ * is in JSON format.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectPATCH
+ * @description
+ * Creates a new request expectation for PATCH requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
+ * receives data string and returns true if the data is as expected, or Object if request body
+ * is in JSON format.
+ * @param {Object=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectJSONP
+ * @description
+ * Creates a new request expectation for JSONP requests. For more info see `expect()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @returns {requestHandler} Returns an object with `respond` method that control how a matched
+ * request is handled.
+ */
+ createShortMethods('expect');
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#flush
+ * @description
+ * Flushes all pending requests using the trained responses.
+ *
+ * @param {number=} count Number of responses to flush (in the order they arrived). If undefined,
+ * all pending requests will be flushed. If there are no pending requests when the flush method
+ * is called an exception is thrown (as this typically a sign of programming error).
+ */
+ $httpBackend.flush = function(count) {
+ $rootScope.$digest();
+ if (!responses.length) throw new Error('No pending request to flush !');
+
+ if (angular.isDefined(count)) {
+ while (count--) {
+ if (!responses.length) throw new Error('No more pending request to flush !');
+ responses.shift()();
+ }
+ } else {
+ while (responses.length) {
+ responses.shift()();
+ }
+ }
+ $httpBackend.verifyNoOutstandingExpectation();
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#verifyNoOutstandingExpectation
+ * @description
+ * Verifies that all of the requests defined via the `expect` api were made. If any of the
+ * requests were not made, verifyNoOutstandingExpectation throws an exception.
+ *
+ * Typically, you would call this method following each test case that asserts requests using an
+ * "afterEach" clause.
+ *
+ * ```js
+ * afterEach($httpBackend.verifyNoOutstandingExpectation);
+ * ```
+ */
+ $httpBackend.verifyNoOutstandingExpectation = function() {
+ $rootScope.$digest();
+ if (expectations.length) {
+ throw new Error('Unsatisfied requests: ' + expectations.join(', '));
+ }
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#verifyNoOutstandingRequest
+ * @description
+ * Verifies that there are no outstanding requests that need to be flushed.
+ *
+ * Typically, you would call this method following each test case that asserts requests using an
+ * "afterEach" clause.
+ *
+ * ```js
+ * afterEach($httpBackend.verifyNoOutstandingRequest);
+ * ```
+ */
+ $httpBackend.verifyNoOutstandingRequest = function() {
+ if (responses.length) {
+ throw new Error('Unflushed requests: ' + responses.length);
+ }
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $httpBackend#resetExpectations
+ * @description
+ * Resets all request expectations, but preserves all backend definitions. Typically, you would
+ * call resetExpectations during a multiple-phase test when you want to reuse the same instance of
+ * $httpBackend mock.
+ */
+ $httpBackend.resetExpectations = function() {
+ expectations.length = 0;
+ responses.length = 0;
+ };
+
+ return $httpBackend;
+
+
+ function createShortMethods(prefix) {
+ angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
+ $httpBackend[prefix + method] = function(url, headers) {
+ return $httpBackend[prefix](method, url, undefined, headers);
+ };
+ });
+
+ angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
+ $httpBackend[prefix + method] = function(url, data, headers) {
+ return $httpBackend[prefix](method, url, data, headers);
+ };
+ });
+ }
+}
+
+function MockHttpExpectation(method, url, data, headers) {
+
+ this.data = data;
+ this.headers = headers;
+
+ this.match = function(m, u, d, h) {
+ if (method != m) return false;
+ if (!this.matchUrl(u)) return false;
+ if (angular.isDefined(d) && !this.matchData(d)) return false;
+ if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
+ return true;
+ };
+
+ this.matchUrl = function(u) {
+ if (!url) return true;
+ if (angular.isFunction(url.test)) return url.test(u);
+ return url == u;
+ };
+
+ this.matchHeaders = function(h) {
+ if (angular.isUndefined(headers)) return true;
+ if (angular.isFunction(headers)) return headers(h);
+ return angular.equals(headers, h);
+ };
+
+ this.matchData = function(d) {
+ if (angular.isUndefined(data)) return true;
+ if (data && angular.isFunction(data.test)) return data.test(d);
+ if (data && angular.isFunction(data)) return data(d);
+ if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
+ return data == d;
+ };
+
+ this.toString = function() {
+ return method + ' ' + url;
+ };
+}
+
+function createMockXhr() {
+ return new MockXhr();
+}
+
+function MockXhr() {
+
+ // hack for testing $http, $httpBackend
+ MockXhr.$$lastInstance = this;
+
+ this.open = function(method, url, async) {
+ this.$$method = method;
+ this.$$url = url;
+ this.$$async = async;
+ this.$$reqHeaders = {};
+ this.$$respHeaders = {};
+ };
+
+ this.send = function(data) {
+ this.$$data = data;
+ };
+
+ this.setRequestHeader = function(key, value) {
+ this.$$reqHeaders[key] = value;
+ };
+
+ this.getResponseHeader = function(name) {
+ // the lookup must be case insensitive,
+ // that's why we try two quick lookups first and full scan last
+ var header = this.$$respHeaders[name];
+ if (header) return header;
+
+ name = angular.lowercase(name);
+ header = this.$$respHeaders[name];
+ if (header) return header;
+
+ header = undefined;
+ angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
+ if (!header && angular.lowercase(headerName) == name) header = headerVal;
+ });
+ return header;
+ };
+
+ this.getAllResponseHeaders = function() {
+ var lines = [];
+
+ angular.forEach(this.$$respHeaders, function(value, key) {
+ lines.push(key + ': ' + value);
+ });
+ return lines.join('\n');
+ };
+
+ this.abort = angular.noop;
+}
+
+
+/**
+ * @ngdoc service
+ * @name $timeout
+ * @description
+ *
+ * This service is just a simple decorator for {@link ng.$timeout $timeout} service
+ * that adds a "flush" and "verifyNoPendingTasks" methods.
+ */
+
+angular.mock.$TimeoutDecorator = function($delegate, $browser) {
+
+ /**
+ * @ngdoc method
+ * @name $timeout#flush
+ * @description
+ *
+ * Flushes the queue of pending tasks.
+ *
+ * @param {number=} delay maximum timeout amount to flush up until
+ */
+ $delegate.flush = function(delay) {
+ $browser.defer.flush(delay);
+ };
+
+ /**
+ * @ngdoc method
+ * @name $timeout#verifyNoPendingTasks
+ * @description
+ *
+ * Verifies that there are no pending tasks that need to be flushed.
+ */
+ $delegate.verifyNoPendingTasks = function() {
+ if ($browser.deferredFns.length) {
+ throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
+ formatPendingTasksAsString($browser.deferredFns));
+ }
+ };
+
+ function formatPendingTasksAsString(tasks) {
+ var result = [];
+ angular.forEach(tasks, function(task) {
+ result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}');
+ });
+
+ return result.join(', ');
+ }
+
+ return $delegate;
+};
+
+angular.mock.$RAFDecorator = function($delegate) {
+ var queue = [];
+ var rafFn = function(fn) {
+ var index = queue.length;
+ queue.push(fn);
+ return function() {
+ queue.splice(index, 1);
+ };
+ };
+
+ rafFn.supported = $delegate.supported;
+
+ rafFn.flush = function() {
+ if(queue.length === 0) {
+ throw new Error('No rAF callbacks present');
+ }
+
+ var length = queue.length;
+ for(var i=0;i<length;i++) {
+ queue[i]();
+ }
+
+ queue = [];
+ };
+
+ return rafFn;
+};
+
+angular.mock.$AsyncCallbackDecorator = function($delegate) {
+ var callbacks = [];
+ var addFn = function(fn) {
+ callbacks.push(fn);
+ };
+ addFn.flush = function() {
+ angular.forEach(callbacks, function(fn) {
+ fn();
+ });
+ callbacks = [];
+ };
+ return addFn;
+};
+
+/**
+ *
+ */
+angular.mock.$RootElementProvider = function() {
+ this.$get = function() {
+ return angular.element('<div ng-app></div>');
+ };
+};
+
+/**
+ * @ngdoc module
+ * @name ngMock
+ * @packageName angular-mocks
+ * @description
+ *
+ * # ngMock
+ *
+ * The `ngMock` module provides support to inject and mock Angular services into unit tests.
+ * In addition, ngMock also extends various core ng services such that they can be
+ * inspected and controlled in a synchronous manner within test code.
+ *
+ *
+ * <div doc-module-components="ngMock"></div>
+ *
+ */
+angular.module('ngMock', ['ng']).provider({
+ $browser: angular.mock.$BrowserProvider,
+ $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
+ $log: angular.mock.$LogProvider,
+ $interval: angular.mock.$IntervalProvider,
+ $httpBackend: angular.mock.$HttpBackendProvider,
+ $rootElement: angular.mock.$RootElementProvider
+}).config(['$provide', function($provide) {
+ $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
+ $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
+ $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
+}]);
+
+/**
+ * @ngdoc module
+ * @name ngMockE2E
+ * @module ngMockE2E
+ * @packageName angular-mocks
+ * @description
+ *
+ * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing.
+ * Currently there is only one mock present in this module -
+ * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock.
+ */
+angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
+ $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
+}]);
+
+/**
+ * @ngdoc service
+ * @name $httpBackend
+ * @module ngMockE2E
+ * @description
+ * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of
+ * applications that use the {@link ng.$http $http service}.
+ *
+ * *Note*: For fake http backend implementation suitable for unit testing please see
+ * {@link ngMock.$httpBackend unit-testing $httpBackend mock}.
+ *
+ * This implementation can be used to respond with static or dynamic responses via the `when` api
+ * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the
+ * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch
+ * templates from a webserver).
+ *
+ * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application
+ * is being developed with the real backend api replaced with a mock, it is often desirable for
+ * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch
+ * templates or static files from the webserver). To configure the backend with this behavior
+ * use the `passThrough` request handler of `when` instead of `respond`.
+ *
+ * Additionally, we don't want to manually have to flush mocked out requests like we do during unit
+ * testing. For this reason the e2e $httpBackend flushes mocked out requests
+ * automatically, closely simulating the behavior of the XMLHttpRequest object.
+ *
+ * To setup the application to run with this http backend, you have to create a module that depends
+ * on the `ngMockE2E` and your application modules and defines the fake backend:
+ *
+ * ```js
+ * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
+ * myAppDev.run(function($httpBackend) {
+ * phones = [{name: 'phone1'}, {name: 'phone2'}];
+ *
+ * // returns the current list of phones
+ * $httpBackend.whenGET('/phones').respond(phones);
+ *
+ * // adds a new phone to the phones array
+ * $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
+ * var phone = angular.fromJson(data);
+ * phones.push(phone);
+ * return [200, phone, {}];
+ * });
+ * $httpBackend.whenGET(/^\/templates\//).passThrough();
+ * //...
+ * });
+ * ```
+ *
+ * Afterwards, bootstrap your app with this new module.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#when
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition.
+ *
+ * @param {string} method HTTP method.
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
+ * object and returns true if the headers match the current definition.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ *
+ * - respond –
+ * `{function([status,] data[, headers, statusText])
+ * | function(function(method, url, data, headers)}`
+ * – The respond method takes a set of static data to be returned or a function that can return
+ * an array containing response status (number), response data (string), response headers
+ * (Object), and the text for the status (string).
+ * - passThrough – `{function()}` – Any request matching a backend definition with
+ * `passThrough` handler will be passed through to the real backend (an XHR request will be made
+ * to the server.)
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenGET
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for GET requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenHEAD
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for HEAD requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenDELETE
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for DELETE requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenPOST
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for POST requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenPUT
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for PUT requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenPATCH
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for PATCH requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @param {(string|RegExp)=} data HTTP request body.
+ * @param {(Object|function(Object))=} headers HTTP headers.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenJSONP
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition for JSONP requests. For more info see `when()`.
+ *
+ * @param {string|RegExp} url HTTP url.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled.
+ */
+angular.mock.e2e = {};
+angular.mock.e2e.$httpBackendDecorator =
+ ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
+
+
+angular.mock.clearDataCache = function() {
+ var key,
+ cache = angular.element.cache;
+
+ for(key in cache) {
+ if (Object.prototype.hasOwnProperty.call(cache,key)) {
+ var handle = cache[key].handle;
+
+ handle && angular.element(handle.elem).off();
+ delete cache[key];
+ }
+ }
+};
+
+
+if(window.jasmine || window.mocha) {
+
+ var currentSpec = null,
+ isSpecRunning = function() {
+ return !!currentSpec;
+ };
+
+
+ (window.beforeEach || window.setup)(function() {
+ currentSpec = this;
+ });
+
+ (window.afterEach || window.teardown)(function() {
+ var injector = currentSpec.$injector;
+
+ angular.forEach(currentSpec.$modules, function(module) {
+ if (module && module.$$hashKey) {
+ module.$$hashKey = undefined;
+ }
+ });
+
+ currentSpec.$injector = null;
+ currentSpec.$modules = null;
+ currentSpec = null;
+
+ if (injector) {
+ injector.get('$rootElement').off();
+ injector.get('$browser').pollFns.length = 0;
+ }
+
+ angular.mock.clearDataCache();
+
+ // clean up jquery's fragment cache
+ angular.forEach(angular.element.fragments, function(val, key) {
+ delete angular.element.fragments[key];
+ });
+
+ MockXhr.$$lastInstance = null;
+
+ angular.forEach(angular.callbacks, function(val, key) {
+ delete angular.callbacks[key];
+ });
+ angular.callbacks.counter = 0;
+ });
+
+ /**
+ * @ngdoc function
+ * @name angular.mock.module
+ * @description
+ *
+ * *NOTE*: This function is also published on window for easy access.<br>
+ *
+ * This function registers a module configuration code. It collects the configuration information
+ * which will be used when the injector is created by {@link angular.mock.inject inject}.
+ *
+ * See {@link angular.mock.inject inject} for usage example
+ *
+ * @param {...(string|Function|Object)} fns any number of modules which are represented as string
+ * aliases or as anonymous module initialization functions. The modules are used to
+ * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
+ * object literal is passed they will be registered as values in the module, the key being
+ * the module name and the value being what is returned.
+ */
+ window.module = angular.mock.module = function() {
+ var moduleFns = Array.prototype.slice.call(arguments, 0);
+ return isSpecRunning() ? workFn() : workFn;
+ /////////////////////
+ function workFn() {
+ if (currentSpec.$injector) {
+ throw new Error('Injector already created, can not register a module!');
+ } else {
+ var modules = currentSpec.$modules || (currentSpec.$modules = []);
+ angular.forEach(moduleFns, function(module) {
+ if (angular.isObject(module) && !angular.isArray(module)) {
+ modules.push(function($provide) {
+ angular.forEach(module, function(value, key) {
+ $provide.value(key, value);
+ });
+ });
+ } else {
+ modules.push(module);
+ }
+ });
+ }
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name angular.mock.inject
+ * @description
+ *
+ * *NOTE*: This function is also published on window for easy access.<br>
+ *
+ * The inject function wraps a function into an injectable function. The inject() creates new
+ * instance of {@link auto.$injector $injector} per test, which is then used for
+ * resolving references.
+ *
+ *
+ * ## Resolving References (Underscore Wrapping)
+ * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this
+ * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable
+ * that is declared in the scope of the `describe()` block. Since we would, most likely, want
+ * the variable to have the same name of the reference we have a problem, since the parameter
+ * to the `inject()` function would hide the outer variable.
+ *
+ * To help with this, the injected parameters can, optionally, be enclosed with underscores.
+ * These are ignored by the injector when the reference name is resolved.
+ *
+ * For example, the parameter `_myService_` would be resolved as the reference `myService`.
+ * Since it is available in the function body as _myService_, we can then assign it to a variable
+ * defined in an outer scope.
+ *
+ * ```
+ * // Defined out reference variable outside
+ * var myService;
+ *
+ * // Wrap the parameter in underscores
+ * beforeEach( inject( function(_myService_){
+ * myService = _myService_;
+ * }));
+ *
+ * // Use myService in a series of tests.
+ * it('makes use of myService', function() {
+ * myService.doStuff();
+ * });
+ *
+ * ```
+ *
+ * See also {@link angular.mock.module angular.mock.module}
+ *
+ * ## Example
+ * Example of what a typical jasmine tests looks like with the inject method.
+ * ```js
+ *
+ * angular.module('myApplicationModule', [])
+ * .value('mode', 'app')
+ * .value('version', 'v1.0.1');
+ *
+ *
+ * describe('MyApp', function() {
+ *
+ * // You need to load modules that you want to test,
+ * // it loads only the "ng" module by default.
+ * beforeEach(module('myApplicationModule'));
+ *
+ *
+ * // inject() is used to inject arguments of all given functions
+ * it('should provide a version', inject(function(mode, version) {
+ * expect(version).toEqual('v1.0.1');
+ * expect(mode).toEqual('app');
+ * }));
+ *
+ *
+ * // The inject and module method can also be used inside of the it or beforeEach
+ * it('should override a version and test the new version is injected', function() {
+ * // module() takes functions or strings (module aliases)
+ * module(function($provide) {
+ * $provide.value('version', 'overridden'); // override version here
+ * });
+ *
+ * inject(function(version) {
+ * expect(version).toEqual('overridden');
+ * });
+ * });
+ * });
+ *
+ * ```
+ *
+ * @param {...Function} fns any number of functions which will be injected using the injector.
+ */
+
+
+
+ var ErrorAddingDeclarationLocationStack = function(e, errorForStack) {
+ this.message = e.message;
+ this.name = e.name;
+ if (e.line) this.line = e.line;
+ if (e.sourceId) this.sourceId = e.sourceId;
+ if (e.stack && errorForStack)
+ this.stack = e.stack + '\n' + errorForStack.stack;
+ if (e.stackArray) this.stackArray = e.stackArray;
+ };
+ ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString;
+
+ window.inject = angular.mock.inject = function() {
+ var blockFns = Array.prototype.slice.call(arguments, 0);
+ var errorForStack = new Error('Declaration Location');
+ return isSpecRunning() ? workFn.call(currentSpec) : workFn;
+ /////////////////////
+ function workFn() {
+ var modules = currentSpec.$modules || [];
+
+ modules.unshift('ngMock');
+ modules.unshift('ng');
+ var injector = currentSpec.$injector;
+ if (!injector) {
+ injector = currentSpec.$injector = angular.injector(modules);
+ }
+ for(var i = 0, ii = blockFns.length; i < ii; i++) {
+ try {
+ /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
+ injector.invoke(blockFns[i] || angular.noop, this);
+ /* jshint +W040 */
+ } catch (e) {
+ if (e.stack && errorForStack) {
+ throw new ErrorAddingDeclarationLocationStack(e, errorForStack);
+ }
+ throw e;
+ } finally {
+ errorForStack = null;
+ }
+ }
+ }
+ };
+}
+
+
+})(window, window.angular);
\ No newline at end of file
diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js
index e8323f4a31..468a746b18 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -37,6 +37,10 @@ angular.module('RecentsController')
filtered.push(room);
}
+ else if ("invite" === room.membership) {
+ // The only information we have about the room is that the user has been invited
+ filtered.push(room);
+ }
});
// And time sort them
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index de50058743..ac8f767d16 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -15,8 +15,8 @@ limitations under the License.
*/
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
-.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
- function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
+.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
+ function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
'use strict';
var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320;
@@ -189,7 +189,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// Notify when a user joins
if ((document.hidden || matrixService.presence.unavailable === mPresence.getState())
&& event.state_key !== $scope.state.user_id && "join" === event.membership) {
- debugger;
var notification = new window.Notification(
event.content.displayname +
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
@@ -704,7 +703,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
if ($rootScope.events.rooms[$scope.room_id]) {
// There is no need to do a 1st pagination (initialSync provided enough to fill a page)
- $scope.state.first_pagination = false;
+ if ($rootScope.events.rooms[$scope.room_id].messages.length) {
+ $scope.state.first_pagination = false;
+ }
+ else {
+ // except if we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway
+ $scope.state.first_pagination = true;
+ }
var members = $rootScope.events.rooms[$scope.room_id].members;
diff --git a/webclient/settings/settings.html b/webclient/settings/settings.html
index 0a5a3db51f..094c846f8b 100644
--- a/webclient/settings/settings.html
+++ b/webclient/settings/settings.html
@@ -51,10 +51,10 @@
<h3>Desktop notifications</h3>
<div class="section" ng-switch="settings.notifications">
<div ng-switch-when="granted">
- Notifications are enabled. You will be alerted when a message contains your user ID or display name.
+ Notifications are enabled.
<div class="section">
- <h4>Additional words to alert on:</h4>
- <p>Leave blank to alert on all messages.</p>
+ <h4>Specific words to alert on:</h4>
+ <p>Leave blank to alert on all messages. Your username & display name always alerts.</p>
<input size=40 name="bingWords" ng-model="settings.bingWords" ng-list placeholder="Enter words separated with , (supports regex)"
ng-blur="saveBingWords()"/>
<ul>
diff --git a/webclient/test/karma.conf.js b/webclient/test/karma.conf.js
new file mode 100644
index 0000000000..22c4eaaafa
--- /dev/null
+++ b/webclient/test/karma.conf.js
@@ -0,0 +1,82 @@
+// Karma configuration
+// Generated on Thu Sep 18 2014 14:25:57 GMT+0100 (BST)
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ // XXX: Order is important, and doing /js/angular* makes the tests not run :/
+ files: [
+ '../js/jquery*',
+ '../js/angular.js',
+ '../js/angular-mocks.js',
+ '../js/angular-route.js',
+ '../js/angular-animate.js',
+ '../js/angular-sanitize.js',
+ '../js/ng-infinite-scroll-matrix.js',
+ '../login/**/*.*',
+ '../room/**/*.*',
+ '../components/**/*.*',
+ '../user/**/*.*',
+ '../home/**/*.*',
+ '../recents/**/*.*',
+ '../settings/**/*.*',
+ '../app.js',
+ '../app*',
+ './unit/**/*.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_DEBUG,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Chrome'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false
+ });
+};
diff --git a/webclient/test/unit/user-controller.spec.js b/webclient/test/unit/user-controller.spec.js
new file mode 100644
index 0000000000..217559114b
--- /dev/null
+++ b/webclient/test/unit/user-controller.spec.js
@@ -0,0 +1,59 @@
+describe("UserCtrl", function() {
+ var scope, ctrl, matrixService, routeParams, $q, $timeout;
+ var userId = "@foo:bar";
+ var displayName = "Foo";
+ var avatarUrl = "avatar.url";
+
+ beforeEach(module('matrixWebClient'));
+
+ beforeEach(function() {
+
+ inject(function($rootScope, $injector, $controller, _$q_, _$timeout_) {
+ $q = _$q_;
+ $timeout = _$timeout_;
+
+ matrixService = {
+ config: function() {
+ return {
+ user_id: userId
+ };
+ },
+
+ getDisplayName: function(uid) {
+ var d = $q.defer();
+ // FIXME: everything goes into fire here
+ d.resolve({
+ data: {
+ displayname: displayName
+ }
+ });
+ return d;
+ },
+
+ getProfilePictureUrl: function(uid) {
+ var d = $q.defer();
+ d.resolve({
+ data: {
+ avatar_url: avatarUrl
+ }
+ });
+ return d;
+ }
+ };
+ scope = $rootScope.$new();
+ routeParams = {
+ user_matrix_id: userId
+ };
+ ctrl = $controller('UserController', {
+ '$scope': scope,
+ '$routeParams': routeParams,
+ 'matrixService': matrixService
+ });
+ console.log("end inject");
+ });
+ });
+
+ it('should display your user id', function() {
+ expect(scope.user_id).toEqual(userId);
+ });
+});
|