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): ...@@ -140,25 +140,3 @@ class ViewsTestCase(TestCase):
else: else:
self.assertNotContains(result, "Classes End") 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, ...@@ -306,7 +306,7 @@ def index(request, course_id, chapter=None, section=None,
'bosh_url': jabber.utils.get_bosh_url(), 'bosh_url': jabber.utils.get_bosh_url(),
'course_room': jabber.utils.get_room_name_for_course(course.id), 'course_room': jabber.utils.get_room_name_for_course(course.id),
'username': "%s@%s" % (user.username, settings.JABBER.get('HOST')), '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) 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 from django.db import models
class JabberUser(models.Model): 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: class Meta:
app_label = 'jabber' app_label = u'jabber'
db_table = 'users' db_table = u'users'
# This is the primary key for our table, since ejabberd doesn't # This is the primary key for our table, since ejabberd doesn't
# put an ID column on this table. This will match the edX # put an ID column on this table. This will match the edX
# username chosen by the user. # 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 # Yes, this is stored in plaintext. ejabberd only knows how to do
# basic string matching, so we don't hash/salt this or anything. # 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 import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.core.exceptions import ImproperlyConfigured
from mock import patch
from nose.plugins.skip import SkipTest
from factory import DjangoModelFactory, Sequence
from jabber.models import JabberUser
import jabber.utils import jabber.utils
class JabberSettingsTests(TestCase):
@override_settings()
def test_valid_settings(self):
pass
def test_missing_settings(self): class JabberUserFactory(DjangoModelFactory):
pass """
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): class UtilsTests(TestCase):
def test_get_bosh_url(self): """
# USE_SSL present (True/False) and absent Tests for the various utility functions in the utils module.
# HOST present (something/empty) and absent TODO: is there a better way to override all of these settings?
# PORT present (int/str) and absent It'd be nice to have a single dict that we just copy and
# PATH present (something/empty) and absent override keys, but that almost looks uglier than this.
pass """
@override_settings(JABBER={
'HOST': 'jabber.edx.org',
def test_get_password_for_user(self): 'PORT': '5208',
# Test JabberUser present/absent 'PATH': 'http-bind/',
pass 'USE_SSL': False,
})
def test_get_room_name_for_course(self): def test_get_bosh_url_standard(self):
# HOST present (something/empty) and absent bosh_url = jabber.utils.get_bosh_url()
# Test course_id parsing self.assertEquals(bosh_url, 'http://jabber.edx.org:5208/http-bind/')
pass
@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 ...@@ -4,6 +4,7 @@ functions to parse the settings, create and retrieve chat-specific
passwords for users, etc. passwords for users, etc.
""" """
import base64 import base64
import math
import os import os
from django.conf import settings from django.conf import settings
...@@ -11,11 +12,6 @@ from django.core.exceptions import ImproperlyConfigured ...@@ -11,11 +12,6 @@ from django.core.exceptions import ImproperlyConfigured
from .models import JabberUser 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(): def get_bosh_url():
""" """
Build a "Bidirectional-streams Over Synchronous HTTP" (BOSH) URL Build a "Bidirectional-streams Over Synchronous HTTP" (BOSH) URL
...@@ -45,7 +41,7 @@ def get_bosh_url(): ...@@ -45,7 +41,7 @@ def get_bosh_url():
bosh_url += ":%s" % str(port) bosh_url += ":%s" % str(port)
# Also optional is the "path", which could possibly use a better # Also optional is the "path", which could possibly use a better
# name...help @jrbl? # name...
path = settings.JABBER.get("PATH") path = settings.JABBER.get("PATH")
if path is not None: if path is not None:
bosh_url += "/%s" % path bosh_url += "/%s" % path
...@@ -53,7 +49,7 @@ def get_bosh_url(): ...@@ -53,7 +49,7 @@ def get_bosh_url():
return 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 Retrieve the password for the user with the given username. If
a password doesn't exist, then we'll create one by generating a a password doesn't exist, then we'll create one by generating a
...@@ -62,7 +58,7 @@ def get_password_for_user(username): ...@@ -62,7 +58,7 @@ def get_password_for_user(username):
try: try:
jabber_user = JabberUser.objects.get(username=username) jabber_user = JabberUser.objects.get(username=username)
except JabberUser.DoesNotExist: except JabberUser.DoesNotExist:
password = __generate_random_string(DEFAULT_PASSWORD_LENGTH) password = __generate_random_string(JabberUser.DEFAULT_PASSWORD_LENGTH)
jabber_user = JabberUser(username=username, jabber_user = JabberUser(username=username,
password=password) password=password)
jabber_user.save() jabber_user.save()
...@@ -100,14 +96,22 @@ def get_room_name_for_course(course_id): ...@@ -100,14 +96,22 @@ def get_room_name_for_course(course_id):
def __generate_random_string(length): 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. suitable for a password that can be stored in a database.
""" """
# Base64 encoding gives us 4 chars for every 3 bytes we give it, # A Base64-encoded string's length is always a multiple of 4. The
# so figure out how many random bytes we need to get a string of # encoding gives us 4 chars for every 3 bytes we give it, so
# just the right length # figure out roughly how many random bytes we'll need to get a
num_bytes = length / 4 * 3 # string of just the right length.
return base64.b64encode(os.urandom(num_bytes)) 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(): def __validate_settings():
......
...@@ -190,3 +190,14 @@ PASSWORD_HASHERS = ( ...@@ -190,3 +190,14 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
# 'django.contrib.auth.hashers.CryptPasswordHasher', # '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