tests.py 13.8 KB
Newer Older
1
"""
Arthur Barrett committed
2
Unit tests for the notes app.
3 4 5
"""

from django.test import TestCase
Arthur Barrett committed
6 7 8
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
Arthur Barrett committed
9
from django.core.exceptions import ValidationError
Arthur Barrett committed
10

Arthur Barrett committed
11 12
import collections
import unittest
Arthur Barrett committed
13 14 15 16 17
import json
import logging

from . import utils, api, models

Arthur Barrett committed
18

Arthur Barrett committed
19
class UtilsTest(TestCase):
Arthur Barrett committed
20 21
    def setUp(self):
        '''
Arthur Barrett committed
22
        Setup a dummy course-like object with a tabs field that can be
Arthur Barrett committed
23
        accessed via attribute lookup.
Arthur Barrett committed
24
        '''
Arthur Barrett committed
25
        self.course = collections.namedtuple('DummyCourse', ['tabs'])
Arthur Barrett committed
26 27 28 29 30 31 32 33 34 35 36 37 38 39
        self.course.tabs = []

    def test_notes_not_enabled(self):
        '''
        Tests that notes are disabled when the course tab configuration does NOT
        contain a tab with type "notes."
        '''
        self.assertFalse(utils.notes_enabled_for_course(self.course))

    def test_notes_enabled(self):
        '''
        Tests that notes are enabled when the course tab configuration contains
        a tab with type "notes."
        '''
Arthur Barrett committed
40 41 42
        self.course.tabs = [{'type': 'foo'},
                            {'name': 'My Notes', 'type': 'notes'},
                            {'type': 'bar'}]
43

Arthur Barrett committed
44 45
        self.assertTrue(utils.notes_enabled_for_course(self.course))

Arthur Barrett committed
46

Arthur Barrett committed
47
class ApiTest(TestCase):
48

Arthur Barrett committed
49 50 51
    def setUp(self):
        self.client = Client()

52
        # Mocks
Arthur Barrett committed
53
        api.api_enabled = self.mock_api_enabled(True)
54

Arthur Barrett committed
55 56 57
        # Create two accounts
        self.password = 'abc'
        self.student = User.objects.create_user('student', 'student@test.com', self.password)
Arthur Barrett committed
58
        self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
Arthur Barrett committed
59 60 61
        self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
        self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
        self.note = {
Arthur Barrett committed
62 63 64 65 66 67 68 69 70 71
            'user': self.student,
            'course_id': self.course_id,
            'uri': '/',
            'text': 'foo',
            'quote': 'bar',
            'range_start': 0,
            'range_start_offset': 0,
            'range_end': 100,
            'range_end_offset': 0,
            'tags': 'a,b,c'
Arthur Barrett committed
72 73
        }

74 75 76
        # Make sure no note with this ID ever exists for testing purposes
        self.NOTE_ID_DOES_NOT_EXIST = 99999

Arthur Barrett committed
77 78
    def mock_api_enabled(self, is_enabled):
        return (lambda request, course_id: is_enabled)
Arthur Barrett committed
79

Arthur Barrett committed
80 81 82
    def login(self, as_student=None):
        username = None
        password = self.password
Arthur Barrett committed
83

Arthur Barrett committed
84 85 86 87 88 89 90 91
        if as_student is None:
            username = self.student.username
        else:
            username = as_student.username

        self.client.login(username=username, password=password)

    def url(self, name, args={}):
Arthur Barrett committed
92
        args.update({'course_id': self.course_id})
Arthur Barrett committed
93 94 95 96 97 98 99 100 101
        return reverse(name, kwargs=args)

    def create_notes(self, num_notes, create=True):
        notes = []
        for n in range(num_notes):
            note = models.Note(**self.note)
            if create:
                note.save()
            notes.append(note)
Arthur Barrett committed
102 103 104 105 106 107
        return notes

    def test_root(self):
        self.login()

        resp = self.client.get(self.url('notes_api_root'))
Arthur Barrett committed
108
        self.assertEqual(resp.status_code, 200)
Arthur Barrett committed
109 110 111 112
        self.assertNotEqual(resp.content, '')

        content = json.loads(resp.content)

Arthur Barrett committed
113
        self.assertEqual(set(('name', 'version')), set(content.keys()))
Arthur Barrett committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127
        self.assertIsInstance(content['version'], int)
        self.assertEqual(content['name'], 'Notes API')

    def test_index_empty(self):
        self.login()

        resp = self.client.get(self.url('notes_api_notes'))
        self.assertEqual(resp.status_code, 200)
        self.assertNotEqual(resp.content, '')

        content = json.loads(resp.content)
        self.assertEqual(len(content), 0)

    def test_index_with_notes(self):
Arthur Barrett committed
128
        num_notes = 3
Arthur Barrett committed
129 130 131 132 133 134 135 136
        self.login()
        self.create_notes(num_notes)

        resp = self.client.get(self.url('notes_api_notes'))
        self.assertEqual(resp.status_code, 200)
        self.assertNotEqual(resp.content, '')

        content = json.loads(resp.content)
Arthur Barrett committed
137
        self.assertIsInstance(content, list)
Arthur Barrett committed
138 139 140 141 142
        self.assertEqual(len(content), num_notes)

    def test_index_max_notes(self):
        self.login()

Arthur Barrett committed
143
        MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT')
Arthur Barrett committed
144 145
        num_notes = MAX_LIMIT + 1
        self.create_notes(num_notes)
146

Arthur Barrett committed
147 148 149
        resp = self.client.get(self.url('notes_api_notes'))
        self.assertEqual(resp.status_code, 200)
        self.assertNotEqual(resp.content, '')
150

Arthur Barrett committed
151
        content = json.loads(resp.content)
Arthur Barrett committed
152
        self.assertIsInstance(content, list)
Arthur Barrett committed
153
        self.assertEqual(len(content), MAX_LIMIT)
Arthur Barrett committed
154 155 156 157 158 159 160 161 162

    def test_create_note(self):
        self.login()

        notes = self.create_notes(1)
        self.assertEqual(len(notes), 1)

        note_dict = notes[0].as_dict()
        excluded_fields = ['id', 'user_id', 'created', 'updated']
Arthur Barrett committed
163
        note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
Arthur Barrett committed
164

Arthur Barrett committed
165 166 167 168
        resp = self.client.post(self.url('notes_api_notes'),
                                json.dumps(note),
                                content_type='application/json',
                                HTTP_X_REQUESTED_WITH='XMLHttpRequest')
Arthur Barrett committed
169 170 171 172 173 174 175 176

        self.assertEqual(resp.status_code, 303)
        self.assertEqual(len(resp.content), 0)

    def test_create_empty_notes(self):
        self.login()

        for empty_test in [None, [], '']:
Arthur Barrett committed
177 178 179 180
            resp = self.client.post(self.url('notes_api_notes'),
                                    json.dumps(empty_test),
                                    content_type='application/json',
                                    HTTP_X_REQUESTED_WITH='XMLHttpRequest')
Arthur Barrett committed
181 182 183 184 185 186 187 188 189 190
            self.assertEqual(resp.status_code, 500)

    def test_create_note_missing_ranges(self):
        self.login()

        notes = self.create_notes(1)
        self.assertEqual(len(notes), 1)
        note_dict = notes[0].as_dict()

        excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges']
Arthur Barrett committed
191
        note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
Arthur Barrett committed
192

Arthur Barrett committed
193 194 195 196
        resp = self.client.post(self.url('notes_api_notes'),
                                json.dumps(note),
                                content_type='application/json',
                                HTTP_X_REQUESTED_WITH='XMLHttpRequest')
Arthur Barrett committed
197 198
        self.assertEqual(resp.status_code, 500)

Arthur Barrett committed
199
    def test_read_note(self):
Arthur Barrett committed
200 201 202 203 204 205
        self.login()

        notes = self.create_notes(3)
        self.assertEqual(len(notes), 3)

        for note in notes:
206
            resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
Arthur Barrett committed
207 208 209 210
            self.assertEqual(resp.status_code, 200)
            self.assertNotEqual(resp.content, '')

            content = json.loads(resp.content)
211
            self.assertEqual(content['id'], note.pk)
Arthur Barrett committed
212 213 214 215 216
            self.assertEqual(content['user_id'], note.user_id)

    def test_note_doesnt_exist_to_read(self):
        self.login()
        resp = self.client.get(self.url('notes_api_note', {
217
            'note_id': self.NOTE_ID_DOES_NOT_EXIST
Arthur Barrett committed
218 219 220 221 222 223 224 225 226 227 228
        }))
        self.assertEqual(resp.status_code, 404)
        self.assertEqual(resp.content, '')

    def test_student_doesnt_have_permission_to_read_note(self):
        notes = self.create_notes(1)
        self.assertEqual(len(notes), 1)
        note = notes[0]

        # set the student id to a different student (not the one that created the notes)
        self.login(as_student=self.student2)
229
        resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
Arthur Barrett committed
230 231 232
        self.assertEqual(resp.status_code, 403)
        self.assertEqual(resp.content, '')

233 234 235 236 237 238 239 240
    def test_delete_note(self):
        self.login()

        notes = self.create_notes(1)
        self.assertEqual(len(notes), 1)
        note = notes[0]

        resp = self.client.delete(self.url('notes_api_note', {
241
            'note_id': note.pk
242 243 244 245 246
        }))
        self.assertEqual(resp.status_code, 204)
        self.assertEqual(resp.content, '')

        with self.assertRaises(models.Note.DoesNotExist):
247
            models.Note.objects.get(pk=note.pk)
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264

    def test_note_does_not_exist_to_delete(self):
        self.login()

        resp = self.client.delete(self.url('notes_api_note', {
            'note_id': self.NOTE_ID_DOES_NOT_EXIST
        }))
        self.assertEqual(resp.status_code, 404)
        self.assertEqual(resp.content, '')

    def test_student_doesnt_have_permission_to_delete_note(self):
        notes = self.create_notes(1)
        self.assertEqual(len(notes), 1)
        note = notes[0]

        self.login(as_student=self.student2)
        resp = self.client.delete(self.url('notes_api_note', {
265
            'note_id': note.pk
266 267 268 269 270
        }))
        self.assertEqual(resp.status_code, 403)
        self.assertEqual(resp.content, '')

        try:
271
            models.Note.objects.get(pk=note.pk)
272 273 274
        except models.Note.DoesNotExist:
            self.fail('note should exist and not be deleted because the student does not have permission to do so')

Arthur Barrett committed
275
    def test_update_note(self):
276 277 278 279 280 281 282 283 284 285
        notes = self.create_notes(1)
        note = notes[0]

        updated_dict = note.as_dict()
        updated_dict.update({
            'text': 'itchy and scratchy',
            'tags': ['simpsons', 'cartoons', 'animation']
        })

        self.login()
286
        resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}),
287 288 289 290 291 292
                               json.dumps(updated_dict),
                               content_type='application/json',
                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        self.assertEqual(resp.status_code, 303)
        self.assertEqual(resp.content, '')

293
        actual = models.Note.objects.get(pk=note.pk)
294 295 296
        actual_dict = actual.as_dict()
        for field in ['text', 'tags']:
            self.assertEqual(actual_dict[field], updated_dict[field])
Arthur Barrett committed
297

Arthur Barrett committed
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    def test_search_note_params(self):
        self.login()

        total = 3
        notes = self.create_notes(total)
        invalid_uri = ''.join([note.uri for note in notes])

        tests = [{'limit': 0, 'offset': 0, 'expected_rows': total},
                 {'limit': 0, 'offset': 2, 'expected_rows': total - 2},
                 {'limit': 0, 'offset': total, 'expected_rows': 0},
                 {'limit': 1, 'offset': 0, 'expected_rows': 1},
                 {'limit': 2, 'offset': 0, 'expected_rows': 2},
                 {'limit': total, 'offset': 2, 'expected_rows': 1},
                 {'limit': total, 'offset': total, 'expected_rows': 0},
                 {'limit': total + 1, 'offset': total + 1, 'expected_rows': 0},
                 {'limit': total + 1, 'offset': 0, 'expected_rows': total},
                 {'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}]

        for test in tests:
            params = dict([(k, str(test[k]))
                          for k in ('limit', 'offset', 'uri')
                          if k in test])
            resp = self.client.get(self.url('notes_api_search'),
                                   params,
                                   content_type='application/json',
                                   HTTP_X_REQUESTED_WITH='XMLHttpRequest')

            self.assertEqual(resp.status_code, 200)
            self.assertNotEqual(resp.content, '')

            content = json.loads(resp.content)

            for expected_key in ('total', 'rows'):
                self.assertTrue(expected_key in content)

            if 'expected_total' in test:
                self.assertEqual(content['total'], test['expected_total'])
            else:
                self.assertEqual(content['total'], total)

            self.assertEqual(len(content['rows']), test['expected_rows'])

            for row in content['rows']:
                self.assertTrue('id' in row)
Arthur Barrett committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362


class NoteTest(TestCase):
    def setUp(self):
        self.password = 'abc'
        self.student = User.objects.create_user('student', 'student@test.com', self.password)
        self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
        self.note = {
            'user': self.student,
            'course_id': self.course_id,
            'uri': '/',
            'text': 'foo',
            'quote': 'bar',
            'range_start': 0,
            'range_start_offset': 0,
            'range_end': 100,
            'range_end_offset': 0,
            'tags': 'a,b,c'
        }

    def test_clean_valid_note(self):
363
        reference_note = models.Note(**self.note)
Arthur Barrett committed
364 365
        body = reference_note.as_dict()

366
        note = models.Note(course_id=self.course_id, user=self.student)
Arthur Barrett committed
367 368 369 370 371 372 373 374 375 376 377 378 379 380
        try:
            note.clean(json.dumps(body))
            self.assertEqual(note.uri, body['uri'])
            self.assertEqual(note.text, body['text'])
            self.assertEqual(note.quote, body['quote'])
            self.assertEqual(note.range_start, body['ranges'][0]['start'])
            self.assertEqual(note.range_start_offset, body['ranges'][0]['startOffset'])
            self.assertEqual(note.range_end, body['ranges'][0]['end'])
            self.assertEqual(note.range_end_offset, body['ranges'][0]['endOffset'])
            self.assertEqual(note.tags, ','.join(body['tags']))
        except ValidationError:
            self.fail('a valid note should not raise an exception')

    def test_clean_invalid_note(self):
381
        note = models.Note(course_id=self.course_id, user=self.student)
Arthur Barrett committed
382 383 384 385 386 387 388 389 390 391 392 393
        for empty_type in (None, '', 0, []):
            with self.assertRaises(ValidationError):
                note.clean(None)

        with self.assertRaises(ValidationError):
            note.clean(json.dumps({
                'text': 'foo',
                'quote': 'bar',
                'ranges': [{} for i in range(10)]  # too many ranges
            }))

    def test_as_dict(self):
394
        note = models.Note(course_id=self.course_id, user=self.student)
Arthur Barrett committed
395 396 397 398
        d = note.as_dict()
        self.assertNotIsInstance(d, basestring)
        self.assertEqual(d['user_id'], self.student.id)
        self.assertTrue('course_id' not in d)