Capstone: Peer-graded Assignment: Little Lemon Web Application, S/W Testing
Important Rules: You have to be authenticated to,
-
To create, that is, POST a booking, but you have to be admin to get all, update or delete bookings. Non-admins can only see there own bookings and will get a Method not allowed error message in Insomia’s response panel, if they try to violate security.
-
To retrieve, that is, do a GET to view all or single menu items, but ONLY admin can create, update or delete menu items. Non-admins will get a Method not allowed error message in menu and a 404 Not Found in Booking if users violate security.
-
Test menu and booking endpoints in DRF Browsable API.
ENDPOINT URL LIST To Be Tested: Tests were done using Insomnia.
Note 1: These 2 links are for Browsable API only:
/api-auth/ then, append login/ to endpoint
/api-auth/ then, append logout/ to endpoint
For more information, instructions and limitations on their use, see project level urls.py
and find endpoint /api-auth/. They do not work in Insomnia, only DRF Browsable API. So,
they were not used in s/w testing below.
Note 2: If you use endpoint http://127.0.0.1:8000/api/users/3/, you will not be able to use the Extra Actions button found in djoser urls. Auth endpoint puts email first, while /api/users/ puts username first. It will also prevent you from using djoser auto-created urls.
Tokens used in Testing: Can be done as Bearer token or by using Header tab in Insomnia. Authorization token are examples.
- Account: admin, role admin HEADER Tab: Authorization Token a85fb0ea4c0487a71f0b8032237907c164a231b6
- Account: jen, non-admin Authorization Token 283a7b8f3434c716e81f23077116de29bfeab0b9
-
Register a new user:
POST http://127.0.0.1:8000/api/registration/ -
ADD or Retrieve users: One must use an admin account to do GET, but non-admin can POST a new account.
GET, POST http://127.0.0.1:8000/auth/users/ -
Get single user id=7:
GET, PUT, PATCH, DELETE http://127.0.0.1:8000/auth/users/7/
-
Get an access token by POST, by providing a username and password to prove authentication is configured.
-
Add, display or update Menu Items: Test in Browsable and Insomnia. Use admin account to create, update and delete. Non-admins can't do that, but they can GET to retrieve the menu item list.
GET, POST http://127.0.0.1:8000/api/menu/ GET, PUT, PATCH, DELETE http://127.0.0.1:8000/api/menu/13/
-
Render to client an HTML 5 Template to display Welcome to Little Lemon Restaurant and serves static HTML content.
http://127.0.0.1:8000/api/ -
Add, display or update bookings: Test in Browsable and Insomnia. Use non-admin accounts to create, update and delete bookings belonging only to them. Admins can do everything.
GET POST http://127.0.0.1:8000/api/booking/ GET, PUT, PATCH, DELETE http://127.0.0.1:8000/api/booking/26/ -
Do unit testing using Power Shell command in the directory containing manage.py:
python manage.py test
S/W Testing Results:
- Does the web application use Django to serve static HTML content? YES.
Renderd to client an HTML 5 Template to display “Welcome to Little Lemon Restaurant”
containing a static content image logo.png from the static directory.
Output in VS Code terminal with Server status: "GET /api/ HTTP/1.1" 200 397 "GET /static/img/logo.png HTTP/1.1" 304 0
HTTP Status Code: 200. Means web page successfully rendered static content in the form of an image called logo.png, 304 means the page was not modified (cache hit).
Test Result: Passed. Answer: YES.
- Has the learner committed the project to a Git repository? YES
The project has been pushed (committed) to its Git repository over 5 times so far using these commands:
Steps to do a Push:
git remote show origin git status git diff git branch git checkout -B littlelemon git add . git push origin littlelemon Last 2 lines of processing are very important.
To https://github.com/getmcd/LittleLemon.git 7a34fcf..8a9cd1a littlelemon -> littlelemon
They mean:
- The remote repository is LittleLemon.git
- The branch littlelemon on my machine was successfully pushed
- The remote branch littlelemon was updated
- It moved from commit 7a34fcf to commit 8a9cd1a Logged into GitHub and opened repository LittleLemon.git.
Created a Pull Request where feature branch littlelemon compared to branch main, which is production. When ready to merge is asserted by GitHub, did the merge and confirmed it.
Next Step, do a pull on remote main onto local main.
Steps to do a pull on main in the repository:
git branch git status git checkout main git pull origin main
If the pull aborts, execute “git stash” and re-run the pull command.
git stash git pull origin main
From https://github.com/getmcd/LittleLemon
- branch main -> FETCH_HEAD Updating 3aed4d6..9fa4c78
Meaning: Git contacted the remote repository:
- https://github.com/getmcd/LittleLemon
- It fetched the remote branch named main
- It temporarily stored the fetched state in a special pointer called: FETCH_HEAD, which lets Git know:
- Here is what the remote branch currently looks like.
- It does not change your local branch yet.
Updating 3aed4d6..9fa4c78 means:
- The branch advanced from commit 3aed4d6 to commit 9fa4c78.
- Git downloaded new commit data
- Git learned the remote branch moved forward The pull synchronizes, ie. Updates, client-side littlelemon with Git’s repository main, also known as production. Now, the client-side littlelemon has an exact copy of production main.
- Does the application connect the backend to a MySQL database? YES.
Used this PS command to install MySQL database.
pip3 install mysqlclient Here is the database configuration for MySQL located in Settings.py file.
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "LittleLemon", "USER": "root", "PASSWORD": "lemon@789!” # for example "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { # keep your strict mode "init_command": ( "SET sql_mode='STRICT_TRANS_TABLES'; " #"SET time_zone = '+00:00';" ) }, } }
Enter this command to prove that the database settings work by connecting to MySql.
Mysql -u root -p
Enter password: lemon@789! # for example
Test database connectivity by doing a SELECT on a table by following these steps.
Step 1:
mysql> show databases; +--------------------+ | Database | +--------------------+ | bookings | | information_schema | | littlelemon | | menu_db | | menu_items | | mysql | | performance_schema | | reservations | | restaurant | | sakila | | sys | | world | +--------------------+ 12 rows in set (0.00 sec)
Step 2:
Tell MySql to use database littlelemon.
mysql> use littlelemon; Database changed
Step 3:
Do a SELECT.
mysql> use littlelemon; Database changed
mysql> Select * from restaurant_booking; +----+------------------+------------------+----------------------------+---------+ | 19 | Pedro's family | 4 | 2026-02-15 18:00:00.000000 | 12 | | 20 | Cookie and Dad | 2 | 2026-02-15 12:43:17.000000 | 7 | | 22 | Jen | 13 | 2026-02-21 20:44:00.000000 | 5 | | 23 | Tom | 2 | 2026-02-15 23:00:00.000000 | 1 | | 24 | admin and family | 6 | 2026-02-15 13:15:35.000000 | 1 | | 25 | admin | 2 | 2026-02-25 20:44:00.000000 | 1 | | 26 | Jen | 22 | 2026-02-16 20:44:00.000000 | 5 | +----+------------------+------------------+----------------------------+---------+ 7 rows in set (0.00 sec)
Answer: Yes
The application connected to the backend’s MySQL littlelemon database and returned 7 records.
- Are the menu and table booking APIs implemented? YES
Yes, here are the results of s/w testing on the menu and table booking API’s. Menu uses path(), while booking uses the DefaultRouter() and does not use a path() mapping view to endpoint.
Used Insomnia to test both APIs.
MENU TESTING:
========================================================================= Tested menu CRUD, authorization and authentication capabilities having Created, retrieved, updated and deleted model instances.
The non-admin account must not be allowed to add, update or delete menu items.
Only the admin account can do that.
Test admin account first: Account: admin, role admin HEADER Tab: Authorization Token a85fb0ea4c0487a71f0b8032237907c164a231b6
Doing the GET is Read Only.
GET http://127.0.0.1:8000/api/menu/
JSON Output: Partial list { "count": 9, "next": "http://127.0.0.1:8000/api/menu/?page=2", "previous": null, "results": [ { "id": 5, "title": "beef pie", "price": "5.95", "inventory": 6 },
HTTP Status Code: 200 OK. 9 menu items successfully retrieved.
POST http://127.0.0.1:8000/api/menu/
With POST, must check with both an admin and a non-admin account. The non-admin account must not be allowed to add, update or delete menu items. Only admin can do that.
Note: If you click link http://127.0.0.1:8000/api/menu/?page=2 in the response panel instead of adding ?page=2 to the input URL box next to the GET button, it will redirect to DRF Browsable API.
JSON Payload { "title": "chicken pie", "price": "9.95", "inventory": 22 }
JSON Output { "id": 14, "title": "chicken pie", "price": "9.95", "inventory": 22 }
HTTP Status Code: 201 Created. Menu item chicken pie successfully created.
Test Result: Passed.
GET http://127.0.0.1:8000/api/menu/14/
JSON Output { "id": 14, "title": "chicken pie", "price": "9.95", "inventory": 22 }
HTTP Status Code: 200 OK. Retrieved and displayed Menu item 14. Test Result: Passed.
PUT http://127.0.0.1:8000/api/menu/14/
JSON Before PUT: { "title": "chicken pie", "price": "8.95", "inventory": 32 } JSON Input Paylaod: { "title": "chicken pie", "price": "9.95", "inventory": 22 } JSON Output: { "id": 14, "title": "chicken pie", "price": "9.95", "inventory": 22 }
HTTP Status Code: 200 OK. Menu item 14 was successfully updated: price 8.95 to 9.95 and inventory from 32 to 22.
Test Result: Passed.
PATCH http://127.0.0.1:8000/api/menu/14/
JSON before PATCH { "title": "chicken pie", "price": "9.95", "inventory": 22 } JSON INPUT Payload { "price": "8.95", "inventory": 32 }
JSON Output { "id": 14, "title": "chicken pie", "price": "8.95", "inventory": 32 }
HTTP Status Code: 200 OK. Retrieved Menu item 6 and successfully updated price to $7.95.
Test Result: Passed.
DELETE http://127.0.0.1:8000/api/menu/3/
OUTPUT No body returned for response
HTTP Status Code: 204 No Content. Retrieved and successfully deleted menu item 6, Strawberry Ice Cream.
Test Result: Passed.
========================================================================= Continue testing endpoint http://127.0.0.1:8000/api/menu/ , but with a non-admin account.
User: jen. Is a non-admin account. NON-ADMIN Account Authorization Token 283a7b8f3434c716e81f23077116de29bfeab0b9
POST http://127.0.0.1:8000/api/menu/
JSON Payload: { "title": "Lamb Stew", "price": "8.95", "inventory": 32 } JSON OUTPUT: { "detail": "You do not have permission to perform this action." }
HTTP Status Code: 403 Forbidden. Success: Non-admin accounts were NOT allowed create new menu items using POST Http Method. The custom IsAdminOrReadOnly permission worked to prevent unauthorized access by non-admins
Test Result: Passed.
GET http://127.0.0.1:8000/api/menu/
JSON Output: { "count": 11, "next": "http://127.0.0.1:8000/api/menu/?page=2", "previous": null, "results": [ { "id": 13, "title": "Avocado Sandwhich", "price": "9.95", "inventory": 14 },# first 11 lines of output
HTTP Status Code: 200 OK. Retrieved and displayed all Menu items successfully using non-admin account.
Test Result: Passed.
GET http://127.0.0.1:8000/api/menu/14/
JSON Output: { "id": 14, "title": "chicken pie", "price": "8.95", "inventory": 32 } HTTP Status Code: 200 OK. Retrieved and displayed Menu item 14 successfully using non-admin account.
Test Result: Passed.
PUT http://127.0.0.1:8000/api/menu/14/
Update inventory to 31.
JSON Payload { "title": "Lamb Stew", "price": "9.95", "inventory": 32 } JSON Output: { "detail": "You do not have permission to perform this action." }
HTTP Status Code: 403 Forbidden. Success: Non-admins were NOT allowed to update menu items.
Test Result: Passed.
PATCH http://127.0.0.1:8000/api/menu/14/
JSON INPUT Payload { "price": "9.95", "inventory": 32 } JSON Output { "detail": "You do not have permission to perform this action." }
HTTP Status Code: 403 Forbidden. Success: Non-admins were NOT allowed to update menu items.
Test Result: Passed.
DELETE http://127.0.0.1:8000/api/menu/6/
JSON OUTPUT { "detail": "You do not have permission to perform this action." }
HTTP Status Code: 403 Forbidden. Success: Non-admins were NOT allowed to delete menu items.
Test Result: Passed.
BOOKING:
=========================================================================
Tested POST, GET, PUT, PATCH, DELETE for endpoint http://127.0.0.1:8000/api/booking/
Test admin account first: Account: admin, role admin HEADER Tab: Authorization Token a85fb0ea4c0487a71f0b8032237907c164a231b6
GET http://127.0.0.1:8000/api/booking/
JSON Output { "count": 6, "next": "http://127.0.0.1:8000/api/booking/?page=2", "previous": null, "results": [ { "id": 22, "name": "Jen", "number_of_guests": 15, "booking_date": "2026-03-24T20:44:00", "user": 5 }, # First 12 lines HTTP Status Code: 200 OK. Admin successfully retrieved all Bookings for all users.
Test Result: Passed.
POST http://127.0.0.1:8000/api/booking/
JSON Input Payload { "name": "admin", "number_of_guests": 2, "booking_date": "2026-02-25T20:44:00" } JSON Output
{ "id": 25, "name": "admin", "number_of_guests": 2, "booking_date": "2026-02-25T20:44:00", "user": 1 }
HTTP Status Code: 201 Created. Admin successfully Booked a table
Test Result: Passed, admin has full control over booking.
GET http://127.0.0.1:8000/api/booking/22/
JSON Ouput: { "id": 22, "name": "Jen", "number_of_guests": 15, "booking_date": "2026-03-24T20:44:00", "user": 5 }
HTTP Status Code: 200 OK. Admin successfully retrieved Booking 22 belonging to non-admin Jen. Test Result: Passed, admin has full control over booking.
PUT http://127.0.0.1:8000/api/booking/22/
Output before PUT: { "id": 22, "name": "Jen", "number_of_guests": 15, "booking_date": "2026-03-24T20:44:00", "user": 5 } JSON Input Payload { "name": "Jen", "number_of_guests": 8, "booking_date": "2026-02-27T20:44:00", "user": 5 } JSON Ouput: { "id": 22, "name": "Jen", "number_of_guests": 8, "booking_date": "2026-02-27T20:44:00", "user": 5 }
HTTP Status Code: 200 OK. Admin successfully updated number of guests and date for Booking 22 belonging to non-admin Jen.
Test Result: Passed, admin has full control over booking.
PATCH http://127.0.0.1:8000/api/booking/22/
JSON Input Payload { "number_of_guests": 2 }
JSON Ouput: { "id": 22, "name": "Jen", "number_of_guests": 2, "booking_date": "2026-02-27T20:44:00", "user": 5 }
HTTP Status Code: 200 OK. Admin successfully updated number of guests for Booking 22 belonging to Jen.
Test Result: Passed, Admin as control over update ooking.
DELETE http://127.0.0.1:8000/api/booking/21/
Ouput:
HTTP Status Code: 204 No Content, No body returned for response. Admin successfully deleted Booking 16 belonging to Jen.
Test Result: Passed, but only on ability to delete a booking.
=========================================================================
GET http://127.0.0.1:8000/api/booking/
JSON Output { "count": 1, "next": null, "previous": null, "results": [ { "id": 22, "name": "Jen", "number_of_guests": 2, "booking_date": "2026-02-27T20:44:00", "user": 5 } ] }
HTTP Status Code: 200 OK. Jen successfully retrieved only Bookings that only belong to her.
Test Result: Passed. Recommend to add IsAdminOrReadOnly
POST http://127.0.0.1:8000/api/booking/
JSON Input Payload { "name": "Jen", "number_of_guests": 22, "booking_date": "2026-02-16T20:44:00" } JSON Output { "id": 26, "name": "Jen", "number_of_guests": 22, "booking_date": "2026-02-16T20:44:00", "user": 5 }
HTTP Status Code: 201 Created. Jen successfully Booked a table
Test Result: Passed.
GET http://127.0.0.1:8000/api/booking/25/
Booking 25 is admin and does not belong to non-admin jen. Instead of responding with a 403 Forbidden, it’s more secure to respond with a 404 not found.
Important: When a non-admin tries:
- PUT /api/bookings/123/ , where booking 123 belongs to someone else, DRF tries to find the object using my filtered queryset: -- Booking.objects.filter(user=request.user).get(pk=123)
- That returns no rows, so DRF concludes: No Booking matches the given query.
- Responds with 404 Not Found.
- This is actually a common security-friendly pattern
- Returning 404 for “not yours” avoids leaking whether the object exists (it prevents “ID probing”).
JSON Ouput: { "detail": "No Booking matches the given query." }
HTTP Status Code: 404 Not Found. Jen was NOT successful at updating Booking 25, which
belongs to user admin, not her.
Test Result: Passed, Security maintained by not responding with a 404 Not Found, NOT a 403 Forbidden.
PUT http://127.0.0.1:8000/api/booking/25/
JSON Input Payload
{
"name": "admin",
"number_of_guests": 4,
"booking_date": "2026-02-25T20:44:00"
}
JSON Ouput:
{
"detail": "No Booking matches the given query."
}
HTTP Status Code: 404 Not Found. Jen was NOT successful at updating Booking 25, which
belongs to user admin, not her.
Test Result: Passed, Security maintained by not responding with a 404 Not Found, NOT a 403 Forbidden.
PATCH http://127.0.0.1:8000/api/booking/25/
JSON Input Payload { "number_of_guests": 20, }
JSON Ouput: { "detail": "No Booking matches the given query." }
HTTP Status Code: 404 Not Found. Jen was NOT successful at updating Booking 25, which
belongs to user admin, not her.
Test Result: Passed, Security maintained by not responding with a 404 Not Found, NOT a 403 Forbidden.
DELETE http://127.0.0.1:8000/api/booking/25/
Ouput: { "detail": "No Booking matches the given query." }
HTTP Status Code: 404 Not Found. Jen was NOT successful deleting Booking 25, which belongs to user admin, not her.
Test Result: Passed, Security maintained by not responding with a 404 Not Found, NOT a 403 Forbidden.
- Is the application set up with user registration and authentication? YES
A. Registration, this endpoint allows a user to create and register a new account using Insomnia with NO access token:
POST http://127.0.0.1:8000/api/registration/
JSON Payload: { "username": "Lory", "email": "lor@littlelemon.com", "password": "lemon@lor!" } JSON Output: { "id": 15, "url": "http://127.0.0.1:8000/api/users/15/", "username": "Lory", "email": "lor@littlelemon.com" }
HTTP Status Code: 201 Created Admin Panel User Status: Active, customer can register and then generate access tokens
Test Result: Passed. API is setup for user registration.
B. Authentication, will create an Authorization Token using endpoint /auth/token/login/.
If my API successfully returns an authorization token, it proves that:
- API has authentication configured
- API is using token-based authentication
This confirms:
- The user’s credentials were validated
- The system generated (or retrieved) a token
- TokenAuthentication is active in my project
POST http://127.0.0.1:8000/auth/token/login/
Endpoint /api/users/ cannot create an authorization token.
Note: Only POST is allowed for this endpoint.
JSON Body Payload: { "username": "jen", "password": "lemon@jen!" } JSON Output: { "auth_token": "283a7b8f3434c716e81f23077116de29bfeab0b9" }
HTTP Status Code: 200 OK Customer can generate Authorization tokens using /auth/token/login
Test Result: Passed. Authenication was used successfully to obtain an Authorization Token.
- Does the application contain unit tests? YES
YES, it contains two unit tests in a tests directory at the project level with an empty init.py to turn it into a package.
Test_models.py:
class MenuItemTest(TestCase): def test_get_item(self): item = MenuItem.objects.create(title="IceCream", price=80, inventory=100) self.assertEqual(str(item), "IceCream : 80.00")
test_views.py:
class MenuViewTest(TestCase): def setUp(self): self.client = APIClient()
# Create a normal user and authenticate
self.user = User.objects.create_user(username="tester", password="pass1234!")
self.client.force_authenticate(user=self.user)
# Create test data
MenuItem.objects.create(title="IceCream", price="8.00", inventory=50)
MenuItem.objects.create(title="Burger", price="10.00", inventory=20)
def test_getall(self):
# Adjust path if yours is different (e.g., "/api/menu/")
response = self.client.get("/api/menu/")
# compares response status code with expected code 200 OK
self.assertEqual(response.status_code, 200)
# If pagination is ON, data is in response.data["results"]
data = response.data.get("results", response.data)
# self.assertEqual(len(data), 2) verifies that the API returned two menu
# items, matching the two objects created in the test setup().
# - Your view is connected
# - Database objects exist
# - Queryset is correct
# - Serialization worked
self.assertEqual(len(data), 2)
# Because you order_by("title"), Burger comes before IceCream
self.assertEqual(data[0]["title"], "Burger")
self.assertEqual(data[1]["title"], "IceCream")
In terminal at PS prompt, enter this command to run the tests automatically.
python manage.py test
PS C:\Capstone-project\littlelemon> python manage.py test
Found 2 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). ..
Ran 2 tests in 0.219s
OK Destroying test database for alias 'default'...
Test Result: Passed. OK displayed, Unit Testing resulted in no errors found.
- Can the API be tested with the Insomnia REST client?
Yes, s/w testing confirmed that all endpoints were successfully tested using Insomnia REST client.