diff --git a/.gitignore b/.gitignore
index 2c9091b..1bd1bb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,3 +130,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# Upload folder
+uploads/**
diff --git a/README.md b/README.md
index f3a4089..d97b8b9 100644
--- a/README.md
+++ b/README.md
@@ -36,17 +36,20 @@ This repository is providing backend service for MonAPI, written in Python and u
- [x] API Test
- [x] Multi-Step API Monitor
- [x] Team Management
-- [ ] Integrated Status Page
+- [x] Integrated Status Page
## Related Documentation
- [Run Development Server](https://github.com/MonAPI-xyz/MonAPI/blob/staging/docs/development.md)
## Latest Release Notes
-Version: v0.5.0
-Date: 28th November 2022
-1. Team Management
-2. Alerts User-Defined Timezone
+Version: v1.0.0
+Date: 12th December 2022
+1. Status Page Integration
+2. Email register verification
+3. Create new category directly when create API Monitor
+4. Partial Assertions Text
+5. Release 1st version of MonAPI
Full release notes can be found in [Release Notes](https://github.com/MonAPI-xyz/MonAPI/blob/staging/docs/release_notes.md)
@@ -56,6 +59,8 @@ Full release notes can be found in [Release Notes](https://github.com/MonAPI-xyz
📝 [Blog Site - https://blog.monapi.xyz](https://blog.monapi.xyz)
+📝 [User Manual - https://docs.monapi.xyz](https://docs.monapi.xyz)
+
## Our Teams
- Lucky Susanto
- Ferdi Fadillah
diff --git a/apimonitor/migrations/0017_apimonitor_status_page_category.py b/apimonitor/migrations/0017_apimonitor_status_page_category.py
new file mode 100644
index 0000000..60cb791
--- /dev/null
+++ b/apimonitor/migrations/0017_apimonitor_status_page_category.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.1.2 on 2022-11-29 02:00
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('statuspage', '0001_initial'),
+ ('apimonitor', '0016_merge_20221116_1054'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='apimonitor',
+ name='status_page_category',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='statuspage.statuspagecategory'),
+ ),
+ ]
diff --git a/apimonitor/migrations/0018_alter_apimonitor_assertion_type.py b/apimonitor/migrations/0018_alter_apimonitor_assertion_type.py
new file mode 100644
index 0000000..5e78826
--- /dev/null
+++ b/apimonitor/migrations/0018_alter_apimonitor_assertion_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2022-12-09 04:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('apimonitor', '0017_apimonitor_status_page_category'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='apimonitor',
+ name='assertion_type',
+ field=models.CharField(choices=[('DISABLED', 'Disabled'), ('TEXT', 'Text'), ('JSON', 'JSON'), ('PARTIAL', 'Partial')], default='DISABLED', max_length=16),
+ ),
+ ]
diff --git a/apimonitor/models.py b/apimonitor/models.py
index 0a4362d..b62eb8e 100644
--- a/apimonitor/models.py
+++ b/apimonitor/models.py
@@ -3,6 +3,7 @@
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from login.models import Team
+from statuspage.models import StatusPageCategory
class APIMonitor(models.Model):
method_choices = [
@@ -34,6 +35,7 @@ class APIMonitor(models.Model):
('DISABLED', 'Disabled'),
('TEXT', 'Text'),
('JSON', 'JSON'),
+ ('PARTIAL', 'Partial'),
]
team = models.ForeignKey(Team, on_delete=models.CASCADE)
@@ -47,6 +49,7 @@ class APIMonitor(models.Model):
assertion_value = models.TextField(blank=True)
is_assert_json_schema_only = models.BooleanField(default=False)
last_notified = models.DateTimeField(null=True, blank=True)
+ status_page_category = models.ForeignKey(StatusPageCategory, null=True, blank=True, on_delete=models.SET_NULL)
class APIMonitorQueryParam(models.Model):
diff --git a/apimonitor/serializers.py b/apimonitor/serializers.py
index 949cc54..de5d269 100644
--- a/apimonitor/serializers.py
+++ b/apimonitor/serializers.py
@@ -2,6 +2,7 @@
from apimonitor.models import APIMonitor, APIMonitorQueryParam, APIMonitorHeader, APIMonitorRawBody, \
APIMonitorResult, AssertionExcludeKey
+from statuspage.serializers import StatusPageCategorySerializers
class APIMonitorQueryParamSerializer(serializers.ModelSerializer):
class Meta:
@@ -76,6 +77,7 @@ class APIMonitorSerializer(serializers.ModelSerializer):
body_form = APIMonitorBodyFormSerializer(many=True, required=False, allow_null=True)
raw_body = APIMonitorRawBodySerializer(required=False, allow_null=True)
previous_step_id = serializers.PrimaryKeyRelatedField(read_only=True, many=False)
+ status_page_category_id = serializers.PrimaryKeyRelatedField(read_only=True, many=False)
exclude_keys = AssertionExcludeKeySerializer(many=True, required=False, allow_null=True)
class Meta:
@@ -96,6 +98,7 @@ class Meta:
'assertion_value',
'is_assert_json_schema_only',
'exclude_keys',
+ 'status_page_category_id',
]
@@ -147,6 +150,7 @@ class APIMonitorDashboardSerializer(serializers.Serializer):
class APIMonitorRetrieveSerializer(APIMonitorSerializer):
success_rate = APIMonitorDetailSuccessRateSerializer(many=True)
response_time = APIMonitorDetailResponseTimeSerializer(many=True)
+ status_page_category = StatusPageCategorySerializers()
class Meta:
model = APIMonitor
@@ -168,4 +172,6 @@ class Meta:
'assertion_value',
'is_assert_json_schema_only',
'exclude_keys',
+ 'status_page_category_id',
+ 'status_page_category',
]
diff --git a/apimonitor/tests.py b/apimonitor/tests.py
index f25319e..0132bea 100644
--- a/apimonitor/tests.py
+++ b/apimonitor/tests.py
@@ -15,6 +15,7 @@
from apimonitor.models import APIMonitor, APIMonitorBodyForm, APIMonitorHeader, APIMonitorQueryParam, APIMonitorRawBody, \
APIMonitorResult, AssertionExcludeKey
from login.models import Team, TeamMember, MonAPIToken
+from statuspage.models import StatusPageCategory
class DetailListAPIMonitor(APITestCase):
@@ -79,7 +80,7 @@ def test_retrieve_30_min_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "30MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body":
- None, 'previous_step_id': None, "success_rate": [
+ None, 'previous_step_id': None, 'status_page_category': None, 'status_page_category_id': None, "success_rate": [
{"start_time": "2022-09-20T09:30:00+07:00", "end_time": "2022-09-20T09:31:00+07:00",
"success": 0, "failed": 0},
{"start_time": "2022-09-20T09:31:00+07:00", "end_time": "2022-09-20T09:32:00+07:00",
@@ -209,7 +210,7 @@ def test_retrieve_30_min_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "30MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body":
- None, 'previous_step_id': None, "success_rate": [
+ None, 'previous_step_id': None, 'status_page_category': None, 'status_page_category_id': None, "success_rate": [
{"start_time": "2022-09-20T09:30:00+07:00", "end_time": "2022-09-20T09:31:00+07:00",
"success": 0, "failed": 0},
{"start_time": "2022-09-20T09:31:00+07:00", "end_time": "2022-09-20T09:32:00+07:00",
@@ -338,6 +339,7 @@ def test_retrieve_1_hour_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "60MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-20T09:00:00+07:00", "end_time": "2022-09-20T09:02:00+07:00",
"success": 0, "failed": 0},
@@ -469,6 +471,7 @@ def test_retrieve_1_hour_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "60MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-20T09:00:00+07:00", "end_time": "2022-09-20T09:02:00+07:00",
"success": 0, "failed": 0},
@@ -601,6 +604,7 @@ def test_retrieve_3_hours_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "180MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-20T07:00:00+07:00", "end_time": "2022-09-20T07:05:00+07:00",
"success": 0, "failed": 0},
@@ -756,6 +760,7 @@ def test_retrieve_3_hours_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "180MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-20T07:00:00+07:00", "end_time": "2022-09-20T07:05:00+07:00",
"success": 0, "failed": 0},
@@ -909,6 +914,7 @@ def test_retrieve_6_hours_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "360MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-20T04:00:00+07:00", "end_time": "2022-09-20T04:10:00+07:00",
"success": 0, "failed": 0},
@@ -1063,7 +1069,7 @@ def test_retrieve_6_hours_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "360MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [],
- "raw_body": None, 'previous_step_id': None, "success_rate": [
+ "raw_body": None, 'previous_step_id': None, 'status_page_category': None, 'status_page_category_id': None, "success_rate": [
{"start_time": "2022-09-20T04:00:00+07:00", "end_time": "2022-09-20T04:10:00+07:00",
"success": 0, "failed": 0},
{"start_time": "2022-09-20T04:10:00+07:00", "end_time": "2022-09-20T04:20:00+07:00",
@@ -1216,7 +1222,8 @@ def test_retrieve_12_hours_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "720MIN",
- "body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ "body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-19T22:00:00+07:00", "end_time": "2022-09-19T22:20:00+07:00",
"success": 0, "failed": 0},
@@ -1372,6 +1379,7 @@ def test_retrieve_12_hours_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "720MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-19T22:00:00+07:00", "end_time": "2022-09-19T22:20:00+07:00",
"success": 0, "failed": 0},
@@ -1527,6 +1535,7 @@ def test_retrieve_24_hours_with_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "1440MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-19T10:00:00+07:00", "end_time": "2022-09-19T10:30:00+07:00",
"success": 0, "failed": 0},
@@ -1625,151 +1634,151 @@ def test_retrieve_24_hours_with_result(self):
"success": 0, "failed": 0},
{"start_time": "2022-09-20T09:30:00+07:00", "end_time": "2022-09-20T10:00:00+07:00",
"success": 1, "failed": 0}], "response_time": [{"start_time":
- "2022-09-19T10:00:00+07:00",
- "end_time": "2022-09-19T10:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T10:30:00+07:00",
- "end_time": "2022-09-19T11:00:00+07:00",
- "avg": 0},
- {
- "start_time": "2022-09-19T11:00:00+07:00",
- "end_time": "2022-09-19T11:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T11:30:00+07:00",
- "end_time": "2022-09-19T12:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T12:00:00+07:00",
- "end_time": "2022-09-19T12:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T12:30:00+07:00",
- "end_time": "2022-09-19T13:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T13:00:00+07:00",
- "end_time": "2022-09-19T13:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T13:30:00+07:00",
- "end_time": "2022-09-19T14:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T14:00:00+07:00",
- "end_time": "2022-09-19T14:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T14:30:00+07:00",
- "end_time": "2022-09-19T15:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T15:00:00+07:00",
- "end_time": "2022-09-19T15:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T15:30:00+07:00",
- "end_time": "2022-09-19T16:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T16:00:00+07:00",
- "end_time": "2022-09-19T16:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T16:30:00+07:00",
- "end_time": "2022-09-19T17:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T17:00:00+07:00",
- "end_time": "2022-09-19T17:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T17:30:00+07:00",
- "end_time": "2022-09-19T18:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T18:00:00+07:00",
- "end_time": "2022-09-19T18:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T18:30:00+07:00",
- "end_time": "2022-09-19T19:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T19:00:00+07:00",
- "end_time": "2022-09-19T19:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T19:30:00+07:00",
- "end_time": "2022-09-19T20:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T20:00:00+07:00",
- "end_time": "2022-09-19T20:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T20:30:00+07:00",
- "end_time": "2022-09-19T21:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T21:00:00+07:00",
- "end_time": "2022-09-19T21:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T21:30:00+07:00",
- "end_time": "2022-09-19T22:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T22:00:00+07:00",
- "end_time": "2022-09-19T22:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T22:30:00+07:00",
- "end_time": "2022-09-19T23:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T23:00:00+07:00",
- "end_time": "2022-09-19T23:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-19T23:30:00+07:00",
- "end_time": "2022-09-20T00:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T00:00:00+07:00",
- "end_time": "2022-09-20T00:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T00:30:00+07:00",
- "end_time": "2022-09-20T01:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T01:00:00+07:00",
- "end_time": "2022-09-20T01:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T01:30:00+07:00",
- "end_time": "2022-09-20T02:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T02:00:00+07:00",
- "end_time": "2022-09-20T02:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T02:30:00+07:00",
- "end_time": "2022-09-20T03:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T03:00:00+07:00",
- "end_time": "2022-09-20T03:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T03:30:00+07:00",
- "end_time": "2022-09-20T04:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T04:00:00+07:00",
- "end_time": "2022-09-20T04:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T04:30:00+07:00",
- "end_time": "2022-09-20T05:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T05:00:00+07:00",
- "end_time": "2022-09-20T05:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T05:30:00+07:00",
- "end_time": "2022-09-20T06:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T06:00:00+07:00",
- "end_time": "2022-09-20T06:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T06:30:00+07:00",
- "end_time": "2022-09-20T07:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T07:00:00+07:00",
- "end_time": "2022-09-20T07:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T07:30:00+07:00",
- "end_time": "2022-09-20T08:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T08:00:00+07:00",
- "end_time": "2022-09-20T08:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T08:30:00+07:00",
- "end_time": "2022-09-20T09:00:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T09:00:00+07:00",
- "end_time": "2022-09-20T09:30:00+07:00",
- "avg": 0}, {
- "start_time": "2022-09-20T09:30:00+07:00",
- "end_time": "2022-09-20T10:00:00+07:00",
- "avg": 100}],
+ "2022-09-19T10:00:00+07:00",
+ "end_time": "2022-09-19T10:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T10:30:00+07:00",
+ "end_time": "2022-09-19T11:00:00+07:00",
+ "avg": 0},
+ {
+ "start_time": "2022-09-19T11:00:00+07:00",
+ "end_time": "2022-09-19T11:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T11:30:00+07:00",
+ "end_time": "2022-09-19T12:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T12:00:00+07:00",
+ "end_time": "2022-09-19T12:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T12:30:00+07:00",
+ "end_time": "2022-09-19T13:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T13:00:00+07:00",
+ "end_time": "2022-09-19T13:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T13:30:00+07:00",
+ "end_time": "2022-09-19T14:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T14:00:00+07:00",
+ "end_time": "2022-09-19T14:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T14:30:00+07:00",
+ "end_time": "2022-09-19T15:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T15:00:00+07:00",
+ "end_time": "2022-09-19T15:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T15:30:00+07:00",
+ "end_time": "2022-09-19T16:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T16:00:00+07:00",
+ "end_time": "2022-09-19T16:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T16:30:00+07:00",
+ "end_time": "2022-09-19T17:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T17:00:00+07:00",
+ "end_time": "2022-09-19T17:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T17:30:00+07:00",
+ "end_time": "2022-09-19T18:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T18:00:00+07:00",
+ "end_time": "2022-09-19T18:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T18:30:00+07:00",
+ "end_time": "2022-09-19T19:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T19:00:00+07:00",
+ "end_time": "2022-09-19T19:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T19:30:00+07:00",
+ "end_time": "2022-09-19T20:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T20:00:00+07:00",
+ "end_time": "2022-09-19T20:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T20:30:00+07:00",
+ "end_time": "2022-09-19T21:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T21:00:00+07:00",
+ "end_time": "2022-09-19T21:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T21:30:00+07:00",
+ "end_time": "2022-09-19T22:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T22:00:00+07:00",
+ "end_time": "2022-09-19T22:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T22:30:00+07:00",
+ "end_time": "2022-09-19T23:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T23:00:00+07:00",
+ "end_time": "2022-09-19T23:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-19T23:30:00+07:00",
+ "end_time": "2022-09-20T00:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T00:00:00+07:00",
+ "end_time": "2022-09-20T00:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T00:30:00+07:00",
+ "end_time": "2022-09-20T01:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T01:00:00+07:00",
+ "end_time": "2022-09-20T01:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T01:30:00+07:00",
+ "end_time": "2022-09-20T02:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T02:00:00+07:00",
+ "end_time": "2022-09-20T02:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T02:30:00+07:00",
+ "end_time": "2022-09-20T03:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T03:00:00+07:00",
+ "end_time": "2022-09-20T03:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T03:30:00+07:00",
+ "end_time": "2022-09-20T04:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T04:00:00+07:00",
+ "end_time": "2022-09-20T04:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T04:30:00+07:00",
+ "end_time": "2022-09-20T05:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T05:00:00+07:00",
+ "end_time": "2022-09-20T05:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T05:30:00+07:00",
+ "end_time": "2022-09-20T06:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T06:00:00+07:00",
+ "end_time": "2022-09-20T06:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T06:30:00+07:00",
+ "end_time": "2022-09-20T07:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T07:00:00+07:00",
+ "end_time": "2022-09-20T07:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T07:30:00+07:00",
+ "end_time": "2022-09-20T08:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T08:00:00+07:00",
+ "end_time": "2022-09-20T08:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T08:30:00+07:00",
+ "end_time": "2022-09-20T09:00:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T09:00:00+07:00",
+ "end_time": "2022-09-20T09:30:00+07:00",
+ "avg": 0}, {
+ "start_time": "2022-09-20T09:30:00+07:00",
+ "end_time": "2022-09-20T10:00:00+07:00",
+ "avg": 100}],
'assertion_type': 'DISABLED', 'assertion_value': '', 'is_assert_json_schema_only': False, 'exclude_keys': []
})
@@ -1779,6 +1788,7 @@ def test_retrieve_24_hours_without_result(self):
self.assertEqual(response.data,
{"id": 1, "name": "Test Monitor", "method": "GET", "url": "Test Path", "schedule": "1440MIN",
"body_type": "FORM", "query_params": [], "headers": [], "body_form": [], "raw_body": None, 'previous_step_id': None,
+ 'status_page_category': None, 'status_page_category_id': None,
"success_rate": [
{"start_time": "2022-09-19T10:00:00+07:00", "end_time": "2022-09-19T10:30:00+07:00",
"success": 0, "failed": 0},
@@ -1820,9 +1830,9 @@ def test_retrieve_24_hours_without_result(self):
"success": 0, "failed": 0},
{"start_time": "2022-09-19T19:30:00+07:00", "end_time": "2022-09-19T20:00:00+07:00",
"success": 0, "failed": 0}, {"start_time":
- "2022-09-19T20:00:00+07:00",
- "end_time": "2022-09-19T20:30:00+07:00", "success": 0,
- "failed": 0},
+ "2022-09-19T20:00:00+07:00",
+ "end_time": "2022-09-19T20:30:00+07:00", "success": 0,
+ "failed": 0},
{"start_time": "2022-09-19T20:30:00+07:00", "end_time": "2022-09-19T21:00:00+07:00",
"success": 0, "failed": 0},
{"start_time": "2022-09-19T21:00:00+07:00", "end_time": "2022-09-19T21:30:00+07:00",
@@ -3311,7 +3321,8 @@ def test_user_can_create_api_monitor_without_previous_step(self):
"assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
}
)
@@ -3365,7 +3376,8 @@ def test_user_can_create_api_monitor_with_empty_previous_step(self):
"assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
}
)
@@ -3428,9 +3440,11 @@ def test_user_can_create_api_monitor_with_valid_previous_step(self):
"assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
}
)
+
def test_user_cannot_create_api_monitor_with_other_user_previous_step_id(self):
# Create a user object
user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
@@ -3519,6 +3533,139 @@ def test_user_can_create_api_monitor_with_invalid_previous_step_1(self):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data['error'], "Please make sure your [previous step id] is valid and exist!")
self.assertEqual(APIMonitor.objects.all().count(), 0)
+
+ def test_user_can_create_api_monitor_with_valid_statuspage_category_id(self):
+ # Create a user object
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+ statuspage_category = StatusPageCategory.objects.create(team=team, name='category')
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ monitor_value = {
+ 'user': 'test@test.com',
+ 'name': 'Test Monitor',
+ 'method': 'GET',
+ 'url': 'Test Path',
+ 'schedule': '10MIN',
+ 'body_type': 'EMPTY',
+ 'status_page_category_id': statuspage_category.id,
+ }
+
+ # Expected JSON from frontend
+ received_json = {
+ 'name': monitor_value['name'],
+ 'method': monitor_value['method'],
+ 'url': monitor_value['url'],
+ 'schedule': monitor_value['schedule'],
+ 'body_type': monitor_value['body_type'],
+ 'status_page_category_id': monitor_value['status_page_category_id'],
+ }
+
+ # Get path
+ create_new_monitor_test_path = reverse('api-monitor-list')
+ response = self.client.post(create_new_monitor_test_path, data=received_json, format='json', **header)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(APIMonitor.objects.all().count(), 1)
+ self.assertEqual(response.data,
+ {
+ "id": 1,
+ "name": "Test Monitor",
+ "method": "GET",
+ "url": "Test Path",
+ "schedule": "10MIN",
+ "body_type": "EMPTY",
+ "query_params": [],
+ "headers": [],
+ "body_form": [],
+ "raw_body": None,
+ "previous_step_id": None,
+ "assertion_type": "DISABLED",
+ "assertion_value": "",
+ "is_assert_json_schema_only": False,
+ "exclude_keys": [],
+ "status_page_category_id": statuspage_category.id,
+ }
+ )
+
+ def test_user_cannot_create_api_monitor_with_other_user_status_page_id(self):
+ # Create a user object
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ user_two = User.objects.create_user(username="test2@test.com", email="test2@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user_two)
+
+ status_page = StatusPageCategory.objects.create(team=team, name='category2')
+
+ monitor_value = {
+ 'user': 'test@test.com',
+ 'name': 'Test Monitor',
+ 'method': 'GET',
+ 'url': 'Test Path',
+ 'schedule': '10MIN',
+ 'body_type': 'EMPTY',
+ 'status_page_category_id': status_page.id,
+ }
+
+ received_json = {
+ 'name': monitor_value['name'],
+ 'method': monitor_value['method'],
+ 'url': monitor_value['url'],
+ 'schedule': monitor_value['schedule'],
+ 'body_type': monitor_value['body_type'],
+ 'status_page_category_id': monitor_value['status_page_category_id'],
+ }
+
+ create_new_monitor_test_path = reverse('api-monitor-list')
+ response = self.client.post(create_new_monitor_test_path, data=received_json, format='json', **header)
+ self.assertEqual(APIMonitor.objects.all().count(), 0)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data['error'], "Please make sure your [status page category] is valid and exist!")
+
+
+ def test_user_can_create_api_monitor_with_invalid_status_page_category(self):
+ # Create a user object
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ monitor_value = {
+ 'user': 'test@test.com',
+ 'name': 'Test Monitor',
+ 'method': 'GET',
+ 'url': 'Test Path',
+ 'schedule': '10MIN',
+ 'status_page_category_id': -1,
+ 'body_type': 'EMPTY',
+ }
+
+ # Expected JSON from frontend
+ received_json = {
+ 'name': monitor_value['name'],
+ 'method': monitor_value['method'],
+ 'url': monitor_value['url'],
+ 'schedule': monitor_value['schedule'],
+ 'status_page_category_id': monitor_value['status_page_category_id'],
+ 'body_type': monitor_value['body_type'],
+ }
+
+ # Get path
+ create_new_monitor_test_path = reverse('api-monitor-list')
+ response = self.client.post(create_new_monitor_test_path, data=received_json, format='json', **header)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data['error'], "Please make sure your [status page category] is valid and exist!")
+ self.assertEqual(APIMonitor.objects.all().count(), 0)
def test_user_can_create_api_monitor_with_invalid_previous_step_2(self):
# Create a user object
@@ -4710,6 +4857,128 @@ def test_user_must_put_valid_previous_step(self):
response = self.client.put(edit_monitor_path, data=received_json, format='json', **header)
self.assertEqual(response.data['error'], ['Please make sure your [previous step id] is valid and exist!'])
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_can_edit_status_page_category(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+ monitor_value = {
+ 'user': 'test@test.com',
+ 'name': 'Test Monitor',
+ 'method': 'GET',
+ 'url': 'Test Path',
+ 'schedule': '10MIN',
+ 'body_type': 'RAW',
+ }
+
+ queryparam_value = [
+ {
+ 'key': 'key1',
+ 'value': 'value1',
+ },
+ {
+ 'key': 'key2',
+ 'value': 'value2'
+ }
+ ]
+ monitorheader_value = [{
+ 'key': 'key3',
+ 'value': 'value3'
+ }]
+ monitorbodyform_value = "This doesn't matter since body type is FORM"
+ monitorrawbody_value = "Valid raw body"
+
+ # Expected JSON from frontend
+ received_json = {
+ 'name': monitor_value['name'],
+ 'method': monitor_value['method'],
+ 'url': monitor_value['url'],
+ 'schedule': monitor_value['schedule'],
+ 'body_type': monitor_value['body_type'],
+ 'query_params': queryparam_value,
+ 'headers': monitorheader_value,
+ 'body_form': monitorbodyform_value,
+ 'raw_body': monitorrawbody_value
+ }
+
+ # 1. Create Object id = 1
+ create_new_monitor_test_path = reverse('api-monitor-list')
+ self.client.post(create_new_monitor_test_path, data=received_json, format='json', **header)
+
+ # 2. Create Status Page Category
+ status_page_category = StatusPageCategory.objects.create(team=team, name='category')
+
+ # 3. Edit object id = 1 status page
+ received_json['status_page_category_id'] = status_page_category.id
+ target_monitor_id = APIMonitor.objects.filter(team=team)[0].id
+ edit_monitor_path = reverse('api-monitor-detail', kwargs={'pk': target_monitor_id})
+ response = self.client.put(edit_monitor_path, data=received_json, format='json', **header)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['status_page_category_id'], status_page_category.id)
+
+ def test_user_input_invalid_status_page_category_then_return_error(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+ monitor_value = {
+ 'user': 'test@test.com',
+ 'name': 'Test Monitor',
+ 'method': 'GET',
+ 'url': 'Test Path',
+ 'schedule': '10MIN',
+ 'body_type': 'RAW',
+ }
+
+ queryparam_value = [
+ {
+ 'key': 'key1',
+ 'value': 'value1',
+ },
+ {
+ 'key': 'key2',
+ 'value': 'value2'
+ }
+ ]
+ monitorheader_value = [{
+ 'key': 'key3',
+ 'value': 'value3'
+ }]
+ monitorbodyform_value = "This doesn't matter since body type is FORM"
+ monitorrawbody_value = "Valid raw body"
+
+ # Expected JSON from frontend
+ received_json = {
+ 'name': monitor_value['name'],
+ 'method': monitor_value['method'],
+ 'url': monitor_value['url'],
+ 'schedule': monitor_value['schedule'],
+ 'body_type': monitor_value['body_type'],
+ 'query_params': queryparam_value,
+ 'headers': monitorheader_value,
+ 'body_form': monitorbodyform_value,
+ 'raw_body': monitorrawbody_value
+ }
+
+ # 1. Create Object id = 1
+ create_new_monitor_test_path = reverse('api-monitor-list')
+ self.client.post(create_new_monitor_test_path, data=received_json, format='json', **header)
+
+ # 2. Create Status Page Category
+ status_page_category = StatusPageCategory.objects.create(team=team, name='category')
+
+ # 3. Edit object id = 1 status page
+ received_json['status_page_category_id'] = '999'
+ target_monitor_id = APIMonitor.objects.filter(team=team)[0].id
+ edit_monitor_path = reverse('api-monitor-detail', kwargs={'pk': target_monitor_id})
+ response = self.client.put(edit_monitor_path, data=received_json, format='json', **header)
+ self.assertEqual(response.data['error'], ['Please make sure your [status page category] is valid and exist!'])
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class TeamMigrationsTest(MigratorTestCase):
diff --git a/apimonitor/views.py b/apimonitor/views.py
index 10a6ee3..bcfc5a8 100644
--- a/apimonitor/views.py
+++ b/apimonitor/views.py
@@ -15,6 +15,7 @@
APIMonitorBodyFormSerializer, APIMonitorRawBodySerializer,
APIMonitorRetrieveSerializer, APIMonitorDashboardSerializer,
AssertionExcludeKeySerializer)
+from statuspage.models import StatusPageCategory
class APIMonitorViewSet(mixins.ListModelMixin,
@@ -41,6 +42,7 @@ def get_monitor_data_from_request(self, request):
'schedule': request.data.get('schedule'),
'body_type': request.data.get('body_type'),
'previous_step_id': None if request.data.get('previous_step_id') == '' else request.data.get('previous_step_id', None),
+ 'status_page_category_id': None if request.data.get('status_page_category_id') == '' else request.data.get('status_page_category_id', None),
'assertion_type': request.data.get('assertion_type', "DISABLED"),
'assertion_value': request.data.get('assertion_value', ""),
'is_assert_json_schema_only': request.data.get('is_assert_json_schema_only', False)
@@ -50,28 +52,33 @@ def get_monitor_data_from_request(self, request):
# PBI-15-edit-api-monitor-backend
def update(self, request, *args, **kwargs):
monitor_data = self.get_monitor_data_from_request(request)
-
- if monitor_data['previous_step_id'] is not None:
- previous_step_id = try_parse_int(monitor_data['previous_step_id'])
- prev_api_monitor = APIMonitor.objects.filter(id=previous_step_id, team=request.auth.team)
- if prev_api_monitor.exists():
- monitor_data['previous_step_obj'] = prev_api_monitor[0]
- else:
- return Response(data={"error": ["Please make sure your [previous step id] is valid and exist!"]},
- status=status.HTTP_400_BAD_REQUEST)
- else:
- monitor_data['previous_step_obj'] = None
-
- api_monitor_serializer = APIMonitorSerializer(data=monitor_data)
+
monitor_obj = APIMonitor.objects.get(pk=kwargs['pk'])
+ api_monitor_serializer = APIMonitorSerializer(data=monitor_data)
+
if (api_monitor_serializer.is_valid()):
+ if monitor_data['previous_step_id'] is not None:
+ previous_step_id = try_parse_int(monitor_data['previous_step_id'])
+ prev_api_monitor = APIMonitor.objects.filter(id=previous_step_id, team=request.auth.team)
+ if not prev_api_monitor.exists():
+ return Response(data={"error": ["Please make sure your [previous step id] is valid and exist!"]},
+ status=status.HTTP_400_BAD_REQUEST)
+
+ if monitor_data['status_page_category_id'] is not None:
+ status_page_category_id = try_parse_int(monitor_data['status_page_category_id'])
+ status_page_category = StatusPageCategory.objects.filter(id=status_page_category_id, team=request.auth.team)
+ if not status_page_category.exists():
+ return Response(data={"error": ["Please make sure your [status page category] is valid and exist!"]},
+ status=status.HTTP_400_BAD_REQUEST)
+
# Saved
monitor_obj.name = monitor_data['name']
monitor_obj.method = monitor_data['method']
monitor_obj.url = monitor_data['url']
monitor_obj.schedule = monitor_data['schedule']
monitor_obj.body_type = monitor_data['body_type']
- monitor_obj.previous_step = monitor_data['previous_step_obj']
+ monitor_obj.previous_step_id = monitor_data['previous_step_id']
+ monitor_obj.status_page_category_id = monitor_data['status_page_category_id']
monitor_obj.assertion_type = monitor_data['assertion_type']
monitor_obj.assertion_value = monitor_data['assertion_value']
monitor_obj.is_assert_json_schema_only = monitor_data['is_assert_json_schema_only']
@@ -142,12 +149,22 @@ def create(self, request, *args, **kwargs):
if api_monitor_serializer.is_valid():
error_log = []
try:
- if monitor_data['previous_step_id'] == None or try_parse_int(monitor_data['previous_step_id']) and APIMonitor.objects.filter(id=monitor_data['previous_step_id'], team=request.auth.team).exists():
- monitor_obj = APIMonitor.objects.create(**monitor_data)
- else:
- error_log += ["Please make sure your [previous step id] is valid and exist!"]
- return Response(data={"error": f"{error_log[0]}"}, status=status.HTTP_400_BAD_REQUEST)
-
+ if monitor_data['previous_step_id'] != None:
+ prev_step_id = try_parse_int(monitor_data['previous_step_id'])
+ prev_step = APIMonitor.objects.filter(id=prev_step_id, team=request.auth.team)
+ if not prev_step.exists():
+ error_log += ["Please make sure your [previous step id] is valid and exist!"]
+ return Response(data={"error": f"{error_log[0]}"}, status=status.HTTP_400_BAD_REQUEST)
+
+ if monitor_data['status_page_category_id'] != None:
+ status_page_category_id = try_parse_int(monitor_data['status_page_category_id'])
+ status_page_category = StatusPageCategory.objects.filter(id=status_page_category_id, team=request.auth.team)
+ if not status_page_category.exists():
+ error_log += ["Please make sure your [status page category] is valid and exist!"]
+ return Response(data={"error": f"{error_log[0]}"}, status=status.HTTP_400_BAD_REQUEST)
+
+ monitor_obj = APIMonitor.objects.create(**monitor_data)
+
if request.data.get('query_params'):
for key_value_pair in request.data.get('query_params'):
if 'key' in key_value_pair and 'value' in key_value_pair:
diff --git a/cron/management/commands/run_cron.py b/cron/management/commands/run_cron.py
index a57d40c..0576d13 100644
--- a/cron/management/commands/run_cron.py
+++ b/cron/management/commands/run_cron.py
@@ -83,6 +83,8 @@ def run_api_monitor_assertions(self, monitor_id, response):
monitor = APIMonitor.objects.get(id=monitor_id)
if monitor.assertion_type == 'TEXT' and response != monitor.assertion_value:
raise AssertionError(f'Assertion text failed.\nExpected: "{monitor.assertion_value}"\nGot: "{response}"')
+ elif monitor.assertion_type == 'PARTIAL' and monitor.assertion_value not in response:
+ raise AssertionError(f'Partial Assertion text failed.\nExpected: "{monitor.assertion_value}"\nGot: "{response}"')
elif monitor.assertion_type == 'JSON':
try:
api_response = json.loads(response)
diff --git a/cron/tests.py b/cron/tests.py
index 8923e9c..5b66be9 100644
--- a/cron/tests.py
+++ b/cron/tests.py
@@ -918,6 +918,62 @@ def test_when_api_monitor_assert_text_failure_then_error(self, *args):
self.assertEqual(result[0].success, False)
self.assertEqual(result[0].log_response, "NonJSON response")
self.assertEqual(result[0].log_error, 'Assertion text failed.\nExpected: ""\nGot: "NonJSON response"')
+
+ @patch("cron.management.commands.run_cron.mock_cron_interrupt", side_effect=InterruptedError)
+ @patch("requests.get", mocked_request_get)
+ def test_when_api_monitor_partial_assert_text_success(self, *args):
+ team = Team.objects.create(name='test team')
+
+ APIMonitor.objects.create(
+ team=team,
+ name='apimonitor',
+ method='GET',
+ url='https://monapinonjson.xyz',
+ schedule='60MIN',
+ body_type='RAW',
+ assertion_type='PARTIAL',
+ assertion_value="NonJSON",
+ )
+
+ try:
+ self.call_command()
+ except InterruptedError:
+ pass
+ time.sleep(0.1)
+
+ result = APIMonitorResult.objects.all()
+ self.assertEqual(len(result), 1)
+ self.assertEqual(result[0].success, True)
+ self.assertEqual(result[0].log_response, "NonJSON response")
+ self.assertEqual(result[0].log_error, '')
+
+ @patch("cron.management.commands.run_cron.mock_cron_interrupt", side_effect=InterruptedError)
+ @patch("requests.get", mocked_request_get)
+ def test_when_api_monitor_partial_assert_text_failure_then_error(self, *args):
+ team = Team.objects.create(name='test team')
+
+ APIMonitor.objects.create(
+ team=team,
+ name='apimonitor',
+ method='GET',
+ url='https://monapinonjson.xyz',
+ schedule='60MIN',
+ body_type='RAW',
+ assertion_type='PARTIAL',
+ assertion_value='Monapi',
+ )
+
+ try:
+ self.call_command()
+ except InterruptedError:
+ pass
+ time.sleep(0.1)
+
+ result = APIMonitorResult.objects.all()
+ self.assertEqual(len(result), 1)
+ self.assertEqual(result[0].success, False)
+ self.assertEqual(result[0].log_response, "NonJSON response")
+ self.assertEqual(result[0].log_error, 'Partial Assertion text failed.\nExpected: "Monapi"\nGot: "NonJSON response"')
@patch("cron.management.commands.run_cron.mock_cron_interrupt", side_effect=InterruptedError)
@patch("requests.get", mocked_request_get)
diff --git a/docs/release_notes.md b/docs/release_notes.md
index 21ff06f..bacd0e7 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -40,3 +40,12 @@ Version: v0.5.0
Date: 28th November 2022
1. Team Management
2. Alerts User-Defined Timezone
+
+### Release 6
+Version: v1.0.0
+Date: 12th December 2022
+1. Status Page Integration
+2. Email register verification
+3. Create new category directly when create API Monitor
+4. Partial Assertions Text
+5. Release 1st version of MonAPI
diff --git a/error_logs/tests.py b/error_logs/tests.py
index a61acbe..59c2894 100644
--- a/error_logs/tests.py
+++ b/error_logs/tests.py
@@ -99,10 +99,11 @@ def test_when_authenticated_and_data_are_exists_then_view_error_logs_list_succes
"body_form": [],
"raw_body": None,
"previous_step_id": None,
- "assertion_type": "DISABLED",
+ "assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
},
"execution_time": "2022-09-20T10:00:00+07:00",
"response_time": 100,
@@ -128,7 +129,8 @@ def test_when_authenticated_and_data_are_exists_then_view_error_logs_list_succes
"assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
},
"execution_time": "2022-09-20T10:00:00+07:00",
"response_time": 75,
@@ -200,7 +202,8 @@ def test_when_authenticated_and_data_are_exists_then_view_error_logs_detail_succ
"assertion_type": "DISABLED",
"assertion_value": "",
"is_assert_json_schema_only": False,
- "exclude_keys": []
+ "exclude_keys": [],
+ "status_page_category_id": None,
},
"execution_time": "2022-09-20T10:00:00+07:00",
"response_time": 100,
diff --git a/forget_password/admin.py b/forget_password/admin.py
index 8c38f3f..6266755 100644
--- a/forget_password/admin.py
+++ b/forget_password/admin.py
@@ -1,3 +1,4 @@
from django.contrib import admin
+from forget_password.models import ForgetPasswordToken
-# Register your models here.
+admin.site.register(ForgetPasswordToken)
diff --git a/login/tests.py b/login/tests.py
index ae4ae5f..32fc5bf 100644
--- a/login/tests.py
+++ b/login/tests.py
@@ -5,12 +5,14 @@
from rest_framework.test import APITestCase
from login.models import MonAPIToken, TeamMember, Team, get_file_path
+from register.models import VerifiedUser
class LoginTest(APITestCase):
login_url = reverse('login')
def test_when_non_authenticated_and_login_without_team_then_return_success(self):
- User.objects.create_user(username='test@gmail.com', email='test@gmail.com', password='Test1234')
+ user = User.objects.create_user(username='test@gmail.com', email='test@gmail.com', password='Test1234')
+ VerifiedUser.objects.create(user=user, verified=True)
request_params = {'email': 'test@gmail.com', 'password': 'Test1234'}
response = self.client.post(self.login_url, request_params)
@@ -22,6 +24,7 @@ def test_when_non_authenticated_and_login_without_team_then_return_success(self)
def test_when_non_authenticated_and_team_exists_then_return_success(self):
user = User.objects.create_user(username='test@gmail.com', email='test@gmail.com', password='Test1234')
+ VerifiedUser.objects.create(user=user, verified=True)
team = Team.objects.create(name='test team')
team_member = TeamMember.objects.create(team=team, user=user, verified=True)
@@ -253,3 +256,21 @@ def test_when_invited_a_user_is_not_verified(self):
team_member = TeamMember.objects.create(user=user, team=team)
self.assertEqual(team_member.verified, False)
+
+class VerifiedUserTest(APITestCase):
+ login_url = reverse('login')
+
+ def test_user_can_not_login_if_account_not_verified(self):
+ user = User.objects.create_user(username="test@gmail.com", email="test@gmail.com", password="Test1234")
+ VerifiedUser.objects.create(user=user)
+ request_params = {'email': 'test@gmail.com', 'password': 'Test1234'}
+ response = self.client.post(self.login_url, request_params)
+ self.assertEqual(response.data['response'], 'User not yet verified.')
+
+ def test_existing_user_is_automatically_a_verified_user(self):
+ past_user = User.objects.create_user(username="test@gmail.com", email="test@gmail.com", password="Test1234")
+ request_params = {'email': 'test@gmail.com', 'password': 'Test1234'}
+ response = self.client.post(self.login_url, request_params)
+ self.assertEqual(response.data['response'], 'Sign-in successful.')
+ self.assertEqual(VerifiedUser.objects.all().count(), 1)
+
diff --git a/login/views.py b/login/views.py
index ee16f26..c38bec6 100644
--- a/login/views.py
+++ b/login/views.py
@@ -7,6 +7,8 @@
from login.serializers import LoginSerializer, TeamSerializers
from login.models import TeamMember, Team
from login.utils import generate_token
+from register.models import VerifiedUser
+from django.core.exceptions import ObjectDoesNotExist
@api_view(["POST"])
@authentication_classes([])
@@ -20,9 +22,23 @@ def user_login(request):
username=request.data['email'],
email=request.data['email'],
password=request.data['password'])
+
if not user:
data['response'] = 'Invalid email or password.'
- return Response(data=data, status=status.HTTP_401_UNAUTHORIZED)
+ return Response(data=data, status=status.HTTP_401_UNAUTHORIZED)
+
+ # Filter user based on VerifiedUser
+ try:
+ is_verified = VerifiedUser.objects.get(user=user).verified
+ except ObjectDoesNotExist:
+ # Legacy Support: Users that already create their account
+ # before this update will automatically be verified
+ VerifiedUser.objects.create(user=user, verified=True)
+ is_verified = True
+
+ if not is_verified:
+ data['response'] = 'User not yet verified.'
+ return Response(data=data, status=status.HTTP_401_UNAUTHORIZED)
data['response'] = 'Sign-in successful.'
data['email']= user.email
diff --git a/logout/admin.py b/logout/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/logout/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/logout/models.py b/logout/models.py
deleted file mode 100644
index 71a8362..0000000
--- a/logout/models.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
diff --git a/logout/tests.py b/logout/tests.py
deleted file mode 100644
index 963052d..0000000
--- a/logout/tests.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.contrib.auth.models import User
-from django.urls import reverse
-from rest_framework import status
-from rest_framework.test import APITestCase
-
-from login.models import Team, TeamMember, MonAPIToken
-
-class ListAPIMonitor(APITestCase):
- logout_url = reverse('logout')
-
- def test_when_non_authenticated_then_return_unauthorized(self):
- response = self.client.post(self.logout_url)
-
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
- self.assertEqual(response.data, {
- "detail": "Authentication credentials were not provided."
- })
-
- def test_when_authenticated_and_logout_then_return_success(self):
- # Create dummy user and authenticate
- user = User.objects.create_user(username='test', email='test@test.com', password='test123')
- team = Team.objects.create(name='test team')
- team_member = TeamMember.objects.create(team=team, user=user)
-
- token = MonAPIToken.objects.create(team_member=team_member)
-
- header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
- response = self.client.post(self.logout_url, **header)
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, 'User Logged out successfully')
\ No newline at end of file
diff --git a/logout/urls.py b/logout/urls.py
deleted file mode 100644
index e346187..0000000
--- a/logout/urls.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.urls import path
-from logout import views
-
-urlpatterns = [
- path('', views.user_logout, name='logout')
-]
\ No newline at end of file
diff --git a/logout/views.py b/logout/views.py
deleted file mode 100644
index 427c046..0000000
--- a/logout/views.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.contrib.auth import logout
-from rest_framework.decorators import api_view, permission_classes
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-
-
-@api_view(["POST"])
-@permission_classes([IsAuthenticated])
-def user_logout(request):
- request.auth.delete()
- logout(request)
-
- return Response('User Logged out successfully')
diff --git a/monapi/settings.py b/monapi/settings.py
index b393cdb..f875d2b 100644
--- a/monapi/settings.py
+++ b/monapi/settings.py
@@ -69,7 +69,6 @@
'alerts',
'apimonitor',
'cron',
- 'logout',
'register',
'password_validators',
'login',
@@ -78,6 +77,7 @@
'apitest',
'team_management',
'invite_team_members',
+ 'statuspage',
]
MIDDLEWARE = [
diff --git a/monapi/urls.py b/monapi/urls.py
index 21a5d14..d5caa18 100644
--- a/monapi/urls.py
+++ b/monapi/urls.py
@@ -28,15 +28,14 @@
path('admin/', admin.site.urls),
path('register/', include('register.urls')),
path('forget-password/', include('forget_password.urls')),
- path('logout/', include('logout.urls')),
path('monitor/', include('apimonitor.urls')),
- path('auth/', include('login.urls')), # New endpoint for authentication
- path('login/', include('login.urls')),
+ path('auth/', include('login.urls')),
path('error-logs/', include('error_logs.urls')),
path('alerts/', include('alerts.urls')),
path('api-test/', include('apitest.urls')),
path('team-management/', include('team_management.urls')),
path('invite-member/', include('invite_team_members.urls')),
+ path('status-page/', include('statuspage.urls')),
]
if settings.DEBUG:
diff --git a/register/admin.py b/register/admin.py
new file mode 100644
index 0000000..746546c
--- /dev/null
+++ b/register/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+from register.models import VerifiedUser, VerifiedUserToken
+
+admin.site.register(VerifiedUser)
+admin.site.register(VerifiedUserToken)
diff --git a/register/migrations/0001_initial.py b/register/migrations/0001_initial.py
new file mode 100644
index 0000000..c48816b
--- /dev/null
+++ b/register/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.1.2 on 2022-12-03 09:21
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='VerifiedUser',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('verified', models.BooleanField(default=False)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='VerifiedUserToken',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('key', models.CharField(default=uuid.uuid4, max_length=256)),
+ ('verified_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='register.verifieduser')),
+ ],
+ ),
+ ]
diff --git a/logout/__init__.py b/register/migrations/__init__.py
similarity index 100%
rename from logout/__init__.py
rename to register/migrations/__init__.py
diff --git a/register/models.py b/register/models.py
index e69de29..fe99340 100644
--- a/register/models.py
+++ b/register/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+from django.contrib.auth.models import User
+import uuid
+
+
+class VerifiedUser(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ verified = models.BooleanField(default=False)
+
+
+class VerifiedUserToken(models.Model):
+ verified_user = models.ForeignKey(VerifiedUser, on_delete=models.CASCADE)
+ key = models.CharField(max_length=256, default=uuid.uuid4)
diff --git a/register/serializers.py b/register/serializers.py
index 2030394..12c84cb 100644
--- a/register/serializers.py
+++ b/register/serializers.py
@@ -35,3 +35,7 @@ def save(self):
user.set_password(password)
user.save()
return user
+
+
+class VerifiedUserTokenSerializer(serializers.Serializer):
+ key = serializers.CharField(max_length=256)
diff --git a/register/tests.py b/register/tests.py
index 9c24a90..5eb1fa1 100644
--- a/register/tests.py
+++ b/register/tests.py
@@ -4,6 +4,7 @@
from django.contrib.auth.models import User
from login.models import Team, TeamMember
+from register.models import VerifiedUser, VerifiedUserToken
class APIViewTestCase(APITestCase):
@@ -28,7 +29,7 @@ def test_user_can_register(self):
self.assertEqual(new_user.check_password('B0tch1ng'), True)
self.assertEqual(Team.objects.count(), 1)
- self.assertEqual(Team.objects.all()[0].name, "User1")
+ self.assertEqual(Team.objects.all()[0].name, "User1's Workspace")
self.assertEqual(TeamMember.objects.count(), 1)
self.assertEqual(TeamMember.objects.all()[0].user, new_user)
@@ -83,4 +84,62 @@ def test_only_one_user_per_email(self):
)
self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response2.data['response'],
- 'User already registered! Please use a different email to register.')
\ No newline at end of file
+ 'User already registered! Please use a different email to register.')
+
+class TestUserVerification(APITestCase):
+
+ def helper_register(self, email="user1@gmail.com", password="B0tch1ng"):
+ response = self.client.post(
+ reverse('register-api'),
+ {
+ 'email': email,
+ 'password': password,
+ 'password2': password,
+ }
+ )
+ return response
+
+ def test_register_create_an_unverified_user(self):
+ self.helper_register()
+ user = User.objects.get(email='user1@gmail.com')
+ verified_user = VerifiedUser.objects.get(user=user)
+ self.assertEqual(verified_user.verified, False)
+
+ def test_passing_valid_key_verify_an_unverified_user(self):
+ self.helper_register()
+ user = User.objects.get(email='user1@gmail.com')
+ verified_user = VerifiedUser.objects.get(user=user)
+ verified_user_token = VerifiedUserToken.objects.get(verified_user=verified_user)
+
+ self.assertEqual(verified_user.verified, False)
+ response = self.client.post(
+ reverse('verify-user'),
+ {
+ 'key': verified_user_token.key
+ }
+ )
+ self.assertEqual(response.data['response'], 'Success')
+ verified_user = VerifiedUser.objects.get(user=user)
+ self.assertEqual(verified_user.verified, True)
+
+ def test_passing_invalid_key_verify_an_unverified_user(self):
+ self.helper_register()
+
+ response = self.client.post(
+ reverse('verify-user'),
+ {
+ 'key': 'false-key'
+ }
+ )
+ self.assertEqual(response.data['response'], 'Token is invalid.')
+
+ def test_passing_non_key_verify_an_unverified_user(self):
+ self.helper_register()
+
+ response = self.client.post(
+ reverse('verify-user'),
+ {
+ 'invalid-key': "None"
+ }
+ )
+ self.assertEqual(response.data['response'], 'Pass a valid token.')
\ No newline at end of file
diff --git a/register/urls.py b/register/urls.py
index fadc675..4317fba 100644
--- a/register/urls.py
+++ b/register/urls.py
@@ -1,6 +1,7 @@
from django.urls import path
-from register.views import registration_view
+from register.views import registration_view, verify_view
urlpatterns = [
- path('api', registration_view, name='register-api')
+ path('api', registration_view, name='register-api'),
+ path('verify', verify_view, name='verify-user')
]
\ No newline at end of file
diff --git a/register/views.py b/register/views.py
index 359f3ec..8c408e7 100644
--- a/register/views.py
+++ b/register/views.py
@@ -1,10 +1,17 @@
+import os
+
from rest_framework.response import Response
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework import status
from django.db.utils import IntegrityError
-from register.serializers import RegistrationSerializer
+from register.serializers import RegistrationSerializer, VerifiedUserTokenSerializer
from login.models import Team, TeamMember
+from register.models import VerifiedUser, VerifiedUserToken
+
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.core.exceptions import ObjectDoesNotExist
@api_view(['POST', ])
@authentication_classes([])
@@ -16,8 +23,31 @@ def registration_view(request):
try:
user = serializer.save()
username = user.username.split('@')[0]
- team = Team.objects.create(name=username.title())
+ team = Team.objects.create(name=f"{username.title()}'s Workspace")
TeamMember.objects.create(user=user, team=team, verified=True)
+ verified_user = VerifiedUser.objects.create(user=user)
+ token = VerifiedUserToken.objects.create(verified_user=verified_user)
+ verify_url = f"{os.environ.get('FRONTEND_URL', '')}/verify?key={token.key}"
+
+ email_content = f'''
+ To verify your registration on MonAPI, please follow the link below.\n
+ If you did not register on MonAPI, you can ignore this email. \n\n
+ {verify_url}
+ '''
+
+ email_content_html = render_to_string('email/verify_email.html', {
+ 'verify_url': verify_url,
+ })
+
+ send_mail(
+ f'MonAPI User Verification',
+ email_content,
+ None,
+ [user.email],
+ html_message=email_content_html,
+ fail_silently=False,
+ )
+
except IntegrityError:
data['response'] = 'User already registered! Please use a different email to register.'
return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -28,3 +58,25 @@ def registration_view(request):
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return Response(data, status=status.HTTP_201_CREATED)
+@api_view(['POST', ])
+@authentication_classes([])
+@permission_classes([])
+def verify_view(request):
+ serializer = VerifiedUserTokenSerializer(data=request.data)
+ if serializer.is_valid():
+ try:
+ verify_token = VerifiedUserToken.objects.get(key=serializer.validated_data['key'])
+ verified_user = verify_token.verified_user
+ verified_user.verified = True
+ verified_user.save()
+ data = {'response': 'Success'}
+ verify_token.delete()
+ return Response(data=data, status=status.HTTP_200_OK)
+ except ObjectDoesNotExist:
+ data = {'response': 'Token is invalid.'}
+ return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ data = {'response': 'Pass a valid token.'}
+ return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
+
+
diff --git a/logout/migrations/__init__.py b/statuspage/__init__.py
similarity index 100%
rename from logout/migrations/__init__.py
rename to statuspage/__init__.py
diff --git a/statuspage/admin.py b/statuspage/admin.py
new file mode 100644
index 0000000..db8e343
--- /dev/null
+++ b/statuspage/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from statuspage.models import StatusPageCategory, StatusPageConfiguration
+
+admin.site.register(StatusPageConfiguration)
+admin.site.register(StatusPageCategory)
diff --git a/logout/apps.py b/statuspage/apps.py
similarity index 61%
rename from logout/apps.py
rename to statuspage/apps.py
index d3a4fdc..7abb2de 100644
--- a/logout/apps.py
+++ b/statuspage/apps.py
@@ -1,6 +1,6 @@
from django.apps import AppConfig
-class LogoutConfig(AppConfig):
+class StatuspageConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
- name = 'logout'
+ name = 'statuspage'
diff --git a/statuspage/migrations/0001_initial.py b/statuspage/migrations/0001_initial.py
new file mode 100644
index 0000000..722285d
--- /dev/null
+++ b/statuspage/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.1.2 on 2022-11-29 02:00
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('login', '0003_teammember_verified'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='StatusPageConfiguration',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('path', models.CharField(blank=True, max_length=64)),
+ ('team', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='login.team')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='StatusPageCategory',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=256)),
+ ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='login.team')),
+ ],
+ ),
+ ]
diff --git a/statuspage/migrations/0002_alter_statuspageconfiguration_path.py b/statuspage/migrations/0002_alter_statuspageconfiguration_path.py
new file mode 100644
index 0000000..c66610e
--- /dev/null
+++ b/statuspage/migrations/0002_alter_statuspageconfiguration_path.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2022-11-29 14:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('statuspage', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='statuspageconfiguration',
+ name='path',
+ field=models.CharField(blank=True, max_length=64, unique=True),
+ ),
+ ]
diff --git a/statuspage/migrations/0003_alter_statuspageconfiguration_path.py b/statuspage/migrations/0003_alter_statuspageconfiguration_path.py
new file mode 100644
index 0000000..0515a8c
--- /dev/null
+++ b/statuspage/migrations/0003_alter_statuspageconfiguration_path.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2022-12-09 04:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('statuspage', '0002_alter_statuspageconfiguration_path'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='statuspageconfiguration',
+ name='path',
+ field=models.CharField(default=None, max_length=64, null=True, unique=True),
+ ),
+ ]
diff --git a/statuspage/migrations/__init__.py b/statuspage/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/statuspage/models.py b/statuspage/models.py
new file mode 100644
index 0000000..82ae181
--- /dev/null
+++ b/statuspage/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+from login.models import Team
+
+class StatusPageConfiguration(models.Model):
+ team = models.OneToOneField(Team, on_delete=models.CASCADE)
+ path = models.CharField(max_length=64, null=True, blank=False, unique=True, default=None)
+
+class StatusPageCategory(models.Model):
+ team = models.ForeignKey(Team, on_delete=models.CASCADE)
+ name = models.CharField(max_length=256)
+
\ No newline at end of file
diff --git a/statuspage/serializers.py b/statuspage/serializers.py
new file mode 100644
index 0000000..69c4398
--- /dev/null
+++ b/statuspage/serializers.py
@@ -0,0 +1,38 @@
+from rest_framework import serializers
+
+from statuspage.models import StatusPageConfiguration, StatusPageCategory
+
+class StatusPageConfgurationSerializers(serializers.ModelSerializer):
+ class Meta:
+ model = StatusPageConfiguration
+ fields = [
+ 'path',
+ ]
+
+
+class StatusPageCategorySerializers(serializers.ModelSerializer):
+ class Meta:
+ model = StatusPageCategory
+ fields = [
+ 'id',
+ 'team',
+ 'name',
+ ]
+
+ read_only_fields = ['team']
+
+class APIMonitorSuccessRateSerializer(serializers.Serializer):
+ start_time = serializers.DateTimeField()
+ end_time = serializers.DateTimeField()
+ success = serializers.IntegerField()
+ failed = serializers.IntegerField()
+
+class StatusPageDashboardSerializers(serializers.ModelSerializer):
+ success_rate_category = APIMonitorSuccessRateSerializer(many=True)
+ class Meta:
+ model = StatusPageCategory
+ fields = [
+ 'id',
+ 'name',
+ 'success_rate_category',
+ ]
\ No newline at end of file
diff --git a/statuspage/tests.py b/statuspage/tests.py
new file mode 100644
index 0000000..951ae64
--- /dev/null
+++ b/statuspage/tests.py
@@ -0,0 +1,294 @@
+import pytz
+from django.utils import timezone
+from django.urls import reverse
+from django.contrib.auth.models import User
+from django.conf import settings
+from datetime import datetime
+
+from rest_framework.test import APITestCase
+from login.models import Team, TeamMember, MonAPIToken
+from apimonitor.models import APIMonitor, APIMonitorResult
+from statuspage.models import StatusPageConfiguration, StatusPageCategory
+
+class StatusPageConfigTest(APITestCase):
+ test_url = reverse('statuspage-config')
+
+ def test_status_page_config_get_first_time_then_return_success(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ response = self.client.get(self.test_url, format='json', **header)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, {
+ "path": None
+ })
+
+ def test_status_page_config_get_when_exists_then_return_success(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ StatusPageConfiguration.objects.create(team=team, path="testpath")
+
+ response = self.client.get(self.test_url, format='json', **header)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, {
+ "path": "testpath"
+ })
+
+ def test_status_page_config_update_then_return_success(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ req_body = {
+ "path": "updatepath"
+ }
+
+ response = self.client.post(self.test_url, data=req_body, format='json', **header)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, {
+ "path": "updatepath"
+ })
+
+ status_config = StatusPageConfiguration.objects.get(team=team)
+ self.assertEqual(status_config.path, "updatepath")
+
+ def test_status_page_config_update_when_empty_param_then_return_error(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ req_body = {
+ "path": "",
+ }
+
+ response = self.client.post(self.test_url, data=req_body, format='json', **header)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.data, {
+ "path": [
+ "This field may not be blank."
+ ]
+ })
+
+ def test_status_page_config_update_when_none_then_return_success(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ req_body = {
+ "path": None,
+ }
+
+ response = self.client.post(self.test_url, data=req_body, format='json', **header)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, {
+ "path": None
+ })
+
+class StatusPageCategoryTest(APITestCase):
+ test_url = reverse('statuspage-category-list')
+
+ def test_status_page_category_get_then_return_list_category(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ StatusPageCategory.objects.create(team=team, name='category1')
+
+ response = self.client.get(self.test_url, format='json', **header)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data[0]['name'], 'category1')
+
+ def test_status_page_category_post_then_create_new_category(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ req_body = {
+ "name": "abc"
+ }
+
+ response = self.client.post(self.test_url, data=req_body, format='json', **header)
+ self.assertEqual(response.status_code, 201)
+ self.assertEqual(response.data['name'], "abc")
+
+ category = StatusPageCategory.objects.get(team=team)
+ self.assertEqual(category.name, "abc")
+
+ def test_status_page_category_post_invalid_param_then_return_error(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ req_body = {}
+
+ response = self.client.post(self.test_url, data=req_body, format='json', **header)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.data, {
+ "name": ["This field is required."]
+ })
+
+ def test_status_page_category_delete_then_return_success(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ status_page = StatusPageCategory.objects.create(team=team, name='categorydel')
+
+ response = self.client.delete(reverse('statuspage-category-detail', kwargs={'pk': status_page.id}), format='json', **header)
+ self.assertEqual(response.status_code, 204)
+
+ count_status = StatusPageCategory.objects.filter(team=team).count()
+ self.assertEqual(count_status, 0)
+
+ def test_status_page_category_delete_not_exists_then_return_error(self):
+ user = User.objects.create_user(username="test@test.com", email="test@test.com", password="Test1234")
+ team = Team.objects.create(name='test team')
+ team_member = TeamMember.objects.create(team=team, user=user)
+
+ token = MonAPIToken.objects.create(team_member=team_member)
+ header = {'HTTP_AUTHORIZATION': f"Token {token.key}"}
+
+ response = self.client.delete(reverse('statuspage-category-detail', kwargs={'pk': 999}), format='json', **header)
+ self.assertEqual(response.status_code, 404)
+
+
+class StatusPageDashboardTest(APITestCase):
+ test_url = reverse('statuspage-dashboard')
+ local_timezone = pytz.timezone(settings.TIME_ZONE)
+ mock_current_time = local_timezone.localize(datetime(2022, 9, 20, 10))
+
+ def setUp(self):
+ # Mock time function
+ timezone.now = lambda: self.mock_current_time
+
+ # status page dashboard deservedly access without authorization because for public
+ def test_unauthorized_and_not_empty_status_page_category_in_api_monitor_then_return_success(self):
+ # create dummy api monitor and the result
+ team = Team.objects.create(name='test team')
+ statusPageCategory1 = StatusPageCategory.objects.create(team=team, name='test-category1')
+ statusPageCategory2 = StatusPageCategory.objects.create(team=team, name='test-category2')
+ StatusPageConfiguration.objects.create(team=team, path='test-path')
+
+ monitor1 = APIMonitor.objects.create(
+ team=team,
+ name='Test Name1',
+ method='GET',
+ url='Test Path',
+ schedule='10MIN',
+ previous_step=None,
+ body_type='EMPTY',
+ status_page_category=statusPageCategory1,
+ )
+
+ monitor2 = APIMonitor.objects.create(
+ team=team,
+ name='Test Name2',
+ method='GET',
+ url='Test Path',
+ schedule='10MIN',
+ previous_step=None,
+ body_type='EMPTY',
+ status_page_category=statusPageCategory2,
+ )
+
+ APIMonitorResult.objects.create(
+ monitor=monitor1,
+ execution_time=self.mock_current_time,
+ response_time=100,
+ success=True,
+ status_code=500,
+ log_response='Log Response',
+ log_error='',
+ )
+
+ APIMonitorResult.objects.create(
+ monitor=monitor2,
+ execution_time=self.mock_current_time,
+ response_time=75,
+ success=False,
+ status_code=500,
+ log_response='',
+ log_error='Log Error'
+ )
+
+ response = self.client.get(self.test_url, data={"path": "test-path"}, format='json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.data), 2)
+ self.assertEqual(response.data[0]['name'], 'test-category1')
+ self.assertEqual(response.data[0]['success_rate_category'][23]['success'], 1)
+ self.assertEqual(response.data[1]['success_rate_category'][23]['failed'], 1)
+
+ def test_empty_status_page_category_then_return_empty_status_page_dashboard(self):
+ # create path
+ team = Team.objects.create(name='test team')
+ StatusPageConfiguration.objects.create(team=team, path='test-path')
+
+ response = self.client.get(self.test_url, data={"path": "test-path"}, format='json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.data), 0)
+
+ def test_path_doesnt_exist_then_return_404_not_found(self):
+ response = self.client.get(self.test_url, data={"path": "no-path"}, format='json')
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.data['error'], "Please make sure your URL path is exist!")
+
+ def test_category_are_exist_but_have_not_assigned_then_return_empty_list_in_success_rate_category(self):
+ # create dummy data
+ team = Team.objects.create(name='test team')
+ StatusPageCategory.objects.create(team=team, name='test-category')
+ StatusPageConfiguration.objects.create(team=team, path='test-path')
+
+ monitor = APIMonitor.objects.create(
+ team=team,
+ name='Test Name1',
+ method='GET',
+ url='Test Path',
+ schedule='10MIN',
+ previous_step=None,
+ body_type='EMPTY',
+ )
+
+ APIMonitorResult.objects.create(
+ monitor=monitor,
+ execution_time=self.mock_current_time,
+ response_time=100,
+ success=True,
+ status_code=500,
+ log_response='Log Response',
+ log_error='',
+ )
+
+ response = self.client.get(self.test_url, data={"path": "test-path"}, format='json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data[0]['name'], 'test-category')
+ self.assertEqual(response.data[0]['success_rate_category'], [])
\ No newline at end of file
diff --git a/statuspage/urls.py b/statuspage/urls.py
new file mode 100644
index 0000000..d6ac433
--- /dev/null
+++ b/statuspage/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path, include
+from rest_framework import routers
+from statuspage.views import StatusPageConfigurationView, StatusPageCategoryViewSet, StatusPageDashboardViewSet
+
+router = routers.DefaultRouter()
+router.register(r'', StatusPageCategoryViewSet, basename='statuspage-category')
+
+urlpatterns = [
+ path('category/', include(router.urls)),
+ path('config/', StatusPageConfigurationView.as_view(), name='statuspage-config'),
+ path('dashboard/', StatusPageDashboardViewSet.as_view({'get': 'list'}), name='statuspage-dashboard'),
+]
\ No newline at end of file
diff --git a/statuspage/views.py b/statuspage/views.py
new file mode 100644
index 0000000..ff2aeb5
--- /dev/null
+++ b/statuspage/views.py
@@ -0,0 +1,105 @@
+from datetime import timedelta
+from django.db.models import Count, Q
+from django.utils import timezone
+from rest_framework import views, status, viewsets, mixins
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from apimonitor.models import APIMonitorResult
+from statuspage.models import StatusPageConfiguration, StatusPageCategory
+from statuspage.serializers import StatusPageConfgurationSerializers, StatusPageCategorySerializers, StatusPageDashboardSerializers
+
+class StatusPageConfigurationView(views.APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, format=None):
+ config, _ = StatusPageConfiguration.objects.get_or_create(team=request.auth.team)
+ serializer = StatusPageConfgurationSerializers(config)
+ return Response(serializer.data)
+
+ def post(self, request, format=None):
+ config, _ = StatusPageConfiguration.objects.get_or_create(team=request.auth.team)
+ serializer = StatusPageConfgurationSerializers(config, data=request.data, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data)
+
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class StatusPageCategoryViewSet(mixins.ListModelMixin,
+ mixins.CreateModelMixin,
+ mixins.DestroyModelMixin,
+ viewsets.GenericViewSet):
+
+ queryset = StatusPageCategory.objects.all()
+ serializer_class = StatusPageCategorySerializers
+ permission_classes = [IsAuthenticated]
+ pagination_class = None
+
+ def get_queryset(self):
+ queryset = StatusPageCategory.objects.filter(team=self.request.auth.team)
+ return queryset
+
+ def create(self, request, *args, **kwargs):
+ serializer = StatusPageCategorySerializers(data=request.data)
+ if serializer.is_valid():
+ status_page = StatusPageCategory.objects.create(
+ team = request.auth.team,
+ name = serializer.data['name'],
+ )
+ serializer = StatusPageCategorySerializers(status_page)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class StatusPageDashboardViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
+
+ queryset = StatusPageCategory.objects.all()
+ serializer_class = StatusPageDashboardSerializers
+ permission_classes = []
+ pagination_class = None
+
+ def list(self, request):
+ status_page_config = StatusPageConfiguration.objects.filter(path=self.request.GET.get('path'))
+ if len(status_page_config) == 0:
+ return Response(data={"error": "Please make sure your URL path is exist!"}, status=status.HTTP_404_NOT_FOUND)
+
+ queryset = StatusPageCategory.objects.filter(team=status_page_config[0].team)
+
+ for category in queryset:
+ success_rate_category = []
+ api_monitor_category = APIMonitorResult.objects \
+ .filter(monitor__status_page_category=category)
+
+ if len(api_monitor_category) == 0:
+ category.success_rate_category = success_rate_category
+ continue
+
+ #success_rate per category
+ last_24_hour = timezone.now() - timedelta(hours=24)
+ for _ in range(24):
+ start_time = last_24_hour
+ end_time = last_24_hour + timedelta(hours=1)
+
+ # Average success rate
+ success_count = api_monitor_category.filter(execution_time__gte=start_time, execution_time__lte=end_time) \
+ .aggregate(
+ s=Count('success', filter=Q(success=True)),
+ f=Count('success', filter=Q(success=False)),
+ total=Count('pk'),
+ )
+
+ success_rate_category.append({
+ "start_time": start_time,
+ "end_time" : end_time,
+ "success": success_count['s'],
+ "failed" : success_count['f']
+ })
+
+ last_24_hour += timedelta(hours=1)
+
+ category.success_rate_category = success_rate_category
+
+ serializer = StatusPageDashboardSerializers(queryset, many=True)
+ return Response(serializer.data)
\ No newline at end of file
diff --git a/template/email/verify_email.html b/template/email/verify_email.html
new file mode 100644
index 0000000..23d8cdc
--- /dev/null
+++ b/template/email/verify_email.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ MonAPI User Verification
+
+
+
+
+
+
MonAPI User Verification
+
+ To verify your registration on MonAPI, please follow the link below.
+ If you did not register on MonAPI, you can ignore this email.
+
+ {{verify_url}}
+
+
+
MonAPI Monitoring Platform
+
+
+
+
+
\ No newline at end of file