diff --git a/.github/workflows/django_unittest.yml b/.github/workflows/django_unittest.yml new file mode 100644 index 0000000..2377c22 --- /dev/null +++ b/.github/workflows/django_unittest.yml @@ -0,0 +1,38 @@ +name: Python application test with pytest and coverage +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - '**.py' + - 'requirements.txt' + - '!./web/**' + - 'Dockerfile' +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Check out code + uses: actions/checkout@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v2 + with: + python-version: 3.11 + + - name: Install poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + poetry install + + - name: Test with pytest and coverage + run: | + pytest tests/ --cov=tests/ + - name: Upload coverage to Codecove + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: true \ No newline at end of file diff --git a/fastrunner/migrations/0024_project_is_deleted.py b/fastrunner/migrations/0024_project_is_deleted.py new file mode 100644 index 0000000..cef6ba8 --- /dev/null +++ b/fastrunner/migrations/0024_project_is_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2024-03-15 01:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fastrunner", "0023_auto_20210910_1155"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="is_deleted", + field=models.IntegerField(default=0, null=True, verbose_name="是否删除"), + ), + ] diff --git a/fastuser/migrations/0007_alter_myuser_first_name.py b/fastuser/migrations/0007_alter_myuser_first_name.py new file mode 100644 index 0000000..628c869 --- /dev/null +++ b/fastuser/migrations/0007_alter_myuser_first_name.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.13 on 2024-03-15 01:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fastuser", "0006_myuser_show_hosts"), + ] + + operations = [ + migrations.AlterField( + model_name="myuser", + name="first_name", + field=models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ] diff --git a/fastuser/urls.py b/fastuser/urls.py index 4877cf1..20ce78f 100644 --- a/fastuser/urls.py +++ b/fastuser/urls.py @@ -22,7 +22,7 @@ urlpatterns = [ # 关闭注册入口,改为django admin创建用户 # path('register/', views.RegisterView.as_view()), - path("login/", views.LoginView.as_view()), + path("login/", views.LoginView.as_view(), name="login"), path("list/", views.UserView.as_view()), path("auto_run_testsuite_pk/", timer_task.auto_run_testsuite_pk, name="auto_run_testsuite_pk"), ] diff --git a/fastuser/views.py b/fastuser/views.py index 2553f62..7fcf345 100644 --- a/fastuser/views.py +++ b/fastuser/views.py @@ -132,7 +132,7 @@ def post(self, request): logger.info(f"Authentication failed for {username=}") return Response(response.LOGIN_FAILED) else: - return Response(serializer.errors) + return Response(serializer.errors, status=400) class UserView(APIView): diff --git a/mock/migrations/0006_mockapi_request_body_mockapi_version_and_more.py b/mock/migrations/0006_mockapi_request_body_mockapi_version_and_more.py new file mode 100644 index 0000000..ca8f944 --- /dev/null +++ b/mock/migrations/0006_mockapi_request_body_mockapi_version_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.1.13 on 2024-03-15 01:18 + +from django.db import migrations, models +import mock.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("mock", "0005_mockapilog_request_id_alter_mockapi_api_id_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="mockapi", + name="request_body", + field=models.JSONField(blank=True, default=dict, null=True), + ), + migrations.AddField( + model_name="mockapi", + name="version", + field=models.IntegerField(default=1), + ), + migrations.AlterField( + model_name="mockapi", + name="api_id", + field=models.CharField( + default=mock.models.generate_uuid, max_length=32, unique=True + ), + ), + migrations.AlterField( + model_name="mockapi", + name="api_name", + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name="mockapi", + name="response_text", + field=models.TextField( + default='\ndef execute(req, resp):\n import requests\n\n url = "http://localhost:8000/api/mock/mock_api/"\n\n payload = {}\n headers = {\n "accept": "application/json",\n "X-CSRFToken": "fk5wQDlKC6ufRjk7r38pfbqyq7mTtyc5NUUqkFN5lbZf6nyHVSbAUVoqbwaGcQHT",\n }\n\n response = requests.request("GET", url, headers=headers, data=payload)\n resp.data = response.json()\n' + ), + ), + migrations.AlterField( + model_name="mockapilog", + name="request_id", + field=models.CharField( + blank=True, + db_index=True, + default=mock.models.generate_uuid, + max_length=100, + null=True, + ), + ), + migrations.AlterField( + model_name="mockproject", + name="project_id", + field=models.CharField( + default=mock.models.generate_uuid, max_length=100, unique=True + ), + ), + ] diff --git a/mock/models.py b/mock/models.py index a84e587..9b3d8b0 100644 --- a/mock/models.py +++ b/mock/models.py @@ -1,12 +1,15 @@ import uuid from django.db import models - from fastuser.models import BaseTable +def generate_uuid(): + return uuid.uuid4().hex + + class MockProject(BaseTable): - project_id = models.CharField(max_length=100, unique=True, default=lambda: uuid.uuid4().hex) + project_id = models.CharField(max_length=100, unique=True, default=generate_uuid) project_name = models.CharField(max_length=100) project_desc = models.CharField(max_length=100) is_active = models.BooleanField(default=True) @@ -53,7 +56,9 @@ class MockAPI(BaseTable): related_name="mock_apis", ) request_path = models.CharField(max_length=100) - request_method = models.CharField(max_length=10, choices=METHOD_CHOICES, default="POST") + request_method = models.CharField( + max_length=10, choices=METHOD_CHOICES, default="POST" + ) request_body = models.JSONField(default=dict, blank=True, null=True) response_text = models.TextField(default=resp_text) is_active = models.BooleanField(default=True) @@ -61,7 +66,7 @@ class MockAPI(BaseTable): api_name = models.CharField(max_length=100) api_desc = models.CharField(max_length=100, null=True, blank=True) # uuid hex - api_id = models.CharField(max_length=32, default=lambda: uuid.uuid4().hex, unique=True) + api_id = models.CharField(max_length=32, default=generate_uuid, unique=True) enabled = models.BooleanField(default=True) # 添加version字段用于乐观锁控制 @@ -78,8 +83,13 @@ class Meta: class MockAPILog(BaseTable): - api = models.ForeignKey(MockAPI, on_delete=models.DO_NOTHING, db_constraint=False, to_field='api_id', - related_name="logs") + api = models.ForeignKey( + MockAPI, + on_delete=models.DO_NOTHING, + db_constraint=False, + to_field="api_id", + related_name="logs", + ) project = models.ForeignKey( MockProject, on_delete=models.DO_NOTHING, @@ -91,7 +101,9 @@ class MockAPILog(BaseTable): ) request_obj = models.JSONField(default=dict, blank=True) response_obj = models.JSONField(default=dict, null=True, blank=True) - request_id = models.CharField(max_length=100, default=lambda: uuid.uuid4().hex, db_index=True, null=True, blank=True) + request_id = models.CharField( + max_length=100, default=generate_uuid, db_index=True, null=True, blank=True + ) class Meta: verbose_name = "mock api log表" diff --git a/poetry.lock b/poetry.lock index aa86e19..aec00c0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "aiocontextvars" @@ -935,6 +935,17 @@ files = [ {file = "inflection-0.5.0.tar.gz", hash = "sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "itypes" version = "1.2.0" @@ -1122,6 +1133,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -1279,6 +1305,44 @@ files = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-django" +version = "4.8.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, + {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + [[package]] name = "python-crontab" version = "3.0.0" @@ -1730,4 +1794,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b95da891a45054699a19fae84a2706937380cd6e2003a3b39fa8401aca41d914" +content-hash = "8b4b2d4aba69e72ec2fbd998cacbaa63b0eb116ab16f099e959143df5658b689" diff --git a/pyproject.toml b/pyproject.toml index f9263e2..bb817c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,8 @@ gevent = "22.10.2" django-filter = ">=2.4.0,<2.5.0" django-auth-ldap = "2.3.0" pymysql = "1.1.0" +pytest = "^8.1.1" +pytest-django = "^4.8.0" [tool.poetry.group.dev.dependencies] diff --git a/tests/test_login_views.py b/tests/test_login_views.py new file mode 100644 index 0000000..a585767 --- /dev/null +++ b/tests/test_login_views.py @@ -0,0 +1,53 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model + +# 假设LoginView用的Url名字是'login' +login_url = reverse("login") + + +@pytest.mark.django_db # 如果你的测试需要数据库操作 +class TestLoginView: + def setup_method(self): + User = get_user_model() + User.objects.create_user('validUser', 'email@example.com', 'validPassword') + + def test_login_with_correct_credentials(self): + client = APIClient() + user_data = {"username": "validUser", "password": "validPassword"} + response = client.post(login_url, user_data, format="json") + assert response.status_code == status.HTTP_200_OK + assert ( + "token" in response.data + ) # assuming that generate_token_and_respond returns a token in response + + def test_login_with_incorrect_credentials(self): + client = APIClient() + user_data = {"username": "invalidUser", "password": "invalidPassword"} + response = client.post(login_url, user_data, format="json") + # body + assert response.json() == {'code': "0103", 'success': False, 'msg': "用户名或密码错误"} + assert ( + response.status_code == status.HTTP_200_OK + ) # assuming LOGIN_FAILED responds with status 401 + + def test_login_with_no_credentials(self): + client = APIClient() + user_data = {} + response = client.post(login_url, user_data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_login_with_partial_credentials(self): + client = APIClient() + + # Missing username + user_data = {"password": "validPassword"} + response = client.post(login_url, user_data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + # Missing password + user_data = {"username": "validUser"} + response = client.post(login_url, user_data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/tests/test_mockapi_serializers.py b/tests/test_mockapi_serializers.py new file mode 100644 index 0000000..de5b26a --- /dev/null +++ b/tests/test_mockapi_serializers.py @@ -0,0 +1,57 @@ +from mock.serializers import MockAPISerializer +from django.test import TestCase +from rest_framework import serializers +import ast +import textwrap + + +class TestMockAPISerializer(TestCase): + def setUp(self): + """ + This will run before every test method. + """ + self.serializer = MockAPISerializer() + + def test_invalid_response_text(self): + # Checking invalid response text + invalid_response_text = "invalid code" + with self.assertRaises(serializers.ValidationError): + self.serializer.validate_response_text(invalid_response_text) + + def test_response_text_no_execute(self): + # Checking a response text with no 'execute' function + response_text_no_execute = """ +def other_function(): + pass +""" + with self.assertRaises(serializers.ValidationError): + self.serializer.validate_response_text(response_text_no_execute) + + def test_response_text_bad_execute(self): + # Checking response text with incorrect 'execute' function arguments + response_text_bad_execute = """ +def execute(a, b, c): + pass +""" + with self.assertRaises(serializers.ValidationError): + self.serializer.validate_response_text(response_text_bad_execute) + + def test_valid_response_text(self): + # Checking valid response text + valid_response_text = """ +def execute(req, resp): + pass +""" + self.assertEqual( + valid_response_text, + self.serializer.validate_response_text(valid_response_text), + ) + + def test_indent_error_text(self): + # Checking response text with incorrect indentation + indent_error_text = """ + def execute(req, resp): + a = '1' +""" + with self.assertRaises(serializers.ValidationError): + self.serializer.validate_response_text(indent_error_text) \ No newline at end of file