Commit b61869f7 by Nate Hardison Committed by Joe Blaylock

Remove unused test

Now that the Jabber settings functionality is refactored into its
own Django app, we don't need this test in the Courseware Django
app.

Rename get_password_for_user to get_or_create_...

The new name describes more accurately what the function does: it
creates a new password for the user if it can't find one in the
database.

Move DEFAULT_PASSWORD_LENGTH to JabberUser model

This seems more appropriate as a property of the model as opposed
to a loose constant in the utils module.

Random string generation returns proper length

Before, the length was only accurate if it was a multiple of four.
Now, we calculate the number of bytes needed from /dev/urandom
properly and we truncate the string to just the right length.
(Base64 returns strings that are a multiple of four in length, but
we truncate since we won't ever care to decode the string.)

Adding working utility tests for Jabber app

The majority of the functionality of the Jabber app is in the utils
module, so we test those functions thoroughly.

The test settings don't currently have settings to connect to a
Jabber database (need @jrbl's help to get the migration files to set
it up properly), so we temporarily skip tests that hit the Jabber
users table.

Add migration for Jabber users table for testing

In testing environments, we want to be able to interact with the
database as we create JabberUser instances. This commit adds a
migration for the purposes of testing; the migration should *not*
be used for anything else though. The actual Jabber users table
should be created by ejabberd during provisioning.

Unskip Jabber tests that interact with database

Now that we have a migration for the Jabber users table in the test
environment, we can hook it up in test and "unskip" the tests that
hit that table.
parent 7d6afbdf
......@@ -140,25 +140,3 @@ class ViewsTestCase(TestCase):
else:
self.assertNotContains(result, "Classes End")
def test_chat_settings(self):
mock_user = MagicMock()
mock_user.username = "johndoe"
mock_course = MagicMock()
mock_course.id = "a/b/c"
# Stub this out in the case that it's not in the settings
domain = "jabber.edx.org"
settings.JABBER_DOMAIN = domain
chat_settings = views.chat_settings(mock_course, mock_user)
# Test the proper format of all chat settings
self.assertEquals(chat_settings['domain'], domain)
self.assertEquals(chat_settings['room'], "a-b-c_class")
self.assertEquals(chat_settings['username'], "johndoe@%s" % domain)
# TODO: this needs to be changed once we figure out how to
# generate/store a real password.
self.assertEquals(chat_settings['password'], "johndoe@%s" % domain)
......@@ -306,7 +306,7 @@ def index(request, course_id, chapter=None, section=None,
'bosh_url': jabber.utils.get_bosh_url(),
'course_room': jabber.utils.get_room_name_for_course(course.id),
'username': "%s@%s" % (user.username, settings.JABBER.get('HOST')),
'password': jabber.utils.get_password_for_user(user.username)
'password': jabber.utils.get_or_create_password_for_user(user.username)
}
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
......
# NOTE: this is for *test purposes only*. We need some sort of DB set
# up for testing, but in dev/prod, whoever provisions ejabberd
# should provision its auth database separately, *not* using
# this migration data (as it's far from complete).
#
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'JabberUser'
db.create_table(u'users', (
('username', self.gf('django.db.models.fields.CharField')(max_length=250, primary_key=True)),
('password', self.gf('django.db.models.fields.TextField')()),
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True)),
))
db.send_create_signal(u'jabber', ['JabberUser'])
def backwards(self, orm):
# Deleting model 'JabberUser'
db.delete_table(u'users')
models = {
u'jabber.jabberuser': {
'Meta': {'object_name': 'JabberUser', 'db_table': "u'users'"},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
'password': ('django.db.models.fields.TextField', [], {}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '250', 'primary_key': 'True'})
}
}
complete_apps = ['jabber']
from django.db import models
class JabberUser(models.Model):
# The default length of the Jabber passwords we create. We set a
# really long default since we're storing these passwords in
# plaintext (ejabberd implementation detail).
DEFAULT_PASSWORD_LENGTH = 256
class Meta:
app_label = 'jabber'
db_table = 'users'
app_label = u'jabber'
db_table = u'users'
# This is the primary key for our table, since ejabberd doesn't
# put an ID column on this table. This will match the edX
# username chosen by the user.
username = models.CharField(max_length=255, db_index=True, primary_key=True)
username = models.CharField(max_length=250, primary_key=True)
# Yes, this is stored in plaintext. ejabberd only knows how to do
# basic string matching, so we don't hash/salt this or anything.
password = models.TextField(default="")
password = models.TextField()
created_at = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
"""
Tests for the Jabber Django app. The vast majority of the
functionality is in the utils module.
"""
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from mock import patch
from nose.plugins.skip import SkipTest
from django.core.exceptions import ImproperlyConfigured
from factory import DjangoModelFactory, Sequence
from jabber.models import JabberUser
import jabber.utils
class JabberSettingsTests(TestCase):
@override_settings()
def test_valid_settings(self):
pass
def test_missing_settings(self):
pass
class JabberUserFactory(DjangoModelFactory):
"""
Simple factory for the JabberUser model.
"""
FACTORY_FOR = JabberUser
# username is a primary key, so each must be unique
username = Sequence(lambda n: "johndoe_{0}".format(n))
password = "abcdefg"
class UtilsTests(TestCase):
def test_get_bosh_url(self):
# USE_SSL present (True/False) and absent
# HOST present (something/empty) and absent
# PORT present (int/str) and absent
# PATH present (something/empty) and absent
pass
def test_get_password_for_user(self):
# Test JabberUser present/absent
pass
def test_get_room_name_for_course(self):
# HOST present (something/empty) and absent
# Test course_id parsing
pass
"""
Tests for the various utility functions in the utils module.
TODO: is there a better way to override all of these settings?
It'd be nice to have a single dict that we just copy and
override keys, but that almost looks uglier than this.
"""
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
'PORT': '5208',
'PATH': 'http-bind/',
'USE_SSL': False,
})
def test_get_bosh_url_standard(self):
bosh_url = jabber.utils.get_bosh_url()
self.assertEquals(bosh_url, 'http://jabber.edx.org:5208/http-bind/')
@override_settings(JABBER={'HOST': 'jabber.edx.org'})
def test_get_bosh_url_host_only(self):
bosh_url = jabber.utils.get_bosh_url()
self.assertEquals(bosh_url, 'http://jabber.edx.org')
@override_settings(JABBER={
'PORT': '5208',
'PATH': 'http-bind/',
'USE_SSL': False,
})
def test_get_bosh_url_no_host(self):
with self.assertRaises(ImproperlyConfigured):
jabber.utils.get_bosh_url()
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
'PORT': 5208,
'PATH': 'http-bind/',
'USE_SSL': False,
})
def test_get_bosh_url_numeric_port(self):
bosh_url = jabber.utils.get_bosh_url()
self.assertEquals(bosh_url, 'http://jabber.edx.org:5208/http-bind/')
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
'PORT': '5208',
'PATH': 'http-bind/',
'USE_SSL': True,
})
def test_get_bosh_url_use_ssl(self):
bosh_url = jabber.utils.get_bosh_url()
self.assertEquals(bosh_url, 'https://jabber.edx.org:5208/http-bind/')
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
'MUC_SUBDOMAIN': 'conference',
})
def test_get_room_name_for_course_standard(self):
course_id = "MITx/6.002x/2013_Spring"
room_name = jabber.utils.get_room_name_for_course(course_id)
self.assertEquals(room_name, '2013_Spring_class@conference.jabber.edx.org')
@override_settings(JABBER={'MUC_SUBDOMAIN': 'conference'})
def test_get_room_name_for_course_no_host(self):
course_id = "MITx/6.002x/2013_Spring"
with self.assertRaises(ImproperlyConfigured):
jabber.utils.get_room_name_for_course(course_id)
@override_settings(JABBER={'HOST': 'jabber.edx.org'})
def test_get_room_name_for_course_no_muc_subdomain(self):
course_id = "MITx/6.002x/2013_Spring"
room_name = jabber.utils.get_room_name_for_course(course_id)
self.assertEquals(room_name, '2013_Spring_class@jabber.edx.org')
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
'MUC_SUBDOMAIN': 'conference',
})
def test_get_room_name_for_course_malformed_course_id(self):
course_id = "MITx_6.002x_2013_Spring"
with self.assertRaises(ValueError):
jabber.utils.get_room_name_for_course(course_id)
course_id = "MITx/6.002x_2013_Spring"
with self.assertRaises(ValueError):
jabber.utils.get_room_name_for_course(course_id)
course_id = "MITx/6.002x/2013/Spring"
with self.assertRaises(ValueError):
jabber.utils.get_room_name_for_course(course_id)
def test_get_password_for_existing_user(self):
jabber_user = JabberUserFactory.create()
pre_jabber_user_count = JabberUser.objects.count()
password = jabber.utils.get_or_create_password_for_user(jabber_user.username)
post_jabber_user_count = JabberUser.objects.count()
jabber_user_delta = post_jabber_user_count - pre_jabber_user_count
self.assertEquals(password, jabber_user.password)
self.assertEquals(jabber_user_delta, 0)
def test_get_password_for_nonexistent_user(self):
pre_jabber_user_count = JabberUser.objects.count()
jabber.utils.get_or_create_password_for_user("nonexistentuser")
post_jabber_user_count = JabberUser.objects.count()
jabber_user_delta = post_jabber_user_count - pre_jabber_user_count
self.assertEquals(jabber_user_delta, 1)
......@@ -4,6 +4,7 @@ functions to parse the settings, create and retrieve chat-specific
passwords for users, etc.
"""
import base64
import math
import os
from django.conf import settings
......@@ -11,11 +12,6 @@ from django.core.exceptions import ImproperlyConfigured
from .models import JabberUser
# The default length of the Jabber passwords we create. We set a
# really long default since we're storing these passwords in
# plaintext (ejabberd implementation detail).
DEFAULT_PASSWORD_LENGTH = 256
def get_bosh_url():
"""
Build a "Bidirectional-streams Over Synchronous HTTP" (BOSH) URL
......@@ -45,7 +41,7 @@ def get_bosh_url():
bosh_url += ":%s" % str(port)
# Also optional is the "path", which could possibly use a better
# name...help @jrbl?
# name...
path = settings.JABBER.get("PATH")
if path is not None:
bosh_url += "/%s" % path
......@@ -53,7 +49,7 @@ def get_bosh_url():
return bosh_url
def get_password_for_user(username):
def get_or_create_password_for_user(username):
"""
Retrieve the password for the user with the given username. If
a password doesn't exist, then we'll create one by generating a
......@@ -62,7 +58,7 @@ def get_password_for_user(username):
try:
jabber_user = JabberUser.objects.get(username=username)
except JabberUser.DoesNotExist:
password = __generate_random_string(DEFAULT_PASSWORD_LENGTH)
password = __generate_random_string(JabberUser.DEFAULT_PASSWORD_LENGTH)
jabber_user = JabberUser(username=username,
password=password)
jabber_user.save()
......@@ -100,14 +96,22 @@ def get_room_name_for_course(course_id):
def __generate_random_string(length):
"""
Generate a Base64-encoded random string of the specified length,
Generate a random, printable string of the specified length,
suitable for a password that can be stored in a database.
"""
# Base64 encoding gives us 4 chars for every 3 bytes we give it,
# so figure out how many random bytes we need to get a string of
# just the right length
num_bytes = length / 4 * 3
return base64.b64encode(os.urandom(num_bytes))
# A Base64-encoded string's length is always a multiple of 4. The
# encoding gives us 4 chars for every 3 bytes we give it, so
# figure out roughly how many random bytes we'll need to get a
# string of just the right length.
num_bytes = math.ceil(float(length) * 3 / 4)
# Grab random bytes from /dev/urandom, which isn't *true*
# randomness, but secure enough for our purposes. (And it doesn't
# block like /dev/random if there's not enough entropy, which is
# important when running on AWS). Truncate the string to just the
# right length, which is fine since we don't care about decoding
# the Base64.
return base64.b64encode(os.urandom(int(num_bytes)))[:length]
def __validate_settings():
......
......@@ -190,3 +190,14 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
# 'django.contrib.auth.hashers.CryptPasswordHasher',
)
################################# CHAT ######################################
# We'll use a SQLite DB just for the purposes of testing out the
# Django side of things. In non-test environments, this should point
# at a MySQL database that's been set up by the ejabberd provisioner.
DATABASES['jabber'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': TEST_ROOT / 'db' / 'jabber.db'
}
INSTALLED_APPS += ('jabber',)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment