lunes, 29 de mayo de 2017

Integrando (Un poco) pyramid en Eclipse: Creando el proyecto

Este artículo muestra la forma más sencilla de empezar un proyecto con Pyramid, ya que a diferencia de Primero pasos con Pyramid no se modifica nada en orden de iniciar un gran proyecto. Además, esta vez hice algunas capturas de pantalla y creo que esa es la principal justificación

Creamos un entorno virtual para el proyecto de nombre proyecto y entramos al directorio creado
virtualenv proyecto-ambiente
cd proyecto-ambiente
Activamos el entorno virtual. Esto lo hacemos siempre que vayamos a usarlo. Así es como usamos las librerías y ejecutables que instalemos en él y no los del sistema.
source bin/activate
En este momento, el entorno se ve más o menos de esta forma:
Instalamos pyramid
pip install pyramid
Creamos un proyecto de nombre proyecto con la plantilla starter
pcreate -s starte proyecto
El pcreate ha creado un directorio proyecto:
Instalamos los paquetes python que este proyecto en específico necesita. Se configuran en proyecto/setup.py, por ahora lo dejamos tal como se configuro por defecto
python proyecto/setup.py develop
Y nuestro proyecto se ve de esta forma
Y eso es todo.

lunes, 13 de marzo de 2017

Usando snapshot para equipos virtualizados con XEN en Debian

Pese al título, esta entrada aún necesita llevar un subtítulo: "Usando qcow en máquinas virtuales en Debian Jessie", y ya, no se necesitaría mayor introducción al respecto.

Cuando se sigue una guía de virtualización en Debian, se cae por defecto en la virtualización con XEN. No hay problema, pero parece que hasta Jessie, las herramientas para administración por defecto siguen sin manejar la cuestión de snapshot para las máquinas virtuales.

Tampoco fuí capaz de encontrar una opción en xen-create-image que permita crear las imágenes con formato qcow. Crear la imagen con qemu-img create y luego apuntarla en xen-create-image con --image-dev y --swap-dev  no funciona porque sin importar qué, xen-create-image convierte las imágenes a raw.

Con todo esto, es necesario complicar el proceso de instalación de la siguiente manera:
  • Creamos la máquina virtual con un disco raw, que es el formato por defecto:
xen-create-image  --hostname=pdc.salud.gob.sv --vcpus=2 --size=20Gb --memory=896Mb --ip=10.20.20.10 --gateway=10.20.20.1 --bridge=xenbr0 --arch=amd64 --role=udev --dir=/var/lib/xen/
La instalación del sistema base empezará creando dos imágenes: disk.img y swap.img , dentro de /var/lib/xen/domains/pdc.salud.gob.sv/ en este caso según el hostname dado.

  • Lo convertimos a qcow (Respecto a preallocation: Resulta que en Jessie no es posible usar full, además, no soy capaz de decir si para el caso de conversión esta opción tiene algún tipo de efecto)
qemu-img convert -O qcow2 -o preallocation=metadata /var/lib/xen/domains/pdc.salud.gob.sv/disk.img /var/lib/xen/domains/pdc.salud.gob.sv/disk.qcow2
  • Ahora, modificamos el fichero que define a la máquina virtual para que apunte al nuevo disco: Por xen-create-image, el fichero había quedado de esta forma:
#
#  Disk device(s).
#
root        = '/dev/xvda2 ro'
disk        = [
                  'file:/var/lib/xen/images/domains/pdc.salud.gob.sv/disk.img,xvda2,w',
                  'file:/var/lib/xen/images/domains/pdc.salud.gob.sv/swap.img,xvda1,w',
              ]
Y el gran cambio será file: por tap:qcow2: y la extensión: .img por .qcow2 en donde se apunte al disco que cambiamos
#
#  Disk device(s).
#
root        = '/dev/xvda2 ro'
disk        = [
                  'tap:qcow2:/var/lib/xen/images/domains/pdc.salud.gob.sv/disk.qcow2,xvda2,w',
                  'file:/var/lib/xen/images/domains/pdc.salud.gob.sv/swap.img,xvda1,w',
              ]
Fuentes:
Using qcow2 images in Xen 4.1 on Debian 

jueves, 30 de junio de 2016

Actualizando owncloud 8.1.8 a 9.0.0

Aunque la relación de Owncloud con Debian no pasa por sus mejores momentos ([Pkg-owncloud-maintainers] About no more ownCloud in Debian) en realidad es posible seguir instalando al primero sobre el segundo.

Siempre es recomendable mantener un ritmo de actualizaciones sano, pero si no es así, parece que es posible actualizar gradualmente (Esto es de suma importancia) de la siguiente forma:

7.0.3-debian >> 7.0.14 >> 8.0.12 >> 8.1.7>> 8.2.4 >> 9.0.2
Según se recoge en Upgrading ownCloud on Debian Stable to official packages, Esta forma gradual de hacer las actualizaciones pueden ser molestas, pero lo contrario (7.0.14 a 8.1.7, por ejemplo) es un proceso temerario que no estará automatizado por las herramientas de owncloud.

Actualizé la instalación en producción desde el punto 8.1.7 (En este contexto, se refieren al release. Mi versión era la 8.1.8) al 8.2.4.
En esta versión hubo un molesto problema con la app Documentos, cuya solución descrita en ASSERTION FAILED: tried to unsubscribe unknown callback from event "input/compositionstart" era actualizar a 9.0.2. Comprobé que con actualizar a 9.0.0 era suficiente.

Los cambios en /etc/apt/sources.list.d/owncloud.list describen básicamente el proceso de actualización:
#deb http://download.opensuse.org/repositories/isv:ownCloud:community/Debian_7.0/ /

# Dejaron de usar el servicio de opensuse
#deb http://download.owncloud.org/download/repositories/8.2/Debian_7.0/ /

# Esta no sirve del todo. Hay un con el repositorio según parece
#deb http://download.owncloud.org/download/repositories/stable/Debian_7.0/ /

## Esta es totalmente estable, aunque no es precisamente la última versión pero si la primera del relase 9.0.0, me pareció buena idea llegar a este punto
deb http://download.owncloud.org/download/repositories/9.0.0/Debian_7.0/ /

Por último, queda señalar que el paquete para la versión 9.0 se ha cambiado a owncloud-files, y que el paquete owncloud-deps no existe para esta versión de Debian. Cuando se instale owncloud-files, apache, amavis y php van a irse por un rato (Se supone que todos serían reemplazados por owncloud-deps). Una vez instalado se instalan todos de nuevo y todo funciona de maravilla.
aptitude install owncloud-files
Se instalarán los siguiente paquetes NUEVOS:     
  owncloud-files{b} 
0 paquetes actualizados, 1 nuevos instalados, 0 para eliminar y 0 sin actualizar.
Necesito descargar 32,7 MB de ficheros. Después de desempaquetar se usarán 93,6 MB.
No se satisfacen las dependencias de los siguientes paquetes:
 owncloud-files : Entra en conflicto: owncloud (<= 8.99.99) pero está instalado 8.2.5-1.1.
                  Entra en conflicto: owncloud-config-apache (<= 8.99.99) pero está instalado 8.2.5-1.1.
                  Entra en conflicto: owncloud-server (<= 8.99.99) pero está instalado 8.2.5-1.1.
Las acciones siguientes resolverán estas dependencias

     Eliminar los paquetes siguientes:
1)     owncloud                       
2)     owncloud-config-apache         
3)     owncloud-server                



¿Acepta esta solución? [Y/n/q/?]y
Se instalarán los siguiente paquetes NUEVOS:
  owncloud-files 
Se ELIMINARÁN los siguientes paquetes:
  apache2{u} clamav{u} clamav-base{u} clamav-freshclam{u} libclamav7{u} libllvm3.0{u} libmcrypt4{u} libpq5{u} owncloud{a} owncloud-config-apache{a} owncloud-server{a} php-xml-parser{u} php5{u} php5-curl{u} php5-intl{u} php5-mcrypt{u} php5-mysql{u} php5-pgsql{u} php5-sqlite{u} 
0 paquetes actualizados, 1 nuevos instalados, 19 para eliminar y 0 sin actualizar.
Necesito descargar 32,7 MB de ficheros. Después de desempaquetar se liberarán 17,9 MB.
¿Quiere continuar? [Y/n/?] y
Y claro, backup. Hacer el backup de un sistema de backup es un poco gracioso pero así las cosas

jueves, 23 de junio de 2016

Sistema de voceo con Elastix

Los sistemas de voceo más comunes suelen estar basados en hardware y ser soluciones específicas, pero es posible configurar Asterisk (El que ya esta configurado por Elastix) para tener uno sustentado en nuestro servicio de VoIP.
Con esto, una tarjeta de sonido para el servidor es el único costo en hardware que vamos a tener.

Lo primero es configurar en /etc/asterisk/modules.conf la carga de los módulos que vamos a necesitar para que Asterisk sea capaz de usar el sistema de sonido del sistema. Buscamos las siguientes líneas para que queden de la siguiente forma:
;
; Load either OSS or ALSA, not both
; By default, load no console driver
;
noload => chan_alsa.so
load => chan_oss.so
Luego, habrá que configurar el fichero /etc/asterisk/oss.conf
con algunas configuraciones propias del módulo. Hay que revisar algo en el sistema si es que existe el dispositivo /dev/dsp o por el contrario será /dev/dsp1. El primero esta configurado por defecto, para el segundo (Y para otros, supongo) usamos la opción device
[general]
autoanswer=yes
context=from-internal
overridecontext=yes
extension=s
language=en
playbackonly=yes
device = /dev/dsp1
Por último, configuramos la extensión a usar en el fichero /etc/asterisk/extensions_custom.conf, que por lo demás es el lugar donde se configurar extensiones de este tipo
[voceo-neomano]
; Primero hay que ver como funciona sin el
exten => 1030,1,Dial(console/dsp,20,A(beep))
exten => 1030,1,Set(PITCH_SHIFT(both)=.15)
exten => 1030,n,Hangup()
A voceo-neomano será necesario agregarlo bajo [from-internal-custom] con include. Como ejemplo, esa sección queda de la siguiente forma:
[from-internal-custom]
exten => 1234,1,Playback(demo-congrats)         ; extensions can dial 1234
exten => 1234,2,Hangup()
exten => h,1,Hangup()
include => agentlogin
include => conferences
include => calendar-event
include => weather-wakeup
include => voceo-neomano

Fuentes:
Sistema de voceo anti-feedback de bajo costo para Elastix
Unable to re-open DSP device /dev/dsp

miércoles, 15 de junio de 2016

Apuntes sobre el error de Squid3 / SquidGuard en Debian Jessie

La configuración mínima necesaria para Squid3 parece ir de la siguiente forma:
acl usuarios src 10.40.20.0/24
acl usuarios src 10.20.20.0/24


acl Safe_ports port 80 443 8080 20 21
## Según https://forums.gentoo.org/viewtopic-t-952948-start-0.html
## Hay que comentar esto en Squid 3.4 porque ya esta configurado por defecto
# acl manager proto cache_object
acl CONNECT method CONNECT
acl NONE method NONE

ftp_passive off

host_verify_strict on 

http_access deny NONE
http_access deny !Safe_ports
http_access allow usuarios
http_access deny all

## Es necesario que hay al menos uno sin intercept
http_port 10.20.20.1:3128 
## TODO: ¿Funcionará al descomentar lo siguiente?
# http_port 10.20.20.1:3128 intercept
http_port 10.40.20.1:3128 intercept

cache_mem 469 MB
## TODO: Ni siquiera recuerdo el origen de estas líneas, pero su funcionamiento no tiene 
## implicaciones sobre nuestro problema
#cache_dir aufs /var/spool/squid3 500 16 256
#cache_dir aufs /var/spool/squid3-${process_number} 500 16 256 min-size=322560 

#debug_options 84,1
#debug_options 85,2
debug_options ALL,2
coredump_dir /var/spool/squid3/dump

url_rewrite_program /usr/bin/squidGuard -c /etc/squidguard/squidGuard.conf -d
url_rewrite_children 5 startup=0 idle=1 concurrency=3
url_rewrite_host_header off 

refresh_pattern .       0   20% 4320

relaxed_header_parser warn
connect_timeout 20 seconds
shutdown_lifetime 3 seconds
cache_mgr fws@salud.gob.sv
httpd_suppress_version_string on
visible_hostname firewall.dominio.com

error_default_language  es-sv
prefer_direct on
check_hostnames on

dns_retransmit_interval 2 seconds
dns_timeout 1 minutes
dns_nameservers 10.10.20.20 10.10.20.21
dns_v4_first on
La configuración mínima necesaria en squidGuard, y estoy hablando que esto es apenas un ejemplo para nada funcional de como va a trabajar realmente:
#
# CONFIG FILE FOR SQUIDGUARD
#
# Caution: do NOT use comments inside { }
#

dbhome /var/lib/squidguard/db
logdir /var/log/squidguard

#
# TIME RULES:
# abbrev for weekdays: 
# s = sun, m = mon, t =tue, w = wed, h = thu, f = fri, a = sat

time laboral {
    weekly * 00:15 - 12:29
    weekly * 13:15 - 23:55
}

#
# SOURCE ADDRESSES:
#

src usuarios {
 ip   10.20.20.0/24 
}

#
# DESTINATION CLASSES:
#
# [see also in file dest-snippet.txt]

dest deportes {
    domainlist BL/recreation/sports/domains
    log deportes.log
}

dest webtv {
    domainlist BL/webtv/domains
    log ocio.log
}

#dest adult {
# domainlist BL/adult/domains
# urllist  BL/adult/urls
# expressionlist BL/adult/expressions
# redirect http://admin.foo.bar.de/cgi-bin/blocked.cgi?clientaddr=%a&clientname=%n&clientuser=%i&clientgroup=%s&targetgroup=%t&url=%u
#}

#
# ACL RULES:
#

acl {
 usuarios {
     pass  !in-addr !deportes !webtv any
 }

 default {
  pass  none
  redirect http://admin.foo.bar.de/cgi-bin/blocked.cgi?clientaddr=%a&clientname=%n&clientuser=%i&clientgroup=%s&targetgroup=%t&url=%u
 }
}
Esto funcionaba en Debian Squeezy/Wheezy perfectamente. Tiene la ventaja de ser presumiblemente rápido a la hora de filtrar tráfico, la configuración se realiza por medio de ACL que son bastante fáciles de entender y tiene altas posibilidades con las listas En Debian Jessie hay un problema con las versiones presentes en los respositorios, precisamente en la forma en que squid3 le comunica los datos de la petición HTTP al redirector SquidGuard. Cuando se hace una prueba a redireccionar desde squidGuard
$ echo "http://anontv.com 10.20.20.11 - - GET" | squidGuard -c /etc/squidguard/squidGuard.conf 
OK rewrite-url="http://admin.foo.bar.de/cgi-bin/blocked.cgi?clientaddr=10.20.20.11&clientname=&clientuser=&clientgroup=usuarios&targetgroup=webtv&url=http://anontv.com"
Mientras que /var/log/squid3/cache.log es posible ver
2016/06/14 19:22:20.711 kid1| client_side_reply.cc(1969) processReplyAccessResult: The reply for GET http://admin.foo.bar.de/cgi-bin/blocked.cgi?clientaddr=http:&clientname=/www.anontv.com/&clientuser=10.20.20.1/firewall.salud.gob.sv&clientgroup=default&targetgroup=none&url=0 is ALLOWED, because it matched 'usuarios'
cuando se hace una petición a squid3 desde un equipo cliente. Usando un navegador, telnet o algo como wget. Por lo pronto no encuentro la solución a este problema, ahora mismo estoy bajando el DVD de Debian Testing, quizá en Strech los paquetes disponibles ya no tengan ese problema. Puede verse como el clientaddr que devuelve SquidGuard es diferente en cada caso. Así que debemos suponer que ese es el valor que squidGuard toma como IP a la hora de relacionarlo con las ACL.

Fuentes:

sábado, 11 de junio de 2016

Colander: O de como validar un JSON en Pyramid

Confieso que he dejado alguna aplicación sin que, una vez validados los datos en el cliente, los valide en el servidor.
Pero esta vez quiero hacer las cosas bien, así que encontré de suerte a Colander (Con una documentación bastante útil hasta ahora) que es capaz de realizar algo como validar los datos JSON una vez llegan a nuestra aplicación. Y se integra bien con Pyramid. Y no hay ningún problema con los test, excepto escribirlos.
Añadimos colander como dependencia de nuestro proyecto en requires de ./ambiente/aplicacion/setup.py
(...)

requires = [
    'pyramid',
    'pyramid_debugtoolbar',
    'waitress',
    'nose',
    'webtest',
    'coverage',
    'colander'
    ]

(...)
Lo instalamos
# Este sí, desde el directorio ./ambiente/aplicacion es la mejor idea
python setup.py develop
Creamos un fichero ./ambiente/aplicacion/aplicacion/schemas/usuario.py el que creamos una clase que, descendiendo de algún tipo específico de Colander, sea capaz de definir la estructura de nuestro objeto JSON, aunque en realidad podemos hacer validaciones de cadenas simples y todo bien.
mkdir ./ambiente/aplicacion/aplicacion/schemas
touch ./ambiente/aplicacion/aplicacion/schemas/__init__.py
Y creamos el fichero ./ambiente/aplicacion/aplicacion/schemas/usuario.py con el siguiente contenido:
# coding: utf-8
import colander

class Palabra(colander.SequenceSchema):
    # Palabra es un string utf-8 de al menos dos caracteres 
    palabra = colander.SchemaNode(colander.String('utf-8'), validator=colander.Length(min=2))

class UsuarioEsquema(colander.MappingSchema):
    # nombre es un string utf-8
    nombre = colander.SchemaNode(colander.String('utf-8'))
    # apellido es un string utf-8
    apellido = colander.SchemaNode(colander.String('utf-8'))
    # palabras es una lista de palabras
    palabras = Palabra(validator=colander.Length(min=1))
if __name__ == '__main__':
    esquema = Data()
    data = {'nombre': 'Alexander', 'apellido': 'Ortíz', 'palabras': ['usuario', 'formidable']}
    data = {'nombre': 389, 'palabras': ['usuario', 'formidable']}
Y en lo que ya parece una costumbre, actualizamos nuestra vista ./ambiente/aplicacion/aplicacion/views/actividades.py
# coding: utf-8
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPBadRequest
from colander import Invalid

from ..fichero_app.ficheros import Usuarios
from ..schemas.usuario import UsuarioEsquema

# Empieza el trabajo con la autenticación
from pyramid.security import Allow, Deny, Everyone, NO_PERMISSION_REQUIRED, Authenticated

# Dejamos por acá un esquema listo para usarse
esquema = UsuarioEsquema()

@view_config(route_name='ficheros_listado', renderer='json', permission='listar')
def ficheros_listado(request):
    """Cuando request_method se configura acà, el mensaje es diferente porque 
    la operación alcanzada es diferente
        The resource could not be found.


     predicate mismatch for view get_listar_ficheros (request_method = GET,HEAD)
    Por tanto lo mejor es configurarlo allá en __init__
    """
    ficheros = Usuarios()
    listado = ficheros.listado()
    return {'respuesta': listado}

@view_config(route_name='ficheros_detalle', renderer='json', permission='detallar')
def ficheros_detalle(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    detalle = ficheros.detalle(usuario)
    return {'respuesta': detalle}

@view_config(route_name='ficheros_creacion', renderer='json', permission='creacion')
def ficheros_creacion(request):
    try:
        usuario = request.json_body['usuario']
        data = esquema.deserialize(request.json_body['data'])
    except Invalid as e:
        return HTTPBadRequest(json_body=e.asdict())
    except Exception as e:
        return HTTPBadRequest()
    ficheros = Usuarios()
    creacion = ficheros.creacion(usuario, data)
    return {'respuesta': creacion}

@view_config(route_name='ficheros_modificacion', renderer='json', permission='modificacion')
def ficheros_modificacion(request):
    usuario = request.matchdict['usuario']
    try:
        data = esquema.deserialize(request.json_body['data'])
    except Invalid as e:
        return HTTPBadRequest(json_body=e.asdict())
    except Exception as e:
        return HTTPBadRequest()
    ficheros = Usuarios()
    modificacion = ficheros.modificacion(usuario, data)
    return {'respuesta': modificacion}

@view_config(route_name='ficheros_borrado', renderer='json', permission='borrado')
def ficheros_borrado(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    borrado = ficheros.borrado(usuario)

    return {'respuesta': borrado}
Y para mantener las buenas costumbres, agregamos a Creacion un método test_ficheros_creacion_malformed en ./ambiente/aplicacion/aplicacion/tests/testFuncionales.py de la siguiente forma:
    def test_ficheros_creacion_malformed(self):
        datos = {'usuario': 'fcornejo',  'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['ente', 'obvio']}}
        # Dañamos el nombre, así nos aseguramos que sea ese contenido el equivocado 
        datos['data']['nombre'] = 389
        respuesta = self.testapp.post_json('/ficheros', status=400, params=datos)
        self.assertRegexpMatches(respuesta.json_body['nombre'], '389 is not a string')

jueves, 9 de junio de 2016

Test funcionales en Pyramid para vistas que requieren autenticación

Los test funcionales no están funcionando en este momento.
======================================================================
ERROR: test_ficheros_borrado (aplicacion.tests.testFuncionales.Borrado)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/var/www/ambiente/aplicacion/aplicacion/tests/testFuncionales.py", line 78, in setUp
    self.testapp.post_json('/ficheros', status=200, params=datos)
  File "/var/www/ambiente/local/lib/python2.7/site-packages/WebTest-2.0.21-py2.7.egg/webtest/utils.py", line 36, in wrapper
    return self._gen_request(method, url, **kw)
  File "/var/www/ambiente/local/lib/python2.7/site-packages/WebTest-2.0.21-py2.7.egg/webtest/app.py", line 740, in _gen_request
    expect_errors=expect_errors)
  File "/var/www/ambiente/local/lib/python2.7/site-packages/WebTest-2.0.21-py2.7.egg/webtest/app.py", line 636, in do_request
    self._check_status(status, res)
  File "/var/www/ambiente/local/lib/python2.7/site-packages/WebTest-2.0.21-py2.7.egg/webtest/app.py", line 671, in _check_status
    "Bad response: %s (not %s)", res_status, status)
AppError: Bad response: 403 Forbidden (not 200)

(...)
Como mencioné, estos ya suceden en un nivel bastante arriba de la aplicación, casi que son las peticiones tan normales como las que hacemos con el navegador.

Los test unitarios siguen testando partes específicas del código sin importarle en realidad la actitud HTTP, incluso aunque en unos de ellos usemos un pyramid.request.Request para la petición. Que es real, pero no tan real como se esperaría, y vemos que eso es un poco bueno.

Por otra parte, webtest.TestApp es tan real que de hecho acepta cookies. Ahora veremos lo conveniente de automatizar los test: Nos seguimos ahorrando hacer un intento de formulario con una simple petición post
self.testapp.post('/login', status=302, params={'username':'vtacius'})
Y algo tan simple en el setUp de cada clase nos permitirá autenticarnos en el sistema y realizar todas las operaciones que queramos comprobar
Así que autenticamos los test de la siguiente forma en el fichero correspondiente ./ambiente/aplicacion/aplicacion/tests/testFuncionales.py:
#coding:utf-8

from unittest import TestCase

class Listado(TestCase):
    def setUp(self):
        from aplicacion import main
        from webtest import TestApp
        
        app = main({})
        self.testapp = TestApp(app)
    
    def test_ficheros_listado(self):
        respuesta = self.testapp.get('/ficheros', status=200, xhr=True)
        self.assertEqual(respuesta.content_type, 'application/json')
        self.assertItemsEqual(respuesta.json_body['respuesta'], ['alortiz', 'kpenate', 'opineda']) 

class Detalle(TestCase):
    def setUp(self):
        from aplicacion import main
        from webtest import TestApp

        app = main({})
        self.testapp = TestApp(app)

    def test_unauth_detalle(self):
        respuesta = self.testapp.get('/ficheros/' + 'alortiz', status=403, xhr=True)
        self.assertRegexpMatches(respuesta.body, 'Access was denied to this resource')

    def test_ficheros_detalle(self):
        # Habrá que loguear en cada test si no se hace en setUp()
        self.testapp.post('/login', status=302, params={'username':'vtacius'})
        respuesta = self.testapp.get('/ficheros/' + 'alortiz', status=200, xhr=True)
        self.assertItemsEqual(respuesta.json_body['respuesta']['palabras'], ['ambiente', 'publico'])

    def test_ficheros_detalle_inexistente(self):
        # Habrá que loguear en cada test si no se hace en setUp()
        self.testapp.post('/login', status=302, params={'username':'vtacius'})
        respuesta = self.testapp.get('/ficheros/' + 'fitzcarraldo', status=200, xhr=True)
        self.assertEqual(respuesta.json_body['respuesta']['error'], 'No such file or directory')

class Creacion(TestCase):
    def setUp(self):
        from aplicacion import main
        from webtest import TestApp

        app = main({})
        self.testapp = TestApp(app)
        # Loguemos en la aplicación con datos reales
        self.testapp.post('/login', status=302, params={'username':'vtacius'})

    def tearDown(self):
        respuesta = self.testapp.delete('/ficheros/' + 'fcornejo', status=200)

    def test_ficheros_creacion(self):
        datos = {'usuario': 'fcornejo', 'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['ente', 'obvio']}}
        respuesta = self.testapp.post_json('/ficheros', status=200, params=datos)
        self.assertDictEqual(respuesta.json_body['respuesta'], datos['data'])

class Modificacion(TestCase):
    def setUp(self):
        from aplicacion import main
        from webtest import TestApp

        app = main({})
        self.testapp = TestApp(app)
        # Loguemos en la aplicación con datos reales
        self.testapp.post('/login', status=302, params={'username':'vtacius'})

        datos = {'usuario': 'fcornejo', 'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['ente', 'obvio']}}
        self.testapp.post_json('/ficheros', status=200, params=datos)
    
    def tearDown(self):
        respuesta = self.testapp.delete('/ficheros/' + 'fcornejo', status=200)
 
    def test_ficheros_modificacion(self):
        datos = {'usuario': 'fcornejo', 'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['espejismo', 'olvido']}}
        respuesta = self.testapp.put_json('/ficheros/' + 'fcornejo', status=200, params=datos)
        self.assertDictEqual(respuesta.json_body['respuesta'], datos['data'])
       
class Borrado(TestCase):
    def setUp(self):
        from aplicacion import main
        from webtest import TestApp

        app = main({})
        self.testapp = TestApp(app)
        # Loguemos en la aplicación con datos reales
        self.testapp.post('/login', status=302, params={'username':'vtacius'})
        
        datos = {'usuario': 'fcornejo', 'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['ente', 'obvio']}}
        self.testapp.post_json('/ficheros', status=200, params=datos)

    def test_ficheros_borrado(self):
        self.testapp.post('/login', status=302, params={'username':'vtacius'})
        
        respuesta = self.testapp.delete('/ficheros/' + 'fcornejo', status=200)

        self.assertEqual(respuesta.json_body['respuesta'], 'fcornejo')

Con un poco de verbosidad, la salida se verá ahora de la siguiente manera:
nosetests -v
test_ficheros_borrado (aplicacion.tests.testFuncionales.Borrado) ... ok
test_ficheros_creacion (aplicacion.tests.testFuncionales.Creacion) ... ok
test_ficheros_detalle (aplicacion.tests.testFuncionales.Detalle) ... ok
test_ficheros_detalle_inexistente (aplicacion.tests.testFuncionales.Detalle) ... ok
test_unauth_detalle (aplicacion.tests.testFuncionales.Detalle) ... ok
test_ficheros_listado (aplicacion.tests.testFuncionales.Listado) ... ok
test_ficheros_modificacion (aplicacion.tests.testFuncionales.Modificacion) ... ok
test_ficheros_borrado (aplicacion.tests.testUnitarios.Borrado) ... ok
test_ficheros_borrado_inexistente (aplicacion.tests.testUnitarios.Borrado) ... ok
test_ficheros_creacion (aplicacion.tests.testUnitarios.Creacion) ... ok
test_ficheros_creacion_json_malformed (aplicacion.tests.testUnitarios.Creacion) ... ok
test_ficheros_detalle (aplicacion.tests.testUnitarios.Detalle) ... ok
test_ficheros_detalle_noexistente (aplicacion.tests.testUnitarios.Detalle) ... ok
test_ficheros_listado (aplicacion.tests.testUnitarios.Listado) ... ok
test_ficheros_modificacion (aplicacion.tests.testUnitarios.Modificacion) ... ok

Name                                  Stmts   Miss  Cover   Missing
-------------------------------------------------------------------
aplicacion.py                            19      0   100%   
aplicacion/fichero_app.py                 0      0   100%   
aplicacion/fichero_app/ficheros.py       46     11    76%   21-22, 36, 52-55, 69-72
aplicacion/resources.py                   5      0   100%   
aplicacion/security.py                    5      0   100%   
aplicacion/tests.py                       0      0   100%   
aplicacion/tests/testFuncionales.py      69      0   100%   
aplicacion/tests/testUnitarios.py        99      0   100%   
aplicacion/views.py                       0      0   100%   
aplicacion/views/actividades.py          36      2    94%   48-49
aplicacion/views/autenticacion.py         7      0   100%   
-------------------------------------------------------------------
TOTAL                                   286     13    95%   
----------------------------------------------------------------------
Ran 15 tests in 1.546s

OK

Otros apuntes interesantes

Otros apuntes interesantes