forked from snok/container-retention-policy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain_tests.py
306 lines (245 loc) · 11.1 KB
/
main_tests.py
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
177
178
179
180
181
182
183
184
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
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
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from functools import partial
from unittest.mock import AsyncMock, Mock
import pytest as pytest
from dateparser import parse
from httpx import AsyncClient
import main
from main import (
AccountType,
ImageName,
Inputs,
TimestampType,
delete_org_package_versions,
delete_package_versions,
get_and_delete_old_versions,
get_image_version_tags,
list_org_package_versions,
list_package_versions,
)
from main import main as main_
from main import parse_image_names, post_deletion_output, validate_inputs
mock_response = Mock()
mock_response.json.return_value = []
mock_response.is_error = False
mock_bad_response = Mock()
mock_bad_response.is_error = True
mock_http_client = AsyncMock()
mock_http_client.get.return_value = mock_response
mock_http_client.delete.return_value = mock_response
@pytest.mark.asyncio
async def test_list_org_package_version():
await list_org_package_versions(org_name='test', image_name=ImageName('test', 'test'), http_client=mock_http_client)
@pytest.mark.asyncio
async def test_list_package_version():
await list_package_versions(image_name=ImageName('test', 'test'), http_client=mock_http_client)
@pytest.mark.asyncio
async def test_delete_org_package_version():
await delete_org_package_versions(org_name='test', image_name=ImageName('test', 'test'), http_client=mock_http_client, version_id=123)
@pytest.mark.asyncio
async def test_delete_package_version():
await delete_package_versions(image_name=ImageName('test', 'test'), http_client=mock_http_client, version_id=123)
def test_post_deletion_output(capsys):
# Happy path
post_deletion_output(mock_response, image_name=ImageName('test', 'test'), version_id=123)
captured = capsys.readouterr()
assert captured.out == 'Deleted old image: test:123\n'
# Bad response
post_deletion_output(mock_bad_response, image_name=ImageName('test', 'test'), version_id=123)
captured = capsys.readouterr()
assert captured.out != 'Deleted old image: test:123\n'
def test_inputs_dataclass():
personal = Inputs(
parsed_cutoff=parse('an hour ago utc'),
timestamp_type=TimestampType('created_at'),
account_type=AccountType('personal'),
untagged_only=False,
skip_tags=[],
keep_at_least=0,
)
assert personal.is_org is False
assert personal.list_package_versions == list_package_versions
assert personal.delete_package == delete_package_versions
org = Inputs(
parsed_cutoff=parse('an hour ago utc'),
timestamp_type=TimestampType('created_at'),
account_type=AccountType('org'),
org_name='abcorp',
untagged_only=False,
skip_tags=[],
keep_at_least=0,
)
assert org.is_org is True
assert isinstance(org.list_package_versions, partial)
assert isinstance(org.delete_package, partial)
def test_get_image_version_tags():
assert (
get_image_version_tags(
{
'metadata': {'container': {'tags': []}},
}
)
== []
)
assert (
get_image_version_tags(
{
'metadata': {'container': {'tags': ['a']}},
}
)
== ['a']
)
assert get_image_version_tags({'metadata': {}}) == []
class TestGetAndDeleteOldVersions:
valid_data = [
{
'created_at': '2021-05-26T14:03:03Z',
'html_url': 'https://github.com/orgs/org-name/packages/container/image-name/1234567',
'id': 1234567,
'metadata': {'container': {'tags': []}, 'package_type': 'container'},
'name': 'sha256:3c6891187412bd31fa04c63b4f06c47417eb599b1b659462632285531aa99c19',
'package_html_url': 'https://github.com/orgs/org-name/packages/container/package/image-name',
'updated_at': '2021-05-26T14:03:03Z',
'url': 'https://api.github.com/orgs/org-name/packages/container/image-name/versions/1234567',
}
]
valid_inputs = {
'parsed_cutoff': parse('an hour ago utc'),
'timestamp_type': TimestampType('created_at'),
'account_type': AccountType('personal'),
'untagged_only': False,
'skip_tags': [],
'keep_at_least': '0',
}
@staticmethod
async def _mock_list_package_versions(data, *args):
return data
@pytest.mark.asyncio
async def test_delete_package(self, capsys):
Inputs.list_package_versions = partial(self._mock_list_package_versions, self.valid_data)
inputs = Inputs(**self.valid_inputs)
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'Deleted old image: a:1234567\n'
@pytest.mark.asyncio
async def test_keep_at_least(self, capsys):
Inputs.list_package_versions = partial(self._mock_list_package_versions, self.valid_data)
inputs = Inputs(**self.valid_inputs | {'keep_at_least': 1})
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'No more versions to delete for a\n'
@pytest.mark.asyncio
async def test_not_beyond_cutoff(self, capsys):
data = [
{
'created_at': str(datetime.now(timezone(timedelta(hours=1)))),
'id': 1234567,
}
]
Inputs.list_package_versions = partial(
self._mock_list_package_versions,
data,
)
inputs = Inputs(**self.valid_inputs)
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'No more versions to delete for a\n'
@pytest.mark.asyncio
async def test_missing_timestamp(self, capsys):
data = [{'created_at': '', 'id': 1234567}]
Inputs.list_package_versions = partial(self._mock_list_package_versions, data)
inputs = Inputs(**self.valid_inputs)
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'Skipping image version 1234567. Unable to parse timestamps.\nNo more versions to delete for a\n'
@pytest.mark.asyncio
async def test_empty_list(self, capsys):
data = []
Inputs.list_package_versions = partial(self._mock_list_package_versions, data)
inputs = Inputs(**self.valid_inputs)
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'No more versions to delete for a\n'
@pytest.mark.asyncio
async def test_skip_tags(self, capsys):
data = deepcopy(self.valid_data)
data[0]['metadata'] = {'container': {'tags': ['abc', 'bcd']}}
Inputs.list_package_versions = partial(self._mock_list_package_versions, data)
inputs = Inputs(**self.valid_inputs | {'skip_tags': 'abc'})
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'No more versions to delete for a\n'
@pytest.mark.asyncio
async def test_untagged_only(self, capsys):
data = deepcopy(self.valid_data)
data[0]['metadata'] = {'container': {'tags': ['abc', 'bcd']}}
Inputs.list_package_versions = partial(self._mock_list_package_versions, data)
inputs = Inputs(**self.valid_inputs | {'untagged_only': 'true'})
await get_and_delete_old_versions(image_name=ImageName('a', 'a'), inputs=inputs, http_client=mock_http_client)
captured = capsys.readouterr()
assert captured.out == 'No more versions to delete for a\n'
def test_inputs_bad_account_type():
defaults = {
'account_type': 'org',
'org_name': 'test',
'timestamp_type': 'updated_at',
'cut_off': '2 hours ago UTC',
'untagged_only': False,
'skip_tags': None,
'keep_at_least': 0,
}
# Account type
validate_inputs(**defaults | {'account_type': 'personal'})
validate_inputs(**defaults | {'account_type': 'org'})
with pytest.raises(ValueError, match="'' is not a valid AccountType"):
validate_inputs(**defaults | {'account_type': ''})
# Org name
validate_inputs(**defaults | {'org_name': '', 'account_type': 'personal'})
with pytest.raises(ValueError, match='org-name is required when account-type is org'):
validate_inputs(**defaults | {'org_name': ''})
# Timestamp type
validate_inputs(**defaults | {'timestamp_type': 'updated_at'})
validate_inputs(**defaults | {'timestamp_type': 'created_at'})
with pytest.raises(ValueError, match="'wat' is not a valid TimestampType"):
validate_inputs(**defaults | {'timestamp_type': 'wat'})
# Cut-off
validate_inputs(**defaults | {'cut_off': '21 July 2013 10:15 pm +0500'})
validate_inputs(**defaults | {'cut_off': '12/12/12 PM EST'})
with pytest.raises(ValueError, match='Timezone is required for the cut-off'):
validate_inputs(**defaults | {'cut_off': '12/12/12'})
with pytest.raises(ValueError, match="Unable to parse 'lolol'"):
validate_inputs(**defaults | {'cut_off': 'lolol'})
# Untagged only
for i in ['true', 'True', '1']:
assert validate_inputs(**defaults | {'untagged_only': i}).untagged_only is True
for j in ['False', 'false', '0']:
assert validate_inputs(**defaults | {'untagged_only': j}).untagged_only is False
assert validate_inputs(**defaults | {'untagged_only': False}).untagged_only is False
# Skip tags
assert validate_inputs(**defaults | {'skip_tags': 'a'}).skip_tags == ['a']
assert validate_inputs(**defaults | {'skip_tags': 'a,b'}).skip_tags == ['a', 'b']
assert validate_inputs(**defaults | {'skip_tags': 'a , b ,c'}).skip_tags == ['a', 'b', 'c']
# Keep at least
with pytest.raises(ValueError, match='keep-at-least must be 0 or positive'):
validate_inputs(**defaults | {'keep_at_least': '-1'})
def test_parse_image_names():
assert parse_image_names('a') == [ImageName('a', 'a')]
assert parse_image_names('a,b') == [ImageName('a', 'a'), ImageName('b', 'b')]
assert parse_image_names(' a , b ') == [ImageName('a', 'a'), ImageName('b', 'b')]
assert parse_image_names('a/a') == [ImageName('a/a', 'a%2Fa')]
@pytest.mark.asyncio
async def test_main(mocker):
mocker.patch.object(AsyncClient, 'get', return_value=mock_response)
mocker.patch.object(AsyncClient, 'delete', return_value=mock_response)
mocker.patch.object(main, 'get_and_delete_old_versions', AsyncMock())
await main_(
**{
'account_type': 'org',
'org_name': 'test',
'image_names': 'a,b,c',
'timestamp_type': 'updated_at',
'cut_off': '2 hours ago UTC',
'token': 'abc',
}
)