friends-0.2.0+14.04.20140217.1/ 0000755 0000152 0177776 00000000000 12300444701 015672 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/ 0000755 0000152 0177776 00000000000 12300444701 017332 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/ChangeLog 0000644 0000152 0177776 00000000000 12300444435 021076 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/configure.ac 0000644 0000152 0177776 00000001110 12300444435 021615 0 ustar pbuser nogroup 0000000 0000000 AC_INIT([friends-service], 0.1)
AM_INIT_AUTOMAKE([no-dist-gzip dist-bzip2])
AC_CONFIG_HEADERS([config.h])
AM_SILENT_RULES([yes])
AM_MAINTAINER_MODE
AM_PROG_VALAC([0.16])
AC_PROG_CC
AM_PROG_CC_STDC
AC_PROG_INSTALL
###########################
# GSETTINGS
###########################
GLIB_GSETTINGS
DEE_REQUIRED=1.0.0
PKG_CHECK_MODULES(BASE,
libaccounts-glib
gio-2.0
dee-1.0 >= $DEE_REQUIRED)
AC_SUBST(BASE_CFLAGS)
AC_SUBST(BASE_LIBS)
AC_CONFIG_FILES([
Makefile
src/Makefile
data/Makefile
])
AC_OUTPUT
friends-0.2.0+14.04.20140217.1/service/NEWS 0000644 0000152 0177776 00000000000 12300444435 020023 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/README 0000644 0000152 0177776 00000000000 12300444435 020204 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/autogen.sh 0000755 0000152 0177776 00000000244 12300444435 021337 0 ustar pbuser nogroup 0000000 0000000 #!/bin/sh
PKG_NAME="friends-service"
which gnome-autogen.sh || {
echo "You need gnome-common from GNOME SVN"
exit 1
}
USE_GNOME2_MACROS=1 \
. gnome-autogen.sh
friends-0.2.0+14.04.20140217.1/service/AUTHORS 0000644 0000152 0177776 00000000000 12300444435 020374 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/Makefile.am 0000644 0000152 0177776 00000000070 12300444435 021367 0 ustar pbuser nogroup 0000000 0000000 SUBDIRS = src data
dist_noinst_SCRIPTS = \
autogen.sh
friends-0.2.0+14.04.20140217.1/service/src/ 0000755 0000152 0177776 00000000000 12300444701 020121 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/src/service.vala 0000644 0000152 0177776 00000020164 12300444435 022435 0 ustar pbuser nogroup 0000000 0000000 /*
* Copyright (C) 2013 Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as published
* by the Free Software Foundation.
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranties of
* MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*
* Authored by Ken VanDine
*/
using Ag;
[DBus (name = "com.canonical.Friends.Dispatcher")]
private interface Dispatcher : GLib.Object {
public abstract void Refresh () throws GLib.IOError;
public abstract async void Do (
string action,
string account_id,
string message_id,
out string result
) throws GLib.IOError;
}
[DBus (name = "com.canonical.Friends.Service")]
public class Master : Object
{
private Dee.Model model;
private Dee.SharedModel shared_model;
private unowned Dee.ResourceManager resources;
private Ag.Manager acct_manager;
private Dispatcher dispatcher;
public int interval { get; set; }
public Master ()
{
acct_manager = new Ag.Manager.for_service_type ("microblogging");
acct_manager.account_deleted.connect ((manager, account_id) => {
debug ("Account %u deleted from UOA, purging...", account_id);
uint purged = 0;
uint rows = model.get_n_rows ();
// Destructively iterate over the Model from back to
// front; I know "i < rows" looks kinda goofy here,
// but what's happening is that i is unsigned, so once
// it hits 0, i-- will overflow to a very large
// number, and then "i < rows" will fail, stopping the
// iteration at index 0.
for (uint i = rows - 1; i < rows; i--) {
var itr = model.get_iter_at_row (i);
if (model.get_uint64 (itr, 1) == account_id) {
model.remove (itr);
purged++;
}
}
debug ("Purged %u rows.", purged);
}
);
acct_manager.account_created.connect ((manager, account_id) => {
debug ("Account %u created from UOA, refreshing", account_id);
try {
dispatcher.Refresh ();
} catch (IOError e) {
warning ("Failed to refresh - %s", e.message);
}
}
);
resources = Dee.ResourceManager.get_default ();
model = new Dee.SequenceModel ();
Dee.SequenceModel? _m = null;
try {
_m = resources.load ("com.canonical.Friends.Streams") as Dee.SequenceModel;
} catch (Error e) {
debug ("Failed to load model from resource manager: %s", e.message);
}
string[] SCHEMA = {};
var file = FileStream.open("/usr/share/friends/model-schema.csv", "r");
string line = null;
while (true)
{
line = file.read_line();
if (line == null) break;
SCHEMA += line.split(",")[1];
}
debug ("Found %u schema columns.", SCHEMA.length);
bool schemaReset = false;
if (_m is Dee.Model && !schemaReset)
{
debug ("Got a valid model");
// Compare columns from cached model's schema
string[] _SCHEMA = _m.get_schema ();
if (_SCHEMA.length != SCHEMA.length)
schemaReset = true;
else
{
for (int i=0; i < _SCHEMA.length; i++)
{
if (_SCHEMA[i] != SCHEMA[i])
{
debug ("SCHEMA MISMATCH");
schemaReset = true;
}
}
}
if (!schemaReset)
model = _m;
else
{
debug ("Setting schema");
model.set_schema_full (SCHEMA);
}
} else {
debug ("Setting schema for a new model");
model.set_schema_full (SCHEMA);
}
var settings = new Settings ("com.canonical.friends");
settings.bind("interval", this, "interval", 0);
if (model is Dee.Model)
{
debug ("Model with %u rows", model.get_n_rows());
Dee.Peer peer = Object.new (typeof(Dee.Peer),
swarm_name: "com.canonical.Friends.Streams",
swarm_owner: true,
null) as Dee.Peer;
peer.peer_found.connect((peername) => {
debug ("new peer: %s", peername);
});
peer.peer_lost.connect((peername) => {
debug ("lost peer: %s", peername);
});
Dee.SharedModel shared_model = Object.new (typeof(Dee.SharedModel),
peer: peer,
back_end: model,
null) as Dee.SharedModel;
debug ("swarm leader: %s", peer.get_swarm_leader());
shared_model.notify["synchronized"].connect(() => {
if (shared_model.is_synchronized()) {
debug ("SYNCHRONIZED");
shared_model.flush_revision_queue();
}
if (shared_model.is_leader())
debug ("LEADER");
else
debug ("NOT LEADER");
});
Timeout.add_seconds (interval * 60, () => {
shared_model.flush_revision_queue();
debug ("Storing model with %u rows", model.get_n_rows());
resources.store ((Dee.SequenceModel)model, "com.canonical.Friends.Streams");
return true;
});
}
Bus.get_proxy.begin(BusType.SESSION,
"com.canonical.Friends.Dispatcher",
"/com/canonical/friends/Dispatcher",
DBusProxyFlags.NONE, null, on_proxy_cb);
}
private void on_proxy_cb (GLib.Object? obj, GLib.AsyncResult res)
{
try {
dispatcher = Bus.get_proxy.end(res);
// Timeout.add_seconds (120, fetch_contacts); // LP#1214639
var ret = on_refresh ();
} catch (IOError e) {
warning (e.message);
}
}
bool on_refresh ()
{
debug ("Interval is %d", interval);
// By default, this happens immediately on startup, and then
// every 15 minutes thereafter.
Timeout.add_seconds ((interval * 60), on_refresh);
try {
dispatcher.Refresh ();
} catch (IOError e) {
warning ("Failed to refresh - %s", e.message);
}
return false;
}
/* Temporarily disabled as requested by Bill Filler, LP#1214639
* Most likely we'll need a gsetting to turn this off or on,
* but just disable it for now.
bool fetch_contacts ()
{
debug ("Fetching contacts...");
// By default, this happens 2 minutes after startup, and then
// every 24 hours thereafter.
Timeout.add_seconds (86400, fetch_contacts);
try {
dispatcher.Do ("contacts", "", "");
} catch (IOError e) {
warning ("Failed to fetch contacts - %s", e.message);
}
return false;
}
*/
}
void on_bus_aquired (DBusConnection conn) {
try {
conn.register_object ("/com/canonical/friends/Service", new Master ());
} catch (IOError e) {
stderr.printf ("Could not register service\n");
}
}
public static int main (string[] args)
{
Bus.own_name (BusType.SESSION, "com.canonical.Friends.Service", BusNameOwnerFlags.NONE,
on_bus_aquired,
() => {},
() => stderr.printf ("Could not aquire name\n"));
new MainLoop().run();
return 0;
}
friends-0.2.0+14.04.20140217.1/service/src/Makefile.am 0000644 0000152 0177776 00000000335 12300444435 022162 0 ustar pbuser nogroup 0000000 0000000 bin_PROGRAMS = \
friends-service
INCLUDES = \
$(BASE_CFLAGS)
VALAFLAGS = \
--pkg libaccounts-glib \
--pkg dee-1.0 \
--pkg gio-2.0
friends_service_LDADD = \
$(BASE_LIBS)
friends_service_SOURCES = \
service.vala
friends-0.2.0+14.04.20140217.1/service/data/ 0000755 0000152 0177776 00000000000 12300444701 020243 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/service/data/com.canonical.Friends.Service.service.in 0000644 0000152 0177776 00000000121 12300444435 027724 0 ustar pbuser nogroup 0000000 0000000 [D-BUS Service]
Name=com.canonical.Friends.Service
Exec=@bindir@/friends-service
friends-0.2.0+14.04.20140217.1/service/data/Makefile.am 0000644 0000152 0177776 00000000474 12300444435 022310 0 ustar pbuser nogroup 0000000 0000000 dbus_servicesdir = $(datadir)/dbus-1/services
service_in_files = com.canonical.Friends.Service.service.in
dbus_services_DATA = $(service_in_files:.service.in=.service)
%.service: %.service.in
sed -e "s|\@bindir\@|$(bindir)|" $< > $@
EXTRA_DIST = \
$(service_in_files)
CLEANFILES = \
$(dbus_services_DATA)
friends-0.2.0+14.04.20140217.1/setup.py 0000644 0000152 0177776 00000003041 12300444435 017406 0 ustar pbuser nogroup 0000000 0000000 # friends -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
from setuptools import setup, find_packages
if sys.version_info[:2] < (3, 2):
raise RuntimeError('Python 3.2 or newer required')
setup(
name='friends',
version='0.1',
packages=find_packages(),
include_package_data=True,
package_data = {
'friends.service.templates': ['*.service.in'],
'friends.tests.data': ['*.dat'],
},
data_files = [
('/usr/share/glib-2.0/schemas',
['data/com.canonical.friends.gschema.xml']),
('/usr/share/friends',
['data/model-schema.csv']),
],
entry_points = {
'console_scripts': ['friends-dispatcher = friends.main:main'],
'distutils.commands': [
'install_service_files = '
'friends.utils.install:install_service_files',
],
},
test_requires = [
'mock',
],
)
friends-0.2.0+14.04.20140217.1/tools/ 0000755 0000152 0177776 00000000000 12300444701 017032 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/tools/debug_live.py 0000755 0000152 0177776 00000004145 12300444435 021524 0 ustar pbuser nogroup 0000000 0000000 #!/usr/bin/env python3
"""Usage: ./tools/debug_live.py PROTOCOL OPERATION [OPTIONS]
Where PROTOCOL is a protocol supported by Friends, such as 'twitter',
OPERATION is an instance method defined in that protocol's class, and OPTIONS
are whatever arguments you'd like to pass to that method (if any), such as
message id's or a status message.
Examples:
./tools/debug_live.py twitter home
./tools/debug_live.py twitter send 'Hello, world!'
This tool is provided to aid with rapid feedback of changes made to the
friends source tree, and as such is designed to be run from the same directory
that contains 'setup.py'.
It is not intended for use with an installed friends package.
"""
import sys
import logging
sys.path.insert(0, '.')
# Ignore system-installed schema.
from friends.tests.mocks import SCHEMA
from gi.repository import GLib
from friends.utils.logging import initialize
# Print all logs for debugging purposes
initialize(debug=True, console=True)
from friends.service.dispatcher import ManageTimers
from friends.utils.account import find_accounts
from friends.utils.base import initialize_caches, _OperationThread
from friends.utils.model import Model
log = logging.getLogger('friends.debug_live')
loop = GLib.MainLoop()
def row_added(model, itr):
row = model.get_row(itr)
print(row)
log.info('ROWS: {}'.format(len(model)))
print()
def setup(model, signal, protocol, args):
_OperationThread.shutdown = loop.quit
initialize_caches()
Model.connect('row-added', row_added)
found = False
for account in find_accounts().values():
if account.protocol._name == protocol.lower():
found = True
with ManageTimers() as cm:
account.protocol(*args)
if not found:
log.error('No {} found in Ubuntu Online Accounts!'.format(protocol))
loop.quit()
if __name__ == '__main__':
if len(sys.argv) < 3:
sys.exit(__doc__)
protocol = sys.argv[1]
args = sys.argv[2:]
ManageTimers.callback = loop.quit
ManageTimers.timeout = 5
Model.connect('notify::synchronized', setup, protocol, args)
loop.run()
friends-0.2.0+14.04.20140217.1/tools/debug_slave.py 0000755 0000152 0177776 00000001761 12300444435 021700 0 ustar pbuser nogroup 0000000 0000000 #!/usr/bin/env python3
"""Usage: ./tools/debug_slave.py
Run this script in parallel with debug_live.py to watch changes to the Friends
database model as it is updated over dbus.
It is not intended for use with an installed friends package.
"""
import sys
sys.path.insert(0, '.')
from gi.repository import Dee
from gi.repository import GLib
from friends.tests.mocks import SCHEMA
class Slave:
def __init__(self):
model_name = 'com.canonical.Friends.Streams'
print('Joining model ' + model_name)
self.model = Dee.SharedModel.new(model_name)
self.model.connect('row-added', self.on_row_added)
self.model.connect('row-changed', self.on_row_added)
def on_row_added(self, model, itr):
row = self.model.get_row(itr)
print('\n' * 5)
for i, col in enumerate(row):
print('{:12}: {}'.format(SCHEMA.NAMES[i], col))
print('ROWS: ', len(self.model))
if __name__ == '__main__':
s = Slave()
GLib.MainLoop().run()
friends-0.2.0+14.04.20140217.1/docs/ 0000755 0000152 0177776 00000000000 12300444701 016622 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/docs/_static/ 0000755 0000152 0177776 00000000000 12300444701 020250 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/docs/index.rst 0000644 0000152 0177776 00000002410 12300444435 020464 0 ustar pbuser nogroup 0000000 0000000 .. friends documentation master file, created by
sphinx-quickstart on Mon Apr 15 19:32:21 2013.
friends -- send & receive messages from any social network
Copyright (C) 2013 Canonical Ltd
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
The Superclass of all Protocols
===============================
.. automodule:: friends.utils.base
:members:
:special-members:
:private-members:
Dispatcher
==========
.. automodule:: friends.service.dispatcher
:members:
:special-members:
:private-members:
libsoup wrappers
================
.. automodule:: friends.utils.http
:members:
:special-members:
:private-members:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
friends-0.2.0+14.04.20140217.1/docs/Makefile 0000644 0000152 0177776 00000012700 12300444435 020266 0 ustar pbuser nogroup 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/friends.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/friends.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/friends"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/friends"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
friends-0.2.0+14.04.20140217.1/docs/_build/ 0000755 0000152 0177776 00000000000 12300444701 020060 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/docs/conf.py 0000644 0000152 0177776 00000020627 12300444435 020134 0 ustar pbuser nogroup 0000000 0000000 #!/usr/bin/env python3
# friends -- send & receive messages from any social network
# Copyright (C) 2013 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# friends documentation build configuration file, created by
# sphinx-quickstart on Mon Apr 15 19:32:21 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'friends'
copyright = '2013, Canonical Ltd'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.2'
# The full version, including alpha/beta/rc tags.
release = '0.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'friendsdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'friends.tex', 'Friends Documentation',
'Robert Bruce Park, Ken VanDine, Barry Warsaw', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'friends', 'Friends Documentation',
['Robert Bruce Park, Ken VanDine, Barry Warsaw'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'friends', 'Friends Documentation',
'Robert Bruce Park, Ken VanDine, Barry Warsaw', 'friends',
'Social networking integration for linux systems.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
friends-0.2.0+14.04.20140217.1/docs/_templates/ 0000755 0000152 0177776 00000000000 12300444701 020757 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/AUTHORS 0000644 0000152 0177776 00000000243 12300444435 016745 0 ustar pbuser nogroup 0000000 0000000 Robert Bruce Park
Barry Warsaw
Ken VanDine
Conor Curran
friends-0.2.0+14.04.20140217.1/friends/ 0000755 0000152 0177776 00000000000 12300444701 017324 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/__init__.py 0000644 0000152 0177776 00000000000 12300444435 021427 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/errors.py 0000644 0000152 0177776 00000004107 12300444435 021220 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Internal Friends exceptions."""
__all__ = [
'AuthorizationError',
'FriendsError',
'UnsupportedProtocolError',
]
try:
from contextlib import ignored
except ImportError:
from contextlib import contextmanager
# This is a fun toy from Python 3.4, but I want it NOW!!
@contextmanager
def ignored(*exceptions):
"""Context manager to ignore specifed exceptions.
with ignored(OSError):
os.remove(somefile)
Thanks to Raymond Hettinger for this.
"""
try:
yield
except exceptions:
pass
class FriendsError(Exception):
"""Base class for all internal Friends exceptions."""
class AuthorizationError(FriendsError):
"""Backend service authorization errors."""
def __init__(self, account, message):
self.account = account
self.message = message
def __str__(self):
return '{} (account: {})'.format(self.message, self.account)
class ContactsError(FriendsError):
"""Errors relating to EDS contact management."""
def __init__(self, message):
self.message = message
def __str__(self):
return 'EDS: {}'.format(self.message)
class UnsupportedProtocolError(FriendsError):
def __init__(self, protocol):
self.protocol = protocol
def __str__(self):
return 'Unsupported protocol: {}'.format(self.protocol)
friends-0.2.0+14.04.20140217.1/friends/service/ 0000755 0000152 0177776 00000000000 12300444701 020764 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/service/__init__.py 0000644 0000152 0177776 00000000000 12300444435 023067 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/service/templates/ 0000755 0000152 0177776 00000000000 12300444701 022762 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/service/templates/__init__.py 0000644 0000152 0177776 00000000000 12300444435 025065 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/service/templates/com.canonical.Friends.Dispatcher.service.in0000644 0000152 0177776 00000000136 12300444435 033137 0 ustar pbuser nogroup 0000000 0000000 [D-BUS Service]
Name=com.canonical.Friends.Dispatcher
Exec={BINDIR}/friends-dispatcher {ARGS}
friends-0.2.0+14.04.20140217.1/friends/service/dispatcher.py 0000644 0000152 0177776 00000030522 12300444435 023472 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""DBus service object for general dispatching of commands."""
__all__ = [
'Dispatcher',
]
import json
import logging
import threading
import dbus
import dbus.service
from gi.repository import GLib
from contextlib import ContextDecorator
from friends.utils.account import find_accounts
from friends.utils.manager import protocol_manager
from friends.utils.menus import MenuManager
from friends.utils.model import Model, persist_model
from friends.utils.shorteners import Short
from friends.errors import ignored
log = logging.getLogger(__name__)
DBUS_INTERFACE = 'com.canonical.Friends.Dispatcher'
STUB = lambda *ignore, **kwignore: None
# Avoid race condition during shut-down
_exit_lock = threading.Lock()
class ManageTimers(ContextDecorator):
"""Exit the dispatcher 30s after the most recent method call returns."""
timers = set()
callback = STUB
timeout = 30
def __enter__(self):
self.clear_all_timers()
def __exit__(self, *ignore):
self.set_new_timer()
def clear_all_timers(self):
while self.timers:
timer_id = self.timers.pop()
log.debug('Clearing timer id: {}'.format(timer_id))
GLib.source_remove(timer_id)
def set_new_timer(self):
# Concurrency will cause two methods to exit near each other,
# causing two timers to be set, so we have to clear them again.
self.clear_all_timers()
log.debug('Starting new shutdown timer...')
self.timers.add(GLib.timeout_add_seconds(self.timeout, self.terminate))
def terminate(self, *ignore):
"""Exit the dispatcher, but only if there are no active subthreads."""
with _exit_lock:
if threading.activeCount() < 2:
log.debug('No threads found, shutting down.')
persist_model()
self.timers.add(GLib.idle_add(self.callback))
else:
log.debug('Delaying shutdown because active threads found.')
self.set_new_timer()
exit_after_idle = ManageTimers()
class Dispatcher(dbus.service.Object):
"""This is the primary handler of dbus method calls."""
__dbus_object_path__ = '/com/canonical/friends/Dispatcher'
def __init__(self, settings, mainloop):
self.settings = settings
self.bus = dbus.SessionBus()
bus_name = dbus.service.BusName(DBUS_INTERFACE, bus=self.bus)
super().__init__(bus_name, self.__dbus_object_path__)
self.mainloop = mainloop
self.accounts = find_accounts()
self._unread_count = 0
self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit)
Model.connect('row-added', self._increment_unread_count)
ManageTimers.callback = mainloop.quit
def _increment_unread_count(self, model, itr):
self._unread_count += 1
self.menu_manager.update_unread_count(self._unread_count)
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE)
def Refresh(self):
"""Download new messages from each connected protocol."""
self._unread_count = 0
log.debug('Refresh requested')
# account.protocol() starts a new thread and then returns
# immediately, so there is no delay or blocking during the
# execution of this method.
for account in self.accounts.values():
with ignored(NotImplementedError):
account.protocol('receive')
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE)
def ClearIndicators(self):
"""Indicate that messages have been read.
example:
import dbus
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
service.ClearIndicators()
"""
self.menu_manager.update_unread_count(0)
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def Do(self, action, account_id='', arg='',
success=STUB, failure=STUB):
"""Performs an arbitrary operation with an optional argument.
This is how the client initiates retweeting, liking,
searching, etc. See Dispatcher.Upload for an example of how to
use the callbacks.
example:
import dbus
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
service.Do('like', '3', 'post_id') # Likes that FB post.
service.Do('search', '', 'search terms') # Searches all accounts.
service.Do('list', '6', 'list_id') # Fetch a single list.
"""
if account_id:
accounts = [self.accounts.get(int(account_id))]
if None in accounts:
message = 'Could not find account: {}'.format(account_id)
failure(message)
log.error(message)
return
else:
accounts = list(self.accounts.values())
called = False
for account in accounts:
log.debug('{}: {} {}'.format(account.id, action, arg))
args = (action, arg) if arg else (action,)
# Not all accounts are expected to implement every action.
with ignored(NotImplementedError):
account.protocol(*args, success=success, failure=failure)
called = True
if not called:
failure('No accounts supporting {} found.'.format(action))
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE,
in_signature='s',
out_signature='s',
async_callbacks=('success','failure'))
def SendMessage(self, message, success=STUB, failure=STUB):
"""Posts a message/status update to all send_enabled accounts.
It takes one argument, which is a message formated as a
string. See Dispatcher.Upload for an example of how to use the
callbacks.
example:
import dbus
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
service.SendMessage('Your message')
"""
sent = False
for account in self.accounts.values():
if account.send_enabled:
sent = True
log.debug(
'Sending message to {}'.format(
account.protocol._Name))
account.protocol(
'send',
message,
success=success,
failure=failure,
)
if not sent:
failure('No send_enabled accounts found.')
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def SendReply(self, account_id, message_id, message,
success=STUB, failure=STUB):
"""Posts a reply to the indicate message_id on account_id.
It takes three arguments, all strings. See Dispatcher.Upload
for an example of how to use the callbacks.
example:
import dbus
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
service.SendReply('6', '34245645347345626', 'Your reply')
"""
log.debug('Replying to {}, {}'.format(account_id, message_id))
account = self.accounts.get(int(account_id))
if account is not None:
account.protocol(
'send_thread',
message_id,
message,
success=success,
failure=failure,
)
else:
message = 'Could not find account: {}'.format(account_id)
failure(message)
log.error(message)
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def Upload(self, account_id, uri, description, success=STUB, failure=STUB):
"""Upload an image to the specified account_id, asynchronously.
It takes five arguments, three strings and two callback
functions. The URI option is parsed by GFile and thus
seamlessly supports uploading from http:// URLs as well as
file:// paths.
example:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
DBusGMainLoop(set_as_default=True)
loop = GLib.MainLoop()
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
def success(destination_url):
print('successfully uploaded to {}.'.format(destination_url))
def failure(message):
print('failed to upload: {}'.format(message))
service.Upload(
'6',
'file:///path/to/image.png',
'A beautiful picture.',
reply_handler=success,
error_handler=failure)
loop.run()
Note also that the callbacks are actually optional; you are
free to ignore error conditions at your peril.
"""
log.debug('Uploading {} to {}'.format(uri, account_id))
account = self.accounts.get(int(account_id))
if account is not None:
account.protocol(
'upload',
uri,
description,
success=success,
failure=failure,
)
else:
message = 'Could not find account: {}'.format(account_id)
failure(message)
log.error(message)
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
def GetFeatures(self, protocol_name):
"""Returns a list of features supported by service as json string.
example:
import dbus, json
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
features = json.loads(service.GetFeatures('facebook'))
"""
protocol = protocol_manager.protocols.get(protocol_name)
return json.dumps(protocol.get_features() if protocol else [])
@exit_after_idle
@dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
def URLShorten(self, message):
"""Shorten all the URLs in a message.
Takes a message as a string, and returns the message with all
it's URLs shortened.
example:
import dbus
url = 'http://www.example.com/this/is/a/long/url'
obj = dbus.SessionBus().get_object(DBUS_INTERFACE,
'/com/canonical/friends/Dispatcher')
service = dbus.Interface(obj, DBUS_INTERFACE)
short_url = service.URLShorten(url)
"""
service_name = self.settings.get_string('urlshorter')
log.info('Shortening with {}'.format(service_name))
if not self.settings.get_boolean('shorten-urls'):
return message
return Short(service_name).sub(message)
friends-0.2.0+14.04.20140217.1/friends/service/mock_service.py 0000644 0000152 0177776 00000007056 12300444435 024023 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""DBus service object for general dispatching of commands."""
__all__ = [
'Dispatcher',
]
import json
import logging
import dbus
import dbus.service
from friends.service.dispatcher import DBUS_INTERFACE, STUB
log = logging.getLogger(__name__)
class Dispatcher(dbus.service.Object):
"""This object mocks the official friends-dispatcher dbus API."""
__dbus_object_path__ = '/com/canonical/friends/Dispatcher'
def __init__(self, *ignore):
self.bus = dbus.SessionBus()
bus_name = dbus.service.BusName(DBUS_INTERFACE, bus=self.bus)
super().__init__(bus_name, self.__dbus_object_path__)
self._succeed = True
@dbus.service.method(DBUS_INTERFACE)
def Refresh(self):
pass
@dbus.service.method(DBUS_INTERFACE)
def ClearIndicators(self):
self._succeed = False
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def Do(self, action, account_id='', arg='',
success=STUB, failure=STUB):
message = "Called with: action={}, account_id={}, arg={}".format(
action, account_id, arg)
success(message) if self._succeed else failure(message)
@dbus.service.method(DBUS_INTERFACE,
in_signature='s',
out_signature='s',
async_callbacks=('success','failure'))
def SendMessage(self, message, success=STUB, failure=STUB):
message = "Called with: message={}".format(message)
success(message) if self._succeed else failure(message)
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def SendReply(self, account_id, message_id, msg,
success=STUB, failure=STUB):
message = "Called with: account_id={}, message_id={}, msg={}".format(
account_id, message_id, msg)
success(message) if self._succeed else failure(message)
@dbus.service.method(DBUS_INTERFACE,
in_signature='sss',
out_signature='s',
async_callbacks=('success','failure'))
def Upload(self, account_id, uri, description, success=STUB, failure=STUB):
message = "Called with: account_id={}, uri={}, description={}".format(
account_id, uri, description)
success(message) if self._succeed else failure(message)
@dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
def GetFeatures(self, protocol_name):
return json.dumps(protocol_name.split())
@dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
def URLShorten(self, url):
return str(len(url))
friends-0.2.0+14.04.20140217.1/friends/tests/ 0000755 0000152 0177776 00000000000 12300444701 020466 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/tests/__init__.py 0000644 0000152 0177776 00000000000 12300444435 022571 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/tests/test_instagram.py 0000644 0000152 0177776 00000021645 12300444435 024100 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Instagram plugin."""
__all__ = [
'TestInstagram',
]
import os
import tempfile
import unittest
import shutil
from friends.protocols.instagram import Instagram
from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
from friends.tests.mocks import TestModel, mock
from friends.tests.mocks import EDSRegistry
from friends.errors import FriendsError, AuthorizationError
from friends.utils.cache import JsonCache
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestInstagram(unittest.TestCase):
"""Test the Instagram API."""
def setUp(self):
self._temp_cache = tempfile.mkdtemp()
self._root = JsonCache._root = os.path.join(
self._temp_cache, '{}.json')
self.account = FakeAccount()
self.protocol = Instagram(self.account)
self.protocol.source_registry = EDSRegistry()
def tearDown(self):
TestModel.clear()
shutil.rmtree(self._temp_cache)
def test_features(self):
# The set of public features.
self.assertEqual(Instagram.get_features(),
['delete_contacts', 'home', 'like', 'receive',
'send_thread', 'unlike'])
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='abc'))
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'instagram-login.dat'))
def test_successful_login(self, *mock):
# Test that a successful response from instagram.com returning
# the user's data, sets up the account dict correctly.
self.protocol._login()
self.assertEqual(self.account.access_token, 'abc')
self.assertEqual(self.account.user_name, 'bpersons')
self.assertEqual(self.account.user_id, '801')
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
def test_login_unsuccessful_authentication(self, *mock):
# The user is not already logged in, but the act of logging in fails.
self.assertRaises(AuthorizationError, self.protocol._login)
self.assertIsNone(self.account.access_token)
self.assertIsNone(self.account.user_name)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='abc'))
@mock.patch('friends.protocols.instagram.Downloader.get_json',
return_value=dict(
error=dict(message='Bad access token',
type='OAuthException',
code=190)))
def test_error_response(self, *mocks):
with LogMock('friends.utils.base',
'friends.protocols.instagram') as log_mock:
self.assertRaises(
FriendsError,
self.protocol.home,
)
contents = log_mock.empty(trim=False)
self.assertEqual(contents, 'Logging in to Instagram\n')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'instagram-full.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.protocols.instagram.Instagram._login',
return_value=True)
def test_receive(self, *mocks):
# Receive the feed for a user.
self.maxDiff = None
self.account.access_token = 'abc'
self.assertEqual(self.protocol.receive(), 14)
self.assertEqual(TestModel.get_n_rows(), 14)
self.assertEqual(list(TestModel.get_row(0)), [
'instagram',
88,
'431474591469914097_223207800',
'messages',
'Josh',
'223207800',
'joshwolp',
False,
'2013-04-11T04:50:01Z',
'joshwolp shared a picture on Instagram.',
'http://images.ak.instagram.com/profiles/profile_223207800_75sq_1347753109.jpg',
'http://instagram.com/joshwolp',
8,
False,
'http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg',
'',
'http://instagram.com/p/X859raK8fx/',
'',
'',
'',
'',
0.0,
0.0,
])
self.assertEqual(list(TestModel.get_row(3)), [
'instagram',
88,
'431462132263145102',
'reply_to/431438012683111856_5891266',
'Syd',
'5917696',
'squidneylol',
False,
'2013-04-11T04:25:15Z',
'I remember pushing that little guy of the swings a few times....',
'http://images.ak.instagram.com/profiles/profile_5917696_75sq_1336705905.jpg',
'',
0,
False,
'',
'',
'',
'',
'',
'',
'',
0.0,
0.0,
])
@mock.patch('friends.protocols.instagram.Downloader')
def test_send_thread(self, dload):
dload().get_json.return_value = dict(id='comment_id')
token = self.protocol._get_access_token = mock.Mock(
return_value='abc')
publish = self.protocol._publish_entry = mock.Mock(
return_value='http://instagram.com/p/post_id')
self.assertEqual(
self.protocol.send_thread('post_id', 'Some witty response!'),
'http://instagram.com/p/post_id')
token.assert_called_once_with()
publish.assert_called_with(entry={'id': 'comment_id'},
stream='reply_to/post_id')
self.assertEqual(
dload.mock_calls,
[mock.call(),
mock.call(
'https://api.instagram.com/v1/media/post_id/comments?access_token=abc',
method='POST',
params=dict(
access_token='abc',
text='Some witty response!')),
mock.call().get_json(),
mock.call('https://api.instagram.com/v1/media/post_id/comments?access_token=abc',
params=dict(access_token='abc')),
mock.call().get_json(),
])
@mock.patch('friends.protocols.instagram.Downloader')
def test_like(self, dload):
dload().get_json.return_value = True
token = self.protocol._get_access_token = mock.Mock(
return_value='insta')
inc_cell = self.protocol._inc_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.like('post_id'), 'post_id')
inc_cell.assert_called_once_with('post_id', 'likes')
set_cell.assert_called_once_with('post_id', 'liked', True)
token.assert_called_once_with()
dload.assert_called_with(
'https://api.instagram.com/v1/media/post_id/likes?access_token=insta',
method='POST',
params=dict(access_token='insta'))
@mock.patch('friends.protocols.instagram.Downloader')
def test_unlike(self, dload):
dload.get_json.return_value = True
token = self.protocol._get_access_token = mock.Mock(
return_value='insta')
dec_cell = self.protocol._dec_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.unlike('post_id'), 'post_id')
dec_cell.assert_called_once_with('post_id', 'likes')
set_cell.assert_called_once_with('post_id', 'liked', False)
token.assert_called_once_with()
dload.assert_called_once_with(
'https://api.instagram.com/v1/media/post_id/likes?access_token=insta',
method='DELETE',
params=dict(access_token='insta'))
friends-0.2.0+14.04.20140217.1/friends/tests/test_foursquare.py 0000644 0000152 0177776 00000010361 12300444435 024300 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the FourSquare plugin."""
__all__ = [
'TestFourSquare',
]
import unittest
from friends.protocols.foursquare import FourSquare
from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
from friends.tests.mocks import TestModel, mock
from friends.errors import AuthorizationError
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestFourSquare(unittest.TestCase):
"""Test the FourSquare API."""
def setUp(self):
self.account = FakeAccount()
self.protocol = FourSquare(self.account)
self.log_mock = LogMock('friends.utils.base',
'friends.protocols.foursquare')
def tearDown(self):
# Ensure that any log entries we haven't tested just get consumed so
# as to isolate out test logger from other tests.
self.log_mock.stop()
# Reset the database.
TestModel.clear()
def test_features(self):
# The set of public features.
self.assertEqual(FourSquare.get_features(),
['delete_contacts', 'receive'])
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
@mock.patch('friends.utils.http.Downloader.get_json',
return_value=None)
def test_unsuccessful_authentication(self, *mocks):
self.assertRaises(AuthorizationError, self.protocol._login)
self.assertIsNone(self.account.user_name)
self.assertIsNone(self.account.user_id)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='tokeny goodness'))
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.protocols.foursquare.Downloader.get_json',
return_value=dict(
response=dict(
user=dict(firstName='Bob',
lastName='Loblaw',
id='1234567'))))
def test_successful_authentication(self, *mocks):
self.assertTrue(self.protocol._login())
self.assertEqual(self.account.user_name, 'Bob Loblaw')
self.assertEqual(self.account.user_id, '1234567')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'foursquare-full.dat'))
@mock.patch('friends.protocols.foursquare.FourSquare._login',
return_value=True)
def test_receive(self, *mocks):
self.account.access_token = 'tokeny goodness'
self.assertEqual(0, TestModel.get_n_rows())
self.assertEqual(self.protocol.receive(), 1)
self.assertEqual(1, TestModel.get_n_rows())
expected = [
'foursquare', 88, '50574c9ce4b0a9a6e84433a0',
'messages', 'Jimbob Smith', '', '', True, '2012-09-17T19:15:24Z',
"Working on friends's foursquare plugin.",
'https://irs0.4sqi.net/img/user/100x100/5IEW3VIX55BBEXAO.jpg',
'', 0, False, '', '', '', '', '', '',
'Pop Soda\'s Coffee House & Gallery',
49.88873164336725, -97.158043384552,
]
self.assertEqual(list(TestModel.get_row(0)), expected)
friends-0.2.0+14.04.20140217.1/friends/tests/test_account.py 0000644 0000152 0177776 00000016047 12300444435 023547 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Account class."""
__all__ = [
'TestAccount',
]
import unittest
from friends.errors import UnsupportedProtocolError
from friends.protocols.flickr import Flickr
from friends.tests.mocks import FakeAccount, LogMock
from friends.tests.mocks import TestModel, LogMock, mock
from friends.utils.account import Account, _find_accounts_uoa
class TestAccount(unittest.TestCase):
"""Test Account class."""
def setUp(self):
self.log_mock = LogMock('friends.utils.account')
def connect_side_effect(signal, callback, account):
# The account service provides a .connect method that connects a
# signal to a callback. We have to mock a side effect into the
# connect() method to record this connection, which some of the
# tests can then call.
self._callback_signal = signal
self._callback = callback
self._callback_account = account
# Set up the mock to return some useful values in the API expected by
# the Account constructor.
self.account_service = mock.Mock(**{
'get_auth_data.return_value': mock.Mock(**{
'get_credentials_id.return_value': 'fake credentials',
'get_method.return_value': 'fake method',
'get_mechanism.return_value': 'fake mechanism',
'get_parameters.return_value': {
'ConsumerKey': 'fake_key',
'ConsumerSecret': 'fake_secret'},
}),
'get_account.return_value': mock.Mock(**{
'get_settings_dict.return_value': dict(send_enabled=True),
'id': 'fake_id',
'get_provider_name.return_value': 'flickr',
}),
'get_service.return_value': mock.Mock(**{
'get_name.return_value': 'fake_service',
}),
'connect.side_effect': connect_side_effect,
})
self.account = Account(self.account_service)
def tearDown(self):
self.log_mock.stop()
def test_account_auth(self):
# Test that the constructor initializes the 'auth' attribute.
auth = self.account.auth
self.assertEqual(auth.get_credentials_id(), 'fake credentials')
self.assertEqual(auth.get_method(), 'fake method')
self.assertEqual(auth.get_mechanism(), 'fake mechanism')
self.assertEqual(auth.get_parameters(),
dict(ConsumerKey='fake_key',
ConsumerSecret='fake_secret'))
def test_account_id(self):
self.assertEqual(self.account.id, 'fake_id')
def test_account_service(self):
# The protocol attribute refers directly to the protocol used.
self.assertIsInstance(self.account.protocol, Flickr)
def test_account_unsupported(self):
# Unsupported protocols raise exceptions in the Account constructor.
mock = self.account_service.get_account()
mock.get_provider_name.return_value = 'no service'
with self.assertRaises(UnsupportedProtocolError) as cm:
Account(self.account_service)
self.assertEqual(cm.exception.protocol, 'no service')
def test_on_account_changed(self):
# Account.on_account_changed() gets called during the Account
# constructor. Test that it has the expected original key value.
self.assertEqual(self.account.send_enabled, True)
def test_dict_filter(self):
# The get_settings_dict() filters everything that doesn't start with
# 'friends/'
self._callback_account.get_settings_dict.assert_called_with('friends/')
def test_on_account_changed_signal(self):
# Test that when the account changes, and a 'changed' signal is
# received, the callback is called and the account is updated.
#
# Start by simulating a change in the account service.
other_dict = dict(
send_enabled=False,
bee='two',
cat='three',
)
adict = self.account_service.get_account().get_settings_dict
adict.return_value = other_dict
# Check that the signal has been connected.
self.assertEqual(self._callback_signal, 'changed')
# Check that the account is the object we expect it to be.
self.assertEqual(self._callback_account,
self.account_service.get_account())
# Simulate the signal.
self._callback(self.account_service, self._callback_account)
# Have the expected updates occurred?
self.assertEqual(self.account.send_enabled, False)
self.assertFalse(hasattr(self.account, 'bee'))
self.assertFalse(hasattr(self.account, 'cat'))
@mock.patch('friends.utils.account.Account._on_account_changed')
@mock.patch('friends.utils.account.protocol_manager')
def test_account_consumer_key(self, *mocks):
account_service = mock.Mock()
account_service.get_auth_data().get_parameters.return_value = (
dict(ConsumerKey='key', ConsumerSecret='secret'))
acct = Account(account_service)
self.assertEqual(acct.consumer_key, 'key')
self.assertEqual(acct.consumer_secret, 'secret')
@mock.patch('friends.utils.account.Account._on_account_changed')
@mock.patch('friends.utils.account.protocol_manager')
def test_account_client_id_sohu_style(self, *mocks):
account_service = mock.Mock()
account_service.get_auth_data().get_parameters.return_value = (
dict(ClientId='key', ClientSecret='secret'))
acct = Account(account_service)
self.assertEqual(acct.consumer_key, 'key')
self.assertEqual(acct.consumer_secret, 'secret')
@mock.patch('friends.utils.account.manager')
@mock.patch('friends.utils.account.Account')
@mock.patch('friends.utils.account.Accounts')
def test_find_accounts(self, accts, acct, manager):
service = mock.Mock()
get_enabled = manager.get_enabled_account_services
get_enabled.return_value = [service]
manager.reset_mock()
accounts = _find_accounts_uoa()
get_enabled.assert_called_once_with()
acct.assert_called_once_with(service)
self.assertEqual(accounts, {acct().id: acct()})
self.assertEqual(self.log_mock.empty(),
'Flickr (fake_id) got send_enabled: True\n'
'Accounts found: 1\n')
friends-0.2.0+14.04.20140217.1/friends/tests/test_notify.py 0000644 0000152 0177776 00000013665 12300444435 023426 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test our libnotify support."""
__all__ = [
'TestNotifications',
]
import unittest
from friends.tests.mocks import FakeAccount, TestModel, mock
from friends.utils.base import Base
from friends.utils.notify import notify
from datetime import datetime, timedelta
RIGHT_NOW = datetime.now().isoformat()
YESTERDAY = (datetime.now() - timedelta(1)).isoformat()
LAST_WEEK = (datetime.now() - timedelta(7)).isoformat()
class TestNotifications(unittest.TestCase):
"""Test notification details."""
def setUp(self):
TestModel.clear()
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.notify.Avatar')
@mock.patch('friends.utils.notify.GdkPixbuf')
@mock.patch('friends.utils.notify.Notify')
def test_publish_avatar_cache(self, notify, gdkpixbuf, avatar):
Base._do_notify = lambda protocol, stream: True
base = Base(FakeAccount())
base._publish(
message='http://example.com!',
message_id='1234',
sender='Benjamin',
timestamp=RIGHT_NOW,
icon_uri='http://example.com/bob.jpg',
)
avatar.get_image.assert_called_once_with('http://example.com/bob.jpg')
gdkpixbuf.Pixbuf.new_from_file_at_size.assert_called_once_with(
avatar.get_image(), 48, 48)
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_no_html(self, notify):
Base._do_notify = lambda protocol, stream: True
base = Base(FakeAccount())
base._publish(
message='http://example.com!',
message_id='1234',
sender='Benjamin',
timestamp=RIGHT_NOW,
)
notify.assert_called_once_with('Benjamin', 'http://example.com!', '')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_no_stale(self, notify):
Base._do_notify = lambda protocol, stream: True
base = Base(FakeAccount())
base._publish(
message='http://example.com!',
message_id='1234',
sender='Benjamin',
timestamp=LAST_WEEK,
)
self.assertEqual(notify.call_count, 0)
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_all(self, notify):
Base._do_notify = lambda protocol, stream: True
base = Base(FakeAccount())
base._publish(
message='notify!',
message_id='1234',
sender='Benjamin',
timestamp=YESTERDAY,
)
notify.assert_called_once_with('Benjamin', 'notify!', '')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_mentions_private(self, notify):
Base._do_notify = lambda protocol, stream: stream in (
'mentions', 'private')
base = Base(FakeAccount())
base._publish(
message='This message is private!',
message_id='1234',
sender='Benjamin',
stream='private',
timestamp=RIGHT_NOW,
)
notify.assert_called_once_with('Benjamin', 'This message is private!', '')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_mention_fail(self, notify):
Base._do_notify = lambda protocol, stream: stream in (
'mentions', 'private')
base = Base(FakeAccount())
base._publish(
message='notify!',
message_id='1234',
sender='Benjamin',
stream='messages',
timestamp=RIGHT_NOW,
)
self.assertEqual(notify.call_count, 0)
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
@mock.patch('friends.utils.base.notify')
def test_publish_mention_none(self, notify):
Base._do_notify = lambda protocol, stream: False
base = Base(FakeAccount())
base._publish(
message='Ignore me!',
message_id='1234',
sender='Benjamin',
stream='messages',
timestamp=RIGHT_NOW,
)
self.assertEqual(notify.call_count, 0)
@mock.patch('friends.utils.notify.Notify')
def test_dont_notify(self, Notify):
notify('', '')
notify('Bob Loblaw', '')
notify('', 'hello, friend!')
self.assertEqual(Notify.Notification.new.call_count, 0)
@mock.patch('friends.utils.notify.Notify')
def test_notify(self, Notify):
notify('Bob Loblaw', 'hello, friend!', pixbuf='hi!')
Notify.Notification.new.assert_called_once_with(
'Bob Loblaw', 'hello, friend!', 'friends')
notification = Notify.Notification.new()
notification.set_icon_from_pixbuf.assert_called_once_with('hi!')
notification.show.assert_called_once_with()
friends-0.2.0+14.04.20140217.1/friends/tests/test_avatars.py 0000644 0000152 0177776 00000013141 12300444435 023544 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Avatar cacher."""
__all__ = [
'TestAvatars',
]
import os
import time
import shutil
import tempfile
import unittest
from datetime import date, timedelta
from gi.repository import GdkPixbuf
from pkg_resources import resource_filename
from friends.tests.mocks import FakeSoupMessage, mock
from friends.utils.avatar import Avatar
@mock.patch('friends.utils.http._soup', mock.Mock())
class TestAvatars(unittest.TestCase):
"""Test Avatar logic."""
def setUp(self):
# Create a temporary cache directory for storing the avatar image
# files. This ensures that the user's operational environment can't
# possibly interfere.
self._temp_cache = tempfile.mkdtemp()
self._avatar_cache = os.path.join(
self._temp_cache, 'friends', 'avatars')
def tearDown(self):
# Clean up the temporary cache directory.
shutil.rmtree(self._temp_cache)
def test_noop(self):
# If a tweet is missing a profile image, silently ignore it.
self.assertEqual(Avatar.get_image(''), '')
def test_hashing(self):
# Check that the path hashing algorithm return a hash based on the
# download url.
with mock.patch('friends.utils.avatar.CACHE_DIR', self._avatar_cache):
path = Avatar.get_path('fake_url')
self.assertEqual(
path.split(os.sep)[-3:],
['friends', 'avatars',
# hashlib.sha1('fake_url'.encode('utf-8')).hexdigest()
'4f37e5dc9d38391db1728048344c3ab5ff8cecb2'])
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'ubuntu.png'))
def test_cache_filled_on_miss(self):
# When the cache is empty, downloading an avatar from a given url
# fills the cache with the image data.
with mock.patch('friends.utils.avatar.CACHE_DIR',
self._avatar_cache) as cache_dir:
# The file has not yet been downloaded because the directory does
# not yet exist. It is created on demand.
self.assertFalse(os.path.isdir(cache_dir))
os.makedirs(cache_dir)
Avatar.get_image('http://example.com')
# Soup.Message() was called once. Get the mock and check it.
from friends.utils.http import Soup
self.assertEqual(Soup.Message.call_count, 1)
# Now the file is there.
self.assertEqual(
sorted(os.listdir(cache_dir)),
# hashlib.sha1('http://example.com'
# .encode('utf-8')).hexdigest()
sorted(['89dce6a446a69d6b9bdc01ac75251e4c322bcdff',
'89dce6a446a69d6b9bdc01ac75251e4c322bcdff.100px']))
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'ubuntu.png'))
def test_cache_used_on_hit(self):
# When the cache already contains the file, it is not downloaded.
with mock.patch('friends.utils.avatar.CACHE_DIR',
self._avatar_cache) as cache_dir:
os.makedirs(cache_dir)
src = resource_filename('friends.tests.data', 'ubuntu.png')
dst = os.path.join(
cache_dir, '89dce6a446a69d6b9bdc01ac75251e4c322bcdff')
shutil.copyfile(src, dst)
# Get the image, resulting in a cache hit.
path = Avatar.get_image('http://example.com')
# No download occurred. Check the mock.
from friends.utils.http import Soup
self.assertEqual(Soup.Message.call_count, 0)
# Confirm that the resulting cache image is actually a PNG.
with open(path, 'rb') as raw:
# This is the PNG file format magic number, living in the first 8
# bytes of the file.
self.assertEqual(raw.read(8), bytes.fromhex('89504E470D0A1A0A'))
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'ubuntu.png'))
def test_cache_file_contains_image(self):
# The image is preserved in the cache file.
with mock.patch('friends.utils.avatar.CACHE_DIR',
self._avatar_cache) as cache_dir:
os.makedirs(cache_dir)
path = Avatar.get_image('http://example.com')
# The image must have been downloaded at least once.
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
self.assertEqual(pixbuf.get_height(), 285)
self.assertEqual(pixbuf.get_width(), 285)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path + '.100px')
self.assertEqual(pixbuf.get_height(), 100)
self.assertEqual(pixbuf.get_width(), 100)
# Confirm that the resulting cache image is actually a PNG.
with open(path, 'rb') as raw:
# This is the PNG file format magic number, living in the first 8
# bytes of the file.
self.assertEqual(raw.read(8), bytes.fromhex('89504E470D0A1A0A'))
friends-0.2.0+14.04.20140217.1/friends/tests/mocks.py 0000644 0000152 0177776 00000026577 12300444435 022201 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Mocks, doubles, and fakes for testing."""
__all__ = [
'FakeAuth',
'FakeAccount',
'FakeSoupMessage',
'LogMock',
'mock',
]
import os
import hashlib
import logging
import threading
import tempfile
import shutil
from io import StringIO
from logging.handlers import QueueHandler
from pkg_resources import resource_listdir, resource_string
from queue import Empty, Queue
from unittest import mock
from urllib.parse import urlsplit
from gi.repository import Dee
# By default, Schema.FILES will look for the system-installed schema
# file first, and then failing that will look for the one in the
# source tree, for performance reasons. During testing though, we want
# to look at the source tree first, so we reverse the list here.
from friends.utils.model import Schema
Schema.FILES = list(reversed(Schema.FILES))
SCHEMA = Schema()
from friends.utils.base import Base
from friends.utils.logging import LOG_FORMAT
NEWLINE = '\n'
# Create a test model that will not interfere with the user's environment.
# We'll use this object as a mock of the real model.
TestModel = Dee.SharedModel.new('com.canonical.Friends.TestSharedModel')
TestModel.set_schema_full(SCHEMA.TYPES)
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base.Base._get_access_token',
mock.Mock(return_value='Access Tolkien'))
@mock.patch('friends.utils.base.Base._get_oauth_headers',
mock.Mock(return_value={}))
def populate_fake_data():
"""Dump a mixture of random data from our testsuite into TestModel.
This is invoked by running 'friends-dispatcher --test' so that you
can have some phony data in the model to test against.
Just remember that the data appears in a separate model so as not
to interfere with the user's official DeeModel stream.
"""
from friends.utils.cache import JsonCache
from friends.protocols.facebook import Facebook
from friends.protocols.flickr import Flickr
from friends.protocols.twitter import Twitter
from gi.repository import Dee
temp_cache = tempfile.mkdtemp()
root = JsonCache._root = os.path.join(temp_cache, '{}.json')
protocols = {
'facebook-full.dat': Facebook(FakeAccount(account_id=1)),
'flickr-full.dat': Flickr(FakeAccount(account_id=2)),
'twitter-home.dat': Twitter(FakeAccount(account_id=3)),
}
for fake_name, protocol in protocols.items():
protocol.source_registry = EDSRegistry()
with mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
fake_name)) as fake:
protocol.receive()
shutil.rmtree(temp_cache)
class FakeAuth:
get_credentials_id = lambda *ignore: 'fakeauth id'
get_method = lambda *ignore: 'fakeauth method'
get_mechanism = lambda *ignore: 'fakeauth mechanism'
get_parameters = lambda *ignore: {
'ConsumerKey': 'fake',
'ConsumerSecret': 'alsofake',
}
class FakeAccount:
"""A fake account object for testing purposes."""
def __init__(self, service=None, account_id=88):
self.consumer_secret = 'secret'
self.consumer_key = 'consume'
self.access_token = None
self.secret_token = None
self.user_full_name = None
self.user_name = None
self.user_id = None
self.auth = FakeAuth()
self.login_lock = threading.Lock()
self.id = account_id
self.protocol = Base(self)
class FakeSoupMessage:
"""Mimic a Soup.Message that returns canned data."""
def __init__(self, path, resource, charset='utf-8', headers=None,
response_code=200):
# resource_string() always returns bytes.
self._data = resource_string(path, resource)
self.call_count = 0
self._charset = charset
self._headers = {} if headers is None else headers
self.status_code = response_code
@property
def response_body(self):
return self
@property
def response_headers(self):
return self
@property
def request_headers(self):
return self
def flatten(self):
return self
def get_data(self):
return self._data
def get_as_bytes(self):
return self._data
def get_content_type(self):
return 'application/x-mock-data', dict(charset=self._charset)
def get_uri(self):
pieces = urlsplit(self.url)
class FakeUri:
host = pieces.netloc
path = pieces.path
return FakeUri()
def get(self, header, default=None):
return self._headers.get(header, default)
def append(self, header, value):
self._headers[header] = value
def set_request(self, *args):
pass
def new(self, method, url):
self.call_count += 1
self.method = method
self.url = url
return self
class LogMock:
"""A mocker for capturing logging output in protocol classes.
This ensures that the standard friends.service log file isn't polluted by
the tests, and that the logging output in a sub-thread can be tested in
the main thread.
This class can be used either in a TestCase's setUp() and tearDown()
methods, or as a context manager (i.e. in a `with` statement). When used
as the latter, be sure to capture the contents of the log inside the
with-clause since exiting the context manager will consume all left over
log contents.
Pass in the list of modules to mock, and it will mock all the 'log'
attributes on those modules. The last component can be a '*' wildcard in
which case it will mock all the modules found in that package.
Instantiating this class automatically starts the mocking; call the
.empty() method to gather the accumulated log messages, even from a
sub-thread. In the .tearDown(), call .stop() to stop mocking.
"""
def __init__(self, *modules):
self._queue = Queue()
self._log = logging.getLogger('friends')
handler = QueueHandler(self._queue)
formatter = logging.Formatter(LOG_FORMAT, style='{')
handler.setFormatter(formatter)
self._log.addHandler(handler)
# Capture effectively everything. This can't be NOTSET because by
# definition, that propagates log messages to the root logger.
self._log.setLevel(1)
self._log.propagate = False
# Create the mock, and then go through all the named modules, mocking
# their 'log' attribute.
self._patchers = []
for path in modules:
prefix, dot, module = path.rpartition('.')
if module == '*':
# Partition again to get the parent package.
subprefix, dot, parent = prefix.rpartition('.')
for filename in resource_listdir(subprefix, parent):
basename, extension = os.path.splitext(filename)
if extension != '.py':
continue
patch_path = '{}.{}.__dict__'.format(prefix, basename)
patcher = mock.patch.dict(patch_path, {'log': self._log})
self._patchers.append(patcher)
else:
patch_path = '{}.__dict__'.format(path)
patcher = mock.patch.dict(patch_path, {'log': self._log})
self._patchers.append(patcher)
# Start all the patchers.
for patcher in self._patchers:
patcher.start()
def stop(self):
# Empty the queue for test isolation.
self.empty()
for patcher in self._patchers:
patcher.stop()
# Get rid of the mock logger.
del logging.Logger.manager.loggerDict['friends']
def empty(self, trim=True):
"""Return all the log messages written to this log.
:param trim: Trim exception text to just the first and last line, with
ellipses in between. You will usually want to do this since the
exception details will contain file tracebacks with paths specific
to your testing environment.
:type trim: bool
"""
output = StringIO()
while True:
try:
record = self._queue.get_nowait()
except Empty:
# The queue is exhausted.
break
# We have to print both the message, and explicitly the exc_text,
# otherwise we won't see the exception traceback in the output.
args = [record.getMessage()]
if record.exc_text is None:
# Nothing to include.
pass
elif trim:
exc_lines = record.exc_text.splitlines()
# Leave just the first and last lines, but put ellipses in
# between.
exc_lines[1:-1] = [' ...']
args.append(NEWLINE.join(exc_lines))
else:
args.append(record.exc_text)
print(*args, file=output)
return output.getvalue()
def __enter__(self):
return self
def __exit__(self, *exception_info):
self.stop()
return False
class EDSBookClientMock:
"""A Mocker object to simulate use of BookClient."""
def __init__(self):
pass
def open_sync(val1, val2, val3):
pass
def add_contact_sync(val1, contact, cancellable):
return True
def get_contacts_sync(val1, val2, val3):
return [True, [{'name':'john doe', 'id': 11111}]]
def remove_contact_sync(val1, val2):
pass
class EDSExtension:
"""A Extension mocker object for testing create source."""
def __init__(self):
pass
def set_backend_name(self, name):
pass
class EDSSource:
"""Simulate a Source object to create address books in EDS."""
def __init__(self, val1, val2):
pass
def set_display_name(self, name):
self.name = name
def get_display_name(self):
return self.name
def set_parent(self, parent):
pass
def get_uid(self):
return self.name
def get_extension(self, extension_name):
return EDSExtension()
class EDSRegistry:
"""A Mocker object for the registry."""
def __init__(self):
pass
def commit_source_sync(self, source, val1):
return True
def list_sources(self, category):
res = []
s1 = EDSSource(None, None)
s1.set_display_name('test-facebook-contacts')
res.append(s1)
s2 = EDSSource(None, None)
s2.set_display_name('test-twitter-contacts')
res.append(s2)
return res
def ref_source(self, src_uid):
s1 = EDSSource(None, None)
s1.set_display_name('friends-testsuite-contacts')
return s1
friends-0.2.0+14.04.20140217.1/friends/tests/test_identica.py 0000644 0000152 0177776 00000024677 12300444435 023703 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Identica plugin."""
__all__ = [
'TestIdentica',
]
import os
import tempfile
import unittest
import shutil
from friends.protocols.identica import Identica
from friends.tests.mocks import FakeAccount, LogMock, TestModel, mock
from friends.utils.cache import JsonCache
from friends.errors import AuthorizationError
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
@mock.patch('friends.utils.base.Model', TestModel)
class TestIdentica(unittest.TestCase):
"""Test the Identica API."""
def setUp(self):
self._temp_cache = tempfile.mkdtemp()
self._root = JsonCache._root = os.path.join(
self._temp_cache, '{}.json')
self.account = FakeAccount()
self.protocol = Identica(self.account)
self.log_mock = LogMock('friends.utils.base',
'friends.protocols.twitter')
def tearDown(self):
# Ensure that any log entries we haven't tested just get consumed so
# as to isolate out test logger from other tests.
self.log_mock.stop()
shutil.rmtree(self._temp_cache)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
@mock.patch('friends.utils.http.Downloader.get_json',
return_value=None)
def test_unsuccessful_authentication(self, *mocks):
self.assertRaises(AuthorizationError, self.protocol._login)
self.assertIsNone(self.account.user_name)
self.assertIsNone(self.account.user_id)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='some clever fake data',
TokenSecret='sssssshhh!'))
def test_successful_authentication(self, *mocks):
get_url = self.protocol._get_url = mock.Mock(
return_value=dict(id='1234', screen_name='therealrobru'))
self.assertTrue(self.protocol._login())
self.assertEqual(self.account.user_name, 'therealrobru')
self.assertEqual(self.account.user_id, '1234')
self.assertEqual(self.account.access_token, 'some clever fake data')
self.assertEqual(self.account.secret_token, 'sssssshhh!')
get_url.assert_called_once_with('http://identi.ca/api/users/show.json')
def test_mentions(self):
get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.mentions()
publish.assert_called_with('tweet', stream='mentions')
get_url.assert_called_with(
'http://identi.ca/api/statuses/mentions.json?count=50')
def test_user(self):
get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.user()
publish.assert_called_with('tweet', stream='messages')
get_url.assert_called_with(
'http://identi.ca/api/statuses/user_timeline.json?screen_name=')
def test_list(self):
self.protocol._get_url = mock.Mock(return_value=['tweet'])
self.protocol._publish_tweet = mock.Mock()
self.assertRaises(NotImplementedError,
self.protocol.list, 'some_list_id')
def test_lists(self):
self.protocol._get_url = mock.Mock(
return_value=[dict(id_str='twitlist')])
self.protocol.list = mock.Mock()
self.assertRaises(NotImplementedError, self.protocol.lists)
def test_private(self):
get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.private()
publish.assert_called_with('tweet', stream='private')
self.assertEqual(
get_url.mock_calls,
[mock.call('http://identi.ca/api/direct_messages.json?count=50'),
mock.call('http://identi.ca/api/direct_messages' +
'/sent.json?count=50')])
def test_send_private(self):
get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.send_private('pumpichank', 'Are you mocking me?')
publish.assert_called_with('tweet', stream='private')
get_url.assert_called_with(
'http://identi.ca/api/direct_messages/new.json',
dict(text='Are you mocking me?', screen_name='pumpichank'))
def test_send(self):
get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.send('Hello, twitterverse!')
publish.assert_called_with('tweet')
get_url.assert_called_with(
'http://identi.ca/api/statuses/update.json',
dict(status='Hello, twitterverse!'))
def test_send_thread(self):
get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.send_thread(
'1234',
'Why yes, I would love to respond to your tweet @pumpichank!')
publish.assert_called_with('tweet', stream='reply_to/1234')
get_url.assert_called_with(
'http://identi.ca/api/statuses/update.json',
dict(status=
'Why yes, I would love to respond to your tweet @pumpichank!',
in_reply_to_status_id='1234'))
def test_delete(self):
get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
publish = self.protocol._unpublish = mock.Mock()
self.protocol.delete('1234')
publish.assert_called_with('1234')
get_url.assert_called_with(
'http://identi.ca/api/statuses/destroy/1234.json',
dict(trim_user='true'))
def test_retweet(self):
tweet=dict(tweet='twit')
get_url = self.protocol._get_url = mock.Mock(return_value=tweet)
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.retweet('1234')
publish.assert_called_with(tweet)
get_url.assert_called_with(
'http://identi.ca/api/statuses/retweet/1234.json',
dict(trim_user='false'))
def test_unfollow(self):
get_url = self.protocol._get_url = mock.Mock()
self.protocol.unfollow('pumpichank')
get_url.assert_called_with(
'http://identi.ca/api/friendships/destroy.json',
dict(screen_name='pumpichank'))
def test_follow(self):
get_url = self.protocol._get_url = mock.Mock()
self.protocol.follow('pumpichank')
get_url.assert_called_with(
'http://identi.ca/api/friendships/create.json',
dict(screen_name='pumpichank', follow='true'))
def test_tag(self):
self.protocol._get_url = mock.Mock(
return_value=dict(statuses=['tweet']))
self.protocol._publish_tweet = mock.Mock()
self.assertRaises(NotImplementedError, self.protocol.tag, 'hashtag')
def test_search(self):
get_url = self.protocol._get_url = mock.Mock(
return_value=dict(results=['tweet']))
publish = self.protocol._publish_tweet = mock.Mock()
self.protocol.search('hello')
publish.assert_called_with('tweet', stream='search/hello')
get_url.assert_called_with(
'http://identi.ca/api/search.json?q=hello')
def test_like(self):
get_url = self.protocol._get_url = mock.Mock()
inc_cell = self.protocol._inc_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.like('1234'), '1234')
inc_cell.assert_called_once_with('1234', 'likes')
set_cell.assert_called_once_with('1234', 'liked', True)
get_url.assert_called_with(
'http://identi.ca/api/favorites/create/1234.json',
dict(id='1234'))
def test_unlike(self):
get_url = self.protocol._get_url = mock.Mock()
dec_cell = self.protocol._dec_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.unlike('1234'), '1234')
dec_cell.assert_called_once_with('1234', 'likes')
set_cell.assert_called_once_with('1234', 'liked', False)
get_url.assert_called_with(
'http://identi.ca/api/favorites/destroy/1234.json',
dict(id='1234'))
def test_contacts(self):
get = self.protocol._get_url = mock.Mock(
return_value=dict(ids=[1,2],name='Bob',screen_name='bobby'))
prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False)
push = self.protocol._push_to_eds = mock.Mock()
self.assertEqual(self.protocol.contacts(), 2)
self.assertEqual(
get.call_args_list,
[mock.call('http://identi.ca/api/friends/ids.json'),
mock.call(url='http://identi.ca/api/users/show.json?user_id=1'),
mock.call(url='http://identi.ca/api/users/show.json?user_id=2')])
self.assertEqual(
prev.call_args_list,
[mock.call('1'), mock.call('2')])
self.assertEqual(
push.call_args_list,
[mock.call(link='https://identi.ca/bobby', nick='bobby',
uid='1', name='Bob'),
mock.call(link='https://identi.ca/bobby', nick='bobby',
uid='2', name='Bob')])
friends-0.2.0+14.04.20140217.1/friends/tests/test_mock_dispatcher.py 0000644 0000152 0177776 00000004143 12300444435 025244 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the dispatcher directly, without dbus."""
__all__ = [
'TestMockDispatcher',
]
import dbus.service
import unittest
from dbus.mainloop.glib import DBusGMainLoop
from friends.service.mock_service import Dispatcher as MockDispatcher
from friends.service.dispatcher import Dispatcher
# Set up the DBus main loop.
DBusGMainLoop(set_as_default=True)
def get_signature(klass, signature):
"""Extract dbus in/out signatures from a dbus class."""
return [
(m, getattr(getattr(klass, m), signature))
for m in dir(klass)
if hasattr(getattr(klass, m), signature)
]
class TestMockDispatcher(unittest.TestCase):
"""Ensure our mock Dispatcher has the same API as the real one."""
def test_api_similarity(self):
real = [m for m in dir(Dispatcher)
if hasattr(getattr(Dispatcher, m), '_dbus_interface')]
mock = [m for m in dir(MockDispatcher)
if hasattr(getattr(MockDispatcher, m), '_dbus_interface')]
self.assertEqual(real, mock)
def test_in_signatures(self):
real = get_signature(Dispatcher, '_dbus_in_signature')
mock = get_signature(MockDispatcher, '_dbus_in_signature')
self.assertEqual(real, mock)
def test_out_signatures(self):
real = get_signature(Dispatcher, '_dbus_out_signature')
mock = get_signature(MockDispatcher, '_dbus_out_signature')
self.assertEqual(real, mock)
friends-0.2.0+14.04.20140217.1/friends/tests/test_authentication.py 0000644 0000152 0177776 00000011655 12300444435 025132 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the authentication service.
We do a lot of mocking so that we don't have to talk to the actual backend
authentication service.
"""
__all__ = [
'TestAuthentication',
]
import unittest
from friends.utils.authentication import Authentication
from friends.tests.mocks import FakeAccount, LogMock, mock
from friends.errors import AuthorizationError
class FakeAuthSession:
results = None
@classmethod
def new(cls, id, method):
return cls()
def process(self, parameters, mechanism, callback, ignore):
# Pass in fake data. The callback expects a session, reply,
# error, and user_data arguments. We'll use the parameters
# argument as a way to specify whether an error occurred during
# authentication or not.
callback(
None,
self.results,
parameters if hasattr(parameters, 'message') else None,
None)
class FakeSignon:
class AuthSession(FakeAuthSession):
results = dict(AccessToken='auth reply')
class FailingSignon:
class AuthSession(FakeAuthSession):
results = dict(NoAccessToken='fail')
class TestAuthentication(unittest.TestCase):
"""Test authentication."""
def setUp(self):
self.log_mock = LogMock('friends.utils.authentication')
self.account = FakeAccount()
self.account.auth.get_credentials_id = lambda *ignore: 'my id'
self.account.auth.get_method = lambda *ignore: 'some method'
self.account.auth.get_parameters = lambda *ignore: 'change me'
self.account.auth.get_mechanism = lambda *ignore: 'whatever'
def tearDown(self):
self.log_mock.stop()
@mock.patch('friends.utils.authentication.Signon', FakeSignon)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
def test_successful_login(self, accounts, manager):
manager.get_account().list_services.return_value = ['foo']
# Prevent an error in the callback.
accounts.AccountService.new().get_auth_data(
).get_parameters.return_value = False
authenticator = Authentication(self.account.id)
reply = authenticator.login()
self.assertEqual(reply, dict(AccessToken='auth reply'))
self.assertEqual(self.log_mock.empty(), '_login_cb completed\n')
@mock.patch('friends.utils.authentication.Signon', FailingSignon)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
def test_missing_access_token(self, accounts, manager):
manager.get_account().list_services.return_value = ['foo']
# Prevent an error in the callback.
self.account.auth.get_parameters = lambda *ignore: False
authenticator = Authentication(self.account.id)
self.assertRaises(AuthorizationError, authenticator.login)
@mock.patch('friends.utils.authentication.Signon', FakeSignon)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
def test_failed_login(self, accounts, manager):
# Trigger an error in the callback.
class Error:
message = 'who are you?'
manager.get_account().list_services.return_value = ['foo']
accounts.AccountService.new(
).get_auth_data().get_parameters.return_value = Error
authenticator = Authentication(self.account.id)
self.assertRaises(AuthorizationError, authenticator.login)
@mock.patch('friends.utils.authentication.Signon', FakeSignon)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
def test_exception_correct_thread(self, accounts, manager):
manager.get_account().list_services.return_value = ['foo']
authenticator = Authentication(self.account)
# If this were to raise any exception for any reason, this
# test will fail. This method can't be allowed to raise
# exceptions because it doesn't run in the safety of a
# subthread where those are caught and logged nicely.
authenticator._login_cb('session', 'reply', 'error', 'data')
self.assertEqual(authenticator._reply, 'reply')
self.assertEqual(authenticator._error, 'error')
friends-0.2.0+14.04.20140217.1/friends/tests/test_flickr.py 0000644 0000152 0177776 00000025520 12300444435 023361 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Flickr plugin."""
__all__ = [
'TestFlickr',
]
import unittest
from friends.errors import AuthorizationError, FriendsError
from friends.protocols.flickr import Flickr
from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
from friends.tests.mocks import TestModel, mock
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestFlickr(unittest.TestCase):
"""Test the Flickr API."""
def setUp(self):
self.maxDiff = None
self.account = FakeAccount()
self.protocol = Flickr(self.account)
self.protocol._get_oauth_headers = lambda *ignore, **kwignore: {}
self.log_mock = LogMock('friends.utils.base',
'friends.protocols.flickr')
TestModel.clear()
def tearDown(self):
self.log_mock.stop()
# Reset the database.
TestModel.clear()
def test_features(self):
# The set of public features.
self.assertEqual(Flickr.get_features(),
['delete_contacts', 'receive', 'upload'])
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
def test_already_logged_in(self):
# Try to get the data when already logged in.
self.account.access_token = 'original token'
# There's no data, and no way to test that the user_nsid was actually
# used, except for the side effect of not getting an
# AuthorizationError.
self.protocol.receive()
# No error messages.
self.assertEqual(self.log_mock.empty(), '')
# But also no photos.
self.assertEqual(TestModel.get_n_rows(), 0)
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
def test_successful_login(self):
# The user is not already logged in, but the act of logging in
# succeeds.
def side_effect():
# Perform a successful login.
self.account.user_id = 'cate'
return True
with mock.patch.object(self.protocol, '_login',
side_effect=side_effect):
self.protocol.receive()
# No error message.
self.assertEqual(self.log_mock.empty(), '')
# But also no photos.
self.assertEqual(TestModel.get_n_rows(), 0)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
def test_login_unsuccessful_authentication_no_callback(self, *mocks):
# Logging in required communication with the account service to get an
# AccessToken, but this fails.
self.assertRaises(AuthorizationError, self.protocol.receive)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(username='Bob Dobbs',
user_nsid='bob',
AccessToken='123',
TokenSecret='abc'))
def test_login_successful_authentication(self, *mocks):
# Logging in required communication with the account service to get an
# AccessToken, but this fails.
self.protocol.receive()
# Make sure our account data got properly updated.
self.assertEqual(self.account.user_name, 'Bob Dobbs')
self.assertEqual(self.account.user_id, 'bob')
self.assertEqual(self.account.access_token, '123')
self.assertEqual(self.account.secret_token, 'abc')
@mock.patch('friends.utils.base.Model', TestModel)
def test_get(self):
# Make sure that the REST GET url looks right.
token = self.protocol._get_access_token = mock.Mock()
class fake:
def get_json(*ignore):
return {}
with mock.patch('friends.protocols.flickr.Downloader') as cm:
cm.return_value = fake()
self.assertEqual(self.protocol.receive(), 0)
token.assert_called_once_with()
# GET was called once.
cm.assert_called_once_with(
'http://api.flickr.com/services/rest',
method='GET',
params=dict(
extras='date_upload,owner_name,icon_server,geo',
format='json',
nojsoncallback='1',
api_key='consume',
method='flickr.photos.getContactsPhotos',
),
headers={})
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
def test_no_photos(self):
# The JSON data in response to the GET request returned no photos.
with mock.patch.object(
self.protocol, '_get_access_token', return_value='token'):
# No photos are returned in the JSON data.
self.assertEqual(self.protocol.receive(), 0)
self.assertEqual(TestModel.get_n_rows(), 0)
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'flickr-full.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
def test_flickr_data(self):
# Start by setting up a fake account id.
self.account.id = 69
with mock.patch.object(self.protocol, '_get_access_token',
return_value='token'):
self.assertEqual(self.protocol.receive(), 10)
self.assertEqual(TestModel.get_n_rows(), 10)
self.assertEqual(
list(TestModel.get_row(0)),
['flickr',
69,
'8552892154',
'images',
'raise my voice',
'47303164@N00',
'raise my voice',
True,
'2013-03-12T19:51:42Z',
'Chocolate chai #yegcoffee',
'http://farm1.static.flickr.com/93/buddyicons/47303164@N00.jpg',
'http://www.flickr.com/photos/47303164@N00/8552892154',
0,
False,
'http://farm9.static.flickr.com/8378/8552892154_a_m.jpg',
'',
'http://www.flickr.com/photos/47303164@N00/8552892154',
'',
'',
'http://farm9.static.flickr.com/8378/8552892154_a_t.jpg',
'',
0.0,
0.0,
])
self.assertEqual(
list(TestModel.get_row(4)),
['flickr',
69,
'8550829193',
'images',
'Nelson Webb',
'27204141@N05',
'Nelson Webb',
True,
'2013-03-12T13:54:10Z',
'St. Michael - The Archangel',
'http://farm3.static.flickr.com/2047/buddyicons/27204141@N05.jpg',
'http://www.flickr.com/photos/27204141@N05/8550829193',
0,
False,
'http://farm9.static.flickr.com/8246/8550829193_e_m.jpg',
'',
'http://www.flickr.com/photos/27204141@N05/8550829193',
'',
'',
'http://farm9.static.flickr.com/8246/8550829193_e_t.jpg',
'',
53.833156,
-112.330784,
])
@mock.patch('friends.utils.http.Soup.form_request_new_from_multipart',
lambda *ignore: FakeSoupMessage('friends.tests.data',
'flickr-xml.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.http.Gio.File')
@mock.patch('friends.protocols.flickr.time.time', lambda: 1361292793)
def test_upload(self, gfile):
self.account.user_name = 'freddyjimbobjones'
gfile.new_for_uri().load_contents.return_value = [True, 'data'.encode()]
token = self.protocol._get_access_token = mock.Mock()
publish = self.protocol._publish = mock.Mock()
avatar = self.protocol._get_avatar = mock.Mock()
avatar.return_value = '/path/to/cached/avatar'
self.assertEqual(
self.protocol.upload(
'file:///path/to/some.jpg',
'Beautiful photograph!'),
'http://www.flickr.com/photos/freddyjimbobjones/8488552823')
token.assert_called_with()
publish.assert_called_with(
message='Beautiful photograph!',
timestamp='2013-02-19T16:53:13Z',
stream='images',
message_id='8488552823',
from_me=True,
sender=None,
sender_nick='freddyjimbobjones',
icon_uri='/path/to/cached/avatar',
url='http://www.flickr.com/photos/freddyjimbobjones/8488552823',
sender_id=None)
@mock.patch('friends.utils.http.Soup.form_request_new_from_multipart',
lambda *ignore: FakeSoupMessage('friends.tests.data',
'flickr-xml-error.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.http.Gio.File')
def test_failing_upload(self, gfile):
gfile.new_for_uri().load_contents.return_value = [True, 'data'.encode()]
token = self.protocol._get_access_token = mock.Mock()
publish = self.protocol._publish = mock.Mock()
self.assertRaises(
FriendsError,
self.protocol.upload,
'file:///path/to/some.jpg',
'Beautiful photograph!')
token.assert_called_with()
self.assertEqual(publish.call_count, 0)
friends-0.2.0+14.04.20140217.1/friends/tests/test_dispatcher.py 0000644 0000152 0177776 00000026115 12300444435 024236 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the dispatcher directly, without dbus."""
__all__ = [
'TestDispatcher',
]
import dbus.service
import unittest
import json
from dbus.mainloop.glib import DBusGMainLoop
from friends.service.dispatcher import Dispatcher, ManageTimers, STUB
from friends.tests.mocks import LogMock, mock
# Set up the DBus main loop.
DBusGMainLoop(set_as_default=True)
@mock.patch('friends.service.dispatcher.GLib.timeout_add_seconds',
mock.Mock(return_value=42))
@mock.patch('friends.service.dispatcher.GLib.source_remove',
mock.Mock(return_value=True))
class TestDispatcher(unittest.TestCase):
"""Test the dispatcher's ability to dispatch."""
@mock.patch('dbus.service.BusName')
@mock.patch('friends.service.dispatcher.find_accounts')
@mock.patch('dbus.service.Object.__init__')
def setUp(self, *mocks):
self.log_mock = LogMock('friends.service.dispatcher',
'friends.utils.account')
self.dispatcher = Dispatcher(mock.Mock(), mock.Mock())
self.dispatcher.accounts = {}
def tearDown(self):
self.log_mock.stop()
@mock.patch('friends.service.dispatcher.threading')
def test_refresh(self, threading_mock):
account = mock.Mock()
threading_mock.activeCount.return_value = 1
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.values.return_value = [account]
self.assertIsNone(self.dispatcher.Refresh())
self.dispatcher.accounts.values.assert_called_once_with()
account.protocol.assert_called_once_with('receive')
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'Refresh requested\n'
'Starting new shutdown timer...\n')
def test_clear_indicators(self):
self.dispatcher.menu_manager = mock.Mock()
self.dispatcher.ClearIndicators()
self.dispatcher.menu_manager.update_unread_count.assert_called_once_with(0)
def test_do(self):
account = mock.Mock()
account.id = '345'
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.get.return_value = account
self.dispatcher.Do('like', '345', '23346356767354626')
self.dispatcher.accounts.get.assert_called_once_with(345)
account.protocol.assert_called_once_with(
'like', '23346356767354626', success=STUB, failure=STUB)
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'345: like 23346356767354626\n'
'Starting new shutdown timer...\n')
def test_failing_do(self):
account = mock.Mock()
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.get.return_value = None
self.dispatcher.Do('unlike', '6', '23346356767354626')
self.dispatcher.accounts.get.assert_called_once_with(6)
self.assertEqual(account.protocol.call_count, 0)
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'Could not find account: 6\n'
'Starting new shutdown timer...\n')
def test_send_message(self):
account1 = mock.Mock()
account2 = mock.Mock()
account3 = mock.Mock()
account2.send_enabled = False
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.values.return_value = [
account1,
account2,
account3,
]
self.dispatcher.SendMessage('Howdy friends!')
self.dispatcher.accounts.values.assert_called_once_with()
account1.protocol.assert_called_once_with(
'send', 'Howdy friends!', success=STUB, failure=STUB)
account3.protocol.assert_called_once_with(
'send', 'Howdy friends!', success=STUB, failure=STUB)
self.assertEqual(account2.protocol.call_count, 0)
def test_send_reply(self):
account = mock.Mock()
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.get.return_value = account
self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')
self.dispatcher.accounts.get.assert_called_once_with(2)
account.protocol.assert_called_once_with(
'send_thread', 'objid', '[Hilarious Response]',
success=STUB, failure=STUB)
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'Replying to 2, objid\n'
'Starting new shutdown timer...\n')
def test_send_reply_failed(self):
account = mock.Mock()
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.get.return_value = None
self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')
self.dispatcher.accounts.get.assert_called_once_with(2)
self.assertEqual(account.protocol.call_count, 0)
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'Replying to 2, objid\n'
'Could not find account: 2\n'
'Starting new shutdown timer...\n')
def test_upload_async(self):
account = mock.Mock()
self.dispatcher.accounts = mock.Mock()
self.dispatcher.accounts.get.return_value = account
success = mock.Mock()
failure = mock.Mock()
self.dispatcher.Upload('2',
'file://path/to/image.png',
'A thousand words',
success=success,
failure=failure)
self.dispatcher.accounts.get.assert_called_once_with(2)
account.protocol.assert_called_once_with(
'upload',
'file://path/to/image.png',
'A thousand words',
success=success,
failure=failure,
)
self.assertEqual(self.log_mock.empty(),
'Clearing timer id: 42\n'
'Uploading file://path/to/image.png to 2\n'
'Starting new shutdown timer...\n')
def test_get_features(self):
self.assertEqual(json.loads(self.dispatcher.GetFeatures('facebook')),
['contacts', 'delete', 'delete_contacts',
'home', 'like', 'receive', 'search', 'send',
'send_thread', 'unlike', 'upload', 'wall'])
self.assertEqual(json.loads(self.dispatcher.GetFeatures('twitter')),
['contacts', 'delete', 'delete_contacts',
'follow', 'home', 'like', 'list', 'lists',
'mentions', 'private', 'receive', 'retweet',
'search', 'send', 'send_private',
'send_thread', 'tag', 'unfollow', 'unlike',
'user'])
self.assertEqual(json.loads(self.dispatcher.GetFeatures('identica')),
['contacts', 'delete', 'delete_contacts',
'follow', 'home', 'like', 'mentions',
'private', 'receive', 'retweet', 'search',
'send', 'send_private', 'send_thread',
'unfollow', 'unlike', 'user'])
self.assertEqual(json.loads(self.dispatcher.GetFeatures('flickr')),
['delete_contacts', 'receive', 'upload'])
self.assertEqual(json.loads(self.dispatcher.GetFeatures('foursquare')),
['delete_contacts', 'receive'])
@mock.patch('friends.service.dispatcher.logging')
def test_urlshorten_already_shortened(self, logging_mock):
self.assertEqual(
'http://tinyurl.com/foo',
self.dispatcher.URLShorten('http://tinyurl.com/foo'))
@mock.patch('friends.service.dispatcher.logging')
@mock.patch('friends.service.dispatcher.Short')
def test_urlshorten(self, short_mock, logging_mock):
short_mock().sub.return_value = 'short url'
short_mock.reset_mock()
self.dispatcher.settings.get_string.return_value = 'is.gd'
long_url = 'http://example.com/really/really/long'
self.assertEqual(
self.dispatcher.URLShorten(long_url),
'short url')
self.dispatcher.settings.get_boolean.assert_called_once_with(
'shorten-urls')
short_mock.assert_called_once_with('is.gd')
short_mock.return_value.sub.assert_called_once_with(
long_url)
@mock.patch('friends.service.dispatcher.GLib')
def test_manage_timers_clear(self, glib):
glib.source_remove.reset_mock()
manager = ManageTimers()
manager.timers = {1}
manager.__enter__()
glib.source_remove.assert_called_once_with(1)
manager.timers = {1, 2, 3}
manager.clear_all_timers()
self.assertEqual(glib.source_remove.call_count, 4)
@mock.patch('friends.service.dispatcher.GLib')
def test_manage_timers_set(self, glib):
glib.timeout_add_seconds.reset_mock()
manager = ManageTimers()
manager.timers = set()
manager.clear_all_timers = mock.Mock()
manager.__exit__()
glib.timeout_add_seconds.assert_called_once_with(30, manager.terminate)
manager.clear_all_timers.assert_called_once_with()
self.assertEqual(len(manager.timers), 1)
@mock.patch('friends.service.dispatcher.persist_model')
@mock.patch('friends.service.dispatcher.threading')
@mock.patch('friends.service.dispatcher.GLib')
def test_manage_timers_terminate(self, glib, thread, persist):
manager = ManageTimers()
manager.timers = set()
thread.activeCount.return_value = 1
manager.terminate()
thread.activeCount.assert_called_once_with()
persist.assert_called_once_with()
glib.idle_add.assert_called_once_with(manager.callback)
@mock.patch('friends.service.dispatcher.persist_model')
@mock.patch('friends.service.dispatcher.threading')
@mock.patch('friends.service.dispatcher.GLib')
def test_manage_timers_dont_kill_threads(self, glib, thread, persist):
manager = ManageTimers()
manager.timers = set()
manager.set_new_timer = mock.Mock()
thread.activeCount.return_value = 10
manager.terminate()
thread.activeCount.assert_called_once_with()
manager.set_new_timer.assert_called_once_with()
friends-0.2.0+14.04.20140217.1/friends/tests/test_facebook.py 0000644 0000152 0177776 00000066345 12300444435 023672 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the Facebook plugin."""
__all__ = [
'TestFacebook',
]
import os
import tempfile
import unittest
import shutil
from gi.repository import GLib, EDataServer, EBookContacts
from pkg_resources import resource_filename
from friends.protocols.facebook import Facebook
from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
from friends.tests.mocks import TestModel, mock
from friends.tests.mocks import EDSBookClientMock, EDSRegistry
from friends.errors import ContactsError, FriendsError, AuthorizationError
from friends.utils.cache import JsonCache
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestFacebook(unittest.TestCase):
"""Test the Facebook API."""
def setUp(self):
self._temp_cache = tempfile.mkdtemp()
self._root = JsonCache._root = os.path.join(
self._temp_cache, '{}.json')
self.account = FakeAccount()
self.protocol = Facebook(self.account)
self.protocol.source_registry = EDSRegistry()
def tearDown(self):
TestModel.clear()
shutil.rmtree(self._temp_cache)
def test_features(self):
# The set of public features.
self.assertEqual(Facebook.get_features(),
['contacts', 'delete', 'delete_contacts', 'home',
'like', 'receive', 'search', 'send', 'send_thread',
'unlike', 'upload', 'wall'])
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='abc'))
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'facebook-login.dat'))
def test_successful_login(self, *mocks):
# Test that a successful response from graph.facebook.com returning
# the user's data, sets up the account dict correctly.
self.protocol._login()
self.assertEqual(self.account.access_token, 'abc')
self.assertEqual(self.account.user_name, 'Bart Person')
self.assertEqual(self.account.user_id, '801')
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
def test_login_unsuccessful_authentication(self, *mocks):
# The user is not already logged in, but the act of logging in fails.
self.assertRaises(AuthorizationError, self.protocol._login)
self.assertIsNone(self.account.access_token)
self.assertIsNone(self.account.user_name)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='abc'))
@mock.patch('friends.protocols.facebook.Downloader.get_json',
return_value=dict(
error=dict(message='Bad access token',
type='OAuthException',
code=190)))
def test_error_response(self, *mocks):
with LogMock('friends.utils.base',
'friends.protocols.facebook') as log_mock:
self.assertRaises(
FriendsError,
self.protocol.home,
)
contents = log_mock.empty(trim=False)
self.assertEqual(contents, 'Logging in to Facebook\n')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'facebook-full.dat'))
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.protocols.facebook.Facebook._login',
return_value=True)
def test_receive(self, *mocks):
# Receive the wall feed for a user.
self.maxDiff = None
self.account.access_token = 'abc'
self.assertEqual(self.protocol.receive(), 12)
self.assertEqual(TestModel.get_n_rows(), 12)
self.assertEqual(list(TestModel.get_row(0)), [
'facebook',
88,
'userid_postid1',
'mentions',
'Yours Truly',
'56789',
'Yours Truly',
False,
'2013-03-13T23:29:07Z',
'Writing code that supports geotagging data from facebook. ' +
'If y\'all could make some geotagged facebook posts for me ' +
'to test with, that\'d be super.',
'https://graph.facebook.com/56789/picture?width=840&height=840',
'https://www.facebook.com/56789/posts/postid1',
1,
False,
'',
'',
'',
'',
'',
'',
'Victoria, British Columbia',
48.4333,
-123.35,
])
self.assertEqual(list(TestModel.get_row(2)), [
'facebook',
88,
'postid1_commentid2',
'reply_to/userid_postid1',
'Father',
'234',
'Father',
False,
'2013-03-12T23:29:45Z',
'don\'t know how',
'https://graph.facebook.com/234/picture?width=840&height=840',
'https://www.facebook.com/234/posts/commentid2',
0,
False,
'',
'',
'',
'',
'',
'',
'',
0.0,
0.0,
])
self.assertEqual(list(TestModel.get_row(6)), [
'facebook',
88,
'161247843901324_629147610444676',
'images',
'Best Western Denver Southwest',
'161247843901324',
'Best Western Denver Southwest',
False,
'2013-03-11T23:51:25Z',
'Today only -- Come meet Caroline and Meredith and Stanley the ' +
'Stegosaurus (& Greg & Joe, too!) at the TechZulu Trend Lounge, ' +
'Hilton Garden Inn 18th floor, 500 N Interstate 35, Austin, ' +
'Texas. Monday, March 11th, 4:00pm to 7:00 pm. Also here ' +
'Hannah Hart (My Drunk Kitchen) and Angry Video Game Nerd ' +
'producer, Sean Keegan. Stanley is in the lobby.',
'https://graph.facebook.com/161247843901324/picture?width=840&height=840',
'https://www.facebook.com/161247843901324/posts/629147610444676',
84,
False,
'http://graph.facebook.com/629147587111345/picture?type=normal',
'',
'https://www.facebook.com/photo.php?fbid=629147587111345&set=a.173256162700492.47377.161247843901324&type=1&relevant_count=1',
'',
'',
'',
'Hilton Garden Inn Austin Downtown/Convention Center',
30.265384957204,
-97.735604602521,
])
self.assertEqual(list(TestModel.get_row(9)), [
'facebook',
88,
'104443_100085049977',
'mentions',
'Guy Frenchie',
'1244414',
'Guy Frenchie',
False,
'2013-03-15T19:57:14Z',
'Guy Frenchie did some things with some stuff.',
'https://graph.facebook.com/1244414/picture?width=840&height=840',
'https://www.facebook.com/1244414/posts/100085049977',
3,
False,
'',
'',
'',
'',
'',
'',
'',
0.0,
0.0,
])
# XXX We really need full coverage of the receive() method, including
# cases where some data is missing, or can't be converted
# (e.g. timestamps), and paginations.
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'facebook-full.dat'))
@mock.patch('friends.protocols.facebook.Facebook._login',
return_value=True)
@mock.patch('friends.utils.base._seen_ids', {})
def test_home_since_id(self, *mocks):
self.account.access_token = 'access'
self.account.secret_token = 'secret'
self.assertEqual(self.protocol.home(), 12)
with open(self._root.format('facebook_ids'), 'r') as fd:
self.assertEqual(fd.read(), '{"messages": "2013-03-15T19:57:14Z"}')
follow = self.protocol._follow_pagination = mock.Mock()
follow.return_value = []
self.assertEqual(self.protocol.home(), 12)
follow.assert_called_once_with(
'https://graph.facebook.com/me/home',
dict(limit=50,
since='2013-03-15T19:57:14Z',
access_token='access',
)
)
@mock.patch('friends.protocols.facebook.Downloader')
def test_send_to_my_wall(self, dload):
dload().get_json.return_value = dict(id='post_id')
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish_entry = mock.Mock(
return_value='http://facebook.com/post_id')
self.assertEqual(
self.protocol.send('I can see the writing on my wall.'),
'http://facebook.com/post_id')
token.assert_called_once_with()
publish.assert_called_with(entry={'id': 'post_id'},
stream='messages')
self.assertEqual(
dload.mock_calls,
[mock.call(),
mock.call('https://graph.facebook.com/me/feed',
method='POST',
params=dict(
access_token='face',
message='I can see the writing on my wall.')),
mock.call().get_json(),
mock.call('https://graph.facebook.com/post_id',
params=dict(access_token='face')),
mock.call().get_json()
])
@mock.patch('friends.protocols.facebook.Downloader')
def test_send_to_my_friends_wall(self, dload):
dload().get_json.return_value = dict(id='post_id')
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish_entry = mock.Mock(
return_value='http://facebook.com/new_post_id')
self.assertEqual(
self.protocol.send('I can see the writing on my friend\'s wall.',
'friend_id'),
'http://facebook.com/new_post_id')
token.assert_called_once_with()
publish.assert_called_with(entry={'id': 'post_id'},
stream='messages')
self.assertEqual(
dload.mock_calls,
[mock.call(),
mock.call(
'https://graph.facebook.com/friend_id/feed',
method='POST',
params=dict(
access_token='face',
message='I can see the writing on my friend\'s wall.')),
mock.call().get_json(),
mock.call('https://graph.facebook.com/post_id',
params=dict(access_token='face')),
mock.call().get_json(),
])
@mock.patch('friends.protocols.facebook.Downloader')
def test_send_thread(self, dload):
dload().get_json.return_value = dict(id='comment_id')
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish_entry = mock.Mock(
return_value='http://facebook.com/private_message_id')
self.assertEqual(
self.protocol.send_thread('post_id', 'Some witty response!'),
'http://facebook.com/private_message_id')
token.assert_called_once_with()
publish.assert_called_with(entry={'id': 'comment_id'},
stream='reply_to/post_id')
self.assertEqual(
dload.mock_calls,
[mock.call(),
mock.call(
'https://graph.facebook.com/post_id/comments',
method='POST',
params=dict(
access_token='face',
message='Some witty response!')),
mock.call().get_json(),
mock.call('https://graph.facebook.com/comment_id',
params=dict(access_token='face')),
mock.call().get_json(),
])
@mock.patch('friends.protocols.facebook.Uploader.get_json',
return_value=dict(post_id='234125'))
@mock.patch('friends.protocols.facebook.time.time',
return_value=1352209748.1254)
def test_upload_local(self, *mocks):
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish = mock.Mock()
src = 'file://' + resource_filename('friends.tests.data', 'ubuntu.png')
self.assertEqual(self.protocol.upload(src, 'This is Ubuntu!'),
'https://www.facebook.com/234125')
token.assert_called_once_with()
publish.assert_called_once_with(
sender_nick=None,
stream='images',
url='https://www.facebook.com/234125',
timestamp='2012-11-06T13:49:08Z',
sender_id=None,
from_me=True,
icon_uri='https://graph.facebook.com/None/picture?type=large',
message='This is Ubuntu!',
message_id='234125',
sender=None)
@mock.patch('friends.utils.http._soup')
@mock.patch('friends.protocols.facebook.Uploader._build_request',
return_value=None)
@mock.patch('friends.protocols.facebook.time.time',
return_value=1352209748.1254)
def test_upload_missing(self, *mocks):
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish = mock.Mock()
src = 'file:///tmp/a/non-existant/path'
self.assertRaises(
ValueError,
self.protocol.upload,
src,
'There is no spoon',
)
token.assert_called_once_with()
self.assertFalse(publish.called)
@mock.patch('friends.utils.http._soup')
def test_upload_not_uri(self, *mocks):
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
publish = self.protocol._publish = mock.Mock()
src = resource_filename('friends.tests.data', 'ubuntu.png')
self.assertRaises(
GLib.GError,
self.protocol.upload,
src,
'There is no spoon',
)
token.assert_called_once_with()
self.assertFalse(publish.called)
def test_search(self):
self.protocol._get_access_token = lambda: '12345'
get_pages = self.protocol._follow_pagination = mock.Mock(
return_value=['search results'])
publish = self.protocol._publish_entry = mock.Mock()
self.assertEqual(self.protocol.search('hello'), 1)
publish.assert_called_with('search results', 'search/hello')
get_pages.assert_called_with(
'https://graph.facebook.com/search',
dict(q='hello', access_token='12345'))
@mock.patch('friends.protocols.facebook.Downloader')
def test_like(self, dload):
dload().get_json.return_value = True
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
inc_cell = self.protocol._inc_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.like('post_id'), 'post_id')
inc_cell.assert_called_once_with('post_id', 'likes')
set_cell.assert_called_once_with('post_id', 'liked', True)
token.assert_called_once_with()
dload.assert_called_with(
'https://graph.facebook.com/post_id/likes',
method='POST',
params=dict(access_token='face'))
@mock.patch('friends.protocols.facebook.Downloader')
def test_unlike(self, dload):
dload.get_json.return_value = True
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
dec_cell = self.protocol._dec_cell = mock.Mock()
set_cell = self.protocol._set_cell = mock.Mock()
self.assertEqual(self.protocol.unlike('post_id'), 'post_id')
dec_cell.assert_called_once_with('post_id', 'likes')
set_cell.assert_called_once_with('post_id', 'liked', False)
token.assert_called_once_with()
dload.assert_called_once_with(
'https://graph.facebook.com/post_id/likes',
method='DELETE',
params=dict(access_token='face'))
@mock.patch('friends.protocols.facebook.Downloader')
def test_delete(self, dload):
dload().get_json.return_value = True
token = self.protocol._get_access_token = mock.Mock(
return_value='face')
unpublish = self.protocol._unpublish = mock.Mock()
self.assertEqual(self.protocol.delete('post_id'), 'post_id')
token.assert_called_once_with()
dload.assert_called_with(
'https://graph.facebook.com/post_id',
method='DELETE',
params=dict(access_token='face'))
unpublish.assert_called_once_with('post_id')
@mock.patch('friends.protocols.facebook.Downloader')
def test_contacts(self, downloader):
downloader().get_json.return_value = dict(
name='Joe Blow', username='jblow', link='example.com', gender='male')
downloader.reset_mock()
self.protocol._get_access_token = mock.Mock(return_value='broken')
follow = self.protocol._follow_pagination = mock.Mock(
return_value=[dict(id='contact1'), dict(id='contact2')])
prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False)
push = self.protocol._push_to_eds = mock.Mock()
self.assertEqual(self.protocol.contacts(), 2)
follow.assert_called_once_with(
params={'access_token': 'broken', 'limit': 1000},
url='https://graph.facebook.com/me/friends',
limit=1000)
self.assertEqual(
prev.call_args_list,
[mock.call('contact1'), mock.call('contact2')])
self.assertEqual(
downloader.call_args_list,
[mock.call(url='https://graph.facebook.com/contact1',
params={'access_token': 'broken'}),
mock.call(url='https://graph.facebook.com/contact2',
params={'access_token': 'broken'})])
self.assertEqual(
push.call_args_list,
[mock.call(gender='male', jabber='-contact1@chat.facebook.com',
nick='jblow', link='example.com', name='Joe Blow',
uid='contact1'),
mock.call(gender='male', jabber='-contact2@chat.facebook.com',
nick='jblow', link='example.com', name='Joe Blow',
uid='contact2')])
def test_create_contact(self, *mocks):
# Receive the users friends.
eds_contact = self.protocol._create_contact(
uid='555555555',
name='Lucy Baron',
nick='lucy.baron5',
gender='female',
link='http:www.facebook.com/lucy.baron5',
jabber='-555555555@chat.facebook.com')
facebook_id_attr = eds_contact.get_attribute('facebook-id')
self.assertEqual(facebook_id_attr.get_value(), '555555555')
web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS')
params= web_service_addrs.get_params()
self.assertEqual(len(params), 5)
# Can't compare the vcard string directly because it is sorted randomly...
vcard = eds_contact.to_string(
EBookContacts.VCardFormat(1)).replace('\r\n ', '')
self.assertIn(
'social-networking-attributes.X-URIS:http:www.facebook.com/lucy.baron5',
vcard)
self.assertIn(
'social-networking-attributes.X-GENDER:female',
vcard)
self.assertIn(
'social-networking-attributes.facebook-id:555555555',
vcard)
self.assertIn(
'FN:Lucy Baron',
vcard)
self.assertIn(
'NICKNAME:lucy.baron5',
vcard)
self.assertIn(
'social-networking-attributes.X-FOLKS-WEB-SERVICES-IDS;',
vcard)
self.assertIn(
'remote-full-name="Lucy Baron"',
vcard)
self.assertIn(
'facebook-id=555555555',
vcard)
self.assertIn(
'jabber="-555555555@chat.facebook.com"',
vcard)
self.assertIn(
'facebook-nick="lucy.baron5"',
vcard)
@mock.patch('friends.utils.base.Base._prepare_eds_connections',
return_value=True)
@mock.patch('gi.repository.EBook.BookClient.new',
return_value=EDSBookClientMock())
def test_successfull_push_to_eds(self, *mocks):
bare_contact = {'name': 'Lucy Baron',
'uid': '555555555',
'nick': 'lucy.baron5',
'link': 'http:www.facebook.com/lucy.baron5'}
self.protocol._address_book = 'test-address-book'
client = self.protocol._book_client = mock.Mock()
client.add_contact_sync.return_value = True
# Implicitely fail test if the following raises any exceptions
self.protocol._push_to_eds(**bare_contact)
@mock.patch('friends.utils.base.Base._prepare_eds_connections',
return_value=None)
def test_unsuccessfull_push_to_eds(self, *mocks):
bare_contact = {'name': 'Lucy Baron',
'uid': '555555555',
'nick': 'lucy.baron5',
'link': 'http:www.facebook.com/lucy.baron5'}
self.protocol._address_book = 'test-address-book'
client = self.protocol._book_client = mock.Mock()
client.add_contact_sync.return_value = False
self.assertRaises(
ContactsError,
self.protocol._push_to_eds,
**bare_contact
)
@mock.patch('gi.repository.EBook.BookClient.connect_sync',
return_value=EDSBookClientMock())
@mock.patch('gi.repository.EDataServer.SourceRegistry.new_sync',
return_value=EDSRegistry())
def test_successful_previously_stored_contact(self, *mocks):
result = self.protocol._previously_stored_contact('11111')
self.assertEqual(result, True)
@mock.patch('gi.repository.EBook.BookClient.connect_sync',
return_value=EDSBookClientMock())
@mock.patch('gi.repository.EDataServer.SourceRegistry.new_sync',
return_value=EDSRegistry())
def test_first_run_prepare_eds_connections(self, *mocks):
self.protocol._name = 'testsuite'
self.assertIsNone(self.protocol._address_book_name)
self.assertIsNone(self.protocol._eds_source_registry)
self.assertIsNone(self.protocol._eds_source)
self.assertIsNone(self.protocol._book_client)
self.protocol._prepare_eds_connections()
self.assertEqual(self.protocol._address_book_name,
'friends-testsuite-contacts')
self.assertEqual(self.protocol._eds_source.get_display_name(),
'friends-testsuite-contacts')
self.assertEqual(self.protocol._eds_source.get_uid(),
'friends-testsuite-contacts')
self.protocol.delete_contacts()
@mock.patch('gi.repository.EDataServer.SourceRegistry')
@mock.patch('gi.repository.EDataServer.Source')
@mock.patch('gi.repository.EBook.BookClient')
def test_mocked_prepare_eds_connections(self, client, source, registry):
self.protocol._name = 'testsuite'
self.assertIsNone(self.protocol._address_book_name)
self.protocol._prepare_eds_connections()
self.protocol._prepare_eds_connections() # Second time harmlessly ignored
self.assertEqual(self.protocol._address_book_name,
'friends-testsuite-contacts')
registry.new_sync.assert_called_once_with(None)
self.assertEqual(self.protocol._eds_source_registry,
registry.new_sync())
registry.new_sync().ref_source.assert_called_once_with(
'friends-testsuite-contacts')
self.assertEqual(self.protocol._eds_source,
registry.new_sync().ref_source())
client.connect_sync.assert_called_once_with(
registry.new_sync().ref_source(), None)
self.assertEqual(self.protocol._book_client,
client.connect_sync())
@mock.patch('gi.repository.EDataServer.SourceRegistry')
@mock.patch('gi.repository.EDataServer.Source')
@mock.patch('gi.repository.EBook.BookClient')
def test_create_new_eds_book(self, client, source, registry):
self.protocol._name = 'testsuite'
self.assertIsNone(self.protocol._address_book_name)
registry.new_sync().ref_source.return_value = None
registry.reset_mock()
self.protocol._prepare_eds_connections()
self.protocol._prepare_eds_connections() # Second time harmlessly ignored
self.assertEqual(self.protocol._address_book_name,
'friends-testsuite-contacts')
registry.new_sync.assert_called_once_with(None)
self.assertEqual(self.protocol._eds_source_registry,
registry.new_sync())
registry.new_sync().ref_source.assert_called_once_with(
'friends-testsuite-contacts')
source.new_with_uid.assert_called_once_with(
'friends-testsuite-contacts', None)
self.assertEqual(self.protocol._eds_source,
source.new_with_uid())
source.new_with_uid().set_display_name.assert_called_once_with(
'friends-testsuite-contacts')
source.new_with_uid().set_parent.assert_called_once_with('local-stub')
source.new_with_uid().get_extension.assert_called_once_with(
EDataServer.SOURCE_EXTENSION_ADDRESS_BOOK)
registry.new_sync().commit_source_sync.assert_called_once_with(
source.new_with_uid(), None)
client.connect_sync.assert_called_once_with(
source.new_with_uid(), None)
self.assertEqual(self.protocol._book_client,
client.connect_sync())
friends-0.2.0+14.04.20140217.1/friends/tests/test_menu.py 0000644 0000152 0177776 00000003170 12300444435 023050 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the MenuManager class."""
__all__ = [
'TestMenu',
]
import unittest
from friends.utils.menus import MenuManager
from friends.tests.mocks import mock
def callback_stub(*ignore):
pass
class TestMenu(unittest.TestCase):
"""Test MenuManager class."""
def setUp(self):
self.menu = MenuManager(callback_stub, callback_stub)
self.menu.messaging = mock.Mock()
def test_unread_count_visible(self):
# Calling update_unread_count() with a non-zero value will make the
# count visible.
self.menu.update_unread_count(42)
self.menu.messaging.set_source_count.assert_called_once_with(
'unread', 42)
def test_unread_count_invisible(self):
# Calling update_unread_count() with a zero value will make the count
# invisible.
self.menu.update_unread_count(0)
self.menu.messaging.remove_source.assert_called_once_with(
'unread')
friends-0.2.0+14.04.20140217.1/friends/tests/test_shortener.py 0000644 0000152 0177776 00000013620 12300444435 024116 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the URL shorteners."""
__all__ = [
'TestShorteners',
]
import unittest
from friends.utils.shorteners import Short
from friends.tests.mocks import FakeSoupMessage, mock
@mock.patch('friends.utils.http._soup', mock.Mock())
class TestShorteners(unittest.TestCase):
"""Test the various shorteners, albeit via mocks."""
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'short.dat'))
def test_isgd(self):
self.assertEqual(
Short('is.gd').make('http://www.python.org'),
'http://sho.rt/')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'short.dat'))
def test_ougd(self):
self.assertEqual(
Short('ou.gd').make('http://www.python.org'),
'http://sho.rt/')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'short.dat'))
def test_linkeecom(self):
self.assertEqual(
Short('linkee.com').make('http://www.python.org'),
'http://sho.rt/')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'short.dat'))
def test_tinyurlcom(self):
self.assertEqual(
Short('tinyurl.com').make('http://www.python.org'),
'http://sho.rt/')
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'durlme.dat'))
def test_durlme(self):
self.assertEqual(
Short('durl.me').make('http://www.python.org'),
'http://durl.me/5o')
def test_missing_or_disabled_lookup(self):
# Looking up a non-existent or disabled shortener gives you one that
# returns the original url back unchanged.
self.assertEqual(
Short('nonexistant').make('http://www.python.org'),
'http://www.python.org')
self.assertEqual(
Short().make('http://www.python.org'),
'http://www.python.org')
def test_is_shortened(self):
# Test URLs that have been shortened.
self.assertTrue(Short.already('http://tinyurl.com/foo'))
self.assertTrue(Short.already('http://is.gd/foo'))
self.assertTrue(Short.already('http://linkee.com/foo'))
self.assertTrue(Short.already('http://ou.gd/foo'))
self.assertTrue(Short.already('http://durl.me/foo'))
def test_is_not_shortened(self):
# Test a URL that has not been shortened.
self.assertFalse(Short.already('http://www.python.org/bar'))
@mock.patch('friends.utils.shorteners.Downloader')
def test_isgd_quoted_properly(self, dl_mock):
Short('is.gd').make('http://example.com/~user/stuff/+things')
dl_mock.assert_called_once_with(
'http://is.gd/api.php?longurl=http%3A%2F%2Fexample.com'
'%2F%7Euser%2Fstuff%2F%2Bthings')
@mock.patch('friends.utils.shorteners.Downloader')
def test_ougd_quoted_properly(self, dl_mock):
Short('ou.gd').make('http://example.com/~user/stuff/+things')
dl_mock.assert_called_once_with(
'http://ou.gd/api.php?format=simple&action=shorturl&url='
'http%3A%2F%2Fexample.com%2F%7Euser%2Fstuff%2F%2Bthings')
@mock.patch('friends.utils.shorteners.Downloader')
def test_linkeecom_quoted_properly(self, dl_mock):
Short('linkee.com').make(
'http://example.com/~user/stuff/+things')
dl_mock.assert_called_once_with(
'http://api.linkee.com/1.0/shorten?format=text&input='
'http%3A%2F%2Fexample.com%2F%7Euser%2Fstuff%2F%2Bthings')
@mock.patch('friends.utils.shorteners.Downloader')
def test_tinyurl_quoted_properly(self, dl_mock):
Short('tinyurl.com').make(
'http://example.com/~user/stuff/+things')
dl_mock.assert_called_once_with(
'http://tinyurl.com/api-create.php?url=http%3A%2F%2Fexample.com'
'%2F%7Euser%2Fstuff%2F%2Bthings')
@mock.patch('friends.utils.shorteners.Downloader')
def test_durlme_quoted_properly(self, dl_mock):
dl_mock().get_string().strip.return_value = ''
dl_mock.reset_mock()
Short('durl.me').make(
'http://example.com/~user/stuff/+things')
dl_mock.assert_called_once_with(
'http://durl.me/api/Create.do?type=json&longurl='
'http%3A%2F%2Fexample.com%2F%7Euser%2Fstuff%2F%2Bthings')
@mock.patch('friends.utils.shorteners.Downloader')
def test_dont_over_shorten(self, dl_mock):
Short('tinyurl.com').make('http://tinyurl.com/page_id')
Short('linkee.com').make('http://ou.gd/page_id')
Short('is.gd').make('http://is.gd/page_id')
Short('ou.gd').make('http://linkee.com/page_id')
self.assertEqual(dl_mock.call_count, 0)
def test_find_all_in_string(self):
shorter = Short()
shorter.make = lambda url: 'zombo.com'
self.assertEqual(
'Welcome to zombo.com, anything is possible. '
'You can do anything at zombo.com!',
shorter.sub(
'Welcome to http://example.com/really/really/long/url, '
'anything is possible. You can do anything at '
'http://example.com!'))
friends-0.2.0+14.04.20140217.1/friends/tests/test_protocols.py 0000644 0000152 0177776 00000050526 12300444435 024137 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test generic protocol support."""
__all__ = [
'TestProtocolManager',
'TestProtocols',
]
import unittest
import threading
from friends.protocols.flickr import Flickr
from friends.protocols.twitter import Twitter
from friends.tests.mocks import SCHEMA, FakeAccount, LogMock, TestModel, mock
from friends.utils.base import Base, feature, linkify_string
from friends.utils.manager import ProtocolManager
from friends.utils.model import Model
class TestProtocolManager(unittest.TestCase):
"""Test the protocol finder."""
def setUp(self):
self.manager = ProtocolManager()
self.log_mock = LogMock('friends.utils.base')
def tearDown(self):
self.log_mock.stop()
def test_find_all_protocols(self):
# The manager can find all the protocol classes.
self.assertEqual(self.manager.protocols['flickr'], Flickr)
self.assertEqual(self.manager.protocols['twitter'], Twitter)
def test_doesnt_find_base(self):
# Make sure that the base protocol class isn't returned.
self.assertNotIn('base', self.manager.protocols)
for protocol_class in self.manager.protocols.values():
self.assertNotEqual(protocol_class, Base)
class MyProtocol(Base):
"""Simplest possible protocol implementation to allow testing of Base."""
def noop(self, one=None, two=None):
return '{}:{}'.format(one, two)
# A non-public method that cannot be called through __call__()
_private = noop
def _locked_login(self, old_token):
self._account.access_token = 'fake_token'
# Two features and two non-features for testing purposes.
@feature
def feature_1(self): pass
@feature
def feature_2(self): pass
def non_feature_1(self): pass
def non_feature_2(self): pass
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestProtocols(unittest.TestCase):
"""Test protocol implementations."""
def setUp(self):
TestModel.clear()
def test_no_operation(self):
# Trying to call a protocol with a missing operation raises an
# AttributeError exception.
fake_account = object()
my_protocol = MyProtocol(fake_account)
with self.assertRaises(NotImplementedError) as cm:
my_protocol('give_me_a_pony')
self.assertEqual(str(cm.exception), 'give_me_a_pony')
def test_private_operation(self):
# Trying to call a protocol with a non-public operation raises an
# AttributeError exception.
fake_account = object()
my_protocol = MyProtocol(fake_account)
# We can call the method directly.
result = my_protocol._private('ant', 'bee')
self.assertEqual(result, 'ant:bee')
# But we cannot call the method through the __call__ interface.
with self.assertRaises(NotImplementedError) as cm:
my_protocol('_private', 'cat', 'dog')
self.assertEqual(str(cm.exception), '_private')
def test_basic_api_synchronous(self):
# Protocols get instantiated with an account, and the instance gets
# called to perform an operation.
fake_account = object()
my_protocol = MyProtocol(fake_account)
result = my_protocol.noop(one='foo', two='bar')
self.assertEqual(result, 'foo:bar')
def test_basic_api_asynchronous(self):
fake_account = object()
my_protocol = MyProtocol(fake_account)
success = mock.Mock()
failure = mock.Mock()
# Using __call__ makes invocation happen asynchronously in a thread.
my_protocol('noop', 'one', 'two',
success=success,
failure=failure)
for thread in threading.enumerate():
# Join all but the main thread.
if thread != threading.current_thread():
thread.join()
success.assert_called_once_with('one:two')
self.assertEqual(failure.call_count, 0)
@mock.patch('friends.utils.base.Model', TestModel)
def test_shared_model_successfully_mocked(self):
count = Model.get_n_rows()
self.assertEqual(TestModel.get_n_rows(), 0)
base = Base(FakeAccount())
base._publish(message_id='alpha', message='a')
base._publish(message_id='beta', message='b')
base._publish(message_id='omega', message='c')
self.assertEqual(Model.get_n_rows(), count)
self.assertEqual(TestModel.get_n_rows(), 3)
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_seen_dicts_successfully_instantiated(self):
from friends.utils.base import _seen_ids
from friends.utils.base import initialize_caches
self.assertEqual(TestModel.get_n_rows(), 0)
base = Base(FakeAccount())
base._publish(message_id='alpha', sender='a', message='a')
base._publish(message_id='beta', sender='a', message='a')
base._publish(message_id='omega', sender='a', message='b')
self.assertEqual(TestModel.get_n_rows(), 3)
_seen_ids.clear()
initialize_caches()
self.assertEqual(
_seen_ids,
dict(alpha=0,
beta=1,
omega=2,
)
)
@mock.patch('friends.utils.base.Model', TestModel)
def test_invalid_argument(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
with self.assertRaises(TypeError) as cm:
base._publish(message_id='message_id', invalid_argument='not good')
self.assertEqual(str(cm.exception),
'Unexpected keyword arguments: invalid_argument')
@mock.patch('friends.utils.base.Model', TestModel)
def test_invalid_arguments(self):
# All invalid arguments are mentioned in the exception message.
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
with self.assertRaises(TypeError) as cm:
base._publish(message_id='p.middy', bad='no', wrong='yes')
self.assertEqual(str(cm.exception),
'Unexpected keyword arguments: bad, wrong')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_one_message(self):
# Test that publishing a message inserts a row into the model.
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
sender_nick='freddy',
from_me=True,
timestamp='today',
message='hello, @jimmy',
likes=10,
liked=True))
self.assertEqual(1, TestModel.get_n_rows())
row = TestModel.get_row(0)
self.assertEqual(
list(row),
['base',
88,
'1234',
'messages',
'fred',
'',
'freddy',
True,
'today',
'hello, @jimmy',
'',
'',
10,
True,
'',
'',
'',
'',
'',
'',
'',
0.0,
0.0,
])
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_unpublish(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(
message_id='1234',
sender='fred',
message='hello, @jimmy'))
self.assertTrue(base._publish(
message_id='1234',
sender='fred',
message='hello, @jimmy'))
self.assertTrue(base._publish(
message_id='5678',
sender='fred',
message='hello, +jimmy'))
self.assertEqual(2, TestModel.get_n_rows())
base._unpublish('1234')
self.assertEqual(1, TestModel.get_n_rows())
base._unpublish('5678')
self.assertEqual(0, TestModel.get_n_rows())
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_duplicate_messages_identified(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
sender_nick='freddy',
from_me=True,
timestamp='today',
message='hello, @jimmy',
likes=10,
liked=True))
# Duplicate
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
sender_nick='freddy',
from_me=True,
timestamp='today',
message='hello jimmy',
likes=10,
liked=False))
# See, we get only one row in the table.
self.assertEqual(1, TestModel.get_n_rows())
# The first published message wins.
row = TestModel.get_row(0)
self.assertEqual(row[SCHEMA.INDICES['message']], 'hello, @jimmy')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_duplicate_ids_not_duplicated(self):
# When two messages are actually identical (same ids and all),
# we need to avoid duplicating the id in the sharedmodel.
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
message='hello, @jimmy'))
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
message='hello, @jimmy'))
self.assertEqual(1, TestModel.get_n_rows())
row = TestModel.get_row(0)
self.assertEqual(
list(row),
['base',
88,
'1234',
'messages',
'fred',
'',
'',
False,
'',
'hello, @jimmy',
'',
'',
0,
False,
'',
'',
'',
'',
'',
'',
'',
0.0,
0.0,
])
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_similar_messages_allowed(self):
# Because both the sender and message contribute to the unique key we
# use to identify messages, if two messages are published with
# different senders, both are inserted into the table.
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
# The key for this row is 'fredhellojimmy'
self.assertTrue(base._publish(
message_id='1234',
stream='messages',
sender='fred',
sender_nick='freddy',
from_me=True,
timestamp='today',
message='hello, @jimmy',
likes=10,
liked=True))
self.assertEqual(1, TestModel.get_n_rows())
# The key for this row is 'tedtholomewhellojimmy'
self.assertTrue(base._publish(
message_id='34567',
stream='messages',
sender='tedtholomew',
sender_nick='teddy',
from_me=False,
timestamp='today',
message='hello, @jimmy',
likes=10,
liked=True))
# See? Two rows in the table.
self.assertEqual(2, TestModel.get_n_rows())
# The first row is the message from fred.
self.assertEqual(TestModel.get_row(0)[SCHEMA.INDICES['sender']],
'fred')
# The second row is the message from tedtholomew.
self.assertEqual(TestModel.get_row(1)[SCHEMA.INDICES['sender']],
'tedtholomew')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_inc_cell(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
base._inc_cell('1234', 'likes')
row = TestModel.get_row(0)
self.assertEqual(
list(row),
['base', 88, '1234', '', '', '', '', False, '', '', '', '', 11,
True, '', '', '', '', '', '', '', 0.0, 0.0])
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_dec_cell(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
base._dec_cell('1234', 'likes')
row = TestModel.get_row(0)
self.assertEqual(
list(row),
['base', 88, '1234', '', '', '', '', False, '', '', '', '', 9,
True, '', '', '', '', '', '', '', 0.0, 0.0])
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_set_cell(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
base._set_cell('1234', 'likes', 500)
base._set_cell('1234', 'liked', False)
base._set_cell('1234', 'message_id', '5678')
row = TestModel.get_row(0)
self.assertEqual(
list(row),
['base', 88, '5678', '', '', '', '', False, '', '', '', '', 500,
False, '', '', '', '', '', '', '', 0.0, 0.0])
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.base._seen_ids', {})
def test_fetch_cell(self):
base = Base(FakeAccount())
self.assertEqual(0, TestModel.get_n_rows())
self.assertTrue(base._publish(message_id='1234', likes=10, liked=True))
self.assertEqual(base._fetch_cell('1234', 'likes'), 10)
def test_basic_login(self):
# Try to log in twice. The second login attempt returns False because
# it's already logged in.
my_protocol = MyProtocol(FakeAccount())
self.assertTrue(my_protocol._login())
self.assertFalse(my_protocol._login())
def test_failing_login(self):
# The first login attempt fails because _locked_login() does not set
# an access token.
class FailingProtocol(MyProtocol):
def _locked_login(self, old_token):
pass
my_protocol = FailingProtocol(FakeAccount())
self.assertFalse(my_protocol._login())
# XXX I think there's a threading test that should be performed, but it
# hurts my brain too much. See the comment at the bottom of the
# with-statement in Base._login().
def test_features(self):
self.assertEqual(MyProtocol.get_features(),
['delete_contacts', 'feature_1', 'feature_2'])
def test_linkify_string(self):
# String with no URL is unchanged.
self.assertEqual('Hello!', linkify_string('Hello!'))
# http:// works.
self.assertEqual(
'http://www.example.com',
linkify_string('http://www.example.com'))
# https:// works, too.
self.assertEqual(
'https://www.example.com',
linkify_string('https://www.example.com'))
# http:// is optional if you include www.
self.assertEqual(
'www.example.com',
linkify_string('www.example.com'))
# Haha, nobody uses ftp anymore!
self.assertEqual(
'ftp://example.com/',
linkify_string('ftp://example.com/'))
# Trailing periods are not linkified.
self.assertEqual(
'http://example.com.',
linkify_string('http://example.com.'))
# URL can contain periods without getting cut off.
self.assertEqual(
''
'http://example.com/products/buy.html.',
linkify_string('http://example.com/products/buy.html.'))
# Don't linkify trailing brackets.
self.assertEqual(
'Example Co (http://example.com).',
linkify_string('Example Co (http://example.com).'))
# Don't linkify trailing exclamation marks.
self.assertEqual(
'Go to https://example.com!',
linkify_string('Go to https://example.com!'))
# Don't linkify trailing commas, also ensure all links are found.
self.assertEqual(
'www.example.com, http://example.com/stuff, and '
'http://example.com/things '
'are my favorite sites.',
linkify_string('www.example.com, http://example.com/stuff, and '
'http://example.com/things are my favorite sites.'))
# Don't linkify trailing question marks.
self.assertEqual(
'Ever been to www.example.com?',
linkify_string('Ever been to www.example.com?'))
# URLs can contain question marks ok.
self.assertEqual(
'Like '
'http://example.com?foo=bar&grill=true?',
linkify_string('Like http://example.com?foo=bar&grill=true?'))
# URLs can contain encoded spaces and parentheses.
self.assertEqual(
''
'http://example.com/foo%20(3).JPG!',
linkify_string('http://example.com/foo%20(3).JPG!'))
# Multi-line strings are also supported.
self.assertEqual(
'Hey, visit us online!\n\n'
'http://example.com',
linkify_string('Hey, visit us online!\n\nhttp://example.com'))
# Don't accidentally duplicate linkification.
self.assertEqual(
'click here!',
linkify_string('click here!'))
self.assertEqual(
'www.example.com',
linkify_string('www.example.com'))
self.assertEqual(
'www.example.com is our website',
linkify_string(
'www.example.com is our website'))
self.assertEqual(
"www.example.com is our website",
linkify_string(
"www.example.com is our website"))
# This, apparently, is valid HTML.
self.assertEqual(
'www.example.com',
linkify_string(
'www.example.com'))
# Pump.io is throwing doctypes at us!
self.assertEqual(
'',
linkify_string(
''))
friends-0.2.0+14.04.20140217.1/friends/tests/test_cache.py 0000644 0000152 0177776 00000005130 12300444435 023145 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the JSON cacher."""
__all__ = [
'TestJsonCache',
]
import os
import shutil
import tempfile
import unittest
from pkg_resources import resource_filename
from friends.utils.cache import JsonCache
class TestJsonCache(unittest.TestCase):
"""Test JsonCache logic."""
def setUp(self):
self._temp_cache = tempfile.mkdtemp()
self._root = JsonCache._root = os.path.join(
self._temp_cache, '{}.json')
def tearDown(self):
# Clean up the temporary cache directory.
shutil.rmtree(self._temp_cache)
def test_creation(self):
cache = JsonCache('foo')
with open(self._root.format('foo'), 'r') as fd:
empty = fd.read()
self.assertEqual(empty, '{}')
def test_values(self):
cache = JsonCache('bar')
cache['hello'] = 'world'
with open(self._root.format('bar'), 'r') as fd:
result = fd.read()
self.assertEqual(result, '{"hello": "world"}')
def test_writes(self):
cache = JsonCache('stuff')
cache.update(dict(pi=289/92))
with open(self._root.format('stuff'), 'r') as fd:
empty = fd.read()
self.assertEqual(empty, '{}')
cache.write()
with open(self._root.format('stuff'), 'r') as fd:
result = fd.read()
self.assertEqual(result, '{"pi": 3.141304347826087}')
def test_invalid_json(self):
shutil.copyfile(
resource_filename('friends.tests.data', 'facebook_ids_not.json'),
os.path.join(self._temp_cache, 'invalid.json'))
cache = JsonCache('invalid')
self.assertEqual(repr(cache), '{}')
def test_total_corruption(self):
shutil.copyfile(
resource_filename('friends.tests.data', 'facebook_ids_corrupt.json'),
os.path.join(self._temp_cache, 'corrupt.json'))
cache = JsonCache('corrupt')
self.assertEqual(repr(cache), '{}')
friends-0.2.0+14.04.20140217.1/friends/tests/test_linkedin.py 0000644 0000152 0177776 00000013537 12300444435 023711 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the LinkedIn plugin."""
__all__ = [
'TestLinkedIn',
]
import unittest
from friends.protocols.linkedin import LinkedIn, make_fullname
from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock
from friends.tests.mocks import TestModel, mock
from friends.errors import AuthorizationError
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.base.notify', mock.Mock())
class TestLinkedIn(unittest.TestCase):
"""Test the LinkedIn API."""
def setUp(self):
TestModel.clear()
self.account = FakeAccount()
self.protocol = LinkedIn(self.account)
self.log_mock = LogMock('friends.utils.base',
'friends.protocols.linkedin')
def tearDown(self):
# Ensure that any log entries we haven't tested just get consumed so
# as to isolate out test logger from other tests.
self.log_mock.stop()
def test_name_logic(self):
self.assertEqual('', make_fullname())
self.assertEqual('', make_fullname(irrelevant_key='foo'))
self.assertEqual('Bob', make_fullname(**dict(firstName='Bob')))
self.assertEqual('LastOnly', make_fullname(**dict(lastName='LastOnly')))
self.assertEqual(
'Bob Loblaw',
make_fullname(**dict(firstName='Bob', lastName='Loblaw')))
self.assertEqual(
'Bob Loblaw',
make_fullname(**dict(firstName='Bob', lastName='Loblaw',
extra='ignored')))
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
@mock.patch('friends.utils.authentication.Signon.AuthSession.new')
@mock.patch('friends.protocols.linkedin.Downloader.get_json',
return_value=None)
def test_unsuccessful_authentication(self, dload, login, *mocks):
self.assertRaises(AuthorizationError, self.protocol._login)
self.assertIsNone(self.account.user_name)
self.assertIsNone(self.account.user_id)
@mock.patch('friends.utils.authentication.manager')
@mock.patch('friends.utils.authentication.Accounts')
@mock.patch('friends.utils.authentication.Authentication.__init__',
return_value=None)
@mock.patch('friends.utils.authentication.Authentication.login',
return_value=dict(AccessToken='some clever fake data'))
@mock.patch('friends.protocols.linkedin.Downloader.get_json',
return_value=dict(id='blerch', firstName='Bob', lastName='Loblaw'))
def test_successful_authentication(self, *mocks):
self.assertTrue(self.protocol._login())
self.assertEqual(self.account.user_name, 'Bob Loblaw')
self.assertEqual(self.account.user_id, 'blerch')
self.assertEqual(self.account.access_token, 'some clever fake data')
@mock.patch('friends.utils.base.Model', TestModel)
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'linkedin_receive.json'))
@mock.patch('friends.protocols.linkedin.LinkedIn._login',
return_value=True)
@mock.patch('friends.utils.base._seen_ids', {})
def test_home(self, *mocks):
self.account.access_token = 'access'
self.assertEqual(0, TestModel.get_n_rows())
self.assertEqual(self.protocol.home(), 1)
self.assertEqual(1, TestModel.get_n_rows())
self.maxDiff = None
self.assertEqual(
list(TestModel.get_row(0)),
['linkedin', 88, 'UNIU-73705-576270369559388-SHARE', 'messages',
'Hobson L', 'ma0LLid', '', False, '2013-07-16T00:47:06Z',
'I\'m looking forward to the Udacity Global meetup event here in '
'Portland: http://lnkd.in/dh5MQz'
'\nGreat way to support the next big thing in c…',
'http://m.c.lnkd.licdn.com/mpr/mprx/0_mVxsC0BnN52aqc24yWvoyA5haqc2Z'
'wLCgzLv0EiBGp7n2jTwX-ls_dzgkSVIZu0',
'https://www.linkedin.com/profile/view?id=7375&authType=name'
'&authToken=-LNy&trk=api*a26127*s26893*',
1, False, '', '', '', '', '', '', '', 0.0, 0.0])
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'linkedin_contacts.json'))
@mock.patch('friends.protocols.linkedin.LinkedIn._login',
return_value=True)
def test_contacts(self, *mocks):
push = self.protocol._push_to_eds = mock.Mock()
prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False)
token = self.protocol._get_access_token = mock.Mock(return_value='foo')
self.protocol._create_contact = lambda arg:arg
self.assertEqual(self.protocol.contacts(), 4)
self.assertEqual(
push.mock_calls,
[mock.call(link='https://www.linkedin.com', name='H A', uid='IFDI'),
mock.call(link='https://www.linkedin.com', name='C A', uid='AefF'),
mock.call(link='https://www.linkedin.com', name='R A', uid='DFdV'),
mock.call(link='https://www.linkedin.com', name='A Z', uid='xkBU')])
friends-0.2.0+14.04.20140217.1/friends/tests/test_download.py 0000644 0000152 0177776 00000024261 12300444435 023717 0 ustar pbuser nogroup 0000000 0000000 # friends-dispatcher -- send & receive messages from any social network
# Copyright (C) 2012 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Test the downloading utilities."""
__all__ = [
'TestDownloader',
]
import json
import time
import datetime
import unittest
import threading
from urllib.error import URLError
from urllib.parse import parse_qs
from urllib.request import urlopen
from wsgiref.simple_server import WSGIRequestHandler, make_server
from wsgiref.util import setup_testing_defaults
from pkg_resources import resource_filename
from friends.tests.mocks import FakeSoupMessage, LogMock, mock
from friends.utils.http import Downloader, Uploader
class _SilentHandler(WSGIRequestHandler):
def log_message(self, format, *args):
pass
def _app(environ, start_response):
"""WSGI application for responding to test queries."""
setup_testing_defaults(environ)
status = '200 OK'
results = []
headers = [('Content-Type', 'text/plain; charset=utf-8')]
path = environ['PATH_INFO']
if path == '/ping':
pass
elif path == '/mirror':
size = int(environ['CONTENT_LENGTH'])
results = [environ['wsgi.input'].read(size)]
elif path == '/json':
results = [json.dumps(dict(answer='hello')).encode('utf-8')]
elif path == '/post':
# Might be a GET or POST.
if environ['REQUEST_METHOD'] == 'POST':
size = int(environ['CONTENT_LENGTH'])
payload = environ['wsgi.input'].read(size)
params = parse_qs(payload)
# We want a mapping from strings to ints, but the POST data will
# be utf-8 encoded byte keys and lists of length 1 with byte
# representations of integers. Do the conversion.
converted = {key.decode('utf-8'): int(value[0])
for key, value in params.items()}
else:
assert environ['REQUEST_METHOD'] == 'GET'
payload = parse_qs(environ['QUERY_STRING'])
# We want a mapping from strings to ints, but the query string
# will be strings and lists of length 1 with string representation
# of integers. Do the conversion.
converted = {key: int(value[0])
for key, value in payload.items()}
results = [json.dumps(converted).encode('utf-8')]
elif path == '/headers':
http_headers = {}
for key, value in environ.items():
if key.startswith('HTTP_X_'):
http_headers[key[7:].lower()] = value
results = [json.dumps(http_headers).encode('utf-8')]
elif path == '/text':
results = [b'hello world']
elif path == '/bytes':
results = [bytes.fromhex('f157f00d')]
else:
status = '404 Bad'
results = [b'Missing']
start_response(status, headers)
return results
class TestDownloader(unittest.TestCase):
"""Test the downloading utilities."""
def setUp(self):
self.log_mock = LogMock('friends.utils.http')
def tearDown(self):
self.log_mock.stop()
@classmethod
def setUpClass(cls):
cls.server = make_server('', 9180, _app, handler_class=_SilentHandler)
cls.thread = threading.Thread(target=cls.server.serve_forever)
cls.thread.start()
# Wait until the server is responding.
until = datetime.datetime.now() + datetime.timedelta(seconds=30)
while datetime.datetime.now() < until:
try:
with urlopen('http://localhost:9180/ping'):
pass
except URLError:
time.sleep(0.1)
else:
break
else:
raise RuntimeError('Server thread did not start up.')
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.thread.join()
def test_simple_json_download(self):
# Test simple downloading of JSON data.
self.assertEqual(Downloader('http://localhost:9180/json').get_json(),
dict(answer='hello'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
'json-utf-8.dat', 'utf-8'))
def test_json_explicit_utf_8(self):
# RFC 4627 $3 with explicit charset=utf-8.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data', 'json-utf-8.dat', None))
def test_json_implicit_utf_8(self):
# RFC 4627 $3 with implicit charset=utf-8.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
'json-utf-16le.dat', None))
def test_json_implicit_utf_16le(self):
# RFC 4627 $3 with implicit charset=utf-16le.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
'json-utf-16be.dat', None))
def test_json_implicit_utf_16be(self):
# RFC 4627 $3 with implicit charset=utf-16be.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
'json-utf-32le.dat', None))
def test_json_implicit_utf_32le(self):
# RFC 4627 $3 with implicit charset=utf-32le.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
@mock.patch('friends.utils.http._soup', mock.Mock())
@mock.patch('friends.utils.http.Soup.Message',
FakeSoupMessage('friends.tests.data',
'json-utf-32be.dat', None))
def test_json_implicit_utf_32be(self):
# RFC 4627 $3 with implicit charset=utf-32be.
self.assertEqual(Downloader('http://example.com').get_json(),
dict(yes='ÑØ'))
def test_simple_text_download(self):
# Test simple downloading of text data.
self.assertEqual(Downloader('http://localhost:9180/text').get_string(),
'hello world')
def test_simple_bytes_download(self):
# Test simple downloading of bytes data.
bytes_data = Downloader('http://localhost:9180/bytes').get_bytes()
self.assertIsInstance(bytes_data, bytes)
self.assertEqual(list(bytes_data), [241, 87, 240, 13])
def test_params_post(self):
# Test posting data.
self.assertEqual(
Downloader('http://localhost:9180/post',
params=dict(one=1, two=2, three=3),
method='POST').get_json(),
dict(one=1, two=2, three=3))
def test_params_get(self):
# Test getting with query string URL.
self.assertEqual(
Downloader('http://localhost:9180/post',
params=dict(one=1, two=2, three=3),
method='GET').get_json(),
dict(one=1, two=2, three=3))
def test_headers(self):
# Provide some additional headers.
self.assertEqual(
Downloader('http://localhost:9180/headers',
headers={'X-Foo': 'baz',
'X-Bar': 'foo'}).get_json(),
dict(foo='baz', bar='foo'))
def test_mirror(self):
self.assertEqual(
Downloader('http://localhost:9180/mirror',
method='POST',
params=dict(foo='bar')).get_string(),
'foo=bar')
def test_uploads(self):
filename = resource_filename('friends.tests.data', 'ubuntu.png')
raw_sent = Uploader(
'http://localhost:9180/mirror',
'file://' + filename,
'Great logo!',
picture_key='source',
desc_key='message',
foo='bar',
).get_bytes()
delimiter = raw_sent[2:66]
self.assertTrue(
raw_sent.startswith(
b'--' + delimiter + b'\r\nContent-Disposition: form-data; '
b'name="foo"\r\n\r\nbar\r\n--' + delimiter +
b'\r\nContent-Disposition: form-data; name="message"\r\n\r\n'
b'Great logo!\r\n--' + delimiter + b'\r\nContent-Disposition: '
b'form-data; name="source"; filename="file://' +
filename.encode() + b'"\r\nContent-Type: '
b'application/octet-stream\r\n\r\n\x89PNG'))
self.assertTrue(
raw_sent.endswith(
b'\r\n--' + delimiter + b'--\r\n'))
@mock.patch('friends.utils.http.Soup')
@mock.patch('friends.utils.http._soup')
def test_upload_happens_only_once(self, _soupmock, Soupmock):
filename = resource_filename('friends.tests.data', 'ubuntu.png')
Uploader(
'http://localhost:9180/mirror',
'file://' + filename,
'Great logo!',
picture_key='source',
desc_key='message',
foo='bar',
).get_bytes()
_soupmock.send_message.assert_called_once_with(
Soupmock.form_request_new_from_multipart())
friends-0.2.0+14.04.20140217.1/friends/tests/data/ 0000755 0000152 0177776 00000000000 12300444701 021377 5 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/tests/data/__init__.py 0000644 0000152 0177776 00000000000 12300444435 023502 0 ustar pbuser nogroup 0000000 0000000 friends-0.2.0+14.04.20140217.1/friends/tests/data/linkedin_receive.json 0000644 0000152 0177776 00000024734 12300444435 025607 0 ustar pbuser nogroup 0000000 0000000 {"_count": 10,
"_start": 0,
"_total": 23,
"values": [{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"likes": {"_total": 1,
"values": [{"person": {"firstName": "Tigran",
"headline": "Software Engineer at Cornerstone OnDemand",
"id": "6f7TDUv",
"lastName": "K",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_zR-8KkCNtotClCYzyiPKFr9rqDrlCYBM60KFPQftJhzvOMjGuObFeOSfTn_tq4rx8jhPrK"}}]},
"numLikes": 1,
"timestamp": 1373935626874,
"updateComments": {"_total": 0},
"updateContent": {"person": {"apiStandardProfileRequest": {"headers": {"_total": 1,
"values": [{"name": "x-li-auth-token",
"value": "name:-LNy"}]},
"url": "http://api.linkedin.com/v1/people/Pa0L6dU"},
"currentStatus": "I'm looking forward to the Udacity Global meetup event here in Portland: http://lnkd.in/dh5MQz\nGreat way to support the next big thing in c…",
"firstName": "Hobson",
"headline": "Developer at Building Energy, Inc",
"id": "ma0LLid",
"lastName": "L",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_mVxsC0BnN52aqc24yWvoyA5haqc2ZwLCgzLv0EiBGp7n2jTwX-ls_dzgkSVIZu0",
"siteStandardProfileRequest": {"url": "https://www.linkedin.com/profile/view?id=7375&authType=name&authToken=-LNy&trk=api*a26127*s26893*"}}},
"updateKey": "UNIU-73705-576270369559388-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373900948273,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-1060864-576255824267264-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373899922654,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-106088-5769411789496-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"likes": {"_total": 3,
"values": [{"person": {"firstName": "J. P.",
"headline": "Software Technologist",
"id": "GUUXiHdd40",
"lastName": "N",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_3Y7iZs_hWCTi0d-ZxAJn8XpT_pU-ZxGoW6GSZxT90nXZSnShgSPB4rdoNQi"}},
{"person": {"firstName": "Tyler",
"headline": "Senior Associate at Toffler Associates",
"id": "RCYVRJ",
"lastName": "S",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_i7W-Lvgud5pur7I-eBpecy7IlZe0yFr_zWdOUMNHHuhiLkRWj8ufDpQBz"}},
{"person": {"firstName": "Paweł",
"headline": "Senior programmer at EMG Systems",
"id": "FjbEJi",
"lastName": "S",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_SEgTdwePXDoInf9XIm-XfHanGqqoLk3uoLT0aMWtuW4ch4ekYCqF"}}]},
"numLikes": 3,
"timestamp": 1373638523241,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-106764-5761479649536-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"likes": {"_total": 2,
"values": [{"person": {"firstName": "João",
"headline": "System Engineer",
"id": "DXmxRB",
"lastName": "M",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_Vu55rTWdTtWwT-xSwZiP4alVriEVYAwgNpq1vWY_xR7bU0kZ8mjE9"}},
{"person": {"firstName": "private",
"id": "private",
"lastName": "private"}}]},
"numLikes": 2,
"timestamp": 1373638247645,
"updateComments": {"_total": 1,
"values": [{"comment": "Forgot to mention: Floor Drees wrote the article and was the workshop coach",
"id": 155125,
"person": {"firstName": "private",
"id": "private",
"lastName": "private"},
"sequenceNumber": 0,
"timestamp": 1373638357000}]},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-10664-57614646232064-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"likes": {"_total": 2,
"values": [{"person": {"firstName": "Tomáš",
"headline": "Invisible software writer, amateur storyteller and wannabe clown.",
"id": "37f-Kc",
"lastName": "J",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_C9Sz75iM9tWx_54nX12tErnWLYY3-joGjcY6V85pwujCGB7"}},
{"person": {"firstName": "Miguel",
"headline": "Senior Consultant at Red Hat",
"id": "RsdH",
"lastName": "P",
"pictureUrl": "http://m.c.lnkd.licdn.com/mpr/mprx/0_9-9rsGcir_ciT05XHD8i2Asq4AM_oi4k1ly6SD"}}]},
"numLikes": 2,
"timestamp": 1373530350025,
"updateComments": {"_total": 1,
"values": [{"comment": "Interesting read.....",
"id": 1369,
"person": {"firstName": "private",
"id": "private",
"lastName": "private"},
"sequenceNumber": 0,
"timestamp": 1373533027000}]},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-1064-51227071488-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373465844294,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-10664-33284581519360-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373358138541,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-106088-3910880256-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373354287104,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-10607-13277696-SHARE",
"updateType": "STAT"},
{"isCommentable": true,
"isLikable": true,
"isLiked": false,
"numLikes": 0,
"timestamp": 1373273818071,
"updateComments": {"_total": 0},
"updateContent": {"person": {"firstName": "private",
"id": "private",
"lastName": "private"}},
"updateKey": "UNIU-1060884-868226281472-SHARE",
"updateType": "STAT"}]}
friends-0.2.0+14.04.20140217.1/friends/tests/data/facebook-login.dat 0000644 0000152 0177776 00000000044 12300444435 024752 0 ustar pbuser nogroup 0000000 0000000 {"id": "801", "name": "Bart Person"} friends-0.2.0+14.04.20140217.1/friends/tests/data/linkedin_contacts.json 0000644 0000152 0177776 00000006041 12300444435 025772 0 ustar pbuser nogroup 0000000 0000000 {"_total": 4,
"values": [{"apiStandardProfileRequest": {"headers": {"_total": 1,
"values": [{"name": "x-li-auth-token",
"value": "name:"}]},
"url": "http://api.linkedin.com"},
"firstName": "H",
"headline": "Unix Administrator at NVIDIA",
"id": "IFDI",
"industry": "Computer Network Security",
"lastName": "A",
"location": {"country": {"code": "in"},
"name": "Pune Area, India"},
"pictureUrl": "http://m.c.lnkd.licdn.com",
"siteStandardProfileRequest": {"url": "https://www.linkedin.com"}},
{"apiStandardProfileRequest": {"headers": {"_total": 1,
"values": [{"name": "x-li-auth-token",
"value": "name:"}]},
"url": "http://api.linkedin.com"},
"firstName": "C",
"headline": "Recent Graduate, Simon Fraser University",
"id": "AefF",
"industry": "Food Production",
"lastName": "A",
"location": {"country": {"code": "ca"},
"name": "Vancouver, Canada Area"},
"pictureUrl": "http://m.c.lnkd.licdn.com",
"siteStandardProfileRequest": {"url": "https://www.linkedin.com"}},
{"apiStandardProfileRequest": {"headers": {"_total": 1,
"values": [{"name": "x-li-auth-token",
"value": "name:"}]},
"url": "http://api.linkedin.com"},
"firstName": "R",
"headline": "Technical Lead at Canonical Ltd.",
"id": "DFdV",
"industry": "Computer Software",
"lastName": "A",
"location": {"country": {"code": "nz"},
"name": "Auckland, New Zealand"},
"pictureUrl": "http://m.c.lnkd.licdn.com",
"siteStandardProfileRequest": {"url": "https://www.linkedin.com"}},
{"apiStandardProfileRequest": {"headers": {"_total": 1,
"values": [{"name": "x-li-auth-token",
"value": "name:"}]},
"url": "http://api.linkedin.com"},
"firstName": "A",
"headline": "Sales manager at McBain Camera",
"id": "xkBU",
"industry": "Photography",
"lastName": "Z",
"location": {"country": {"code": "ca"},
"name": "Edmonton, Canada Area"},
"pictureUrl": "http://m.c.lnkd.licdn.com",
"siteStandardProfileRequest": {"url": "https://www.linkedin.com"}}]}
friends-0.2.0+14.04.20140217.1/friends/tests/data/twitter-multiple-links.dat 0000644 0000152 0177776 00000010547 12300444435 026555 0 ustar pbuser nogroup 0000000 0000000 {
"created_at": "Thu Jan 23 11:40:35 +0000 2014",
"id": 426318539796930560,
"id_str": "426318539796930560",
"text": "An old people's home has recreated famous movie scenes for a wonderful calendar http://t.co/jjqteYzur0 http://t.co/JxQTPG7WLL",
"source": "TweetDeck",
"truncated": false,
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user": {
"id": 16973333,
"id_str": "16973333",
"name": "The Independent",
"screen_name": "Independent",
"location": "London, United Kingdom",
"description": "News, comment and features from The Independent. Also follow: @IndyVoices, \r\n@IndyPolitics, @IndyWorld and our journalists at http://t.co/YjS7NcXK4A",
"url": "http://t.co/wDS5ly0QoO",
"entities": {
"url": {
"urls": [
{
"url": "http://t.co/wDS5ly0QoO",
"expanded_url": "http://www.independent.co.uk",
"display_url": "independent.co.uk",
"indices": [
0,
22
]
}
]
},
"description": {
"urls": [
{
"url": "http://t.co/YjS7NcXK4A",
"expanded_url": "http://ind.pn/Wdlm9a",
"display_url": "ind.pn/Wdlm9a",
"indices": [
126,
148
]
}
]
}
},
"protected": false,
"followers_count": 505759,
"friends_count": 1747,
"listed_count": 7890,
"created_at": "Sun Oct 26 00:00:29 +0000 2008",
"favourites_count": 29,
"utc_offset": 0,
"time_zone": "London",
"geo_enabled": false,
"verified": true,
"statuses_count": 58003,
"lang": "en",
"contributors_enabled": false,
"is_translator": false,
"profile_background_color": "EBEBEB",
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png",
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/378800000119288704/4ac964c83462c88837dc1e735aa1a45e.png",
"profile_background_tile": true,
"profile_image_url": "http://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg",
"profile_image_url_https": "https://pbs.twimg.com/profile_images/378800000706113664/d1a957578723e496c025be1e2577d06d_normal.jpeg",
"profile_link_color": "FC051A",
"profile_sidebar_border_color": "FFFFFF",
"profile_sidebar_fill_color": "FFFFFF",
"profile_text_color": "333333",
"profile_use_background_image": true,
"default_profile": false,
"default_profile_image": false,
"following": false,
"follow_request_sent": false,
"notifications": false
},
"geo": null,
"coordinates": null,
"place": null,
"contributors": null,
"retweet_count": 131,
"favorite_count": 83,
"entities": {
"hashtags": [],
"symbols": [],
"urls": [
{
"url": "http://t.co/jjqteYzur0",
"expanded_url": "http://ind.pn/1g3wX9q",
"display_url": "ind.pn/1g3wX9q",
"indices": [
80,
102
]
}
],
"user_mentions": [],
"media": [
{
"id": 426318539692056600,
"id_str": "426318539692056576",
"indices": [
103,
125
],
"media_url": "http://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg",
"media_url_https": "https://pbs.twimg.com/media/BeqWc_-CIAAhmdc.jpg",
"url": "http://t.co/JxQTPG7WLL",
"display_url": "pic.twitter.com/JxQTPG7WLL",
"expanded_url": "http://twitter.com/Independent/status/426318539796930560/photo/1",
"type": "photo",
"sizes": {
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"large": {
"w": 1024,
"h": 682,
"resize": "fit"
},
"small": {
"w": 340,
"h": 226,
"resize": "fit"
},
"medium": {
"w": 600,
"h": 400,
"resize": "fit"
}
}
}
]
},
"favorited": false,
"retweeted": false,
"possibly_sensitive": false,
"lang": "en"
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/json-utf-8.dat 0000644 0000152 0177776 00000000027 12300444435 024006 0 ustar pbuser nogroup 0000000 0000000 {"yes": "\u00d1\u00d8"} friends-0.2.0+14.04.20140217.1/friends/tests/data/flickr-nophotos.dat 0000644 0000152 0177776 00000000032 12300444435 025211 0 ustar pbuser nogroup 0000000 0000000 {"photos": {"photo": []}}
friends-0.2.0+14.04.20140217.1/friends/tests/data/instagram-login.dat 0000644 0000152 0177776 00000000524 12300444435 025171 0 ustar pbuser nogroup 0000000 0000000 {
"meta": {
"code": 200
},
"data": {
"username": "bpersons",
"bio": "",
"website": "",
"profile_picture": "http://images.ak.instagram.com/profiles/anonymousUser.jpg",
"full_name": "Bart Person",
"counts": {
"media": 174,
"followed_by": 100,
"follows": 503
},
"id": "801"
}
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/facebook_ids_not.json 0000644 0000152 0177776 00000000017 12300444435 025564 0 ustar pbuser nogroup 0000000 0000000 }invalid json{
friends-0.2.0+14.04.20140217.1/friends/tests/data/twitter-retweet.dat 0000644 0000152 0177776 00000010471 12300444435 025257 0 ustar pbuser nogroup 0000000 0000000 {"place":null,"user":{"id":836242932,"profile_background_tile":false,"geo_enabled":false,"profile_sidebar_fill_color":"DDEEF6","utc_offset":null,"entities":{"description":{"urls":[]}},"name":"Robert Bruce","is_translator":false,"default_profile_image":false,"profile_background_color":"C0DEED","favourites_count":9,"statuses_count":96,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2631306428\/2a509db8a05b4310394b832d34a137a4_normal.png","protected":false,"created_at":"Thu Sep 20 19:53:35 +0000 2012","lang":"en","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_link_color":"0084B4","friends_count":44,"follow_request_sent":false,"profile_use_background_image":true,"default_profile":true,"profile_text_color":"333333","screen_name":"therealrobru","contributors_enabled":false,"url":null,"verified":false,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","time_zone":null,"followers_count":8,"following":false,"id_str":"836242932","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2631306428\/2a509db8a05b4310394b832d34a137a4_normal.png","location":null,"profile_sidebar_border_color":"C0DEED","description":"Misanthrope. No pants. Tied to a tree with a skipping rope.","listed_count":0,"notifications":false},"favorited":false,"contributors":null,"in_reply_to_user_id":null,"id_str":"324220250889543682","retweet_count":1,"truncated":true,"created_at":"Tue Apr 16 17:58:26 +0000 2013","geo":null,"in_reply_to_status_id_str":null,"retweeted":true,"coordinates":null,"text":"RT @tarek_ziade: Just found a \"Notification of Inspection\" card in the bottom of my bag. looks like they were curious about those raspbe ...","in_reply_to_user_id_str":null,"retweeted_status":{"place":null,"user":{"id":15821652,"profile_background_tile":true,"geo_enabled":true,"profile_sidebar_fill_color":"DAECF4","utc_offset":3600,"entities":{"url":{"urls":[{"indices":[0,16],"url":"http:\/\/ziade.org","display_url":null,"expanded_url":null}]},"description":{"urls":[]}},"name":"Tarek Ziad\u00e9","is_translator":false,"default_profile_image":false,"profile_background_color":"C6E2EE","favourites_count":175,"statuses_count":8673,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/3308520017\/f35acb40d385f320eea8a789c2a5a37b_normal.jpeg","protected":false,"created_at":"Tue Aug 12 12:40:35 +0000 2008","lang":"en","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/800421101\/30f37bcf2208ea3700fe07ce22722667.jpeg","profile_link_color":"1F98C7","friends_count":93,"follow_request_sent":false,"profile_use_background_image":true,"default_profile":false,"profile_text_color":"663B12","screen_name":"tarek_ziade","contributors_enabled":false,"url":"http:\/\/ziade.org","verified":false,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/800421101\/30f37bcf2208ea3700fe07ce22722667.jpeg","time_zone":"Paris","followers_count":3032,"following":true,"id_str":"15821652","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/3308520017\/f35acb40d385f320eea8a789c2a5a37b_normal.jpeg","location":"Turcey, France","profile_sidebar_border_color":"FFFFFF","description":"Country-side Python developer \u00b7 Works at Mozilla \u00b7 wrote some books","listed_count":279,"notifications":false},"favorited":false,"contributors":null,"in_reply_to_user_id":null,"id_str":"324200722273009666","retweet_count":1,"truncated":false,"created_at":"Tue Apr 16 16:40:50 +0000 2013","geo":null,"in_reply_to_status_id_str":null,"retweeted":true,"coordinates":null,"text":"Just found a \"Notification of Inspection\" card in the bottom of my bag. looks like they were curious about those raspberry-pi :O","in_reply_to_user_id_str":null,"in_reply_to_status_id":null,"source":"\u003Ca href=\"http:\/\/www.tweetdeck.com\" rel=\"nofollow\"\u003ETweetDeck\u003C\/a\u003E","in_reply_to_screen_name":null,"id":324200722273009666,"entities":{"urls":[],"hashtags":[],"user_mentions":[]}},"in_reply_to_status_id":null,"source":"\u003Ca href=\"http:\/\/www.ubuntu.com\" rel=\"nofollow\"\u003EUbuntu Online Accounts\u003C\/a\u003E","in_reply_to_screen_name":null,"id":324220250889543682,"entities":{"urls":[],"hashtags":[],"user_mentions":[{"name":"Tarek Ziad\u00e9","id_str":"15821652","indices":[3,15],"screen_name":"tarek_ziade","id":15821652}]}}
friends-0.2.0+14.04.20140217.1/friends/tests/data/short.dat 0000644 0000152 0177776 00000000017 12300444435 023232 0 ustar pbuser nogroup 0000000 0000000 http://sho.rt/
friends-0.2.0+14.04.20140217.1/friends/tests/data/json-utf-16le.dat 0000644 0000152 0177776 00000000056 12300444435 024410 0 ustar pbuser nogroup 0000000 0000000 { " y e s " : " \ u 0 0 d 1 \ u 0 0 d 8 " } friends-0.2.0+14.04.20140217.1/friends/tests/data/durlme.dat 0000644 0000152 0177776 00000000155 12300444435 023366 0 ustar pbuser nogroup 0000000 0000000 {
"longUrl":"http://www.yahoo.com/somepath",
"status":"ok",
"shortUrl":"http://durl.me/5o",
"key":"5o"
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/flickr-full.dat 0000644 0000152 0177776 00000007630 12300444435 024315 0 ustar pbuser nogroup 0000000 0000000 { "photos": {
"photo": [
{ "id": "8552892154", "secret": "a", "server": "8378", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Chocolate chai #yegcoffee", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363117902", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8552845358", "secret": "b", "server": "8085", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Torah ark #yegjew", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363116818", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8552661200", "secret": "c", "server": "8522", "farm": 9, "owner": "60551783@N00", "username": "Reinhard.Pantke", "title": "Henningsvaer", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363112533", "ownername": "Reinhard.Pantke", "iconserver": 8, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8550946245", "secret": "d", "server": "8107", "farm": 9, "owner": "60551783@N00", "username": "Reinhard.Pantke", "title": "Summerfeeling on Lofoten", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363098878", "ownername": "Reinhard.Pantke", "iconserver": 8, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8550829193", "secret": "e", "server": "8246", "farm": 9, "owner": "27204141@N05", "username": "Nelson Webb", "title": "St. Michael - The Archangel", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363096450", "ownername": "Nelson Webb", "iconserver": "2047", "iconfarm": 3, "latitude": 53.833156, "longitude": -112.330784, "accuracy": 15, "context": 0, "place_id": "4Y55lnhZVrNO", "woeid": "8496", "geo_is_family": 0, "geo_is_friend": 0, "geo_is_contact": 0, "geo_is_public": 1 },
{ "id": "8551930826", "secret": "f", "server": "8247", "farm": 9, "owner": "27204141@N05", "username": "Nelson Webb", "title": "Pine scented air freshener", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363096449", "ownername": "Nelson Webb", "iconserver": "2047", "iconfarm": 3, "latitude": 53.878136, "longitude": -112.335162, "accuracy": 15, "context": 0, "place_id": "4Y55lnhZVrNO", "woeid": "8496", "geo_is_family": 0, "geo_is_friend": 0, "geo_is_contact": 0, "geo_is_public": 1 },
{ "id": "8549658873", "secret": "g", "server": "8239", "farm": 9, "owner": "30584843@N00", "username": "Mark Iocchelli", "title": "Sleepy Hollow", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363055714", "ownername": "Mark Iocchelli", "iconserver": 22, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8548811967", "secret": "h", "server": "8229", "farm": 9, "owner": "47303164@N00", "username": "raise my voice", "title": "Trying out The Wokkery #yegfood", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363028798", "ownername": "raise my voice", "iconserver": 93, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8548753789", "secret": "i", "server": "8512", "farm": 9, "owner": "30584843@N00", "username": "Mark Iocchelli", "title": "Alberta Rail Pipeline", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363027071", "ownername": "Mark Iocchelli", "iconserver": 22, "iconfarm": 1, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 },
{ "id": "8549607582", "secret": "j", "server": "8087", "farm": 9, "owner": "60404254@N00", "username": "tavis_mcnally", "title": "26 weeks", "ispublic": 1, "isfriend": 0, "isfamily": 0, "dateupload": "1363022277", "ownername": "tavis_mcnally", "iconserver": "2182", "iconfarm": 3, "latitude": 0, "longitude": 0, "accuracy": 0, "context": 0 }
], "total": "290", "page": 1, "per_page": 10, "pages": 29 }, "stat": "ok" }
friends-0.2.0+14.04.20140217.1/friends/tests/data/foursquare-full.dat 0000644 0000152 0177776 00000003164 12300444435 025235 0 ustar pbuser nogroup 0000000 0000000 {"meta":{"code":200},"notifications":[{"type":"notificationTray","item":{"unreadCount":0}}],"response":{"recent":[{"id":"50574c9ce4b0a9a6e84433a0","createdAt":1347898524,"type":"checkin","shout":"Working on friends's foursquare plugin.","timeZoneOffset":-300,"user":{"id":"37199983","firstName":"Jimbob","lastName":"Smith","relationship":"self","photo":{"prefix":"https:\/\/irs0.4sqi.net\/img\/user\/","suffix":"\/5IEW3VIX55BBEXAO.jpg"}},"venue":{"id":"4e73f722fa76059700582a27","name":"Pop Soda's Coffee House & Gallery","contact":{"phone":"2044157666","formattedPhone":"(204) 415-7666","twitter":"PopSodasWpg"},"location":{"address":"625 Portage Ave.","crossStreet":"Furby St.","lat":49.88873164336725,"lng":-97.158043384552,"postalCode":"R3B 2G4","city":"Winnipeg","state":"MB","country":"Canada","cc":"CA"},"categories":[{"id":"4bf58dd8d48988d16d941735","name":"Café","pluralName":"Cafés","shortName":"Café","icon":{"prefix":"https:\/\/foursquare.com\/img\/categories_v2\/food\/cafe_","suffix":".png"},"primary":true}],"verified":false,"stats":{"checkinsCount":216,"usersCount":84,"tipCount":8},"url":"http:\/\/www.popsodascoffeehouse.com","likes":{"count":0,"groups":[]},"friendVisits":{"count":0,"summary":"You've been here","items":[{"visitedCount":1,"liked":false,"user":{"id":"37199983","firstName":"Jimbob","lastName":"Smith","relationship":"self","photo":{"prefix":"https:\/\/irs0.4sqi.net\/img\/user\/","suffix":"\/5IEW3VIX55BBEXAO.jpg"}}}]},"beenHere":{"count":1,"marked":true},"specials":{"count":0}},"source":{"name":"foursquare for Android","url":"https:\/\/foursquare.com\/download\/#\/android"},"photos":{"count":0,"items":[]}}]}}
friends-0.2.0+14.04.20140217.1/friends/tests/data/instagram-full.dat 0000644 0000152 0177776 00000041116 12300444435 025025 0 ustar pbuser nogroup 0000000 0000000 {
"pagination": {
"next_url": "https://api.instagram.com/v1/users/self/feed?access_token=4abc&max_id=431429392441037546_46931811",
"next_max_id": "431429392441037546_46931811"
},
"meta": {
"code": 200
},
"data": [
{
"attribution": null,
"tags": [],
"type": "image",
"location": null,
"comments": {
"count": 1,
"data": [
{
"created_time": "1365680633",
"text": "Wtf is that from?",
"from": {
"username": "ellllliottttt",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg",
"id": "13811917",
"full_name": "Elliott Markowitz"
},
"id": "431682899841631793"
}
]
},
"filter": "Normal",
"created_time": "1365655801",
"link": "http://instagram.com/p/X859raK8fx/",
"likes": {
"count": 8,
"data": [
{
"username": "noahvargass",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_1037856_75sq_1354178959.jpg",
"id": "1037856",
"full_name": "Noah Vargas"
},
{
"username": "deniboring",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_16828740_75sq_1345748703.jpg",
"id": "16828740",
"full_name": "deniboring"
},
{
"username": "gabriel_cordero",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_185127499_75sq_1340358021.jpg",
"id": "185127499",
"full_name": "cheeseburger pocket"
},
{
"username": "inmyheadache",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_22645548_75sq_1343529900.jpg",
"id": "22645548",
"full_name": "Tim F"
},
{
"username": "ellllliottttt",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_13811917_75sq_1322106636.jpg",
"id": "13811917",
"full_name": "Elliott Markowitz"
},
{
"username": "eyesofsatan",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_2526196_75sq_1352254863.jpg",
"id": "2526196",
"full_name": "Christopher Hansell"
},
{
"username": "lakeswimming",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_3610143_75sq_1363648525.jpg",
"id": "3610143",
"full_name": "lakeswimming"
},
{
"username": "jtesnakis",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_214679263_75sq_1346185198.jpg",
"id": "214679263",
"full_name": "Jonathan"
}
]
},
"images": {
"low_resolution": {
"url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_6.jpg",
"width": 306,
"height": 306
},
"thumbnail": {
"url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_5.jpg",
"width": 150,
"height": 150
},
"standard_resolution": {
"url": "http://distilleryimage9.s3.amazonaws.com/44ad8486a26311e2872722000a1fd26f_7.jpg",
"width": 612,
"height": 612
}
},
"caption": null,
"user_has_liked": false,
"id": "431474591469914097_223207800",
"user": {
"username": "joshwolp",
"website": "",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_223207800_75sq_1347753109.jpg",
"full_name": "Josh",
"bio": "",
"id": "223207800"
}
},
{
"attribution": null,
"tags": [
"nofilter"
],
"type": "image",
"location": {
"latitude": 40.702485554,
"longitude": -73.929230548
},
"comments": {
"count": 3,
"data": [
{
"created_time": "1365654315",
"text": "I remember pushing that little guy of the swings a few times....",
"from": {
"username": "squidneylol",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_5917696_75sq_1336705905.jpg",
"id": "5917696",
"full_name": "Syd"
},
"id": "431462132263145102"
},
{
"created_time": "1365665741",
"text": "Stop it!!!",
"from": {
"username": "nightruiner",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg",
"id": "31239607",
"full_name": "meredith gaydosh"
},
"id": "431557973837598336"
},
{
"created_time": "1365691283",
"text": "You know I hate being held.",
"from": {
"username": "piratemaddi",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_191388805_75sq_1341897870.jpg",
"id": "191388805",
"full_name": "piratemaddi"
},
"id": "431772240369143639"
}
]
},
"filter": "Normal",
"created_time": "1365651440",
"link": "http://instagram.com/p/X8xpYwk82w/",
"likes": {
"count": 17,
"data": [
{
"username": "meaghanlou",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_41007792_75sq_1334443633.jpg",
"id": "41007792",
"full_name": "meaghanlou"
},
{
"username": "shahwiththat",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_40353923_75sq_1361330343.jpg",
"id": "40353923",
"full_name": "Soraya Shah"
},
{
"username": "marsyred",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_216361031_75sq_1346524176.jpg",
"id": "216361031",
"full_name": "marsyred"
},
{
"username": "thewhitewizzard",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_189027880_75sq_1341183134.jpg",
"id": "189027880",
"full_name": "Drew Mack"
},
{
"username": "juddymagee",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_8993099_75sq_1315066258.jpg",
"id": "8993099",
"full_name": "juddymagee"
},
{
"username": "pixxamonster",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_1672624_75sq_1364673157.jpg",
"id": "1672624",
"full_name": "Sonia 👓 + Dita ðŸˆ"
},
{
"username": "tongue_in_cheek",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_5710964_75sq_1345359909.jpg",
"id": "5710964",
"full_name": "merrily."
},
{
"username": "nightruiner",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_31239607_75sq_1358968326.jpg",
"id": "31239607",
"full_name": "meredith gaydosh"
},
{
"username": "eliz_mortals",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_36417303_75sq_1364179217.jpg",
"id": "36417303",
"full_name": "eliz_mortals"
},
{
"username": "caranicoletti",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_14162814_75sq_1359848230.jpg",
"id": "14162814",
"full_name": "Cara Nicoletti"
}
]
},
"images": {
"low_resolution": {
"url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_6.jpg",
"width": 306,
"height": 306
},
"thumbnail": {
"url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_5.jpg",
"width": 150,
"height": 150
},
"standard_resolution": {
"url": "http://distilleryimage8.s3.amazonaws.com/1d99da5ca25911e2822f22000a9f09ca_7.jpg",
"width": 612,
"height": 612
}
},
"caption": {
"created_time": "1365651465",
"text": "National siblings day @piratemaddi ? You mustn't've known. #nofilter",
"from": {
"username": "what_a_handsome_boy",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg",
"id": "5891266",
"full_name": "Fence!"
},
"id": "431438224520630210"
},
"user_has_liked": false,
"id": "431438012683111856_5891266",
"user": {
"username": "what_a_handsome_boy",
"website": "",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_5891266_75sq_1345460082.jpg",
"full_name": "Fence!",
"bio": "",
"id": "5891266"
}
},
{
"attribution": null,
"tags": [
"canada",
"iphone",
"punklife",
"acap",
"doom",
"crust"
],
"type": "image",
"location": null,
"comments": {
"count": 7,
"data": [
{
"created_time": "1365651434",
"text": "I have not seen him for a minute, he is a great guy!",
"from": {
"username": "rocktoberblood",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_210506849_75sq_1355190976.jpg",
"id": "210506849",
"full_name": "Nate Phillips"
},
"id": "431437959929388541"
},
{
"created_time": "1365652422",
"text": "Saw him puke in the elevator at the Hilton when Doom played chaos.",
"from": {
"username": "occult_obsession",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_6080835_75sq_1364154159.jpg",
"id": "6080835",
"full_name": "Evan Vellela"
},
"id": "431446251162427175"
},
{
"created_time": "1365652900",
"text": "Lol ^ who DIDN'T do this at chaos",
"from": {
"username": "laurababbili",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_1238692_75sq_1361065720.jpg",
"id": "1238692",
"full_name": "Laura Babbili"
},
"id": "431450261906907041"
},
{
"created_time": "1365655310",
"text": "I tried to kiss the singer of cocksparrer in that elevator.",
"from": {
"username": "gregdaly",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
"id": "31133446",
"full_name": "greg daly"
},
"id": "431470473175752176"
},
{
"created_time": "1365655687",
"text": "Leant that dude a 900 Marshall a time ago! Love that dude!!",
"from": {
"username": "nickatnightengale",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_29366372_75sq_1354241971.jpg",
"id": "29366372",
"full_name": "Nick Poulos"
},
"id": "431473639539727956"
},
{
"created_time": "1365658654",
"text": "Hahaha!!",
"from": {
"username": "jeremyhush",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_43750226_75sq_1335024423.jpg",
"id": "43750226",
"full_name": "Jeremy Hush"
},
"id": "431498525729480083"
},
{
"created_time": "1365685226",
"text": "There's an app for that",
"from": {
"username": "liam_wilson",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_4357248_75sq_1327009309.jpg",
"id": "4357248",
"full_name": "Liam Wilson"
},
"id": "431721432141388885"
}
]
},
"filter": "X-Pro II",
"created_time": "1365650801",
"link": "http://instagram.com/p/X8wbTQtd3Y/",
"likes": {
"count": 36,
"data": [
{
"username": "fatbenatar",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_280136224_75sq_1357073366.jpg",
"id": "280136224",
"full_name": "China Oxendine"
},
{
"username": "tejleo",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_10403332_75sq_1354058520.jpg",
"id": "10403332",
"full_name": "tejleo"
},
{
"username": "tonyromeo13",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_255933637_75sq_1360272947.jpg",
"id": "255933637",
"full_name": "Tony Romeo"
},
{
"username": "libraryrat4",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_17715046_75sq_1325381597.jpg",
"id": "17715046",
"full_name": "libraryrat4"
},
{
"username": "boredwithapathy",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_174679368_75sq_1338500455.jpg",
"id": "174679368",
"full_name": "Lisa McCarthy"
},
{
"username": "justinpittney",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_10976940_75sq_1361306615.jpg",
"id": "10976940",
"full_name": "Justin Pittney"
},
{
"username": "nuclearhell",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_212702314_75sq_1361642319.jpg",
"id": "212702314",
"full_name": "Rachel Whittaker"
},
{
"username": "gregelk",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_240084204_75sq_1359223473.jpg",
"id": "240084204",
"full_name": "Greg Elk"
},
{
"username": "bradhasher",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_283278325_75sq_1357514041.jpg",
"id": "283278325",
"full_name": "Brett Ellingson"
},
{
"username": "zaimlmzim",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_180060277_75sq_1363118292.jpg",
"id": "180060277",
"full_name": "zaimlmzim"
}
]
},
"images": {
"low_resolution": {
"url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_6.jpg",
"width": 306,
"height": 306
},
"thumbnail": {
"url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_5.jpg",
"width": 150,
"height": 150
},
"standard_resolution": {
"url": "http://distilleryimage4.s3.amazonaws.com/a051f882a25711e282e122000a1f9aae_7.jpg",
"width": 612,
"height": 612
}
},
"caption": {
"created_time": "1365650915",
"text": "This is my friend Scoot. He plays bass in #doom and is a mega rock star in newfoundland. Here you can see him signing an autograph on an iphone. #crust #punklife #iphone #canada #acap",
"from": {
"username": "gregdaly",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
"id": "31133446",
"full_name": "greg daly"
},
"id": "431433605218426202"
},
"user_has_liked": false,
"id": "431432646660578776_31133446",
"user": {
"username": "gregdaly",
"website": "",
"profile_picture": "http://images.ak.instagram.com/profiles/profile_31133446_75sq_1333488205.jpg",
"full_name": "greg daly",
"bio": "",
"id": "31133446"
}
}
]
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/json-utf-32be.dat 0000644 0000152 0177776 00000000134 12300444435 024371 0 ustar pbuser nogroup 0000000 0000000 { " y e s " : " \ u 0 0 d 1 \ u 0 0 d 8 " } friends-0.2.0+14.04.20140217.1/friends/tests/data/facebook_ids_corrupt.json 0000644 0000152 0177776 00000000110 12300444435 026454 0 ustar pbuser nogroup 0000000 0000000 BÖÀæ:@•7g¥6Ãw3ÞÐb‚ïÍb#±Ûî3#d²;æ_ôþí§K9ÌEÊÞþsL¥P-8çuôÆýÚéñƒ{†¶ friends-0.2.0+14.04.20140217.1/friends/tests/data/json-utf-32le.dat 0000644 0000152 0177776 00000000134 12300444435 024403 0 ustar pbuser nogroup 0000000 0000000 { " y e s " : " \ u 0 0 d 1 \ u 0 0 d 8 " } friends-0.2.0+14.04.20140217.1/friends/tests/data/twitter-send.dat 0000644 0000152 0177776 00000005703 12300444435 024533 0 ustar pbuser nogroup 0000000 0000000 {
"coordinates": null,
"truncated": false,
"created_at": "Tue Aug 28 21:16:23 +0000 2012",
"favorited": false,
"id_str": "240558470661799936",
"in_reply_to_user_id_str": null,
"entities": {
"urls": [
],
"hashtags": [
],
"user_mentions": [
]
},
"text": "just another test",
"contributors": null,
"id": 240558470661799936,
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"source": "OAuth Dancer Reborn",
"user": {
"name": "OAuth Dancer",
"profile_sidebar_fill_color": "DDEEF6",
"profile_background_tile": true,
"profile_sidebar_border_color": "C0DEED",
"profile_image_url": "http://a0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
"created_at": "Wed Mar 03 19:37:35 +0000 2010",
"location": "San Francisco, CA",
"follow_request_sent": false,
"id_str": "119476949",
"is_translator": false,
"profile_link_color": "0084B4",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "http://bit.ly/oauth-dancer",
"indices": [
0,
26
],
"display_url": null
}
]
},
"description": null
},
"default_profile": false,
"url": "http://bit.ly/oauth-dancer",
"contributors_enabled": false,
"favourites_count": 7,
"utc_offset": null,
"profile_image_url_https": "https://si0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
"id": 119476949,
"listed_count": 1,
"profile_use_background_image": true,
"profile_text_color": "333333",
"followers_count": 28,
"lang": "en",
"protected": false,
"geo_enabled": true,
"notifications": false,
"description": "",
"profile_background_color": "C0DEED",
"verified": false,
"time_zone": null,
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/80151733/oauth-dance.png",
"statuses_count": 166,
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/80151733/oauth-dance.png",
"default_profile_image": false,
"friends_count": 14,
"following": false,
"show_all_inline_media": false,
"screen_name": "oauth_dancer"
},
"in_reply_to_screen_name": null,
"in_reply_to_status_id": null
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/flickr-xml.dat 0000644 0000152 0177776 00000000135 12300444435 024144 0 ustar pbuser nogroup 0000000 0000000
8488552823
friends-0.2.0+14.04.20140217.1/friends/tests/data/twitter-home.dat 0000644 0000152 0177776 00000027225 12300444435 024535 0 ustar pbuser nogroup 0000000 0000000 [
{
"coordinates": null,
"truncated": false,
"created_at": "Tue Aug 28 21:16:23 +0000 2012",
"favorited": false,
"id_str": "240558470661799936",
"in_reply_to_user_id_str": null,
"entities": {
"urls": [
],
"hashtags": [
],
"user_mentions": [
]
},
"text": "just another test",
"contributors": null,
"id": 240558470661799936,
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"source": "OAuth Dancer Reborn",
"user": {
"name": "OAuth Dancer",
"profile_sidebar_fill_color": "DDEEF6",
"profile_background_tile": true,
"profile_sidebar_border_color": "C0DEED",
"profile_image_url": "http://a0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
"created_at": "Wed Mar 03 19:37:35 +0000 2010",
"location": "San Francisco, CA",
"follow_request_sent": false,
"id_str": "119476949",
"is_translator": false,
"profile_link_color": "0084B4",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "http://bit.ly/oauth-dancer",
"indices": [
0,
26
],
"display_url": null
}
]
},
"description": null
},
"default_profile": false,
"url": "http://bit.ly/oauth-dancer",
"contributors_enabled": false,
"favourites_count": 7,
"utc_offset": null,
"profile_image_url_https": "https://si0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg",
"id": 119476949,
"listed_count": 1,
"profile_use_background_image": true,
"profile_text_color": "333333",
"followers_count": 28,
"lang": "en",
"protected": false,
"geo_enabled": true,
"notifications": false,
"description": "",
"profile_background_color": "C0DEED",
"verified": false,
"time_zone": null,
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/80151733/oauth-dance.png",
"statuses_count": 166,
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/80151733/oauth-dance.png",
"default_profile_image": false,
"friends_count": 14,
"following": false,
"show_all_inline_media": false,
"screen_name": "oauth_dancer"
},
"in_reply_to_screen_name": null,
"in_reply_to_status_id": null
},
{
"coordinates": {
"coordinates": [
-122.25831,
37.871609
],
"type": "Point"
},
"truncated": false,
"created_at": "Tue Aug 28 21:08:15 +0000 2012",
"favorited": false,
"id_str": "240556426106372096",
"in_reply_to_user_id_str": null,
"entities": {
"media":[{"type":"photo",
"sizes":{
"thumb":{"h":150, "resize":"crop", "w":150},
"large":{"h":238, "resize":"fit", "w":226},
"medium":{"h":238, "resize":"fit", "w":226},
"small":{"h":238, "resize":"fit", "w":226}},
"indices":[78,98], "url":"http://t.co/rJC5Pxsu",
"media_url":"http://p.twimg.com/AZVLmp-CIAAbkyy.jpg",
"display_url":"pic.twitter.com\/rJC5Pxsu",
"id":114080493040967680, "id_str":"114080493040967680",
"expanded_url": "http://twitter.com/yunorno/status/114080493036773378/photo/1",
"media_url_https":"https://p.twimg.com/AZVLmp-CIAAbkyy.jpg"}],
"hashtags": [
],
"user_mentions": [
{
"name": "Cal",
"id_str": "17445752",
"id": 17445752,
"indices": [
60,
64
],
"screen_name": "Cal"
},
{
"name": "Othman Laraki",
"id_str": "20495814",
"id": 20495814,
"indices": [
70,
77
],
"screen_name": "othman"
}
]
},
"text": "lecturing at the \"analyzing big data with twitter\" class at @cal with @othman http://t.co/rJC5Pxsu",
"contributors": null,
"id": 240556426106372096,
"retweet_count": 3,
"in_reply_to_status_id_str": null,
"geo": {
"coordinates": [
37.871609,
-122.25831
],
"type": "Point"
},
"retweeted": false,
"possibly_sensitive": false,
"in_reply_to_user_id": null,
"place": {
"name": "Berkeley",
"country_code": "US",
"country": "United States",
"attributes": {
},
"url": "http://api.twitter.com/1/geo/id/5ef5b7f391e30aff.json",
"id": "5ef5b7f391e30aff",
"bounding_box": {
"coordinates": [
[
[
-122.367781,
37.835727
],
[
-122.234185,
37.835727
],
[
-122.234185,
37.905824
],
[
-122.367781,
37.905824
]
]
],
"type": "Polygon"
},
"full_name": "Berkeley, CA",
"place_type": "city"
},
"source": "Safari on iOS",
"user": {
"name": "Raffi Krikorian",
"profile_sidebar_fill_color": "DDEEF6",
"profile_background_tile": false,
"profile_sidebar_border_color": "C0DEED",
"profile_image_url": "http://a0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png",
"created_at": "Sun Aug 19 14:24:06 +0000 2007",
"location": "San Francisco, California",
"follow_request_sent": false,
"id_str": "8285392",
"is_translator": false,
"profile_link_color": "0084B4",
"entities": {
"url": {
"urls": [
{
"expanded_url": "http://about.me/raffi.krikorian",
"url": "http://t.co/eNmnM6q",
"indices": [
0,
19
],
"display_url": "about.me/raffi.krikorian"
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": true,
"url": "http://t.co/eNmnM6q",
"contributors_enabled": false,
"favourites_count": 724,
"utc_offset": -28800,
"profile_image_url_https": "https://si0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png",
"id": 8285392,
"listed_count": 619,
"profile_use_background_image": true,
"profile_text_color": "333333",
"followers_count": 18752,
"lang": "en",
"protected": false,
"geo_enabled": true,
"notifications": false,
"description": "Director of @twittereng's Platform Services. I break things.",
"profile_background_color": "C0DEED",
"verified": false,
"time_zone": "Pacific Time (US & Canada)",
"profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png",
"statuses_count": 5007,
"profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png",
"default_profile_image": false,
"friends_count": 701,
"following": true,
"show_all_inline_media": true,
"screen_name": "raffi"
},
"in_reply_to_screen_name": null,
"in_reply_to_status_id": null
},
{
"coordinates": null,
"truncated": false,
"created_at": "Tue Aug 28 19:59:34 +0000 2012",
"favorited": false,
"id_str": "240539141056638977",
"in_reply_to_user_id_str": null,
"entities": {
"urls": [
],
"hashtags": [
],
"user_mentions": [
]
},
"text": "You'd be right more often if you thought you were wrong.",
"contributors": null,
"id": 240539141056638977,
"retweet_count": 1,
"in_reply_to_status_id_str": null,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"source": "web",
"user": {
"name": "Taylor Singletary",
"profile_sidebar_fill_color": "FBFBFB",
"profile_background_tile": true,
"profile_sidebar_border_color": "000000",
"profile_image_url": "http://a0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg",
"created_at": "Wed Mar 07 22:23:19 +0000 2007",
"location": "San Francisco, CA",
"follow_request_sent": false,
"id_str": "819797",
"is_translator": false,
"profile_link_color": "c71818",
"entities": {
"url": {
"urls": [
{
"expanded_url": "http://www.rebelmouse.com/episod/",
"url": "http://t.co/Lxw7upbN",
"indices": [
0,
20
],
"display_url": "rebelmouse.com/episod/"
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": false,
"url": "http://t.co/Lxw7upbN",
"contributors_enabled": false,
"favourites_count": 15990,
"utc_offset": -28800,
"profile_image_url_https": "https://si0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg",
"id": 819797,
"listed_count": 340,
"profile_use_background_image": true,
"profile_text_color": "D20909",
"followers_count": 7126,
"lang": "en",
"protected": false,
"geo_enabled": true,
"notifications": false,
"description": "Reality Technician, Twitter API team, synthesizer enthusiast; a most excellent adventure in timelines. I know it's hard to believe in something you can't see.",
"profile_background_color": "000000",
"verified": false,
"time_zone": "Pacific Time (US & Canada)",
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png",
"statuses_count": 18076,
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png",
"default_profile_image": false,
"friends_count": 5444,
"following": true,
"show_all_inline_media": true,
"screen_name": "episod"
},
"in_reply_to_screen_name": null,
"in_reply_to_status_id": null
}
]
friends-0.2.0+14.04.20140217.1/friends/tests/data/facebook-full.dat 0000644 0000152 0177776 00000024074 12300444435 024615 0 ustar pbuser nogroup 0000000 0000000 {
"data": [
{
"id": "userid_postid1",
"from": {
"name": "Yours Truly",
"id": "56789"
},
"message": "Writing code that supports geotagging data from facebook. If y'all could make some geotagged facebook posts for me to test with, that'd be super.",
"actions": [
{
"name": "Comment",
"link": "https://www.facebook.com/userid/posts/postid1"
},
{
"name": "Like",
"link": "https://www.facebook.com/userid/posts/postid1"
}
],
"privacy": {
"value": ""
},
"place": {
"id": "103135879727382",
"name": "Victoria, British Columbia",
"location": {
"street": "",
"zip": "",
"latitude": 48.4333,
"longitude": -123.35
}
},
"type": "status",
"status_type": "mobile_status_update",
"created_time": "2013-03-12T21:27:56+0000",
"updated_time": "2013-03-13T23:29:07+0000",
"likes": {
"data": [
{
"name": "Anna",
"id": "12345"
}
],
"count": 1
},
"comments": {
"data": [
{
"id": "postid1_commentid1",
"from": {
"name": "Grandma",
"id": "9876"
},
"message": "If I knew what a geotagged facebook post was I might be able to comply!",
"created_time": "2013-03-12T22:56:17+0000"
},
{
"id": "postid1_commentid2",
"from": {
"name": "Father",
"id": "234"
},
"message": "don't know how",
"created_time": "2013-03-12T23:29:45+0000"
},
{
"id": "postid1_commentid3",
"from": {
"name": "Mother",
"id": "456"
},
"message": "HUH!!!!",
"created_time": "2013-03-13T02:20:27+0000"
},
{
"id": "postid1_commentid4",
"from": {
"name": "Yours Truly",
"id": "56789"
},
"message": "Coming up with tons of fake data is hard!",
"created_time": "2013-03-13T23:29:07+0000"
}
],
"count": 4
}
},
{
"id": "270843027745_10151370303782746",
"from": {
"category": "Shopping/retail",
"category_list": [
{
"id": "128003127270269",
"name": "Bike Shop"
}
],
"name": "Western Cycle Source for Sports",
"id": "270843027745"
},
"story": "Western Cycle Source for Sports updated their cover photo.",
"story_tags": {
"0": [
{
"id": "270843027745",
"name": "Western Cycle Source for Sports",
"offset": 0,
"length": 31,
"type": "page"
}
]
},
"picture": "https://fbcdn-photos-a.akamaihd.net/hphotos-ak-snc7/482418_10151370303672746_1924798223_s.jpg",
"link": "https://www.facebook.com/photo.php?fbid=10151370303672746&set=a.10150598301902746.381693.270843027745&type=1&relevant_count=1",
"icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yz/r/StEh3RhPvjk.gif",
"actions": [
{
"name": "Comment",
"link": "https://www.facebook.com/270843027745/posts/10151370303782746"
},
{
"name": "Like",
"link": "https://www.facebook.com/270843027745/posts/10151370303782746"
}
],
"privacy": {
"value": ""
},
"place": {
"id": "270843027745",
"name": "Western Cycle Source for Sports",
"location": {
"street": "1550 8th Ave",
"city": "Regina",
"state": "SK",
"country": "Canada",
"zip": "S4R 1E4",
"latitude": 50.45679,
"longitude": -104.60276
}
},
"type": "photo",
"object_id": "10151370303672746",
"created_time": "2013-03-11T23:46:06+0000",
"updated_time": "2013-03-11T23:46:06+0000",
"likes": {
"data": [
{
"name": "Lou Schwindt",
"id": "57"
},
{
"name": "Maureen Daniel",
"id": "72"
},
{
"name": "Lee Watson",
"id": "696"
},
{
"name": "Rob Nelson",
"id": "40"
}
],
"count": 10
},
"comments": {
"count": 0
}
},
{
"id": "161247843901324_629147610444676",
"from": {
"category": "Hotel",
"category_list": [
{
"id": "164243073639257",
"name": "Hotel"
}
],
"name": "Best Western Denver Southwest",
"id": "161247843901324"
},
"message": "Today only -- Come meet Caroline and Meredith and Stanley the Stegosaurus (& Greg & Joe, too!) at the TechZulu Trend Lounge, Hilton Garden Inn 18th floor, 500 N Interstate 35, Austin, Texas. Monday, March 11th, 4:00pm to 7:00 pm. Also here Hannah Hart (My Drunk Kitchen) and Angry Video Game Nerd producer, Sean Keegan. Stanley is in the lobby.",
"picture": "https://fbcdn-photos-a.akamaihd.net/hphotos-ak-snc7/601266_629147587111345_968504279_s.jpg",
"link": "https://www.facebook.com/photo.php?fbid=629147587111345&set=a.173256162700492.47377.161247843901324&type=1&relevant_count=1",
"icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yz/r/StEh3RhPvjk.gif",
"actions": [
{
"name": "Comment",
"link": "https://www.facebook.com/161247843901324/posts/629147610444676"
},
{
"name": "Like",
"link": "https://www.facebook.com/161247843901324/posts/629147610444676"
}
],
"privacy": {
"value": ""
},
"place": {
"id": "132709090079327",
"name": "Hilton Garden Inn Austin Downtown/Convention Center",
"location": {
"street": "500 North Interstate 35",
"city": "Austin",
"state": "TX",
"country": "United States",
"zip": "78701",
"latitude": 30.265384957204,
"longitude": -97.735604602521
}
},
"type": "photo",
"status_type": "added_photos",
"object_id": "629147587111345",
"created_time": "2013-03-11T20:49:10+0000",
"updated_time": "2013-03-11T23:51:25+0000",
"likes": {
"data": [
{
"name": "Andrew Henninger",
"id": "11"
},
{
"name": "Sarah Brents",
"id": "22"
},
{
"name": "Thomas Bush",
"id": "33"
},
{
"name": "Jennifer Tornetta",
"id": "44"
}
],
"count": 84
},
"comments": {
"data": [
{
"id": "1612484301324_6294760444676_1126376",
"from": {
"name": "Amy Gibbs",
"id": "55"
},
"message": "You have to love a family that travels with their stegasaurus.",
"created_time": "2013-03-11T23:51:05+0000",
"likes": 2
},
{
"id": "1612843901324_6294761044676_1124378",
"from": {
"name": "Amy Gibbs",
"id": "55"
},
"message": "*stegosaurus...sorry!",
"created_time": "2013-03-11T23:51:25+0000",
"likes": 1
}
],
"count": 11
}
},
{
"id": "104443_100085049977",
"from": {
"name": "Guy Frenchie",
"id": "1244414"
},
"story": "Guy Frenchie did some things with some stuff.",
"story_tags": {
"0": [
{
"id": "1244414",
"name": "Guy Frenchie",
"offset": 0,
"length": 16,
"type": "user"
}
],
"26": [
{
"id": "37067557",
"name": "somebody",
"offset": 26,
"length": 10,
"type": "page"
}
],
"48": [
{
"id": "50681138",
"name": "What do you think about things and stuff?",
"offset": 48,
"length": 52
}
]
},
"icon": "https://fbstatic-a.akamaihd.net/rsrc.php/v2/yg/r/5PpICR5KcPe.png",
"actions": [
{
"name": "Comment",
"link": "https://www.facebook.com/1244414/posts/100085049977"
},
{
"name": "Like",
"link": "https://www.facebook.com/1244414/posts/100085049977"
}
],
"privacy": {
"value": ""
},
"type": "question",
"object_id": "584616119",
"application": {
"name": "Questions",
"id": "101502535258"
},
"created_time": "2013-03-15T19:57:14+0000",
"updated_time": "2013-03-15T19:57:14+0000",
"likes": {
"data": [
{
"name": "Kevin Diner",
"id": "55520"
},
{
"name": "Bozo the Clown",
"id": "13960"
}
],
"count": 3
},
"comments": {
"data": [
{
"id": "14446143_102008355988977_100927",
"from": {
"name": "Seymour Butts",
"id": "505677"
},
"message": "seems legit",
"created_time": "2013-03-13T12:20:19+0000",
"likes": 2
},
{
"id": "120143_1020035588977_1019440",
"from": {
"name": "Andre the Giant",
"id": "100390199"
},
"message": "Anybody want a peanut?",
"created_time": "2013-03-13T12:23:25+0000"
}
],
"count": 22
}
}
],
"paging": {
"previous": "https://graph.facebook.com/me/home&limit=25&since=1234",
"next": "https://graph.facebook.com/me/home&limit=25&until=4321"
}
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/twitter-hashtags.dat 0000644 0000152 0177776 00000005272 12300444435 025405 0 ustar pbuser nogroup 0000000 0000000 {
"created_at": "Fri Jan 17 14:23:41 +0000 2014",
"id": 424185261375766500,
"id_str": "424185261375766530",
"text": "A service that filters food pictures from Instagram. #MillionDollarIdea",
"source": "web",
"truncated": false,
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user": {
"id": 17339829,
"id_str": "17339829",
"name": "Kai Mast",
"screen_name": "Kai_Mast",
"location": "Bamberg",
"description": "Computer Science Student, Grad School Applicant, C++ Fanboy",
"url": "http://t.co/1myW31Mlhl",
"entities": {
"url": {
"urls": [
{
"url": "http://t.co/1myW31Mlhl",
"expanded_url": "http://kai-mast.de",
"display_url": "kai-mast.de",
"indices": [
0,
22
]
}
]
},
"description": {
"urls": []
}
},
"protected": false,
"followers_count": 415,
"friends_count": 904,
"listed_count": 36,
"created_at": "Wed Nov 12 14:23:09 +0000 2008",
"favourites_count": 939,
"utc_offset": 3600,
"time_zone": "Berlin",
"geo_enabled": true,
"verified": false,
"statuses_count": 7886,
"lang": "en",
"contributors_enabled": false,
"is_translator": false,
"profile_background_color": "8B542B",
"profile_background_image_url": "http://abs.twimg.com/images/themes/theme8/bg.gif",
"profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme8/bg.gif",
"profile_background_tile": false,
"profile_image_url": "http://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png",
"profile_image_url_https": "https://pbs.twimg.com/profile_images/424181800701673473/Q6Ggqg7P_normal.png",
"profile_banner_url": "https://pbs.twimg.com/profile_banners/17339829/1360006443",
"profile_link_color": "9D582E",
"profile_sidebar_border_color": "D9B17E",
"profile_sidebar_fill_color": "EADEAA",
"profile_text_color": "333333",
"profile_use_background_image": true,
"default_profile": false,
"default_profile_image": false,
"following": false,
"follow_request_sent": false,
"notifications": false
},
"geo": null,
"coordinates": null,
"place": null,
"contributors": null,
"retweet_count": 0,
"favorite_count": 0,
"entities": {
"hashtags": [
{
"text": "MillionDollarIdea",
"indices": [
53,
71
]
}
],
"symbols": [],
"urls": [],
"user_mentions": []
},
"favorited": false,
"retweeted": false,
"lang": "en"
}
friends-0.2.0+14.04.20140217.1/friends/tests/data/ubuntu.png 0000644 0000152 0177776 00000021224 12300444435 023434 0 ustar pbuser nogroup 0000000 0000000 ‰PNG
IHDR 1j³{ tEXtSoftware Adobe ImageReadyqÉe<