• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / SHARE / 13726310894

07 Mar 2025 05:48PM UTC coverage: 82.671% (-9.1%) from 91.752%
13726310894

Pull #848

github

web-flow
Merge be7e19c4d into cf198c88f
Pull Request #848: [chore] update github actions workflow

16382 of 19816 relevant lines covered (82.67%)

1.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.11
/tests/api/test_validator.py
1
import json
2✔
2
import pytest
2✔
3
import requests
2✔
4
from unittest import mock
2✔
5

6
from tests import factories
2✔
7

8
invalid_work = {
2✔
9
    'data': {
10
        'type': 'NormalizedData',
11
        'attributes': {
12
            'suid': 'whatever',
13
            'data': {
14
                '@graph': [
15
                    {
16
                        '@type': 'InvalidWorkType',
17
                        'title': 'Abstract Work',
18
                        '@id': '_:1bf1bf86939d433d96402090c33251d6',
19
                    }
20
                ]
21
            }
22
        }
23
    }
24
}
25

26
invalid_proxy_work = {
2✔
27
    'data': {
28
        'type': 'NormalizedData',
29
        'attributes': {
30
            'suid': 'whatever',
31
            'data': {
32
                '@graph': [
33
                    {
34
                        '@type': 'AbstractCreativeWork',
35
                        'title': 'Abstract Work',
36
                        '@id': '_:1bf1bf86939d433d96402090c33251d6',
37
                    }
38
                ]
39
            }
40
        }
41
    }
42
}
43

44
valid_work_valid_agent = {
2✔
45
    'data': {
46
        'type': 'NormalizedData',
47
        'attributes': {
48
            'suid': 'whatever',
49
            'data': {
50
                '@graph': [
51
                    {
52
                        '@type': 'Organization',
53
                        '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
54
                        'name': 'Publishing Group'
55
                    },
56
                    {
57
                        'agent': {
58
                            '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
59
                            '@type': 'Organization'
60
                        },
61
                        'creative_work': {
62
                            '@id': '_:1bf1bf86939d433d96402090c33251d6',
63
                            '@type': 'Article'
64
                        },
65
                        '@id': '_:76c520ec6fe54d5097c2413886ff027e',
66
                        '@type': 'Publisher'
67
                    },
68
                    {
69
                        '@type': 'Article',
70
                        'title': 'Published article',
71
                        'related_agents': [{
72
                            '@id': '_:76c520ec6fe54d5097c2413886ff027e',
73
                            '@type': 'Publisher'
74
                        }],
75
                        '@id': '_:1bf1bf86939d433d96402090c33251d6',
76
                    }
77
                ]
78
            }
79
        }
80
    }
81
}
82

83
valid_work_invalid_agent = {
2✔
84
    'data': {
85
        'type': 'NormalizedData',
86
        'attributes': {
87
            'suid': 'whatever',
88
            'data': {
89
                '@graph': [
90
                    {
91
                        '@type': 'Organization',
92
                        '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
93
                        'name': 'Publishing Group'
94
                    },
95
                    {
96
                        'agent': {
97
                            '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
98
                            '@type': 'AbstractAgent',
99
                        },
100
                        'creative_work': {
101
                            '@id': '_:1bf1bf86939d433d96402090c33251d6',
102
                            '@type': 'Article'
103
                        },
104
                        '@id': '_:76c520ec6fe54d5097c2413886ff027e',
105
                        '@type': 'Publisher'
106
                    },
107
                    {
108
                        '@type': 'Article',
109
                        'title': 'Publisher',
110
                        'related_agents': [{
111
                            '@id': '_:76c520ec6fe54d5097c2413886ff027e',
112
                            '@type': 'Organization'
113
                        }],
114
                        '@id': '_:1bf1bf86939d433d96402090c33251d6',
115
                    }
116
                ]
117
            }
118
        }
119
    }
120
}
121

122
valid_work_invalid_agent_field = {
2✔
123
    'data': {
124
        'type': 'NormalizedData',
125
        'attributes': {
126
            'suid': 'whatever',
127
            'data': {
128
                '@graph': [
129
                    {
130
                        '@type': 'Organization',
131
                        '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
132
                        'name': 'Publishing Group',
133
                        'family_name': 'Person Field'
134
                    },
135
                    {
136
                        'agent': {
137
                            '@id': '_:697f809c05ea4a6fba7cff3beb1ad316',
138
                            '@type': 'Organization'
139
                        },
140
                        'creative_work': {
141
                            '@id': '_:1bf1bf86939d433d96402090c33251d6',
142
                            '@type': 'Article'
143
                        },
144
                        '@id': '_:76c520ec6fe54d5097c2413886ff027e',
145
                        '@type': 'Publisher'
146
                    },
147
                    {
148
                        '@type': 'Article',
149
                        'title': 'Published',
150
                        'publishers': [{
151
                            '@id': '_:76c520ec6fe54d5097c2413886ff027e',
152
                            '@type': 'Publisher'
153
                        }],
154
                        '@id': '_:1bf1bf86939d433d96402090c33251d6',
155
                    }
156
                ]
157
            }
158
        }
159
    }
160
}
161

162

163
class Response:
2✔
164
    def __init__(self, status_code=200, json=None, keys=None):
2✔
165
        self.status_code = status_code
2✔
166
        self._json = json or {}
2✔
167
        self._keys = keys
2✔
168

169
    def json(self):
2✔
170
        return self._json
2✔
171

172
    def __eq__(self, other):
2✔
173
        assert other.status_code == self.status_code
2✔
174

175
        if self._keys:
2✔
176
            assert set(other.json().keys()) == self._keys
2✔
177
        else:
178
            assert other.json() == self.json()
2✔
179

180
        return True
2✔
181

182

183
class TestValidator:
2✔
184

185
    POST_CASES = [{
2✔
186
        'out': Response(400, json={
187
            'errors': [{
188
                'code': 'required',
189
                'detail': 'This field is required.',
190
                'source': {'pointer': '/data/attributes/data'},
191
                'status': '400'
192
            }]
193
        }),
194
        'in': requests.Request('POST', json={
195
            'data': {
196
                'type': 'NormalizedData',
197
                'attributes': {}
198
            }
199
        })
200
    }, {
201
        'out': Response(400, json={
202
            'errors': [{
203
                'code': 'parse_error',
204
                'detail': 'JSON parse error - Expecting value: line 1 column 1 (char 0)',
205
                'source': {'pointer': '/data'},
206
                'status': '400'
207
            }]
208
        }),
209
        'in': requests.Request('POST', data='<html!>')
210
    }, {
211
        'out': Response(400, json={
212
            'errors': [{
213
                'code': 'invalid',
214
                'detail': '@graph may not be empty',
215
                'source': {'pointer': '/data/attributes/data'},
216
                'status': '400'
217
            }]
218
        }),
219
        'in': requests.Request('POST', json={
220
            'data': {
221
                'type': 'NormalizedData',
222
                'attributes': {
223
                    'data': {
224
                        '@graph': []
225
                    }
226
                }
227
            }
228
        })
229
    }, {
230
        'out': Response(202, keys={'data'}),
231
        'in': requests.Request('POST', json={
232
            'data': {
233
                'type': 'NormalizedData',
234
                'attributes': {
235
                    'suid': 'jim',
236
                    'data': {
237
                        '@graph': [{
238
                            '@id': '_:100',
239
                            '@type': 'Person',
240
                            'given_name': 'Jim',
241
                        }]
242
                    }
243
                }
244
            }
245
        })
246
    }, {
247
        'out': Response(400, json={
248
            'errors': [{
249
                'code': 'invalid',
250
                'detail': "'@id' is a required property at /@graph/0",
251
                'source': {'pointer': '/data/attributes/data'},
252
                'status': '400'
253
            }]
254
        }),
255
        'in': requests.Request('POST', json={
256
            'data': {
257
                'type': 'NormalizedData',
258
                'attributes': {
259
                    'suid': 'jim',
260
                    'data': {
261
                        '@graph': [{
262
                            '@type': 'Person',
263
                            'given_name': 'Jim',
264
                        }]
265
                    }
266
                }
267
            }
268
        })
269
    }, {
270
        'out': Response(400, json={
271
            'errors': [{
272
                'code': 'invalid',
273
                'detail': "'AbstractAgent' is not one of ["
274
                          "'AGENT', 'Agent', 'CONSORTIUM', 'Consortium', "
275
                          "'DEPARTMENT', 'Department', "
276
                          "'INSTITUTION', 'Institution', 'ORGANIZATION', "
277
                          "'Organization', 'PERSON', 'Person', 'agent', "
278
                          "'consortium', 'department', 'institution', 'organization', 'person'"
279
                          "] at /@graph/1/agent/@type",
280
                'source': {'pointer': '/data/attributes/data'},
281
                'status': '400'
282
            }]
283
        }),
284
        'in': requests.Request('POST', json=valid_work_invalid_agent)
285
    }, {
286
        'out': Response(400, json={
287
            'errors': [{
288
                'code': 'invalid',
289
                'detail': "'AbstractCreativeWork' is not a valid type",
290
                'source': {'pointer': '/data/attributes/data'},
291
                'status': '400'
292
            }]
293
        }),
294
        'in': requests.Request('POST', json=invalid_proxy_work)
295
    }, {
296
        'out': Response(400, json={
297
            'errors': [{
298
                'code': 'invalid',
299
                'detail': "'InvalidWorkType' is not a valid type",
300
                'source': {'pointer': '/data/attributes/data'},
301
                'status': '400'
302
            }]
303
        }),
304
        'in': requests.Request('POST', json=invalid_work)
305
    }, {
306
        'out': Response(202, keys={'data'}),
307
        'in': requests.Request('POST', json=valid_work_valid_agent)
308
    }, {
309
        'out': Response(400, json={
310
            'errors': [{
311
                'code': 'invalid',
312
                'detail': "Additional properties are not allowed ('publishers' was unexpected) at /@graph/2",
313
                'source': {'pointer': '/data/attributes/data'},
314
                'status': '400'
315
            }]
316
        }),
317
        'in': requests.Request('POST', json=valid_work_invalid_agent_field)
318
    }, {
319
        # does not break because the raw information is not processed
320
        'out': Response(202, keys={'data'}),
321
        'in': requests.Request('POST', json={
322
            'data': {
323
                'type': 'NormalizedData',
324
                'attributes': {
325
                    'raw': {'type': 'RawData', 'id': 'invalid_id'},
326
                    'suid': 'whatever',
327
                    'data': valid_work_valid_agent['data']['attributes']['data']
328
                }
329
            }
330
        })
331
    }, {
332
        # does not break because the task information is not processed
333
        'out': Response(202, keys={'data'}),
334
        'in': requests.Request('POST', json={
335
            'data': {
336
                'type': 'NormalizedData',
337
                'attributes': {
338
                    'tasks': ['invalid_task'],
339
                    'suid': 'whatever',
340
                    'data': valid_work_valid_agent['data']['attributes']['data']
341
                }
342
            }
343
        })
344
    }]
345

346
    @pytest.mark.django_db
2✔
347
    @pytest.mark.parametrize('_request, response', [(case['in'], case['out']) for case in POST_CASES])
2✔
348
    def test_validator(self, trusted_user, client, _request, response):
2✔
349
        args, kwargs = (), {'content_type': 'application/vnd.api+json'}
2✔
350

351
        if _request.data:
2✔
352
            kwargs['data'] = _request.data
2✔
353
        elif _request.json is not None:
2✔
354
            kwargs['data'] = json.dumps(_request.json)
2✔
355

356
        kwargs['HTTP_AUTHORIZATION'] = 'Bearer {}'.format(trusted_user.oauth2_provider_accesstoken.first())
2✔
357

358
        with mock.patch('api.normalizeddata.views.digestive_tract') as mock_digestive_tract:
2✔
359
            mock_digestive_tract.swallow__sharev2_legacy.return_value = '123'
2✔
360
            assert response == client.post('/api/v2/normalizeddata/', *args, **kwargs)
2✔
361

362
    @pytest.mark.django_db
2✔
363
    def test_robot_validator(self, robot_user, raw_data_id, client):
2✔
364
        args, kwargs = (), {'content_type': 'application/vnd.api+json'}
2✔
365

366
        normalizer_task = factories.CeleryTaskResultFactory()
2✔
367

368
        _request = requests.Request('POST', json={
2✔
369
            'data': {
370
                'type': 'NormalizedData',
371
                'attributes': {
372
                    'tasks': [normalizer_task.id],
373
                    'raw': {'type': 'RawData', 'id': raw_data_id},
374
                    'suid': 'whatever',
375
                    'data': valid_work_valid_agent['data']['attributes']['data']
376
                }
377
            }
378
        })
379

380
        if _request.data:
2✔
381
            kwargs['data'] = _request.data
×
382
        elif _request.json is not None:
2✔
383
            kwargs['data'] = json.dumps(_request.json)
2✔
384

385
        kwargs['HTTP_AUTHORIZATION'] = 'Bearer {}'.format(robot_user.oauth2_provider_accesstoken.first())
2✔
386

387
        with mock.patch('api.normalizeddata.views.digestive_tract') as mock_digestive_tract:
2✔
388
            mock_digestive_tract.swallow__sharev2_legacy.return_value = '123'
2✔
389
            response = client.post('/api/v2/normalizeddata/', *args, **kwargs)
2✔
390

391
        assert response.status_code == 202
2✔
392
        assert response.json()['data']['type'] == 'NormalizedData'
2✔
393
        assert response.json()['data']['attributes'].keys() == {'task'}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc