[test] Added unit tests

- Moved the TransactionNotFound exception to a more appropriate place.
- Changed the serialization for AccountingExceptions
- Override the Exception.__init__ method in AccountingException
- Added __eq__ methods to accounting.models.*
- Catch the TransactionNotFound exception in transaction_get and return a
  404 instead. This could be improved, perhaps in the jsonify_exceptions
  decorator so that all endpoints that raise a TransactionNotFound
  exception automatically return a 404.
This commit is contained in:
Joar Wandborg 2013-12-21 00:24:37 +01:00
parent 36d91dd0b3
commit 281d6fed47
7 changed files with 70 additions and 41 deletions

View file

@ -7,4 +7,11 @@ class AccountingException(Exception):
Used as a base for exceptions that are returned to the caller via the
jsonify_exceptions decorator
'''
def __init__(self, message, **kw):
self.message = message
for key, value in kw.items():
setattr(self, key, value)
class TransactionNotFound(AccountingException):
pass

View file

@ -1,14 +1,18 @@
# Part of accounting-api project:
# https://gitorious.org/conservancy/accounting-api
# License: AGPLv3-or-later
import datetime
import uuid
from decimal import Decimal
class Transaction:
def __init__(self, id=None, date=None, payee=None, postings=None,
metadata=None, _generate_id=False):
if type(date) == datetime.datetime:
date = date.date()
self.id = id
self.date = date
self.payee = payee
@ -21,6 +25,9 @@ class Transaction:
def generate_id(self):
self.id = str(uuid.uuid4())
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return ('<{self.__class__.__name__} {self.id} {date}' +
' {self.payee} {self.postings}').format(
@ -34,6 +41,9 @@ class Posting:
self.amount = amount
self.metadata = metadata if metadata is not None else {}
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return ('<{self.__class__.__name__} "{self.account}"' +
' {self.amount}>').format(self=self)
@ -44,6 +54,9 @@ class Amount:
self.amount = Decimal(amount)
self.symbol = symbol
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return ('<{self.__class__.__name__} {self.symbol}' +
' {self.amount}>').format(self=self)
@ -55,6 +68,9 @@ class Account:
self.amounts = amounts
self.accounts = accounts
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' +
' {self.accounts}>').format(self=self)

View file

@ -4,8 +4,6 @@
from abc import ABCMeta, abstractmethod
from accounting.exceptions import AccountingException
class Storage:
'''
@ -47,7 +45,3 @@ class Storage:
@abstractmethod
def reverse_transaction(self, transaction_id):
raise NotImplementedError
class TransactionNotFound(AccountingException):
pass

View file

@ -12,9 +12,9 @@ from datetime import datetime
from xml.etree import ElementTree
from contextlib import contextmanager
from accounting.exceptions import AccountingException
from accounting.exceptions import AccountingException, TransactionNotFound
from accounting.models import Account, Transaction, Posting, Amount
from accounting.storage import Storage, TransactionNotFound
from accounting.storage import Storage
_log = logging.getLogger(__name__)
@ -129,6 +129,10 @@ class Ledger(Storage):
while True:
line = process.stdout.read(1) # XXX: This is a hack
if len(line) > 0:
pass
#_log.debug('line: %s', line)
output += line
if b'\n] ' in output:
@ -148,6 +152,8 @@ class Ledger(Storage):
if isinstance(command, str):
command = command.encode('utf8')
_log.debug('Sending command: %r', command)
p.stdin.write(command + b'\n')
p.stdin.flush()
@ -263,8 +269,8 @@ class Ledger(Storage):
if transaction.id == transaction_id:
return transaction
raise TransactionNotFound('No transaction with id %s found',
transaction_id)
raise TransactionNotFound(
'No transaction with id {0} found'.format(transaction_id))
def reg(self):
output = self.send_command('xml')
@ -321,7 +327,13 @@ class Ledger(Storage):
metadata.update({key: value})
# Add a Transaction instance to the list
try:
id = metadata.pop('Id')
except KeyError:
_log.warning('Transaction on %s with payee %s does not have an'
' Id attribute. A temporary ID will be used.',
date, payee)
id = 'NO-ID'
entries.append(
Transaction(id=id, date=date, payee=payee, postings=postings,
metadata=metadata))

View file

@ -6,6 +6,7 @@ from datetime import datetime
from flask import json
from accounting.exceptions import AccountingException
from accounting.models import Amount, Transaction, Posting, Account
@ -40,10 +41,10 @@ class AccountingEncoder(json.JSONEncoder):
amount=str(o.amount),
symbol=o.symbol
)
elif isinstance(o, Exception):
elif isinstance(o, AccountingException):
return dict(
__type__=o.__class__.__name__,
args=o.args
type=o.__class__.__name__,
message=o.message
)
return json.JSONEncoder.default(self, o)
@ -58,7 +59,7 @@ class AccountingDecoder(json.JSONDecoder):
return d
types = {c.__name__: c for c in [Amount, Transaction, Posting,
Account]}
Account, AccountingException]}
_type = d.pop('__type__')

View file

@ -10,7 +10,7 @@ import sys
import logging
import argparse
from flask import Flask, jsonify, request, render_template
from flask import Flask, jsonify, request, render_template, abort
from flask.ext.script import Manager
from flask.ext.migrate import Migrate, MigrateCommand
@ -19,32 +19,18 @@ from accounting.storage import Storage
from accounting.storage.ledgercli import Ledger
from accounting.storage.sql import SQLStorage
from accounting.transport import AccountingEncoder, AccountingDecoder
from accounting.exceptions import AccountingException
from accounting.exceptions import AccountingException, TransactionNotFound
from accounting.decorators import jsonify_exceptions, cors
app = Flask('accounting')
app.config.from_pyfile('config.py')
storage = Storage()
if isinstance(storage, SQLStorage):
# TODO: Move migration stuff into SQLStorage
db = storage.db
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
app.ledger = Storage()
@app.before_request
def init_ledger():
'''
:py:meth:`flask.Flask.before_request`-decorated method that initializes an
:py:class:`accounting.Ledger` object.
'''
global ledger
#ledger = Ledger(ledger_file=app.config['LEDGER_FILE'])
app.ledger = Ledger(app)
# These will convert output from our internal classes to JSON and back
@ -85,9 +71,12 @@ def transaction_get(transaction_id=None):
Returns the JSON-serialized output of :meth:`accounting.Ledger.reg`
'''
if transaction_id is None:
return jsonify(transactions=storage.get_transactions())
return jsonify(transactions=app.ledger.get_transactions())
return jsonify(transaction=storage.get_transaction(transaction_id))
try:
return jsonify(transaction=app.ledger.get_transaction(transaction_id))
except TransactionNotFound:
abort(404)
@app.route('/transaction/<string:transaction_id>', methods=['POST'])
@ -105,7 +94,7 @@ def transaction_update(transaction_id=None):
elif transaction.id is None:
transaction.id = transaction_id
storage.update_transaction(transaction)
app.ledger.update_transaction(transaction)
return jsonify(status='OK')
@ -117,7 +106,7 @@ def transaction_delete(transaction_id=None):
if transaction_id is None:
raise AccountingException('Transaction ID cannot be None')
storage.delete_transaction(transaction_id)
app.ledger.delete_transaction(transaction_id)
return jsonify(status='OK')
@ -184,7 +173,7 @@ def transaction_post():
transaction_ids = []
for transaction in transactions:
transaction_ids.append(storage.add_transaction(transaction))
transaction_ids.append(app.ledger.add_transaction(transaction))
return jsonify(status='OK', transaction_ids=transaction_ids)
@ -201,8 +190,7 @@ def main(argv=None):
help=('Filter logging output. Possible values:' +
' CRITICAL, ERROR, WARNING, INFO, DEBUG'))
global storage
storage = Ledger(app=app)
init_ledger()
args = parser.parse_args(argv)

View file

@ -23,6 +23,7 @@
Assets:Checking $50.00
2011/04/20 (2) Baz Hosting Services, LLC
;Party: Baz Hosting Services, LLC
;Id: this is probably unique
Expenses:Blah:Hosting $250.00
;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf
@ -44,3 +45,13 @@
;Id: 31048b9d-a5b6-41d7-951a-e7128e7c53c0
Income:Donations:PayPal $ -20.18
Assets:Checking $ 20.18
2013-12-19 AngularJS Donation
;Id: 593d921c-07f9-496d-a1b7-47bb68ff00d4
Assets:Checking USD -10.00
Expenses:Donations USD 10.00
2013-12-20 PageKite donation
;Id: b3576a94-2a53-4d40-bd92-06b2532937f5
Assets:Checking USD -10.00
Expenses:Donations USD 10.00