Browse Source

Add Opaque Data Panel

Change-Id: I66e1dd24e831c7e1fb59d24ba46a9d0ff1d85b76
Kaitlin Farr 1 year ago
parent
commit
21504848d6

+ 2
- 1
README.rst View File

@@ -57,7 +57,8 @@ And enable it in Horizon::
57 57
     ln -s ../castellan-ui/castellan_ui/enabled/_91_project_key_manager_x509_certificates_panel.py openstack_dashboard/local/enabled
58 58
     ln -s ../castellan-ui/castellan_ui/enabled/_92_project_key_manager_private_key_panel.py openstack_dashboard/local/enabled
59 59
     ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_public_key_panel.py openstack_dashboard/local/enabled
60
-    ln -s ../castellan-ui/castellan_ui/enabled/_93_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled
60
+    ln -s ../castellan-ui/castellan_ui/enabled/_94_project_key_manager_symmetric_key_panel.py openstack_dashboard/local/enabled
61
+    ln -s ../castellan-ui/castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py openstack_dashboard/local/enabled
61 62
 
62 63
 To run horizon with the newly enabled Castellan UI plugin run::
63 64
 

+ 0
- 0
castellan_ui/content/opaque_data/__init__.py View File


+ 108
- 0
castellan_ui/content/opaque_data/forms.py View File

@@ -0,0 +1,108 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+
14
+import base64
15
+import binascii
16
+from django.utils.translation import ugettext_lazy as _
17
+
18
+from castellan.common.objects import opaque_data
19
+from horizon import exceptions
20
+from horizon import forms
21
+from horizon import messages
22
+
23
+from castellan_ui.api import client
24
+from castellan_ui.content import shared_forms
25
+
26
+
27
+class ImportOpaqueData(forms.SelfHandlingForm):
28
+    name = forms.RegexField(required=False,
29
+                            max_length=255,
30
+                            label=_("Data Name"),
31
+                            regex=shared_forms.NAME_REGEX,
32
+                            error_messages=shared_forms.ERROR_MESSAGES)
33
+    source_type = forms.ChoiceField(
34
+        label=_('Source'),
35
+        required=False,
36
+        choices=[('file', _('File')),
37
+                 ('raw', _('Direct Input'))],
38
+        widget=forms.ThemableSelectWidget(
39
+            attrs={'class': 'switchable', 'data-slug': 'source'}))
40
+    object_file = forms.FileField(
41
+        label=_("Choose file"),
42
+        help_text=_("A local file to upload."),
43
+        widget=forms.FileInput(
44
+            attrs={'class': 'switched', 'data-switch-on': 'source',
45
+                   'data-source-file': _('File')}),
46
+        required=False)
47
+    direct_input = forms.CharField(
48
+        label=_('Object Bytes'),
49
+        help_text=_('The bytes of the object, represented in hex.'),
50
+        widget=forms.widgets.Textarea(
51
+            attrs={'class': 'switched', 'data-switch-on': 'source',
52
+                   'data-source-raw': _('Bytes')}),
53
+        required=False)
54
+
55
+    def __init__(self, request, *args, **kwargs):
56
+        super(ImportOpaqueData, self).__init__(request, *args, **kwargs)
57
+
58
+    def clean(self):
59
+        data = super(ImportOpaqueData, self).clean()
60
+
61
+        # The data can be missing based on particular upload
62
+        # conditions. Code defensively for it here...
63
+        data_file = data.get('object_file', None)
64
+        data_raw = data.get('direct_input', None)
65
+
66
+        if data_raw and data_file:
67
+            raise forms.ValidationError(
68
+                _("Cannot specify both file and direct input."))
69
+        if not data_raw and not data_file:
70
+            raise forms.ValidationError(
71
+                _("No input was provided for the object value."))
72
+        try:
73
+            if data_file:
74
+                data_bytes = self.files['object_file'].read()
75
+            else:
76
+                data_str = data['direct_input']
77
+                data_bytes = binascii.unhexlify(data_str)
78
+            data['object_bytes'] = base64.b64encode(data_bytes)
79
+        except Exception as e:
80
+            msg = _('There was a problem loading the object: %s. '
81
+                    'Is the object valid and in the correct format?') % e
82
+            raise forms.ValidationError(msg)
83
+
84
+        return data
85
+
86
+    def handle(self, request, data):
87
+        try:
88
+            data_bytes = data.get('object_bytes')
89
+            data_uuid = client.import_object(
90
+                request,
91
+                data=data_bytes,
92
+                name=data['name'],
93
+                object_type=opaque_data.OpaqueData)
94
+
95
+            if data['name']:
96
+                data_identifier = data['name']
97
+            else:
98
+                data_identifier = data_uuid
99
+            messages.success(request,
100
+                             _('Successfully imported object: %s')
101
+                             % data_identifier)
102
+            return data_uuid
103
+        except Exception as e:
104
+            msg = _('Unable to import object: %s')
105
+            messages.error(msg % e)
106
+            exceptions.handle(request, ignore=True)
107
+            self.api_error(_('Unable to import object.'))
108
+            return False

+ 23
- 0
castellan_ui/content/opaque_data/panel.py View File

@@ -0,0 +1,23 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.utils.translation import ugettext_lazy as _
14
+import horizon
15
+
16
+# This panel will be loaded from horizon, because specified in enabled file.
17
+# To register REST api, import below here.
18
+from castellan_ui.api import client  # noqa: F401
19
+
20
+
21
+class OpaqueData(horizon.Panel):
22
+    name = _("Opaque Data")
23
+    slug = "opaque_data"

+ 84
- 0
castellan_ui/content/opaque_data/tables.py View File

@@ -0,0 +1,84 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+
14
+from castellan_ui.content import filters
15
+from django.core.urlresolvers import reverse
16
+from django.utils.translation import ugettext_lazy as _
17
+from django.utils.translation import ungettext_lazy
18
+
19
+from castellan_ui.api import client
20
+from horizon import tables
21
+
22
+
23
+class ImportOpaqueData(tables.LinkAction):
24
+    name = "import_opaque_data"
25
+    verbose_name = _("Import Opaque Data")
26
+    url = "horizon:project:opaque_data:import"
27
+    classes = ("ajax-modal",)
28
+    icon = "upload"
29
+    policy_rules = ()
30
+
31
+
32
+class DownloadOpaqueData(tables.LinkAction):
33
+    name = "download"
34
+    verbose_name = _("Download Opaque Data")
35
+    url = "horizon:project:opaque_data:download"
36
+    classes = ("btn-download",)
37
+    policy_rules = ()
38
+
39
+    def get_link_url(self, datum):
40
+        return reverse(self.url,
41
+                       kwargs={'object_id': datum.id})
42
+
43
+
44
+class DeleteOpaqueData(tables.DeleteAction):
45
+    policy_rules = ()
46
+    help_text = _("You should not delete an object unless you are "
47
+                  "certain it is not being used anywhere.")
48
+
49
+    @staticmethod
50
+    def action_present(count):
51
+        return ungettext_lazy(
52
+            u"Delete Opaque Data",
53
+            u"Delete Opaque Data",
54
+            count
55
+        )
56
+
57
+    @staticmethod
58
+    def action_past(count):
59
+        return ungettext_lazy(
60
+            u"Deleted Opaque Data",
61
+            u"Deleted Opaque Data",
62
+            count
63
+        )
64
+
65
+    def delete(self, request, obj_id):
66
+        client.delete(request, obj_id)
67
+
68
+
69
+class OpaqueDataTable(tables.DataTable):
70
+    detail_link = "horizon:project:opaque_data:detail"
71
+    uuid = tables.Column("id", verbose_name=_("Object ID"), link=detail_link)
72
+    name = tables.Column("name", verbose_name=_("Name"))
73
+    created_date = tables.Column("created",
74
+                                 verbose_name=_("Created Date"),
75
+                                 filters=(filters.timestamp_to_iso,))
76
+
77
+    def get_object_display(self, datum):
78
+        return datum.name if datum.name else datum.id
79
+
80
+    class Meta(object):
81
+        name = "opaque_data"
82
+        table_actions = (ImportOpaqueData,
83
+                         DeleteOpaqueData,)
84
+        row_actions = (DownloadOpaqueData, DeleteOpaqueData)

+ 26
- 0
castellan_ui/content/opaque_data/urls.py View File

@@ -0,0 +1,26 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from castellan_ui.content.opaque_data import views
14
+from django.conf.urls import url
15
+
16
+urlpatterns = [
17
+    url(r'^$', views.IndexView.as_view(), name='index'),
18
+    url(r'^import/$', views.ImportView.as_view(), name='import'),
19
+    url(r'^(?P<object_id>[^/]+)/$',
20
+        views.DetailView.as_view(),
21
+        name='detail'),
22
+    url(r'^download/$', views.download_key, name='download'),
23
+    url(r'^(?P<object_id>[^/]+)/download$',
24
+        views.download_key,
25
+        name='download'),
26
+]

+ 124
- 0
castellan_ui/content/opaque_data/views.py View File

@@ -0,0 +1,124 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from django.core.urlresolvers import reverse
14
+from django.core.urlresolvers import reverse_lazy
15
+from django.http import HttpResponse
16
+from django.utils.translation import ugettext_lazy as _
17
+
18
+import binascii
19
+from castellan.common.objects import opaque_data
20
+from castellan_ui.api import client
21
+from castellan_ui.content.opaque_data import forms as opaque_data_forms
22
+from castellan_ui.content.opaque_data import tables
23
+from datetime import datetime
24
+from horizon import exceptions
25
+from horizon import forms
26
+from horizon.tables import views as tables_views
27
+from horizon.utils import memoized
28
+from horizon import views
29
+
30
+
31
+def download_key(request, object_id):
32
+    try:
33
+        obj = client.get(request, object_id)
34
+        data = obj.get_encoded()
35
+        response = HttpResponse()
36
+        response.write(data)
37
+        response['Content-Disposition'] = ('attachment; '
38
+                                           'filename="%s.opaque"' % object_id)
39
+        response['Content-Length'] = str(len(response.content))
40
+        return response
41
+
42
+    except Exception:
43
+        redirect = reverse('horizon:project:opaque_data:index')
44
+        msg = _('Unable to download opaque_data "%s".')\
45
+            % (object_id)
46
+        exceptions.handle(request, msg, redirect=redirect)
47
+
48
+
49
+class IndexView(tables_views.MultiTableView):
50
+    table_classes = [
51
+        tables.OpaqueDataTable
52
+    ]
53
+    template_name = 'opaque_data.html'
54
+
55
+    def get_opaque_data_data(self):
56
+        try:
57
+            return client.list(
58
+                self.request, object_type=opaque_data.OpaqueData)
59
+        except Exception as e:
60
+            msg = _('Unable to list objects: "%s".') % (e.message)
61
+            exceptions.handle(self.request, msg)
62
+            return []
63
+
64
+
65
+class ImportView(forms.ModalFormView):
66
+    form_class = opaque_data_forms.ImportOpaqueData
67
+    template_name = 'opaque_data_import.html'
68
+    submit_url = reverse_lazy(
69
+        "horizon:project:opaque_data:import")
70
+    success_url = reverse_lazy('horizon:project:opaque_data:index')
71
+    submit_label = page_title = _("Import Opaque Data")
72
+
73
+    def get_object_id(self, key_uuid):
74
+        return key_uuid
75
+
76
+
77
+class DetailView(views.HorizonTemplateView):
78
+    template_name = 'opaque_data_detail.html'
79
+    page_title = _("Opaque Data Details")
80
+
81
+    @memoized.memoized_method
82
+    def _get_data(self):
83
+        try:
84
+            obj = client.get(self.request, self.kwargs['object_id'])
85
+        except Exception:
86
+            redirect = reverse('horizon:project:opaque_data:index')
87
+            msg = _('Unable to retrieve details for opaque_data "%s".')\
88
+                % (self.kwargs['object_id'])
89
+            exceptions.handle(self.request, msg,
90
+                              redirect=redirect)
91
+        return obj
92
+
93
+    @memoized.memoized_method
94
+    def _get_data_created_date(self, obj):
95
+        try:
96
+            created_date = datetime.utcfromtimestamp(obj.created).isoformat()
97
+        except Exception:
98
+            redirect = reverse('horizon:project:opaque_data:index')
99
+            msg = _('Unable to retrieve details for opaque_data "%s".')\
100
+                % (self.kwargs['object_id'])
101
+            exceptions.handle(self.request, msg,
102
+                              redirect=redirect)
103
+        return created_date
104
+
105
+    @memoized.memoized_method
106
+    def _get_data_bytes(self, obj):
107
+        try:
108
+            data_bytes = binascii.hexlify(obj.get_encoded())
109
+        except Exception:
110
+            redirect = reverse('horizon:project:opaque_data:index')
111
+            msg = _('Unable to retrieve details for opaque_data "%s".')\
112
+                % (self.kwargs['object_id'])
113
+            exceptions.handle(self.request, msg,
114
+                              redirect=redirect)
115
+        return data_bytes
116
+
117
+    def get_context_data(self, **kwargs):
118
+        """Gets the context data for key."""
119
+        context = super(DetailView, self).get_context_data(**kwargs)
120
+        obj = self._get_data()
121
+        context['object'] = obj
122
+        context['object_created_date'] = self._get_data_created_date(obj)
123
+        context['object_bytes'] = self._get_data_bytes(obj)
124
+        return context

+ 23
- 0
castellan_ui/enabled/_95_project_key_manager_opaque_data_panel.py View File

@@ -0,0 +1,23 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
14
+PANEL = 'opaque_data'
15
+# The slug of the panel group the PANEL is associated with.
16
+PANEL_GROUP = 'key_manager'
17
+# The slug of the dashboard the PANEL associated with. Required.
18
+PANEL_DASHBOARD = 'project'
19
+
20
+ADD_INSTALLED_APP = ['castellan_ui', ]
21
+
22
+# Python panel class of the PANEL to be added.
23
+ADD_PANEL = 'castellan_ui.content.opaque_data.panel.OpaqueData'

+ 9
- 0
castellan_ui/templates/_opaque_data_import.html View File

@@ -0,0 +1,9 @@
1
+{% extends '_object_import.html' %}
2
+{% load i18n %}
3
+
4
+{% block modal-body-right %}
5
+  <p>{% trans "When importing your object as a file, the raw bytes of the file will be the raw bytes of the object. If you open the file using a text editor, it may not be human-readable because the bytes may not map to ASCII characters." %}</p>
6
+  <p>{% trans "To import your object using direct input, use the hex dump of the value of the object. For example, it may look like this:" %}</p>
7
+  <p><pre>00112233445566778899aabbccddeeff</pre></p>
8
+{% endblock %}
9
+

+ 23
- 0
castellan_ui/templates/opaque_data.html View File

@@ -0,0 +1,23 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Opaque Data" %}{% endblock %}
4
+
5
+{% block breadcrumb_nav %}
6
+  <ol class = "breadcrumb">
7
+    <li>{% trans "Project" %}</li>
8
+    <li>{% trans "Key Manager" %}</li>
9
+    <li class="active">{% trans "Opaque Data" %}</li>
10
+  </ol>
11
+{% endblock %}
12
+
13
+{% block page_header %}
14
+  <hz-page-header header="{% trans "Opaque Data" %}"></hz-page-header>
15
+{% endblock page_header %}
16
+
17
+{% block main %}
18
+<div class="row">
19
+  <div class="col-sm-12">
20
+  {{ opaque_data_table.render }}
21
+  </div>
22
+</div>
23
+{% endblock %}

+ 24
- 0
castellan_ui/templates/opaque_data_detail.html View File

@@ -0,0 +1,24 @@
1
+{% extends 'base.html' %}
2
+{% load i18n parse_date %}
3
+
4
+{% block title %}{{ page_title }}{% endblock %}
5
+
6
+{% block page_header %}
7
+  {% include "horizon/common/_detail_header.html" %}
8
+{% endblock %}
9
+
10
+{% block main %}
11
+<div class="detail">
12
+  <dl class="dl-horizontal">
13
+    <dt>{% trans "Name" %}</dt>
14
+    <dd>{{ object.name|default:_("None") }}</dd>
15
+    <dt>{% trans "Created" %}</dt>
16
+    <dd>{{ object_created_date|parse_date}}</dd>
17
+    <dt>{% trans "Object Value (in hex)" %}</dt>
18
+    <dd>
19
+      <div class="key-text word-wrap">{{ object_bytes|default:_("None") }}</div>
20
+    </dd>
21
+
22
+  </dl>
23
+</div>
24
+{% endblock %}

+ 7
- 0
castellan_ui/templates/opaque_data_import.html View File

@@ -0,0 +1,7 @@
1
+{% extends 'base.html' %}
2
+{% load i18n %}
3
+{% block title %}{{ page_title }}{% endblock %}
4
+
5
+{% block main %}
6
+  {% include '_opaque_data_import.html' %}
7
+{% endblock %}

+ 0
- 0
castellan_ui/test/content/opaque_data/__init__.py View File


+ 109
- 0
castellan_ui/test/content/opaque_data/tests.py View File

@@ -0,0 +1,109 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    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, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+import base64
14
+import binascii
15
+from django.core.handlers import wsgi
16
+from django.core.urlresolvers import reverse
17
+from horizon import messages as horizon_messages
18
+import mock
19
+
20
+from castellan.common.objects import opaque_data
21
+from castellan_ui.api import client as api_castellan
22
+from castellan_ui.test import helpers as tests
23
+from castellan_ui.test import test_data
24
+
25
+INDEX_URL = reverse('horizon:project:opaque_data:index')
26
+
27
+
28
+class OpaqueDataViewTest(tests.APITestCase):
29
+
30
+    def setUp(self):
31
+        super(OpaqueDataViewTest, self).setUp()
32
+        self.data = test_data.opaque_data
33
+        self.data_b64_bytes = base64.b64encode(self.data.get_encoded())
34
+        self.mock_object(
35
+            api_castellan, "get", mock.Mock(return_value=self.data))
36
+        self.mock_object(api_castellan, "list", mock.Mock(return_value=[]))
37
+        self.mock_object(horizon_messages, "success")
38
+        FAKE_ENVIRON = {'REQUEST_METHOD': 'GET', 'wsgi.input': 'fake_input'}
39
+        self.request = wsgi.WSGIRequest(FAKE_ENVIRON)
40
+
41
+    def test_index(self):
42
+        data_list = [test_data.opaque_data, test_data.nameless_opaque_data]
43
+
44
+        self.mock_object(
45
+            api_castellan, "list", mock.Mock(return_value=data_list))
46
+
47
+        res = self.client.get(INDEX_URL)
48
+        self.assertEqual(res.status_code, 200)
49
+        self.assertTemplateUsed(res, 'opaque_data.html')
50
+        api_castellan.list.assert_called_with(
51
+            mock.ANY, object_type=opaque_data.OpaqueData)
52
+
53
+    def test_detail_view(self):
54
+        url = reverse('horizon:project:opaque_data:detail',
55
+                      args=[self.data.id])
56
+        self.mock_object(
57
+            api_castellan, "list", mock.Mock(return_value=[self.data]))
58
+        self.mock_object(
59
+            api_castellan, "get", mock.Mock(return_value=self.data))
60
+
61
+        res = self.client.get(url)
62
+        self.assertContains(
63
+            res, "<dt>Name</dt>\n    <dd>%s</dd>" % self.data.name, 1, 200)
64
+        api_castellan.get.assert_called_once_with(mock.ANY, self.data.id)
65
+
66
+    def test_import_data(self):
67
+        self.mock_object(
68
+            api_castellan, "list", mock.Mock(return_value=[self.data]))
69
+        url = reverse('horizon:project:opaque_data:import')
70
+        self.mock_object(
71
+            api_castellan, "import_object", mock.Mock(return_value=self.data))
72
+
73
+        data_input = (
74
+            binascii.hexlify(self.data.get_encoded()).decode('utf-8')
75
+        )
76
+
77
+        data_form_data = {
78
+            'source_type': 'raw',
79
+            'name': self.data.name,
80
+            'direct_input': data_input,
81
+        }
82
+
83
+        self.client.post(url, data_form_data)
84
+
85
+        api_castellan.import_object.assert_called_once_with(
86
+            mock.ANY,
87
+            object_type=opaque_data.OpaqueData,
88
+            data=self.data_b64_bytes,
89
+            name=self.data.name,
90
+        )
91
+
92
+    def test_delete_data(self):
93
+        self.mock_object(
94
+            api_castellan, "list", mock.Mock(return_value=[self.data]))
95
+        self.mock_object(api_castellan, "delete")
96
+
97
+        data_form_data = {
98
+            'action': 'opaque_data__delete__%s' % self.data.id
99
+        }
100
+
101
+        res = self.client.post(INDEX_URL, data_form_data)
102
+
103
+        api_castellan.list.assert_called_with(
104
+            mock.ANY, object_type=opaque_data.OpaqueData)
105
+        api_castellan.delete.assert_called_once_with(
106
+            mock.ANY,
107
+            self.data.id,
108
+        )
109
+        self.assertRedirectsNoFollow(res, INDEX_URL)

+ 12
- 0
castellan_ui/test/test_data.py View File

@@ -72,3 +72,15 @@ nameless_symmetric_key = objects.symmetric_key.SymmetricKey(
72 72
     name=None,
73 73
     created=1448088699,
74 74
     id=u'11111111-1111-1111-1111-111111111111')
75
+
76
+opaque_data = objects.opaque_data.OpaqueData(
77
+    data=b'\xde\xad\xbe\xef',
78
+    name=u'test opaque data',
79
+    created=1448088699,
80
+    id=u'00000000-0000-0000-0000-000000000000')
81
+
82
+nameless_opaque_data = objects.opaque_data.OpaqueData(
83
+    data=b'\xde\xad\xbe\xef',
84
+    name=None,
85
+    created=1448088699,
86
+    id=u'11111111-1111-1111-1111-111111111111')

Loading…
Cancel
Save