From 5ebc714cf07657e999c1f74c7d40f3c1c7b489f6 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 11:44:26 +0300 Subject: [PATCH 01/47] config: Add configuration for oauth2 and oidc Added configuration for oauth2 and oidc: - Added localhost to ALLOWED HOSTS - Added oauth config to: INSTALLED_APPS TEMPLATES MIDDLEWARE AUTHENTICATION_BACKENDS REST_FRAMEWORK SOCIALACCOUNT_PROVIDERS - Set SITE_ID to 1 - Added LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL --- api/settings.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/api/settings.py b/api/settings.py index d35d8e6..6d2724c 100644 --- a/api/settings.py +++ b/api/settings.py @@ -26,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost'] # Application definition @@ -38,10 +38,20 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'project', # Third party apps 'rest_framework', - 'project' + + # Allauth + 'django.contrib.sites', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + + # OAuth toolkit + 'oauth2_provider', ] MIDDLEWARE = [ @@ -52,6 +62,9 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + + # Allauth + 'allauth.account.middleware.AccountMiddleware', ] ROOT_URLCONF = 'api.urls' @@ -67,6 +80,9 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + + # allauth + 'django.template.context_processors.request', ], }, }, @@ -130,3 +146,38 @@ # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Allauth +AUTHENTICATION_BACKENDS = [ + # Needed to login by username in Django admin, regardless of `allauth` + 'django.contrib.auth.backends.ModelBackend', + + # `allauth` specific authentication methods, such as login by email + 'allauth.account.auth_backends.AuthenticationBackend', +] + +# allauth +SITE_ID = 1 + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # OAuth2 Auth + 'rest_framework.authentication.SessionAuthentication', # Session Auth (for logged-in users via the web) + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', # Restrict access to authenticated users + ), +} + +SOCIALACCOUNT_PROVIDERS = { + 'google': { + 'APP': { + 'client_id': config("GOOGLE_CLIENT_ID"), + 'secret': config("GOOGLE_CLIENT_SECRET"), + 'key': '' + } + } +} + +LOGIN_REDIRECT_URL = '/api/customers/' +LOGOUT_REDIRECT_URL = '/accounts/login/' From 7a7e16472cc1a3388aafd008e3b0cdcb93d39466 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 11:50:46 +0300 Subject: [PATCH 02/47] feat: Add path to allauth.urls --- api/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/urls.py b/api/urls.py index 9444962..d91b1ce 100644 --- a/api/urls.py +++ b/api/urls.py @@ -20,4 +20,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('project.urls')), + path('accounts/', include('allauth.urls')), ] From d02e561054e18b76d10947fd3d5be9ff8bf232e1 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 11:51:45 +0300 Subject: [PATCH 03/47] feat: Add permission_classes to the views --- project/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/project/views.py b/project/views.py index 2de553f..04f21d1 100644 --- a/project/views.py +++ b/project/views.py @@ -1,25 +1,34 @@ from rest_framework import generics +from rest_framework.permissions import IsAuthenticated from .models import Customer, Order from .serializers import CustomerSerializer, OrderSerializer # Customer Views class CustomerListCreateView(generics.ListCreateAPIView): + permission_classes = [IsAuthenticated] + queryset = Customer.objects.all() serializer_class = CustomerSerializer class CustomerRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [IsAuthenticated] + queryset = Customer.objects.all() serializer_class = CustomerSerializer # Order Views class OrderListCreateView(generics.ListCreateAPIView): + permission_classes = [IsAuthenticated] + queryset = Order.objects.all() serializer_class = OrderSerializer class OrderRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [IsAuthenticated] + queryset = Order.objects.all() serializer_class = OrderSerializer From 8dced7b5ae337dd0abab073a983d652c7891b6ff Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 11:54:45 +0300 Subject: [PATCH 04/47] feat: Create an OAuth2 token for sample user Created an OAuth2 token for sample user for authenticated requests and to access the views when testing. --- project/tests.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/project/tests.py b/project/tests.py index 99acd8f..be1ac90 100644 --- a/project/tests.py +++ b/project/tests.py @@ -1,12 +1,34 @@ from django.urls import reverse +from django.test import TestCase +from django.contrib.auth.models import User from rest_framework.test import APITestCase +from rest_framework.test import APIClient +from oauth2_provider.models import AccessToken from rest_framework import status from .models import Customer, Order +from django.utils import timezone +from datetime import timedelta class VitabuAPITestCase(APITestCase): def setUp(self): """Setup to create sample data""" + # Create a test user + self.user = User.objects.create_user( + username='testuser', password='testpassword') + + # Create an OAuth2 token for the user + self.client = APIClient() + self.token = AccessToken.objects.create( + user=self.user, + token='testtoken123', # Mock token + expires=timezone.now() + timedelta(hours=1) # token expiration + ) + + # Add the token to the client for authenticated requests + self.client.credentials( + HTTP_AUTHORIZATION='Bearer ' + self.token.token) + # Create a sample customer for testing self.customer = Customer.objects.create( name="John Doe", From 92413c08db74dc8188a53b9b0fdc0cb05c38e4ee Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 11:58:33 +0300 Subject: [PATCH 05/47] feat: Add oauth2 packages to Pipfile Added these packages to Pipfile - coverage - django-allauth[socialaccount] - django-oauth-toolkit --- Pipfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Pipfile b/Pipfile index ee3731d..23372f7 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,9 @@ django = "*" python-decouple = "*" psycopg = {extras = ["binary"], version = "*"} djangorestframework = "*" +coverage = "*" +django-allauth = {extras = ["socialaccount"], version = "*"} +django-oauth-toolkit = "*" [dev-packages] From 278e16e5d922ba5ab991dae18db41ba49fe75b90 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:00:27 +0300 Subject: [PATCH 06/47] chore: Add .coverage directory to be untracked --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c3ef6b..1f5d49a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Pipfile.lock project/views1.py project/urls1.py +.coverage \ No newline at end of file From 728e36d6862782c4daa0ebdb635daa8d214e4a9e Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:03:06 +0300 Subject: [PATCH 07/47] config: Add AllAuth Google settings --- envsample | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/envsample b/envsample index 3a9da94..214b254 100644 --- a/envsample +++ b/envsample @@ -2,8 +2,14 @@ SECRET_KEY='django-insecure-_s_=zugj&+ewj33yjykhz^py+*b#npvdi@7hx3o^5p)zbo3j2z' # Database settings [Postgres] +# Change to your database credentials DB_NAME=postgres DB_USER=postgres DB_PASSWORD=password DB_HOST=localhost DB_PORT=5432 + +# AllAuth settings +# Change to your Google credentials +GOOGLE_CLIENT_ID='' +GOOGLE_CLIENT_SECRET='' From 15435a72002999339f3fb5ab0886e88d92d0713d Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:12:54 +0300 Subject: [PATCH 08/47] chore: Add __pycache__ files to be untracked --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1f5d49a..0d1edaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ .env +.coverage # Pipenv Pipfile.lock project/views1.py project/urls1.py -.coverage \ No newline at end of file +api/__pycache__ +project/__pycache__ +project/migrations/__pycache__ From 5898ad0ae5b93366e836349c6d3df4c9c606edfc Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:43:45 +0300 Subject: [PATCH 09/47] Remove tests.py file Removed tests.py file because there is now a tests folder where all tests for the project reside. --- project/tests.py | 239 ----------------------------------------------- 1 file changed, 239 deletions(-) delete mode 100644 project/tests.py diff --git a/project/tests.py b/project/tests.py deleted file mode 100644 index be1ac90..0000000 --- a/project/tests.py +++ /dev/null @@ -1,239 +0,0 @@ -from django.urls import reverse -from django.test import TestCase -from django.contrib.auth.models import User -from rest_framework.test import APITestCase -from rest_framework.test import APIClient -from oauth2_provider.models import AccessToken -from rest_framework import status -from .models import Customer, Order -from django.utils import timezone -from datetime import timedelta - - -class VitabuAPITestCase(APITestCase): - def setUp(self): - """Setup to create sample data""" - # Create a test user - self.user = User.objects.create_user( - username='testuser', password='testpassword') - - # Create an OAuth2 token for the user - self.client = APIClient() - self.token = AccessToken.objects.create( - user=self.user, - token='testtoken123', # Mock token - expires=timezone.now() + timedelta(hours=1) # token expiration - ) - - # Add the token to the client for authenticated requests - self.client.credentials( - HTTP_AUTHORIZATION='Bearer ' + self.token.token) - - # Create a sample customer for testing - self.customer = Customer.objects.create( - name="John Doe", - email="johndoe@example.com", - code="CUST123" - ) - - # Create a sample order for testing - self.order = Order.objects.create( - customer=self.customer, - item="Book B", - amount=20.00, - ) - - # Prepare URLs for customer and order APIs - self.customer_url = reverse('customer-list') - self.order_url = reverse('order-list') - - # Sample data for POST requests - self.valid_customer_data = { - "name": "Jane Doe", - "email": "janedoe@example.com", - "code": "CUST456" - } - self.valid_order_data = { - "customer_id": self.customer.id, - "item": "Book A", - "amount": 10.99, - "time": "2024-09-19T10:00:00Z" - } - - def test_create_customer(self): - """Test to create a new customer""" - # Send a POST request to create a customer - response = self.client.post( - self.customer_url, - self.valid_customer_data, - format='json' - ) - # print("Response Data:", response.data) # for debugging - - # Check if the response status is 201 Created - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # Check if the customer was created in the database - self.assertEqual(Customer.objects.count(), 2) - self.assertEqual(Customer.objects.last().name, "Jane Doe") - - def test_get_customers(self): - """Test to get all customers""" - # Send a GET request to retrieve customers - response = self.client.get(self.customer_url) - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Check if the response contains the sample customer - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['name'], "John Doe") - self.assertEqual(response.data[0]['code'], "CUST123") - self.assertEqual(response.data[0]['email'], "johndoe@example.com") - - def test_create_order(self): - """Test to create a new order""" - # Send a POST request to create an order - response = self.client.post( - self.order_url, - self.valid_order_data, - format='json' - ) - # print("Response Data:", response.data) # for debugging - - # Check if the response status is 201 Created - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # Check if the order was created in the database - self.assertEqual(Order.objects.count(), 2) - self.assertEqual(Order.objects.last().item, "Book A") - - def test_get_orders(self): - """Test to get all orders""" - # Send a GET request to retrieve orders - response = self.client.get(self.order_url) - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Check if the response contains the sample order - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['item'], "Book B") - - def test_retrieve_customer(self): - """Test retrieving a single customer""" - # Send a GET request to retrieve the customer - url = reverse('customer-detail', args=[self.customer.id]) - response = self.client.get(url) - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Verify the response data - self.assertEqual(response.data['name'], self.customer.name) - self.assertEqual(response.data['email'], self.customer.email) - - def test_update_customer(self): - """Test updating a customer""" - # Send a PUT request to update the customer - updated_data = { - "name": "John Updated", - "email": "johnupdated@example.com", - "code": "CUST123UPDATED" - } - url = reverse('customer-detail', args=[self.customer.id]) - response = self.client.put(url, updated_data, format='json') - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Check if the customer data was updated in the database - self.customer.refresh_from_db() - self.assertEqual(self.customer.name, updated_data['name']) - self.assertEqual(self.customer.email, updated_data['email']) - - def test_delete_customer(self): - """Test deleting a customer""" - # Send a DELETE request to delete the customer - url = reverse('customer-detail', args=[self.customer.id]) - response = self.client.delete(url) - - # Check if the response status is 204 No Content - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - # Check if the customer was removed from the database - self.assertEqual(Customer.objects.count(), 0) - - def test_retrieve_order(self): - """Test retrieving a single order""" - # Send a GET request to retrieve the order - url = reverse('order-detail', args=[self.order.id]) - response = self.client.get(url) - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Verify the response data - self.assertEqual(response.data['item'], self.order.item) - self.assertEqual(response.data['amount'], - format(self.order.amount, '.2f')) - self.assertEqual(response.data['customer']['name'], self.customer.name) - - def test_update_order(self): - """Test updating an order""" - # Send a PUT request to update the order - updated_order_data = { - "customer_id": self.customer.id, - "item": "Updated Book C", - "amount": 35.00, - "time": "2024-09-20T10:00:00Z" - } - url = reverse('order-detail', args=[self.order.id]) - response = self.client.put(url, updated_order_data, format='json') - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Check if the order data was updated in the database - self.order.refresh_from_db() - self.assertEqual(self.order.item, updated_order_data['item']) - self.assertEqual(self.order.amount, updated_order_data['amount']) - - def test_delete_order(self): - """Test deleting an order""" - # Send a DELETE request to delete the order - url = reverse('order-detail', args=[self.order.id]) - response = self.client.delete(url) - - # Check if the response status is 204 No Content - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - # Check if the order was removed from the database - self.assertEqual(Order.objects.count(), 0) - - def test_list_orders_for_customer(self): - """Test listing all orders for a customer""" - # Create a couple of orders - Order.objects.create( - customer=self.customer, - item="Book E", - amount=50.00 - ) - Order.objects.create( - customer=self.customer, - item="Book F", - amount=60.00 - ) - - # Send a GET request to list orders for the customer - url = reverse('order-list') + f'?customer={self.customer.id}' - response = self.client.get(url) - # print("Response data: ", response.data) # for debugging - - # Check if the response status is 200 OK - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Verify the response contains two orders - self.assertEqual(len(response.data), 3) - self.assertEqual(response.data[1]['item'], "Book E") - self.assertEqual(response.data[2]['item'], "Book F") From a1e6e7c29d7dd9dbf752e1a921c0dae09e064d1d Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:46:41 +0300 Subject: [PATCH 10/47] feat: Add tests directory to project app Added tests directory to separate tests for various files --- project/tests/__init__.py | 0 project/tests/test_views.py | 239 ++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 project/tests/__init__.py create mode 100644 project/tests/test_views.py diff --git a/project/tests/__init__.py b/project/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/tests/test_views.py b/project/tests/test_views.py new file mode 100644 index 0000000..916f5a2 --- /dev/null +++ b/project/tests/test_views.py @@ -0,0 +1,239 @@ +from django.urls import reverse +from django.test import TestCase +from django.contrib.auth.models import User +from rest_framework.test import APITestCase +from rest_framework.test import APIClient +from oauth2_provider.models import AccessToken +from rest_framework import status +from ..models import Customer, Order +from django.utils import timezone +from datetime import timedelta + + +class VitabuAPITestCase(APITestCase): + def setUp(self): + """Setup to create sample data""" + # Create a test user + self.user = User.objects.create_user( + username='testuser', password='testpassword') + + # Create an OAuth2 token for the user + self.client = APIClient() + self.token = AccessToken.objects.create( + user=self.user, + token='testtoken123', # Mock token + expires=timezone.now() + timedelta(hours=1) # token expiration + ) + + # Add the token to the client for authenticated requests + self.client.credentials( + HTTP_AUTHORIZATION='Bearer ' + self.token.token) + + # Create a sample customer for testing + self.customer = Customer.objects.create( + name="John Doe", + email="johndoe@example.com", + code="CUST123" + ) + + # Create a sample order for testing + self.order = Order.objects.create( + customer=self.customer, + item="Book B", + amount=20.00, + ) + + # Prepare URLs for customer and order APIs + self.customer_url = reverse('customer-list') + self.order_url = reverse('order-list') + + # Sample data for POST requests + self.valid_customer_data = { + "name": "Jane Doe", + "email": "janedoe@example.com", + "code": "CUST456" + } + self.valid_order_data = { + "customer_id": self.customer.id, + "item": "Book A", + "amount": 10.99, + "time": "2024-09-19T10:00:00Z" + } + + def test_create_customer(self): + """Test to create a new customer""" + # Send a POST request to create a customer + response = self.client.post( + self.customer_url, + self.valid_customer_data, + format='json' + ) + # print("Response Data:", response.data) # for debugging + + # Check if the response status is 201 Created + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Check if the customer was created in the database + self.assertEqual(Customer.objects.count(), 2) + self.assertEqual(Customer.objects.last().name, "Jane Doe") + + def test_get_customers(self): + """Test to get all customers""" + # Send a GET request to retrieve customers + response = self.client.get(self.customer_url) + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check if the response contains the sample customer + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], "John Doe") + self.assertEqual(response.data[0]['code'], "CUST123") + self.assertEqual(response.data[0]['email'], "johndoe@example.com") + + def test_create_order(self): + """Test to create a new order""" + # Send a POST request to create an order + response = self.client.post( + self.order_url, + self.valid_order_data, + format='json' + ) + # print("Response Data:", response.data) # for debugging + + # Check if the response status is 201 Created + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Check if the order was created in the database + self.assertEqual(Order.objects.count(), 2) + self.assertEqual(Order.objects.last().item, "Book A") + + def test_get_orders(self): + """Test to get all orders""" + # Send a GET request to retrieve orders + response = self.client.get(self.order_url) + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check if the response contains the sample order + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['item'], "Book B") + + def test_retrieve_customer(self): + """Test retrieving a single customer""" + # Send a GET request to retrieve the customer + url = reverse('customer-detail', args=[self.customer.id]) + response = self.client.get(url) + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify the response data + self.assertEqual(response.data['name'], self.customer.name) + self.assertEqual(response.data['email'], self.customer.email) + + def test_update_customer(self): + """Test updating a customer""" + # Send a PUT request to update the customer + updated_data = { + "name": "John Updated", + "email": "johnupdated@example.com", + "code": "CUST123UPDATED" + } + url = reverse('customer-detail', args=[self.customer.id]) + response = self.client.put(url, updated_data, format='json') + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check if the customer data was updated in the database + self.customer.refresh_from_db() + self.assertEqual(self.customer.name, updated_data['name']) + self.assertEqual(self.customer.email, updated_data['email']) + + def test_delete_customer(self): + """Test deleting a customer""" + # Send a DELETE request to delete the customer + url = reverse('customer-detail', args=[self.customer.id]) + response = self.client.delete(url) + + # Check if the response status is 204 No Content + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # Check if the customer was removed from the database + self.assertEqual(Customer.objects.count(), 0) + + def test_retrieve_order(self): + """Test retrieving a single order""" + # Send a GET request to retrieve the order + url = reverse('order-detail', args=[self.order.id]) + response = self.client.get(url) + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify the response data + self.assertEqual(response.data['item'], self.order.item) + self.assertEqual(response.data['amount'], + format(self.order.amount, '.2f')) + self.assertEqual(response.data['customer']['name'], self.customer.name) + + def test_update_order(self): + """Test updating an order""" + # Send a PUT request to update the order + updated_order_data = { + "customer_id": self.customer.id, + "item": "Updated Book C", + "amount": 35.00, + "time": "2024-09-20T10:00:00Z" + } + url = reverse('order-detail', args=[self.order.id]) + response = self.client.put(url, updated_order_data, format='json') + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check if the order data was updated in the database + self.order.refresh_from_db() + self.assertEqual(self.order.item, updated_order_data['item']) + self.assertEqual(self.order.amount, updated_order_data['amount']) + + def test_delete_order(self): + """Test deleting an order""" + # Send a DELETE request to delete the order + url = reverse('order-detail', args=[self.order.id]) + response = self.client.delete(url) + + # Check if the response status is 204 No Content + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # Check if the order was removed from the database + self.assertEqual(Order.objects.count(), 0) + + def test_list_orders_for_customer(self): + """Test listing all orders for a customer""" + # Create a couple of orders + Order.objects.create( + customer=self.customer, + item="Book E", + amount=50.00 + ) + Order.objects.create( + customer=self.customer, + item="Book F", + amount=60.00 + ) + + # Send a GET request to list orders for the customer + url = reverse('order-list') + f'?customer={self.customer.id}' + response = self.client.get(url) + # print("Response data: ", response.data) # for debugging + + # Check if the response status is 200 OK + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify the response contains two orders + self.assertEqual(len(response.data), 3) + self.assertEqual(response.data[1]['item'], "Book E") + self.assertEqual(response.data[2]['item'], "Book F") From 983db58bc72907d6a949548ae2cd082a1289d470 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 12:47:37 +0300 Subject: [PATCH 11/47] chore: Add __pycache__ files from tests directory --- .gitignore | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0d1edaf..52d9945 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ Pipfile.lock project/views1.py project/urls1.py -api/__pycache__ -project/__pycache__ -project/migrations/__pycache__ +api/__pycache__/ +project/__pycache__/ +project/migrations/__pycache__/ +project/tests/__pycache__/ From c193ad9ef0a63d59ae7fbc944b57ecc9b59a5fbf Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 13:39:10 +0300 Subject: [PATCH 12/47] feat: Create tests for Customer and Order models Created these tests for customer and Order models: - test_customer_creation() - test_customer_str_method() - test_order_creation() - test_order_str_method() --- project/tests/test_models.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 project/tests/test_models.py diff --git a/project/tests/test_models.py b/project/tests/test_models.py new file mode 100644 index 0000000..e147b08 --- /dev/null +++ b/project/tests/test_models.py @@ -0,0 +1,53 @@ +from django.test import TestCase +from ..models import Customer, Order +from django.utils import timezone + + +class CustomerModelTest(TestCase): + + def setUp(self): + # Create a customer object for testing + self.customer = Customer.objects.create( + name='John Doe', + email='johndoe@example.com', + code='CUST123' + ) + + def test_customer_creation(self): + """Test that a customer object is created successfully""" + self.assertEqual(self.customer.name, 'John Doe') + self.assertEqual(self.customer.email, 'johndoe@example.com') + self.assertEqual(self.customer.code, 'CUST123') + + def test_customer_str_method(self): + """Test the __str__ method of the Customer model""" + self.assertEqual(str(self.customer), 'John Doe (CUST123)') + + +class OrderModelTest(TestCase): + + def setUp(self): + # Create a customer object + self.customer = Customer.objects.create( + name='Jane Doe', + email='janedoe@example.com', + code='CUST456' + ) + # Create an order object + self.order = Order.objects.create( + item='Book A', + amount=20.00, + customer=self.customer, + time=timezone.now() + ) + + def test_order_creation(self): + """Test that an order object is created successfully""" + self.assertEqual(self.order.item, 'Book A') + self.assertEqual(self.order.amount, 20.00) + self.assertEqual(self.order.customer, self.customer) + + def test_order_str_method(self): + """Test the __str__ method of the Order model""" + expected_str = f"Order of Book A for 20.00 by {self.customer.name} at {self.order.time}" + self.assertEqual(str(self.order), expected_str) From 0d11380b093d99b1a8c39d77c8a16a2c1aa1207d Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 13:41:32 +0300 Subject: [PATCH 13/47] fix: Change amount to two decimals in __str__ --- project/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/models.py b/project/models.py index da2e066..8971751 100644 --- a/project/models.py +++ b/project/models.py @@ -17,4 +17,4 @@ class Order(models.Model): customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='orders') def __str__(self): - return f"Order of {self.item} for {self.amount} by {self.customer.name} at {self.time}" + return f"Order of {self.item} for {self.amount:.2f} by {self.customer.name} at {self.time}" From de6f34095d46d899ed137b4fe421532ed21e24f0 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 13:43:48 +0300 Subject: [PATCH 14/47] chore: add pycodestyling to the file --- project/tests/test_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project/tests/test_models.py b/project/tests/test_models.py index e147b08..070307a 100644 --- a/project/tests/test_models.py +++ b/project/tests/test_models.py @@ -4,7 +4,7 @@ class CustomerModelTest(TestCase): - + def setUp(self): # Create a customer object for testing self.customer = Customer.objects.create( @@ -49,5 +49,6 @@ def test_order_creation(self): def test_order_str_method(self): """Test the __str__ method of the Order model""" - expected_str = f"Order of Book A for 20.00 by {self.customer.name} at {self.order.time}" + expected_str = f"Order of Book A for 20.00 by \ + {self.customer.name} at {self.order.time}" self.assertEqual(str(self.order), expected_str) From a97f2e5a819477b5f90c23eea8bf26d5072ac01c Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 13:44:41 +0300 Subject: [PATCH 15/47] chore: Add htmlcov directory to be untracked --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 52d9945..b50aad2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env .coverage +htmlcov/ # Pipenv Pipfile.lock From 1df901abde21551cd1ec8bc92a8edbf9456a46c4 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Mon, 23 Sep 2024 23:34:33 +0300 Subject: [PATCH 16/47] feat: Add tests for oidc Added tests for oidc: - test_google_login_url() - test_protected_view_requires_authentication() - test_authentication_access() --- project/tests/test_oidc.py | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 project/tests/test_oidc.py diff --git a/project/tests/test_oidc.py b/project/tests/test_oidc.py new file mode 100644 index 0000000..5508464 --- /dev/null +++ b/project/tests/test_oidc.py @@ -0,0 +1,62 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status +from rest_framework.authtoken.models import Token +from oauth2_provider.models import AccessToken +from django.urls import reverse +from django.contrib.auth.models import User +from django.utils import timezone +from datetime import timedelta + + +class OIDCTestCase(TestCase): + + def setUp(self): + """Setup to create sample data""" + self.client = APIClient() + self.protected_url = reverse('customer-list') + + def test_google_login_url(self): + """Test that the Google OIDC login URL is reachable""" + url = reverse('account_login') # Django-allauth's login URL + response = self.client.get(url) + # print(response.content) # debugging + + # Ensure the login page loads + self.assertEqual(response.status_code, 200) + # Ensure the page contains Google login option + self.assertContains(response, "Google") + + # def test_google_oidc_redirect(self): + # """Test that the user is redirected to Google for OIDC login""" + # # Get the Google login URL + # google_login_url = reverse('accounts/google/login/callback/') + # response = self.client.get(google_login_url) + # # Ensure redirection to Google + # self.assertEqual(response.status_code, 302) + # # Ensure the redirect is to Google's login page + # self.assertIn('accounts.google.com', response.url) + + def test_protected_view_requires_authentication(self): + """Test that an unauthenticated user cannot access the API""" + response = self.client.get(self.protected_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_authenticated_access(self): + """Test that an authenticated user can access the API""" + # Create a test user + self.user = User.objects.create_user( + username='testuser', password='testpassword') + + # Create an OAuth2 token for the user + self.token = AccessToken.objects.create( + user=self.user, + token='testtoken123', # Mock token + expires=timezone.now() + timedelta(hours=1) # token expiration + ) + + # Add the token to the client for authenticated requests + self.client.credentials( + HTTP_AUTHORIZATION='Bearer ' + self.token.token) + response = self.client.get(self.protected_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) From 71d232bfd106e2ff4831fa2576dde2bae1c05ba9 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 19:57:42 +0300 Subject: [PATCH 17/47] feat: Add SMS alerts functionality Added SMS alerts functionality using Africa's Talking SMS gateway. Implemented the function send_sms_alert() --- project/utils/__init__.py | 0 project/utils/sms_utils.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 project/utils/__init__.py create mode 100644 project/utils/sms_utils.py diff --git a/project/utils/__init__.py b/project/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/utils/sms_utils.py b/project/utils/sms_utils.py new file mode 100644 index 0000000..9f20df0 --- /dev/null +++ b/project/utils/sms_utils.py @@ -0,0 +1,17 @@ +import africastalking +from decouple import config + +# Initialize SDK +africastalking.initialize(config('SMS_USERNAME'), config('SMS_API_KEY')) + +# Get the SMS service +sms = africastalking.SMS + +def send_sms_alert(phone_number, message): + try: + response = sms.send(message, [phone_number]) + print(f"SMS sent successfully: {response}") + return response + except Exception as e: + print(f"Failed to send SMS: {e}") + return None From 92c66ec46f7c64d24c7f2be938dff78be50976ba Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:00:47 +0300 Subject: [PATCH 18/47] feat: Add signals when an order is created Added signals.py that implements a signal to be sent when an order is created to send an SMS to the customer's phone number. --- project/signals.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 project/signals.py diff --git a/project/signals.py b/project/signals.py new file mode 100644 index 0000000..8e3c139 --- /dev/null +++ b/project/signals.py @@ -0,0 +1,10 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Order +from .utils.sms_utils import send_sms_alert + +@receiver(post_save, sender=Order) +def send_order_confirmation_sms(sender, instance, created, **kwargs): + if created: + message = f"Hi {instance.customer.name}, your order for {instance.item} has been placed successfully." + send_sms_alert(instance.customer.phone_number, message) From 830b905ae2a248a346f5d18f7fe3c3b8b446ac82 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:02:26 +0300 Subject: [PATCH 19/47] feat: Add phone_number field to the CustomerSerializer class --- project/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/serializers.py b/project/serializers.py index 1a8e918..2b6f2fe 100644 --- a/project/serializers.py +++ b/project/serializers.py @@ -5,7 +5,7 @@ class CustomerSerializer(serializers.ModelSerializer): class Meta: model = Customer - fields = ['id', 'name', 'email', 'code'] + fields = ['id', 'name', 'email', 'code', 'phone_number'] class OrderSerializer(serializers.ModelSerializer): From 8c0f7f90230f88af877fdc5839fdaca9733d3f24 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:04:18 +0300 Subject: [PATCH 20/47] feat: Add phone_number field to the Customer model --- project/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/models.py b/project/models.py index 8971751..db072e1 100644 --- a/project/models.py +++ b/project/models.py @@ -5,6 +5,7 @@ class Customer(models.Model): name = models.CharField(max_length=255) email = models.EmailField(unique=True) code = models.CharField(max_length=50, unique=True, blank=True, null=True) + phone_number = models.CharField(max_length=20, blank=True, null=True) def __str__(self) -> str: return f"{self.name} ({self.code})" From 7b6d74f08545a099d90c549236accf581967ae4c Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:05:10 +0300 Subject: [PATCH 21/47] feat: Register signals to the ProjectConfig class Added/Registered the created signals in signals.py in apps.py --- project/apps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/apps.py b/project/apps.py index 9f1761b..22f1cb7 100644 --- a/project/apps.py +++ b/project/apps.py @@ -4,3 +4,6 @@ class ProjectConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'project' + + def ready(self): + import project.signals From 0ea9b2dc5d6d8f06acac95ca00339c4511dac62f Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:07:54 +0300 Subject: [PATCH 22/47] feat: Add the phone_number field to the db new file: project/migrations/0002_customer_phone_number.py --- .../migrations/0002_customer_phone_number.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 project/migrations/0002_customer_phone_number.py diff --git a/project/migrations/0002_customer_phone_number.py b/project/migrations/0002_customer_phone_number.py new file mode 100644 index 0000000..1a04724 --- /dev/null +++ b/project/migrations/0002_customer_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-09-23 21:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('project', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='phone_number', + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] From 1b8dab967e371fd77ee9d56987f3997f95d7d6aa Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:08:46 +0300 Subject: [PATCH 23/47] chore: Add pycache files in utils direcotry to be untracked --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b50aad2..22c03cc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ api/__pycache__/ project/__pycache__/ project/migrations/__pycache__/ project/tests/__pycache__/ +project/utils/__pycache__/ From dbdf214562a6cc1e7e2dbb2944326572a5556226 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 20:10:55 +0300 Subject: [PATCH 24/47] config: Add Africas Talking settings Added Africas Talking settings for the initialization - SMS_USERNAME - SMS_API_KEY --- envsample | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/envsample b/envsample index 214b254..1fb3417 100644 --- a/envsample +++ b/envsample @@ -13,3 +13,8 @@ DB_PORT=5432 # Change to your Google credentials GOOGLE_CLIENT_ID='' GOOGLE_CLIENT_SECRET='' + +# Africas Talking settings +# Add your API KEY +SMS_USERNAME='sandbox' +SMS_API_KEY='' From 8df080911555e6d8ecf813bf92f8b11c7bdce5d0 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 22:40:43 +0300 Subject: [PATCH 25/47] feat: Add mock to simulate sms sending Added mock to the OrderModelTest class on the setUp method before creating an order. This is to simulate an SMS has been sent when an order is created. Also added a phone number to the customer instance created. --- project/tests/test_models.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/project/tests/test_models.py b/project/tests/test_models.py index 070307a..09c1a4b 100644 --- a/project/tests/test_models.py +++ b/project/tests/test_models.py @@ -1,6 +1,8 @@ from django.test import TestCase from ..models import Customer, Order from django.utils import timezone +from unittest.mock import patch +from unittest import mock class CustomerModelTest(TestCase): @@ -26,13 +28,18 @@ def test_customer_str_method(self): class OrderModelTest(TestCase): - def setUp(self): + @patch('project.signals.send_sms_alert') + def setUp(self, mock_send_sms): # Create a customer object self.customer = Customer.objects.create( name='Jane Doe', email='janedoe@example.com', - code='CUST456' + code='CUST456', + phone_number='+254705' ) + + mock_send_sms.return_value = None + # Create an order object self.order = Order.objects.create( item='Book A', @@ -40,6 +47,7 @@ def setUp(self): customer=self.customer, time=timezone.now() ) + mock_send_sms.assert_called_once_with(self.customer.phone_number, mock.ANY) def test_order_creation(self): """Test that an order object is created successfully""" @@ -49,6 +57,8 @@ def test_order_creation(self): def test_order_str_method(self): """Test the __str__ method of the Order model""" - expected_str = f"Order of Book A for 20.00 by \ - {self.customer.name} at {self.order.time}" + expected_str = ( + f"Order of Book A for 20.00 by {self.customer.name} " + f"at {self.order.time}" + ) self.assertEqual(str(self.order), expected_str) From 4caeeed442ea213c34d91c12ddc4e508acc435aa Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 22:44:27 +0300 Subject: [PATCH 26/47] feat: Add mock to simulate sms sending Added mock to the test_create_order method. This is to simulate an SMS has been sent when an order is created. Also added a phone number to the valid_customer_data in the setUp method.. --- project/tests/test_views.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/project/tests/test_views.py b/project/tests/test_views.py index 916f5a2..dee5328 100644 --- a/project/tests/test_views.py +++ b/project/tests/test_views.py @@ -8,6 +8,8 @@ from ..models import Customer, Order from django.utils import timezone from datetime import timedelta +from unittest.mock import patch +from unittest import mock class VitabuAPITestCase(APITestCase): @@ -33,7 +35,8 @@ def setUp(self): self.customer = Customer.objects.create( name="John Doe", email="johndoe@example.com", - code="CUST123" + code="CUST123", + phone_number="+2547050" ) # Create a sample order for testing @@ -51,7 +54,8 @@ def setUp(self): self.valid_customer_data = { "name": "Jane Doe", "email": "janedoe@example.com", - "code": "CUST456" + "code": "CUST456", + "phone_number": "+2547056" } self.valid_order_data = { "customer_id": self.customer.id, @@ -91,8 +95,12 @@ def test_get_customers(self): self.assertEqual(response.data[0]['code'], "CUST123") self.assertEqual(response.data[0]['email'], "johndoe@example.com") - def test_create_order(self): + @patch('project.signals.send_sms_alert') + def test_create_order(self, mock_send_sms): """Test to create a new order""" + # Mock the SMS function to do nothing + mock_send_sms.return_value = None + # Send a POST request to create an order response = self.client.post( self.order_url, @@ -108,6 +116,10 @@ def test_create_order(self): self.assertEqual(Order.objects.count(), 2) self.assertEqual(Order.objects.last().item, "Book A") + # Check that the mock SMS function was called once + mock_send_sms.assert_called_once_with( + self.customer.phone_number, mock.ANY) + def test_get_orders(self): """Test to get all orders""" # Send a GET request to retrieve orders From ba4345439a32032521da741528d52392e99959d2 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Tue, 24 Sep 2024 23:14:57 +0300 Subject: [PATCH 27/47] feat: Add tests for send_sms_alert function Added tests for africastalking functionality --- project/tests/test_sms_utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 project/tests/test_sms_utils.py diff --git a/project/tests/test_sms_utils.py b/project/tests/test_sms_utils.py new file mode 100644 index 0000000..365c8b1 --- /dev/null +++ b/project/tests/test_sms_utils.py @@ -0,0 +1,30 @@ +from unittest import TestCase, mock +from ..utils.sms_utils import send_sms_alert + +class SendSmsAlertTestCase(TestCase): + + @mock.patch('project.utils.sms_utils.sms.send') # Mock the sms.send method + def test_send_sms_alert_success(self, mock_send): + """Test that SMS is sent successfully""" + # Arrange: Set up the mock response to simulate a successful send + mock_send.return_value = {"status": "success"} + + # Act: Call the function + response = send_sms_alert("+1234567890", "Test message") + + # Assert: Verify that the send method was called with the correct arguments + mock_send.assert_called_once_with("Test message", ["+1234567890"]) + self.assertEqual(response, {"status": "success"}) + + @mock.patch('project.utils.sms_utils.sms.send') # Mock the sms.send method + def test_send_sms_alert_failure(self, mock_send): + """Test handling of SMS sending failure""" + # Arrange: Simulate an exception when sending SMS + mock_send.side_effect = Exception("Network error") + + # Act: Call the function + response = send_sms_alert("+1234567890", "Test message") + + # Assert: Check that the response is None due to the exception + mock_send.assert_called_once_with("Test message", ["+1234567890"]) + self.assertIsNone(response) From 87c70825239ed20dda5fdae2f73e2dc5ce75d193 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 00:26:09 +0300 Subject: [PATCH 28/47] docs: Add api documentation for the project --- api.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 api.md diff --git a/api.md b/api.md new file mode 100644 index 0000000..addf18c --- /dev/null +++ b/api.md @@ -0,0 +1,82 @@ +# API Documentation +### Customer Endpoints +1. List All Customers +- URL: /api/customers +- Method: GET +- Description: Retrieve a list of all customers. +- Response Example: + ```sh + [ + { + "id": 2, + "name": "Mwangi", + "email": "mvangicode@gmail.com", + "code": "CUST254", + "phone_number": "+25470...." + }, + ... + ] + ``` + +2. Retrieve, Update, or Delete a Specific Customer + +- URL: api/customers/\/ +- Methods: GET, PUT, DELETE +- Description: Retrieve, update, or delete a specific customer by their primary key. +- Response Example (GET): + ```sh + { + "id": 2, + "name": "Mwangi", + "email": "mvangicode@gmail.com", + "code": "CUST254", + "phone_number": "+25470...." + } + ``` + +### Order Endpoints +3. List All Orders +- URL: /orders/ +- Method: GET +- Description: Retrieve a list of all orders. +- Response Example: + ```sh + [ + { + "id": 2, + "item": "Book A", + "amount": "20.00", + "time": "2024-09-24T21:21:28.341126Z", + "customer": { + "id": 2, + "name": "Mwangi", + "email": "mvangicode@gmail.com", + "code": "CUST254", + "phone_number": "+25470..." + } + }, + ... + ] + ``` + +4. Retrieve, Update, or Delete a Specific Order + +- URL: /orders/\/ +- Methods: GET, PUT, PATCH, DELETE +- Description: Retrieve, update, or delete a specific order by their primary key. +- Response Example (GET): + ```sh + { + "id": 2, + "item": "Book A", + "amount": "20.00", + "time": "2024-09-24T21:21:28.341126Z", + "customer": { + "id": 2, + "name": "Mwangi", + "email": "mvangicode@gmail.com", + "code": "CUST254", + "phone_number": "+254705305054" + } + } + ``` From effe1a430d90e14038c783b7bb47f5fa58c27281 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 00:26:39 +0300 Subject: [PATCH 29/47] docs: Add README file for the project --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb2e23c --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Vitabu +This API provides a simple service for managing customers and their orders, implementing secure authentication using OpenID Connect (OIDC) and sending SMS notifications upon order creation. The application is built using Django and Django REST Framework (DRF) with PostgreSQL as the database, and leverages Africa’s Talking SMS gateway for real-time alerts. + +## Key Features +1. **Customer Management:** Create, update, and view customers, including details such as name, email, and unique codes. + +2. **Order Management:** Create and view orders linked to customers, with details such as item name, amount, and order timestamp. + +3. **Authentication and Authorization:** Secure access to the API using OpenID Connect (OIDC) via django-allauth, enabling seamless integration with identity providers like Google for OAuth2-based authentication. + +4. **SMS Notifications:** Upon successful order creation, the API sends an SMS notification to the customer's registered phone number using Africa’s Talking SMS gateway. + +## Technologies Used +- **Backend:** Python, Django, Django REST Framework +Database: PostgreSQL +- **Authentication:** OpenID Connect (OIDC) via django-allauth +- **SMS Alerts:** Africa’s Talking SMS gateway + +## Setup & Installation +1. Clone the repository. +2. Install pipenv: + - On Linux + ```sh + sudo apt-get install pipenv + ``` + - On macOS or Windows + ```sh + pip install pipenv + ``` +3. Setup project environment: + ```sh + pipenv install + ``` +4. Activate the project environment: + ```sh + pipenv shell + ``` + For more information about Pipenv, check: [Pipenv: A Guide to The New Python Packaging Tool - Real Pyhton](https://realpython.com/pipenv-guide/) + +5. Copy the envsample file to the root folder with the name ```.env```. + Change settings and configurations on the ```.env``` file: + - Database settings: Used PostgreSQL for this project. + - All Auth settings: Google's client_id and client_secret. + - Africa's Talking Settings: username and api key + +6. Make changes to your database: + ```sh + python manage.py migrate + ``` +7. Usage: + ```sh + cd Vitabu-api + python manage.py runserver + ``` + On your browser, run the following link: http://localhost:8000/accounts/login. + Tap on Google to sign in with Google to access the API. + +## API Documentation +The documentation of the API created using Django Rest Framework (DRF) is on the file ```api.md```. + +## Contributing +Want to make TradersIn better? +- Fork the project. +- Create a new branch to work on ```git checkout -b ``` +- You can name the branch with the prefix ```feature_``` +- Add your changes and push them to the branch: ```git push``` +- Open a pull request + + +## Authors +Lionel Gicheru [LinkedIn](https://www.linkedin.com/in/lionelmwangi/) From a7ab5312c6f0c6de624aee5fcc1bbb5decb96271 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 09:30:04 +0300 Subject: [PATCH 30/47] feat: Add africastaliking package to Pipfile --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 23372f7..0dd765c 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ djangorestframework = "*" coverage = "*" django-allauth = {extras = ["socialaccount"], version = "*"} django-oauth-toolkit = "*" +africastalking = "*" [dev-packages] From 95808a19f4ab40b83ff01104e99a2cdb72e9d388 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 09:47:58 +0300 Subject: [PATCH 31/47] feat: Add GitHub Actions workflows --- .github/workflows/ci-cd.yml | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..fe5a930 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,79 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test_db + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install Pipenv + run: | + python -m pip install --upgrade pip + pip install pipenv + + - name: Install dependencies + run: | + pipenv install --dev + + - name: Set up Database + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + run: | + python manage.py migrate + python manage.py collectstatic --noinput + + - name: Run Tests + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + run: | + # Activate the pipenv environment + pipenv run python manage.py test --verbosity=2 + + - name: Upload Coverage Report + if: success() + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: htmlcov/ + + deploy: + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Deploy to your environment + # This is a placeholder. Configure your deployment steps here. + run: echo "Deploying application..." + From 68f1e945fc621623d7b90c2020853b473590fe8b Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 09:52:17 +0300 Subject: [PATCH 32/47] fix: Add command to activate pipenv to setup database --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index fe5a930..79f1b3e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,6 +49,7 @@ jobs: env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db run: | + pipenv shell python manage.py migrate python manage.py collectstatic --noinput From 48dd6f5d8cb738560593ee0a136cfdedd4cb5a1f Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 09:57:44 +0300 Subject: [PATCH 33/47] fix: Use pipenv run to run database commands Changed the command from using pipenv shell to pipenv each command to avoid errors --- .github/workflows/ci-cd.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 79f1b3e..c50bdb1 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -49,9 +49,8 @@ jobs: env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db run: | - pipenv shell - python manage.py migrate - python manage.py collectstatic --noinput + pipenv run python manage.py migrate + pipenv run python manage.py collectstatic --noinput - name: Run Tests env: From 33c26739506138937868824a123b31ddd4dff691 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 10:13:22 +0300 Subject: [PATCH 34/47] feat: Add SECRET_KEY to env Added SECRET_KEY as added in secrets in GitHub --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c50bdb1..591c5f3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -47,6 +47,7 @@ jobs: - name: Set up Database env: + SECRET_KEY: ${{ secrets.SECRET_KEY }} DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db run: | pipenv run python manage.py migrate From b72e0fa2366dc60ee766ec12ebd1026b7272cd2a Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 10:25:00 +0300 Subject: [PATCH 35/47] feat: Change DATABASE_URL to use secret variables Changed the DATABASE_URL to use the secret variables defined in GitHub secrets --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 591c5f3..d9fa41f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Database env: SECRET_KEY: ${{ secrets.SECRET_KEY }} - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/{{ secrets.DB_NAME }} run: | pipenv run python manage.py migrate pipenv run python manage.py collectstatic --noinput From e6fc1b33a3b2490fbdfda700819eeaa1482f93ae Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 10:38:46 +0300 Subject: [PATCH 36/47] feat: Add database variables to env Added database variables to env of settin up database - DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME --- .github/workflows/ci-cd.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d9fa41f..d9af489 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -48,6 +48,11 @@ jobs: - name: Set up Database env: SECRET_KEY: ${{ secrets.SECRET_KEY }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_PORT: ${{ secrets.DB_PORT }} + DB_NAME: ${{ secrets.DB_NAME }} DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/{{ secrets.DB_NAME }} run: | pipenv run python manage.py migrate From 6a76c0d1d48507336b2eb777dcb43fbaed094a47 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 10:49:23 +0300 Subject: [PATCH 37/47] feat: Add Google variables secrets to env Added Google Client_id and client_secret to env on Set up Database. Also added sms_username and sms_api_key variables --- .github/workflows/ci-cd.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d9af489..013c883 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -52,7 +52,11 @@ jobs: DB_PASSWORD: ${{ secrets.DB_PASSWORD }} DB_HOST: ${{ secrets.DB_HOST }} DB_PORT: ${{ secrets.DB_PORT }} - DB_NAME: ${{ secrets.DB_NAME }} + DB_NAME: ${{ secrets.DB_NAME }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + SMS_USERNAME: ${{ secrets.SMS_USERNAME }} + SMS_API_KEY: ${{ secrets.SMS_API_KEY }} DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/{{ secrets.DB_NAME }} run: | pipenv run python manage.py migrate From eeb5d7ef6e473c01e664d91eabc78146f26979cd Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 10:57:15 +0300 Subject: [PATCH 38/47] fix: Change DATABASE_URL for Set up Database Changed the port to match the one defined in services --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 013c883..65b335f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -57,14 +57,14 @@ jobs: GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} SMS_USERNAME: ${{ secrets.SMS_USERNAME }} SMS_API_KEY: ${{ secrets.SMS_API_KEY }} - DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/{{ secrets.DB_NAME }} + DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@postgres:5432/${{ secrets.DB_NAME }} run: | pipenv run python manage.py migrate pipenv run python manage.py collectstatic --noinput - name: Run Tests env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@postgres:5432/${{ secrets.DB_NAME }} run: | # Activate the pipenv environment pipenv run python manage.py test --verbosity=2 From c6fe0d653d4a94e5aa9e2289d98a1e698655efb9 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 11:25:35 +0300 Subject: [PATCH 39/47] feat: Change services settings to secrets --- .github/workflows/ci-cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 65b335f..453e6c0 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -16,13 +16,13 @@ jobs: postgres: image: postgres:latest env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: test_db + POSTGRES_USER: ${{ secrets.DB_USER }} + POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }} + POSTGRES_DB: ${{ secrets.DB_NAME }} ports: - 5432:5432 options: >- - --health-cmd="pg_isready -U postgres" + --health-cmd="pg_isready -U ${{ secrets.DB_USER }}" --health-interval=10s --health-timeout=5s --health-retries=5 From bd4556c67ee54ba053f5065e79a67c4a17055ec1 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 11:49:17 +0300 Subject: [PATCH 40/47] fix: Configure DATABASE_URL correctly Configured the url to be in line with the services section --- .github/workflows/ci-cd.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 453e6c0..ca58699 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -16,13 +16,13 @@ jobs: postgres: image: postgres:latest env: - POSTGRES_USER: ${{ secrets.DB_USER }} - POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }} - POSTGRES_DB: ${{ secrets.DB_NAME }} + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb ports: - 5432:5432 options: >- - --health-cmd="pg_isready -U ${{ secrets.DB_USER }}" + --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5 @@ -57,14 +57,14 @@ jobs: GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} SMS_USERNAME: ${{ secrets.SMS_USERNAME }} SMS_API_KEY: ${{ secrets.SMS_API_KEY }} - DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@postgres:5432/${{ secrets.DB_NAME }} + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb run: | pipenv run python manage.py migrate pipenv run python manage.py collectstatic --noinput - name: Run Tests env: - DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@postgres:5432/${{ secrets.DB_NAME }} + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb run: | # Activate the pipenv environment pipenv run python manage.py test --verbosity=2 From 83668229e84c472cb07e3cdf7dee92916af36ee4 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:02:13 +0300 Subject: [PATCH 41/47] Remove DB variables from database env Removed the DB variables since we are going to use the default ones from services. --- .github/workflows/ci-cd.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index ca58699..7840079 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -48,11 +48,6 @@ jobs: - name: Set up Database env: SECRET_KEY: ${{ secrets.SECRET_KEY }} - DB_USER: ${{ secrets.DB_USER }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_NAME: ${{ secrets.DB_NAME }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} SMS_USERNAME: ${{ secrets.SMS_USERNAME }} From f63a84a1da2ce3678406a36d77941ae0140657f0 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:08:39 +0300 Subject: [PATCH 42/47] config: Add defaults for DATABASE settings Added defaults for database settings to be in line with GitHub Actions workflows --- api/settings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/settings.py b/api/settings.py index 6d2724c..62fb28d 100644 --- a/api/settings.py +++ b/api/settings.py @@ -97,11 +97,11 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': config("DB_NAME"), - 'USER': config("DB_USER"), - 'PASSWORD': config("DB_PASSWORD"), - 'HOST': config("DB_HOST"), - 'PORT': config("DB_PORT"), + 'NAME': config("DB_NAME", default="testdb"), + 'USER': config("DB_USER", default="postgres"), + 'PASSWORD': config("DB_PASSWORD", default="postgres"), + 'HOST': config("DB_HOST", default="localhost"), + 'PORT': config("DB_PORT", default="5432"), } } From b37f5228b5999d010025a6939870fc9d0a2ccdbc Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:13:07 +0300 Subject: [PATCH 43/47] config: Remove collectstatic command Removed collectstatic command since there is no static files for this project --- .github/workflows/ci-cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7840079..7f1998e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -55,7 +55,6 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb run: | pipenv run python manage.py migrate - pipenv run python manage.py collectstatic --noinput - name: Run Tests env: From babb990953f751c017a74e1e43d71558b69fc5d3 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:21:14 +0300 Subject: [PATCH 44/47] config: Set up environment variables as a step This is to ensure Set up database and Run Tests can use the variables without repeating the config --- .github/workflows/ci-cd.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7f1998e..83a57fa 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -44,23 +44,21 @@ jobs: - name: Install dependencies run: | pipenv install --dev + + - name: Set up environment variables + run: | + echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV + echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> $GITHUB_ENV + echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> $GITHUB_ENV + echo "SMS_USERNAME=${{ secrets.SMS_USERNAME }}" >> $GITHUB_ENV + echo "SMS_API_KEY=${{ secrets.SMS_API_KEY }}" >> $GITHUB_ENV - name: Set up Database - env: - SECRET_KEY: ${{ secrets.SECRET_KEY }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - SMS_USERNAME: ${{ secrets.SMS_USERNAME }} - SMS_API_KEY: ${{ secrets.SMS_API_KEY }} - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb run: | pipenv run python manage.py migrate - name: Run Tests - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb run: | - # Activate the pipenv environment pipenv run python manage.py test --verbosity=2 - name: Upload Coverage Report From c90c2c8ef390baa4d86e0f316fc5ebd777c7964f Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:44:50 +0300 Subject: [PATCH 45/47] config: add config file for coverage --- .coveragerc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..520aa17 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,22 @@ +[run] +source = + project + +omit = + */migrations/* + */tests/* + */admin.py + */apps.py + */urls.py + manage.py + +[report] +exclude_lines = + pragma: no cover + def __str__ + def __repr__ + pass + raise NotImplementedError + +[html] +directory = htmlcov From c78554e8994dd53ac97ef2f9a4e37da3703b90b1 Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:46:56 +0300 Subject: [PATCH 46/47] config: Use coverage to run tests Used coverage to run tests and generate a report in section Run Tests with coverage --- .github/workflows/ci-cd.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 83a57fa..334542c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -57,9 +57,11 @@ jobs: run: | pipenv run python manage.py migrate - - name: Run Tests + - name: Run Tests with coverage run: | - pipenv run python manage.py test --verbosity=2 + pipenv run coverage run manage.py test + pipenv run coverage html + pipenv run coverage report - name: Upload Coverage Report if: success() From 6cb0ad420a84fce0fcbe2aa068d8729bb8258cbb Mon Sep 17 00:00:00 2001 From: LionelMv Date: Wed, 25 Sep 2024 12:52:49 +0300 Subject: [PATCH 47/47] docs: Add Technologies used Added CICD to technologies used and Key Features --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cb2e23c..c315081 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ This API provides a simple service for managing customers and their orders, impl 3. **Authentication and Authorization:** Secure access to the API using OpenID Connect (OIDC) via django-allauth, enabling seamless integration with identity providers like Google for OAuth2-based authentication. 4. **SMS Notifications:** Upon successful order creation, the API sends an SMS notification to the customer's registered phone number using Africa’s Talking SMS gateway. +5. **Testing and CI/CD:** Includes comprehensive unit tests to ensure reliability, with continuous integration and deployment setups for automated testing and deployment. ## Technologies Used - **Backend:** Python, Django, Django REST Framework Database: PostgreSQL - **Authentication:** OpenID Connect (OIDC) via django-allauth - **SMS Alerts:** Africa’s Talking SMS gateway +- **CI/CD:** GitHub Actions ## Setup & Installation 1. Clone the repository.