test_model_data.py 18 KB
Newer Older
1 2 3
"""
Test for lms courseware app, module data (runtime data storage for XBlocks)
"""
4
import json
5
from mock import Mock, patch
6 7
from functools import partial

Calen Pennington committed
8 9
from courseware.model_data import DjangoKeyValueStore
from courseware.model_data import InvalidScopeError, FieldDataCache
10
from courseware.models import StudentModule
11 12 13
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField

from student.tests.factories import UserFactory
14
from courseware.tests.factories import StudentModuleFactory as cmfStudentModuleFactory, location, course_id
Calen Pennington committed
15
from courseware.tests.factories import UserStateSummaryFactory
16 17
from courseware.tests.factories import StudentPrefsFactory, StudentInfoFactory

18
from xblock.fields import Scope, BlockScope, ScopeIds
19 20
from xblock.exceptions import KeyValueMultiSaveError
from xblock.core import XBlock
21
from django.test import TestCase
22
from django.db import DatabaseError
23 24


25 26 27 28 29 30
def mock_field(scope, name):
    field = Mock()
    field.scope = scope
    field.name = name
    return field

31

Calen Pennington committed
32
def mock_descriptor(fields=[]):
33
    descriptor = Mock(entry_point=XBlock.entry_point)
34
    descriptor.scope_ids = ScopeIds('user1', 'mock_problem', location('def_id'), location('usage_id'))
Calen Pennington committed
35 36
    descriptor.module_class.fields.values.return_value = fields
    descriptor.fields.values.return_value = fields
37
    descriptor.module_class.__name__ = 'MockProblemModule'
38 39
    return descriptor

40 41 42
# The user ids here are 1 because we make a student in the setUp functions, and
# they get an id of 1.  There's an assertion in setUp to ensure that assumption
# is still true.
43 44 45 46
user_state_summary_key = partial(DjangoKeyValueStore.Key, Scope.user_state_summary, None, location('usage_id'))
settings_key = partial(DjangoKeyValueStore.Key, Scope.settings, None, location('usage_id'))
user_state_key = partial(DjangoKeyValueStore.Key, Scope.user_state, 1, location('usage_id'))
prefs_key = partial(DjangoKeyValueStore.Key, Scope.preferences, 1, 'mock_problem')
47
user_info_key = partial(DjangoKeyValueStore.Key, Scope.user_info, 1, None)
48 49


50
class StudentModuleFactory(cmfStudentModuleFactory):
51
    module_state_key = location('usage_id')
52 53 54 55 56
    course_id = course_id


class TestInvalidScopes(TestCase):
    def setUp(self):
57
        super(TestInvalidScopes, self).setUp()
58
        self.user = UserFactory.create(username='user')
Calen Pennington committed
59 60
        self.field_data_cache = FieldDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
        self.kvs = DjangoKeyValueStore(self.field_data_cache)
61 62

    def test_invalid_scopes(self):
63 64 65
        for scope in (Scope(user=True, block=BlockScope.DEFINITION),
                      Scope(user=False, block=BlockScope.TYPE),
                      Scope(user=False, block=BlockScope.ALL)):
Calen Pennington committed
66
            key = DjangoKeyValueStore.Key(scope, None, None, 'field')
67 68 69 70 71 72

            self.assertRaises(InvalidScopeError, self.kvs.get, key)
            self.assertRaises(InvalidScopeError, self.kvs.set, key, 'value')
            self.assertRaises(InvalidScopeError, self.kvs.delete, key)
            self.assertRaises(InvalidScopeError, self.kvs.has, key)
            self.assertRaises(InvalidScopeError, self.kvs.set_many, {key: 'value'})
73 74


75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
class OtherUserFailureTestMixin(object):
    """
    Mixin class to add test cases for failures when a user trying to use the kvs is not
    the one that instantiated the kvs.
    Doing a mixin rather than modifying StorageTestBase (below) because some scopes don't fail in this case, because
    they aren't bound to a particular user

    assumes that this is mixed into a class that defines other_key_factory and existing_field_name
    """
    def test_other_user_kvs_get_failure(self):
        """
        Test for assert failure when a user who didn't create the kvs tries to get from it it
        """
        with self.assertRaises(AssertionError):
            self.kvs.get(self.other_key_factory(self.existing_field_name))

    def test_other_user_kvs_set_failure(self):
        """
        Test for assert failure when a user who didn't create the kvs tries to get from it it
        """
        with self.assertRaises(AssertionError):
            self.kvs.set(self.other_key_factory(self.existing_field_name), "new_value")


class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
    """Tests for user_state storage via StudentModule"""
    other_key_factory = partial(DjangoKeyValueStore.Key, Scope.user_state, 2, location('usage_id'))  # user_id=2, not 1
    existing_field_name = "a_field"
103 104

    def setUp(self):
105
        super(TestStudentModuleStorage, self).setUp()
106
        student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value', 'b_field': 'b_value'}))
107
        self.user = student_module.student
108
        self.assertEqual(self.user.id, 1)   # check our assumption hard-coded in the key functions above.
109 110 111

        # There should be only one query to load a single descriptor with a single user_state field
        with self.assertNumQueries(1):
112 113 114 115 116
            self.field_data_cache = FieldDataCache(
                [mock_descriptor([mock_field(Scope.user_state, 'a_field')])],
                course_id,
                self.user
            )
117

Calen Pennington committed
118
        self.kvs = DjangoKeyValueStore(self.field_data_cache)
119 120 121

    def test_get_existing_field(self):
        "Test that getting an existing field in an existing StudentModule works"
122 123 124
        # This should only read from the cache, not the database
        with self.assertNumQueries(0):
            self.assertEquals('a_value', self.kvs.get(user_state_key('a_field')))
125 126 127

    def test_get_missing_field(self):
        "Test that getting a missing field from an existing StudentModule raises a KeyError"
128 129 130
        # This should only read from the cache, not the database
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
131 132

    def test_set_existing_field(self):
133
        "Test that setting an existing user_state field changes the value"
134 135
        # We are updating a problem, so we write to courseware_studentmodulehistory
        # as well as courseware_studentmodule
136
        with self.assertNumQueries(2):
137
            self.kvs.set(user_state_key('a_field'), 'new_value')
138
        self.assertEquals(1, StudentModule.objects.all().count())
139
        self.assertEquals({'b_field': 'b_value', 'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
140 141

    def test_set_missing_field(self):
142
        "Test that setting a new user_state field changes the value"
143 144
        # We are updating a problem, so we write to courseware_studentmodulehistory
        # as well as courseware_studentmodule
145
        with self.assertNumQueries(2):
146
            self.kvs.set(user_state_key('not_a_field'), 'new_value')
147
        self.assertEquals(1, StudentModule.objects.all().count())
148
        self.assertEquals({'b_field': 'b_value', 'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
149 150 151

    def test_delete_existing_field(self):
        "Test that deleting an existing field removes it from the StudentModule"
152 153
        # We are updating a problem, so we write to courseware_studentmodulehistory
        # as well as courseware_studentmodule
154
        with self.assertNumQueries(2):
155
            self.kvs.delete(user_state_key('a_field'))
156
        self.assertEquals(1, StudentModule.objects.all().count())
157
        self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
158 159 160

    def test_delete_missing_field(self):
        "Test that deleting a missing field from an existing StudentModule raises a KeyError"
161 162
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.delete, user_state_key('not_a_field'))
163
        self.assertEquals(1, StudentModule.objects.all().count())
164
        self.assertEquals({'b_field': 'b_value', 'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
165

166 167
    def test_has_existing_field(self):
        "Test that `has` returns True for existing fields in StudentModules"
168 169
        with self.assertNumQueries(0):
            self.assertTrue(self.kvs.has(user_state_key('a_field')))
170 171 172

    def test_has_missing_field(self):
        "Test that `has` returns False for missing fields in StudentModule"
173 174
        with self.assertNumQueries(0):
            self.assertFalse(self.kvs.has(user_state_key('not_a_field')))
175

176
    def construct_kv_dict(self):
177
        """Construct a kv_dict that can be passed to set_many"""
178 179 180 181 182 183 184
        key1 = user_state_key('field_a')
        key2 = user_state_key('field_b')
        new_value = 'new value'
        newer_value = 'newer value'
        return {key1: new_value, key2: newer_value}

    def test_set_many(self):
185
        "Test setting many fields that are scoped to Scope.user_state"
186
        kv_dict = self.construct_kv_dict()
187 188 189 190

        # Scope.user_state is stored in a single row in the database, so we only
        # need to send a single update to that table.
        # We also are updating a problem, so we write to courseware student module history
191
        with self.assertNumQueries(2):
192
            self.kvs.set_many(kv_dict)
193 194 195 196 197

        for key in kv_dict:
            self.assertEquals(self.kvs.get(key), kv_dict[key])

    def test_set_many_failure(self):
198
        "Test failures when setting many fields that are scoped to Scope.user_state"
199 200 201 202 203 204 205 206 207 208 209
        kv_dict = self.construct_kv_dict()
        # because we're patching the underlying save, we need to ensure the
        # fields are in the cache
        for key in kv_dict:
            self.kvs.set(key, 'test_value')

        with patch('django.db.models.Model.save', side_effect=DatabaseError):
            with self.assertRaises(KeyValueMultiSaveError) as exception_context:
                self.kvs.set_many(kv_dict)
        self.assertEquals(len(exception_context.exception.saved_field_names), 0)

210 211 212

class TestMissingStudentModule(TestCase):
    def setUp(self):
213 214
        super(TestMissingStudentModule, self).setUp()

215
        self.user = UserFactory.create(username='user')
216
        self.assertEqual(self.user.id, 1)   # check our assumption hard-coded in the key functions above.
217 218 219 220

        # The descriptor has no fields, so FDC shouldn't send any queries
        with self.assertNumQueries(0):
            self.field_data_cache = FieldDataCache([mock_descriptor()], course_id, self.user)
Calen Pennington committed
221
        self.kvs = DjangoKeyValueStore(self.field_data_cache)
222 223 224

    def test_get_field_from_missing_student_module(self):
        "Test that getting a field from a missing StudentModule raises a KeyError"
225 226
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field'))
227 228 229

    def test_set_field_in_missing_student_module(self):
        "Test that setting a field in a missing StudentModule creates the student module"
Calen Pennington committed
230
        self.assertEquals(0, len(self.field_data_cache.cache))
231 232
        self.assertEquals(0, StudentModule.objects.all().count())

233 234
        # We are updating a problem, so we write to courseware_studentmodulehistory
        # as well as courseware_studentmodule
235
        with self.assertNumQueries(2):
236
            self.kvs.set(user_state_key('a_field'), 'a_value')
237

Calen Pennington committed
238
        self.assertEquals(1, len(self.field_data_cache.cache))
239 240 241 242 243
        self.assertEquals(1, StudentModule.objects.all().count())

        student_module = StudentModule.objects.all()[0]
        self.assertEquals({'a_field': 'a_value'}, json.loads(student_module.state))
        self.assertEquals(self.user, student_module.student)
244
        self.assertEquals(location('usage_id').replace(run=None), student_module.module_state_key)
245 246 247 248
        self.assertEquals(course_id, student_module.course_id)

    def test_delete_field_from_missing_student_module(self):
        "Test that deleting a field from a missing StudentModule raises a KeyError"
249 250
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field'))
251

252 253
    def test_has_field_for_missing_student_module(self):
        "Test that `has` returns False for missing StudentModules"
254 255
        with self.assertNumQueries(0):
            self.assertFalse(self.kvs.has(user_state_key('a_field')))
256 257


258
class StorageTestBase(object):
259 260 261 262
    """
    A base class for that gets subclassed when testing each of the scopes.

    """
263 264
    # Disable pylint warnings that arise because of the way the child classes call
    # this base class -- pylint's static analysis can't keep up with it.
265
    # pylint: disable=no-member, not-callable
266

267 268 269 270
    factory = None
    scope = None
    key_factory = None
    storage_class = None
271 272

    def setUp(self):
273 274 275 276 277
        field_storage = self.factory.create()
        if hasattr(field_storage, 'student'):
            self.user = field_storage.student
        else:
            self.user = UserFactory.create()
278 279 280
        self.mock_descriptor = mock_descriptor([
            mock_field(self.scope, 'existing_field'),
            mock_field(self.scope, 'other_existing_field')])
281 282 283 284
        # Each field is stored as a separate row in the table,
        # but we can query them in a single query
        with self.assertNumQueries(1):
            self.field_data_cache = FieldDataCache([self.mock_descriptor], course_id, self.user)
Calen Pennington committed
285 286
        self.kvs = DjangoKeyValueStore(self.field_data_cache)

287
    def test_set_and_get_existing_field(self):
288
        with self.assertNumQueries(1):
289 290 291
            self.kvs.set(self.key_factory('existing_field'), 'test_value')
        with self.assertNumQueries(0):
            self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
292

293
    def test_get_existing_field(self):
294
        "Test that getting an existing field in an existing Storage Field works"
295 296
        with self.assertNumQueries(0):
            self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))
297 298

    def test_get_missing_field(self):
299
        "Test that getting a missing field from an existing Storage Field raises a KeyError"
300 301
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.get, self.key_factory('missing_field'))
302 303 304

    def test_set_existing_field(self):
        "Test that setting an existing field changes the value"
305
        with self.assertNumQueries(1):
306
            self.kvs.set(self.key_factory('existing_field'), 'new_value')
307 308
        self.assertEquals(1, self.storage_class.objects.all().count())
        self.assertEquals('new_value', json.loads(self.storage_class.objects.all()[0].value))
309 310 311

    def test_set_missing_field(self):
        "Test that setting a new field changes the value"
312
        with self.assertNumQueries(1):
313
            self.kvs.set(self.key_factory('missing_field'), 'new_value')
314 315 316
        self.assertEquals(2, self.storage_class.objects.all().count())
        self.assertEquals('old_value', json.loads(self.storage_class.objects.get(field_name='existing_field').value))
        self.assertEquals('new_value', json.loads(self.storage_class.objects.get(field_name='missing_field').value))
317 318 319

    def test_delete_existing_field(self):
        "Test that deleting an existing field removes it"
320 321
        with self.assertNumQueries(1):
            self.kvs.delete(self.key_factory('existing_field'))
322
        self.assertEquals(0, self.storage_class.objects.all().count())
323 324

    def test_delete_missing_field(self):
325
        "Test that deleting a missing field from an existing Storage Field raises a KeyError"
326 327
        with self.assertNumQueries(0):
            self.assertRaises(KeyError, self.kvs.delete, self.key_factory('missing_field'))
328
        self.assertEquals(1, self.storage_class.objects.all().count())
329

330 331
    def test_has_existing_field(self):
        "Test that `has` returns True for an existing Storage Field"
332 333
        with self.assertNumQueries(0):
            self.assertTrue(self.kvs.has(self.key_factory('existing_field')))
334

335 336
    def test_has_missing_field(self):
        "Test that `has` return False for an existing Storage Field"
337 338
        with self.assertNumQueries(0):
            self.assertFalse(self.kvs.has(self.key_factory('missing_field')))
339

340
    def construct_kv_dict(self):
341
        """Construct a kv_dict that can be passed to set_many"""
342 343 344 345 346 347 348 349 350 351
        key1 = self.key_factory('existing_field')
        key2 = self.key_factory('other_existing_field')
        new_value = 'new value'
        newer_value = 'newer value'
        return {key1: new_value, key2: newer_value}

    def test_set_many(self):
        """Test that setting many regular fields at the same time works"""
        kv_dict = self.construct_kv_dict()

352 353
        # Each field is a separate row in the database, hence
        # a separate query
354
        with self.assertNumQueries(len(kv_dict)):
355
            self.kvs.set_many(kv_dict)
356 357 358 359 360 361
        for key in kv_dict:
            self.assertEquals(self.kvs.get(key), kv_dict[key])

    def test_set_many_failure(self):
        """Test that setting many regular fields with a DB error """
        kv_dict = self.construct_kv_dict()
362 363
        for key in kv_dict:
            with self.assertNumQueries(1):
364
                self.kvs.set(key, 'test value')
365 366 367 368 369 370 371 372 373

        with patch('django.db.models.Model.save', side_effect=[None, DatabaseError]):
            with self.assertRaises(KeyValueMultiSaveError) as exception_context:
                self.kvs.set_many(kv_dict)

        exception = exception_context.exception
        self.assertEquals(len(exception.saved_field_names), 1)


374 375
class TestUserStateSummaryStorage(StorageTestBase, TestCase):
    """Tests for UserStateSummaryStorage"""
Calen Pennington committed
376 377 378
    factory = UserStateSummaryFactory
    scope = Scope.user_state_summary
    key_factory = user_state_summary_key
379
    storage_class = factory.FACTORY_FOR
380 381


382 383
class TestStudentPrefsStorage(OtherUserFailureTestMixin, StorageTestBase, TestCase):
    """Tests for StudentPrefStorage"""
384
    factory = StudentPrefsFactory
385 386
    scope = Scope.preferences
    key_factory = prefs_key
387
    storage_class = XModuleStudentPrefsField
388 389
    other_key_factory = partial(DjangoKeyValueStore.Key, Scope.preferences, 2, 'mock_problem')  # user_id=2, not 1
    existing_field_name = "existing_field"
390 391


392 393
class TestStudentInfoStorage(OtherUserFailureTestMixin, StorageTestBase, TestCase):
    """Tests for StudentInfoStorage"""
394
    factory = StudentInfoFactory
395 396
    scope = Scope.user_info
    key_factory = user_info_key
397
    storage_class = XModuleStudentInfoField
398 399
    other_key_factory = partial(DjangoKeyValueStore.Key, Scope.user_info, 2, 'mock_problem')  # user_id=2, not 1
    existing_field_name = "existing_field"