Skip to content

Commit df9ec54

Browse files
xmunozewdurbin
authored andcommitted
Add admin interface to view and enable checks (#7134)
* Add admin interface to view and enable checks - Implement list, detail and change_state views (#7133) - Add unit tests for check admin view * Add comprehensive test coverage for check admin
1 parent 49b1999 commit df9ec54

File tree

10 files changed

+425
-11
lines changed

10 files changed

+425
-11
lines changed

tests/common/db/malware.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import datetime
14+
15+
import factory
16+
import factory.fuzzy
17+
18+
from warehouse.malware.models import MalwareCheck, MalwareCheckState, MalwareCheckType
19+
20+
from .base import WarehouseFactory
21+
22+
23+
class MalwareCheckFactory(WarehouseFactory):
24+
class Meta:
25+
model = MalwareCheck
26+
27+
name = factory.fuzzy.FuzzyText(length=12)
28+
version = 1
29+
short_description = factory.fuzzy.FuzzyText(length=80)
30+
long_description = factory.fuzzy.FuzzyText(length=300)
31+
check_type = factory.fuzzy.FuzzyChoice([e for e in MalwareCheckType])
32+
hook_name = (
33+
"project:release:file:upload"
34+
if check_type == MalwareCheckType.event_hook
35+
else None
36+
)
37+
state = factory.fuzzy.FuzzyChoice([e for e in MalwareCheckState])
38+
created = factory.fuzzy.FuzzyNaiveDateTime(
39+
datetime.datetime.utcnow() - datetime.timedelta(days=7)
40+
)

tests/unit/admin/test_routes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,13 @@ def test_includeme():
123123
pretend.call("admin.flags.edit", "/admin/flags/edit/", domain=warehouse),
124124
pretend.call("admin.squats", "/admin/squats/", domain=warehouse),
125125
pretend.call("admin.squats.review", "/admin/squats/review/", domain=warehouse),
126+
pretend.call("admin.checks.list", "/admin/checks/", domain=warehouse),
127+
pretend.call(
128+
"admin.checks.detail", "/admin/checks/{check_name}", domain=warehouse
129+
),
130+
pretend.call(
131+
"admin.checks.change_state",
132+
"/admin/checks/{check_name}/change_state",
133+
domain=warehouse,
134+
),
126135
]

tests/unit/admin/views/test_checks.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import uuid
14+
15+
import pretend
16+
import pytest
17+
18+
from pyramid.httpexceptions import HTTPNotFound
19+
20+
from warehouse.admin.views import checks as views
21+
from warehouse.malware.models import MalwareCheckState
22+
23+
from ....common.db.malware import MalwareCheckFactory
24+
25+
26+
class TestListChecks:
27+
def test_get_checks_none(self, db_request):
28+
assert views.get_checks(db_request) == {"checks": []}
29+
30+
def test_get_checks(self, db_request):
31+
checks = [MalwareCheckFactory.create() for _ in range(10)]
32+
assert views.get_checks(db_request) == {"checks": checks}
33+
34+
def test_get_checks_different_versions(self, db_request):
35+
checks = [MalwareCheckFactory.create() for _ in range(5)]
36+
checks_same = [
37+
MalwareCheckFactory.create(name="MyCheck", version=i) for i in range(1, 6)
38+
]
39+
checks.append(checks_same[-1])
40+
assert views.get_checks(db_request) == {"checks": checks}
41+
42+
43+
class TestGetCheck:
44+
def test_get_check(self, db_request):
45+
check = MalwareCheckFactory.create()
46+
db_request.matchdict["check_name"] = check.name
47+
assert views.get_check(db_request) == {
48+
"check": check,
49+
"checks": [check],
50+
"states": MalwareCheckState,
51+
}
52+
53+
def test_get_check_many_versions(self, db_request):
54+
check1 = MalwareCheckFactory.create(name="MyCheck", version="1")
55+
check2 = MalwareCheckFactory.create(name="MyCheck", version="2")
56+
db_request.matchdict["check_name"] = check1.name
57+
assert views.get_check(db_request) == {
58+
"check": check2,
59+
"checks": [check2, check1],
60+
"states": MalwareCheckState,
61+
}
62+
63+
def test_get_check_not_found(self, db_request):
64+
db_request.matchdict["check_name"] = "DoesNotExist"
65+
with pytest.raises(HTTPNotFound):
66+
views.get_check(db_request)
67+
68+
69+
class TestChangeCheckState:
70+
def test_change_to_enabled(self, db_request):
71+
check = MalwareCheckFactory.create(
72+
name="MyCheck", state=MalwareCheckState.disabled
73+
)
74+
75+
db_request.POST = {"id": check.id, "check_state": "enabled"}
76+
db_request.matchdict["check_name"] = check.name
77+
78+
db_request.session = pretend.stub(
79+
flash=pretend.call_recorder(lambda *a, **kw: None)
80+
)
81+
db_request.route_path = pretend.call_recorder(
82+
lambda *a, **kw: "/admin/checks/MyCheck/change_state"
83+
)
84+
85+
views.change_check_state(db_request)
86+
87+
assert db_request.session.flash.calls == [
88+
pretend.call("Changed 'MyCheck' check to 'enabled'!", queue="success")
89+
]
90+
assert check.state == MalwareCheckState.enabled
91+
92+
def test_change_to_invalid_state(self, db_request):
93+
check = MalwareCheckFactory.create(name="MyCheck")
94+
initial_state = check.state
95+
invalid_check_state = "cancelled"
96+
db_request.POST = {"id": check.id, "check_state": invalid_check_state}
97+
db_request.matchdict["check_name"] = check.name
98+
99+
db_request.session = pretend.stub(
100+
flash=pretend.call_recorder(lambda *a, **kw: None)
101+
)
102+
db_request.route_path = pretend.call_recorder(
103+
lambda *a, **kw: "/admin/checks/MyCheck/change_state"
104+
)
105+
106+
views.change_check_state(db_request)
107+
108+
assert db_request.session.flash.calls == [
109+
pretend.call("Invalid check state provided.", queue="error")
110+
]
111+
assert check.state == initial_state
112+
113+
def test_check_not_found(self, db_request):
114+
db_request.POST = {"id": uuid.uuid4(), "check_state": "enabled"}
115+
db_request.matchdict["check_name"] = "DoesNotExist"
116+
117+
db_request.route_path = pretend.call_recorder(
118+
lambda *a, **kw: "/admin/checks/DoesNotExist/change_state"
119+
)
120+
121+
with pytest.raises(HTTPNotFound):
122+
views.change_check_state(db_request)

warehouse/admin/routes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,14 @@ def includeme(config):
128128
# Squats
129129
config.add_route("admin.squats", "/admin/squats/", domain=warehouse)
130130
config.add_route("admin.squats.review", "/admin/squats/review/", domain=warehouse)
131+
132+
# Malware checks
133+
config.add_route("admin.checks.list", "/admin/checks/", domain=warehouse)
134+
config.add_route(
135+
"admin.checks.detail", "/admin/checks/{check_name}", domain=warehouse
136+
)
137+
config.add_route(
138+
"admin.checks.change_state",
139+
"/admin/checks/{check_name}/change_state",
140+
domain=warehouse,
141+
)

warehouse/admin/templates/admin/base.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@
125125
<i class="fa fa-dumbbell"></i> <span>Squats</span>
126126
</a>
127127
</li>
128+
<li>
129+
<a href="{{ request.route_path('admin.checks.list') }}">
130+
<i class="fa fa-check"></i> <span>Checks</span>
131+
</a>
132+
</li>
128133
</ul>
129134
</section>
130135
</aside>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "admin/base.html" %}
15+
16+
{% block title %}{{ check.name }}{% endblock %}
17+
18+
{% block breadcrumb %}
19+
<li><a href="{{ request.route_path('admin.checks.list') }}">Checks</a></li>
20+
<li class="active">{{ check.name }}</li>
21+
{% endblock %}
22+
23+
{% block content %}
24+
<div class="box box-primary">
25+
<div class="box-body box-profile">
26+
<p>{{ check.long_description }}</p>
27+
<h4>Revision History</h4>
28+
<div class="box-body box-attributes">
29+
<table class="table table-hover">
30+
<tr>
31+
<th>Version</th>
32+
<th>State</th>
33+
<th>Created</th>
34+
</tr>
35+
{% for c in checks %}
36+
<tr>
37+
<td>{{ c.version }}</td>
38+
<td>{{ c.state.value }}</td>
39+
<td>{{ c.created }}</td>
40+
</tr>
41+
{% endfor %}
42+
</table>
43+
</div>
44+
</div>
45+
</div>
46+
<div class="box box-primary">
47+
<div class="box-header with-border">
48+
<h3 class="box-title">Change State</h3>
49+
</div>
50+
<form method="POST" action="{{ request.route_path('admin.checks.change_state', check_name=check.name) }}">
51+
<input type="hidden" name="id" value="{{ check.id }}">
52+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
53+
<div class="box-body">
54+
<div class="form-group col-sm-4">
55+
<select name="check_state" id="check_state">
56+
{% for state in states %}
57+
<option value="{{ state.value }}" {{'disabled selected' if check.state == state else ''}}>
58+
{{ state.value }}
59+
</option>
60+
{% endfor %}
61+
</select>
62+
</div>
63+
<div class="pull-right col-sm-4">
64+
<button type="submit" class="btn btn-primary pull-right">Save</button>
65+
</div>
66+
</div>
67+
</form>
68+
</div>
69+
</div>
70+
{% endblock %}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "admin/base.html" %}
15+
16+
{% block title %}Malware Checks{% endblock %}
17+
18+
{% block breadcrumb %}
19+
<li class="active">Checks</li>
20+
{% endblock %}
21+
22+
{% block content %}
23+
<div class="box box-primary">
24+
<div class="box-body table-responsive no-padding">
25+
<table class="table table-hover">
26+
<tr>
27+
<th>Check Name</th>
28+
<th>State</th>
29+
<th>Revisions</th>
30+
<th>Last Modified</th>
31+
<th>Description</th>
32+
</tr>
33+
{% for check in checks %}
34+
<tr>
35+
<td>
36+
<a href="{{ request.route_path('admin.checks.detail', check_name=check.name) }}">
37+
{{ check.name }}
38+
</a>
39+
</td>
40+
<td>{{ check.state.value }}</td>
41+
<td>{{ check.version }}</td>
42+
<td>{{ check.created }}</td>
43+
<td>{{ check.short_description }}</td>
44+
</tr>
45+
{% else %}
46+
<tr>
47+
<td colspan="5">
48+
<center>
49+
<i>No checks!</i>
50+
</center>
51+
</td>
52+
</tr>
53+
{% endfor %}
54+
</table>
55+
</div>
56+
</div>
57+
{% endblock content %}

0 commit comments

Comments
 (0)