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

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

Arthur Barrett committed
12
import collections
Arthur Barrett committed
13 14 15 16
import json

from . import utils, api, models

Arthur Barrett committed
17

Arthur Barrett committed
18
class UtilsTest(TestCase):
Arthur Barrett committed
19 20
    def setUp(self):
        '''
Arthur Barrett committed
21
        Setup a dummy course-like object with a tabs field that can be
Arthur Barrett committed
22
        accessed via attribute lookup.
Arthur Barrett committed
23
        '''
Arthur Barrett committed
24
        self.course = collections.namedtuple('DummyCourse', ['tabs'])
Arthur Barrett committed
25 26 27 28 29 30 31 32 33 34 35 36 37 38
        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
39 40 41
        self.course.tabs = [{'type': 'foo'},
                            {'name': 'My Notes', 'type': 'notes'},
                            {'type': 'bar'}]
42

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

Arthur Barrett committed
45

Arthur Barrett committed
46
class ApiTest(TestCase):
47

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

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

Arthur Barrett committed
54 55 56
        # Create two accounts
        self.password = 'abc'
        self.student = User.objects.create_user('student', 'student@test.com', self.password)
Arthur Barrett committed
57
        self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
Arthur Barrett committed
58
        self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
59
        self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
Arthur Barrett committed
60
        self.note = {
Arthur Barrett committed
61
            'user': self.student,
62
            'course_id': self.course_key,
Arthur Barrett committed
63 64 65 66 67 68 69 70
            '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
71 72
        }

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

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

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

Arthur Barrett committed
83 84 85 86 87 88 89 90
        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={}):
91
        args.update({'course_id': self.course_key.to_deprecated_string()})
Arthur Barrett committed
92 93 94 95 96 97 98 99 100
        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
101 102 103 104 105 106
        return notes

    def test_root(self):
        self.login()

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

        content = json.loads(resp.content)

Arthur Barrett committed
112
        self.assertEqual(set(('name', 'version')), set(content.keys()))
Arthur Barrett committed
113 114 115 116 117 118 119 120 121 122 123 124 125 126
        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
127
        num_notes = 3
Arthur Barrett committed
128 129 130 131 132 133 134 135
        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
136
        self.assertIsInstance(content, list)
Arthur Barrett committed
137 138 139 140 141
        self.assertEqual(len(content), num_notes)

    def test_index_max_notes(self):
        self.login()

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

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

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

    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
162
        note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
Arthur Barrett committed
163

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

        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
176 177 178 179
            resp = self.client.post(self.url('notes_api_notes'),
                                    json.dumps(empty_test),
                                    content_type='application/json',
                                    HTTP_X_REQUESTED_WITH='XMLHttpRequest')
180
            self.assertEqual(resp.status_code, 400)
Arthur Barrett committed
181 182 183 184 185 186 187 188 189

    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
190
        note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields])
Arthur Barrett committed
191

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

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

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

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

            content = json.loads(resp.content)
210
            self.assertEqual(content['id'], note.pk)
Arthur Barrett committed
211 212 213 214 215
            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', {
216
            'note_id': self.NOTE_ID_DOES_NOT_EXIST
Arthur Barrett committed
217 218 219 220 221 222 223 224 225 226 227
        }))
        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)
228
        resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk}))
Arthur Barrett committed
229 230 231
        self.assertEqual(resp.status_code, 403)
        self.assertEqual(resp.content, '')

232 233 234 235 236 237 238 239
    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', {
240
            'note_id': note.pk
241 242 243 244 245
        }))
        self.assertEqual(resp.status_code, 204)
        self.assertEqual(resp.content, '')

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

    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', {
264
            'note_id': note.pk
265 266 267 268 269
        }))
        self.assertEqual(resp.status_code, 403)
        self.assertEqual(resp.content, '')

        try:
270
            models.Note.objects.get(pk=note.pk)
271 272 273
        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
274
    def test_update_note(self):
275 276 277 278 279 280 281 282 283 284
        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()
285
        resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}),
286 287 288 289 290 291
                               json.dumps(updated_dict),
                               content_type='application/json',
                               HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        self.assertEqual(resp.status_code, 303)
        self.assertEqual(resp.content, '')

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

Arthur Barrett committed
297 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
    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
341 342 343 344 345 346


class NoteTest(TestCase):
    def setUp(self):
        self.password = 'abc'
        self.student = User.objects.create_user('student', 'student@test.com', self.password)
347
        self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
Arthur Barrett committed
348 349
        self.note = {
            'user': self.student,
350
            'course_id': self.course_key,
Arthur Barrett committed
351 352 353 354 355 356 357 358 359 360 361
            '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):
362
        reference_note = models.Note(**self.note)
Arthur Barrett committed
363 364
        body = reference_note.as_dict()

365
        note = models.Note(course_id=self.course_key, user=self.student)
Arthur Barrett committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379
        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):
380
        note = models.Note(course_id=self.course_key, user=self.student)
Arthur Barrett committed
381 382 383 384 385 386 387 388 389 390 391 392
        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):
393
        note = models.Note(course_id=self.course_key, user=self.student)
Arthur Barrett committed
394 395 396 397
        d = note.as_dict()
        self.assertNotIsInstance(d, basestring)
        self.assertEqual(d['user_id'], self.student.id)
        self.assertTrue('course_id' not in d)