Skip to content

Commit

Permalink
Add admin interface to view and enable checks (#7134)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
xmunoz authored and ewdurbin committed Jan 8, 2020
1 parent 6048247 commit dc50d93
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 11 deletions.
40 changes: 40 additions & 0 deletions tests/common/db/malware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime

import factory
import factory.fuzzy

from warehouse.malware.models import MalwareCheck, MalwareCheckState, MalwareCheckType

from .base import WarehouseFactory


class MalwareCheckFactory(WarehouseFactory):
class Meta:
model = MalwareCheck

name = factory.fuzzy.FuzzyText(length=12)
version = 1
short_description = factory.fuzzy.FuzzyText(length=80)
long_description = factory.fuzzy.FuzzyText(length=300)
check_type = factory.fuzzy.FuzzyChoice([e for e in MalwareCheckType])
hook_name = (
"project:release:file:upload"
if check_type == MalwareCheckType.event_hook
else None
)
state = factory.fuzzy.FuzzyChoice([e for e in MalwareCheckState])
created = factory.fuzzy.FuzzyNaiveDateTime(
datetime.datetime.utcnow() - datetime.timedelta(days=7)
)
9 changes: 9 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,13 @@ def test_includeme():
pretend.call("admin.flags.edit", "/admin/flags/edit/", domain=warehouse),
pretend.call("admin.squats", "/admin/squats/", domain=warehouse),
pretend.call("admin.squats.review", "/admin/squats/review/", domain=warehouse),
pretend.call("admin.checks.list", "/admin/checks/", domain=warehouse),
pretend.call(
"admin.checks.detail", "/admin/checks/{check_name}", domain=warehouse
),
pretend.call(
"admin.checks.change_state",
"/admin/checks/{check_name}/change_state",
domain=warehouse,
),
]
122 changes: 122 additions & 0 deletions tests/unit/admin/views/test_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import uuid

import pretend
import pytest

from pyramid.httpexceptions import HTTPNotFound

from warehouse.admin.views import checks as views
from warehouse.malware.models import MalwareCheckState

from ....common.db.malware import MalwareCheckFactory


class TestListChecks:
def test_get_checks_none(self, db_request):
assert views.get_checks(db_request) == {"checks": []}

def test_get_checks(self, db_request):
checks = [MalwareCheckFactory.create() for _ in range(10)]
assert views.get_checks(db_request) == {"checks": checks}

def test_get_checks_different_versions(self, db_request):
checks = [MalwareCheckFactory.create() for _ in range(5)]
checks_same = [
MalwareCheckFactory.create(name="MyCheck", version=i) for i in range(1, 6)
]
checks.append(checks_same[-1])
assert views.get_checks(db_request) == {"checks": checks}


class TestGetCheck:
def test_get_check(self, db_request):
check = MalwareCheckFactory.create()
db_request.matchdict["check_name"] = check.name
assert views.get_check(db_request) == {
"check": check,
"checks": [check],
"states": MalwareCheckState,
}

def test_get_check_many_versions(self, db_request):
check1 = MalwareCheckFactory.create(name="MyCheck", version="1")
check2 = MalwareCheckFactory.create(name="MyCheck", version="2")
db_request.matchdict["check_name"] = check1.name
assert views.get_check(db_request) == {
"check": check2,
"checks": [check2, check1],
"states": MalwareCheckState,
}

def test_get_check_not_found(self, db_request):
db_request.matchdict["check_name"] = "DoesNotExist"
with pytest.raises(HTTPNotFound):
views.get_check(db_request)


class TestChangeCheckState:
def test_change_to_enabled(self, db_request):
check = MalwareCheckFactory.create(
name="MyCheck", state=MalwareCheckState.disabled
)

db_request.POST = {"id": check.id, "check_state": "enabled"}
db_request.matchdict["check_name"] = check.name

db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None)
)
db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/checks/MyCheck/change_state"
)

views.change_check_state(db_request)

assert db_request.session.flash.calls == [
pretend.call("Changed 'MyCheck' check to 'enabled'!", queue="success")
]
assert check.state == MalwareCheckState.enabled

def test_change_to_invalid_state(self, db_request):
check = MalwareCheckFactory.create(name="MyCheck")
initial_state = check.state
invalid_check_state = "cancelled"
db_request.POST = {"id": check.id, "check_state": invalid_check_state}
db_request.matchdict["check_name"] = check.name

db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None)
)
db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/checks/MyCheck/change_state"
)

views.change_check_state(db_request)

assert db_request.session.flash.calls == [
pretend.call("Invalid check state provided.", queue="error")
]
assert check.state == initial_state

def test_check_not_found(self, db_request):
db_request.POST = {"id": uuid.uuid4(), "check_state": "enabled"}
db_request.matchdict["check_name"] = "DoesNotExist"

db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/checks/DoesNotExist/change_state"
)

with pytest.raises(HTTPNotFound):
views.change_check_state(db_request)
11 changes: 11 additions & 0 deletions warehouse/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ def includeme(config):
# Squats
config.add_route("admin.squats", "/admin/squats/", domain=warehouse)
config.add_route("admin.squats.review", "/admin/squats/review/", domain=warehouse)

# Malware checks
config.add_route("admin.checks.list", "/admin/checks/", domain=warehouse)
config.add_route(
"admin.checks.detail", "/admin/checks/{check_name}", domain=warehouse
)
config.add_route(
"admin.checks.change_state",
"/admin/checks/{check_name}/change_state",
domain=warehouse,
)
5 changes: 5 additions & 0 deletions warehouse/admin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
<i class="fa fa-dumbbell"></i> <span>Squats</span>
</a>
</li>
<li>
<a href="{{ request.route_path('admin.checks.list') }}">
<i class="fa fa-check"></i> <span>Checks</span>
</a>
</li>
</ul>
</section>
</aside>
Expand Down
70 changes: 70 additions & 0 deletions warehouse/admin/templates/admin/malware/checks/detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "admin/base.html" %}

{% block title %}{{ check.name }}{% endblock %}

{% block breadcrumb %}
<li><a href="{{ request.route_path('admin.checks.list') }}">Checks</a></li>
<li class="active">{{ check.name }}</li>
{% endblock %}

{% block content %}
<div class="box box-primary">
<div class="box-body box-profile">
<p>{{ check.long_description }}</p>
<h4>Revision History</h4>
<div class="box-body box-attributes">
<table class="table table-hover">
<tr>
<th>Version</th>
<th>State</th>
<th>Created</th>
</tr>
{% for c in checks %}
<tr>
<td>{{ c.version }}</td>
<td>{{ c.state.value }}</td>
<td>{{ c.created }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Change State</h3>
</div>
<form method="POST" action="{{ request.route_path('admin.checks.change_state', check_name=check.name) }}">
<input type="hidden" name="id" value="{{ check.id }}">
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
<div class="box-body">
<div class="form-group col-sm-4">
<select name="check_state" id="check_state">
{% for state in states %}
<option value="{{ state.value }}" {{'disabled selected' if check.state == state else ''}}>
{{ state.value }}
</option>
{% endfor %}
</select>
</div>
<div class="pull-right col-sm-4">
<button type="submit" class="btn btn-primary pull-right">Save</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
57 changes: 57 additions & 0 deletions warehouse/admin/templates/admin/malware/checks/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "admin/base.html" %}

{% block title %}Malware Checks{% endblock %}

{% block breadcrumb %}
<li class="active">Checks</li>
{% endblock %}

{% block content %}
<div class="box box-primary">
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>Check Name</th>
<th>State</th>
<th>Revisions</th>
<th>Last Modified</th>
<th>Description</th>
</tr>
{% for check in checks %}
<tr>
<td>
<a href="{{ request.route_path('admin.checks.detail', check_name=check.name) }}">
{{ check.name }}
</a>
</td>
<td>{{ check.state.value }}</td>
<td>{{ check.version }}</td>
<td>{{ check.created }}</td>
<td>{{ check.short_description }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">
<center>
<i>No checks!</i>
</center>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock content %}
Loading

0 comments on commit dc50d93

Please sign in to comment.