test_validation.py 13.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Test special validators, e.g. for time overlap."""
# pylint: disable=redefined-outer-name

import pytest


@pytest.fixture
def lecture(app):
    """Create a lecture, return its id."""
    with app.admin():
        lecture = {
            'title': "Time and Space",
            'department': "itet",
            'year': 2,
        }
        return app.client.post('lectures',
                               data=lecture,
                               assert_status=201)['_id']


def test_start_time_before_end(app, lecture):
    """Test that any start time must come before end time."""
    correct = {
        'lecture': lecture,
        'spots': 10,
        'datetimes': [{
            'start': '2019-01-09T10:00:00Z',
            'end': '2019-01-09T13:00:00Z',
        }],
    }

    wrong = {
        'lecture': lecture,
        'spots': 10,
        'datetimes': [{
            'start': '2019-02-09T13:00:00Z',
            'end': '2019-02-09T10:00:00Z',
        }],
    }

    with app.admin():
        app.client.post('courses', data=correct, assert_status=201)
        app.client.post('courses', data=wrong, assert_status=422)


@pytest.fixture
def courses(app, lecture):
    """Return data for courses that overlap. (And a control course)"""
    with app.admin():

        same_time = {
            'lecture': lecture,
            'spots': 10,
            'datetimes': [{
                'start': '2019-01-09T10:00:00Z',
                'end': '2019-01-09T13:00:00Z',
            }]
        }
        first = {'room': 'ETZ E 6'}
        first.update(same_time)
        first_id = app.client.post('courses',
                                   data=first,
                                   assert_status=201)['_id']
        second = {'room': 'ETZ E 8'}
        second.update(same_time)
        second_id = app.client.post('courses',
                                    data=second,
                                    assert_status=201)['_id']

        # Now a third course without overlap
        control = {
            'lecture': lecture,
            'spots': 10,
            'datetimes': [{
                'start': '2019-02-10T10:00:00Z',
                'end': '2019-02-10T13:00:00Z',
            }],
            'room': 'ETZ E 9'
        }
        control_id = app.client.post('courses',
                                     data=control,
                                     assert_status=201)['_id']

        return (first_id, second_id, control_id)


# The base timeslot will be from 10:00 to 13:00,
@pytest.mark.parametrize("start_hour,end_hour", [
    (7, 9),    # before
    (7, 10),   # direcly before, but no overlap
    (14, 17),  # after
    (13, 17),  # directly after
])
def test_no_timeslot_overlap(app, lecture, start_hour, end_hour):
    """The timeslots for a given course must not overlap."""
    with app.admin():
        data = {
            'lecture': lecture,
            'room': 'someroom',
            'spots': 10,
            'datetimes': [{
                'start': '2015-06-05T10:00:00Z',
                'end': '2015-06-05T13:00:00Z',
            }, {
                'start': '2015-06-05T%s:00:00Z' % start_hour,
                'end': '2015-06-05T%s:00:00Z' % end_hour,
            }],
        }
        app.client.post('courses', data=data, assert_status=201)


# The base timeslot will be from 10:00 to 13:00,
@pytest.mark.parametrize("start_hour,end_hour", [
    (9, 11),   # end overlaps with base
    (12, 14),  # start overlaps
    (9, 14),   # contains other timeslot
    (11, 12),  # contained by other timeslot
    (10, 13),  # same timeslot
])
def test_timeslot_overlap(app, lecture, start_hour, end_hour):
    """Test different ways of overlapping timeslots."""
    with app.admin():
        data = {
            'lecture': lecture,
            'room': 'someroom',
            'spots': 10,
            'datetimes': [{
                'start': '2015-06-05T10:00:00Z',
                'end': '2015-06-05T13:00:00Z',
            }, {
                'start': '2015-06-05T%s:00:00Z' % start_hour,
                'end': '2015-06-05T%s:00:00Z' % end_hour,
            }],
        }
        app.client.post('courses', data=data, assert_status=422)


def test_no_double_booking_of_room(app, lecture):
    """Any given room can not be assigned to two courses at the same time."""
    with app.admin():
        room = 'LEE E 12'
        first = {
            'lecture': lecture,
            'room': room,
            'spots': 10,
            'datetimes': [{
                'start': '2018-12-06T10:00:00Z',
                'end': '2018-12-06T13:00:00Z',
            }],
        }

        second = {
            'lecture': lecture,
            'room': room,
            'spots': 20,
            'datetimes': [{
                # Contains the first course
                'start': '2018-12-06T9:00:00Z',
                'end': '2018-12-06T15:00:00Z',
            }, {
                'start': '2018-12-10T13:00:00Z',
                'end': '2018-12-10T14:00:00Z',
            }, {
                'start': '2018-12-10T15:00:00Z',
                'end': '2018-12-10T16:00:00Z',
            }],
        }

        # Posting only one is ok, but the second one will fail
        app.client.post('courses', data=first, assert_status=201)
        app.client.post('courses', data=second, assert_status=422)

        # Posting the second course with a different room is ok
        second['room'] = 'other %s' % room
        response = app.client.post('courses', data=second, assert_status=201)

177
        # Patching without overlap is ok
178
179
180
181
182
183
184
        url = 'courses/%s' % response['_id']
        separate_room = {'room': 'another different %s' % room}
        response = app.client.patch(url, data=separate_room,
                                    headers={'If-Match': response['_etag']},
                                    assert_status=200)


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def test_unique_assistant(app, lecture):
    """Test that an assistant cannot be set for overlapping courses."""
    with app.admin():
        # Create dummy assistants
        assistant = str(app.data.driver.db['assistants'].insert({}))
        other_assistant = str(app.data.driver.db['assistants'].insert({}))
        another_assistant = str(app.data.driver.db['assistants'].insert({}))

        first = {
            'lecture': lecture,
            'assistant': assistant,
            'spots': 10,
            'datetimes': [{
                'start': '2018-12-06T10:00:00Z',
                'end': '2018-12-06T13:00:00Z',
            }],
        }

        second = {
            'lecture': lecture,
            'assistant': assistant,
            'spots': 20,
            'datetimes': [{
                # Contains the first course
                'start': '2018-12-06T9:00:00Z',
                'end': '2018-12-06T15:00:00Z',
            }, {
                'start': '2018-12-10T13:00:00Z',
                'end': '2018-12-10T14:00:00Z',
            }, {
                'start': '2018-12-10T15:00:00Z',
                'end': '2018-12-10T16:00:00Z',
            }],
        }

        # Posting only one is ok, but the second one will fail
        app.client.post('courses', data=first, assert_status=201)
        app.client.post('courses', data=second, assert_status=422)

        # Posting the second course with a different assistant is ok
        second['assistant'] = other_assistant
        response = app.client.post('courses', data=second, assert_status=201)

        # Patching without overlap is ok
        url = 'courses/%s' % response['_id']
        separate_room = {'assistant': another_assistant}
        response = app.client.patch(url, data=separate_room,
                                    headers={'If-Match': response['_etag']},
                                    assert_status=200)


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
@pytest.fixture
def patch_courses(app, lecture):
    """Courses that nearly overlap."""
    first = {
        'lecture': lecture,
        'room': 'ETZ E 1',
        'spots': 10,
        'datetimes': [{
            'start': '2018-12-06T10:00:00Z',
            'end': '2018-12-06T13:00:00Z',
        }],
    }
    # Second course has same room but different time
    second = {
        'lecture': lecture,
        'room': 'ETZ E 1',
        'spots': 10,
        'datetimes': [{
            'start': '2018-11-06T10:00:00Z',
            'end': '2018-11-06T13:00:00Z',
        }],
    }
    # Third course has different room but same time
    third = {
        'lecture': lecture,
        'room': 'ETZ E 2',
        'spots': 10,
        'datetimes': [{
            'start': '2018-12-06T10:00:00Z',
            'end': '2018-12-06T13:00:00Z',
        }],
    }
    # Control course has different room and time
    control = {
        'lecture': lecture,
        'room': 'ETZ E 3',
        'spots': 10,
        'datetimes': [{
            'start': '2018-01-06T10:00:00Z',
            'end': '2018-01-06T13:00:00Z',
        }],
    }

    def _create(data):
        with app.admin():
            return app.client.post('courses', data=data, assert_status=201)

    # Return data of created courses
    return tuple(_create(data) for data in (first, second, third, control))


def test_patch_time_different_room(app, patch_courses):
    """If the course have different rooms, patching to same time is ok."""
    with app.admin():
        app.client.patch('courses/%s' % patch_courses[2]['_id'],
                         headers={'If-Match': patch_courses[2]['_etag']},
                         data={'datetimes': patch_courses[0]['datetimes']},
                         assert_status=200)


def test_patch_time_same_room(app, patch_courses):
    """If the course have the same room, patching to same time is not ok."""
    with app.admin():
        app.client.patch('courses/%s' % patch_courses[1]['_id'],
                         headers={'If-Match': patch_courses[1]['_etag']},
                         data={'datetimes': patch_courses[0]['datetimes']},
                         assert_status=422)


def test_patch_room_different_time(app, patch_courses):
    """If the course have different times, patching to same room is ok."""
    with app.admin():
        app.client.patch('courses/%s' % patch_courses[1]['_id'],
                         headers={'If-Match': patch_courses[1]['_etag']},
                         data={'room': patch_courses[0]['room']},
                         assert_status=200)


def test_patch_room_same_time(app, patch_courses):
    """If the course have the same times, patching to same room is not ok."""
    with app.admin():
        app.client.patch('courses/%s' % patch_courses[2]['_id'],
                         headers={'If-Match': patch_courses[2]['_etag']},
                         data={'room': patch_courses[0]['room']},
                         assert_status=422)


def test_patch_both(app, patch_courses):
    """Patching both at the same time causes overlap."""
    with app.admin():
        app.client.patch('courses/%s' % patch_courses[3]['_id'],
                         headers={'If-Match': patch_courses[3]['_etag']},
                         data={'room': patch_courses[0]['room'],
                               'datetimes': patch_courses[0]['datetimes']},
                         assert_status=422)


def test_patch_self_overlap(app, lecture):
    """Patch the course such that it would overlap with its old version."""
    time_1 = {
        'start': '2018-01-06T10:00:00Z',
        'end': '2018-01-06T13:00:00Z',
    }
    time_2 = {
        'start': '2018-01-06T14:00:00Z',
        'end': '2018-01-06T18:00:00Z',
    }
    time_3 = {
        'start': '2018-01-06T18:00:00Z',
        'end': '2018-01-06T19:00:00Z',
    }
    data = {
        'lecture': lecture,
        'room': 'someroom',
        'spots': 10,
        'datetimes': [time_1, time_2],
    }
    overlapping_span = {
        'datetimes': [time_2, time_3],
    }

    with app.admin():
        course = app.client.post('courses', data=data, assert_status=201)
        app.client.patch('courses/%s' % course['_id'],
                         headers={'If-Match': course['_etag']},
                         data=overlapping_span,
                         assert_status=200)


# We can use the same test for selection and signup (same data structure)
@pytest.mark.parametrize('resource', ['signups', 'selections'])
def test_no_overlap_for_user(app, resource, courses):
    """A user can not sign up for two courses that happen at the same time."""
    with app.admin():
        signup = {
            'nethz': 'pablito',
            'course': courses[0]
        }
        parallel = {
            'nethz': 'pablito',
            'course': courses[1]
        }

        control = {
            'nethz': 'anon',
            'course': courses[1]
        }

        # Posting only one is ok, but the second one will fail
        app.client.post(resource, data=signup, assert_status=201)
        app.client.post(resource, data=parallel, assert_status=422)

        # Other users are not influenced
        app.client.post(resource, data=control, assert_status=201)


@pytest.mark.parametrize('resource', ['signups', 'selections'])
def test_no_patch_overlap(app, resource, courses):
    """A user can change the course of a singup/selection to cause overlap."""
    with app.admin():
        signup = {
            'nethz': 'pablito',
            'course': courses[0]
        }
        separate = {
            'nethz': 'pablito',
            'course': courses[2]
        }

        # Both singups can be created since the course do not overlap
        app.client.post(resource, data=signup, assert_status=201)
        response = app.client.post(resource, data=separate, assert_status=201)

        print('User has course', courses[0])
        print('User wants course', courses[1])

        # Patching a signup to an overlapping course does not work
        app.client.patch('%s/%s' % (resource, response['_id']),
                         data={'course': courses[1]},
                         headers={'If-Match': response['_etag']},
                         assert_status=422)