- Group Information
- Project Goal
- Setup AWS EC2 Instance
- How to Deploy and Run the Project Manually
- How to Deploy and Run the Project Using the Provided Bash Script
- Unit and Integration Testing Overview
- Setting Up Tests
- Running Tests
- Test File Structure
- Test Coverage
- Viewing Test Results
- Adding New Tests
- Node.js CI Workflow
- Project Screenshot
-
Group Name: PakPok
-
Members:
Name Student ID Sakkapong Phiphatpornchaikul 6309545033 Chayanon Khanrit 6409682538 Supakorn Oupkaew 6409682918 Anallena Libha 6409682942
The goal of the FoodAdvisor project is to create a user-friendly platform that facilitates seamless content management and enhances user engagement. The aim is to further enhance this platform to make it even more user-friendly and customizable to meet users' needs. This will deliver a comprehensive solution that simplifies content management, improves the user experience, and fosters collaboration within the food community.
- Feature 1: Dynamic Food Menu
- Backend: Strapi V4
- Frontend: Next.js
- Hosting/Deployment: AWS EC2
- Database: SQLite
| software | version |
|---|---|
| node | 16 |
| npm | 8 |
| yarn | 1 |
| curl | 7 |
| nvm | 0.3 |
| git | 2 |
Note
-
Launch AWS EC2 Instance:
- Instance Type: t2.medium
- AMI: Amazon Linux 2023 AMI
- Operating System: Amazon Linux
- Storage: 1x16 GiB gp3 Root volume
-
Configure Security Group Rules:
Type Protocol Port Range Source SSH TCP 22 ::/0 HTTP TCP 80 0.0.0.0/0, ::/0 Custom TCP Rule TCP 1337 0.0.0.0/0 Custom TCP Rule TCP 3000 0.0.0.0/0
-
Log into AWS Management Console:
- Go to the AWS Management Console and
sign into your AWS account.
- Go to the AWS Management Console and
-
Select EC2 Service:
- Click on
Servicesand then selectEC2from the list of services.
- Click on
-
Launch Instance:
- Click on
Launch Instanceto start the process of creating a new EC2 Instance.
- Click on
-
Choose an Amazon Machine Image (AMI):
- Select Amazon Linux 2023 AMI.
-
- Select t2.medium .
-
- Choose 16 Gib gp3.
-
(Optional) Add Tags:
- Add tags for resource management, such as Name: MyEC2Instance.
-
Configure Security Group Rules:
- Create a new Security Group or select an existing one to set rules for accessing the Instance.
-
Select Key Pair:
- Create a new Key Pair or select an existing Key Pair to allow SSH access to the Instance.
-
Launch Instance:
- Click
Launch Instancesand wait for the EC2 Instance to be created.
- Click
-
Access EC2 Instance:
-
Use command
ssh -i "path/to/your-key.pem" ec2-user@<EC2-Public-IP>to connect to the EC2 Instance using the IP Address and Key Pair you created.
-
After launching your EC2 instance and downloading the key pair for SSH access, follow these steps:
-
Run the commands in order:
-
Updates the package lists for available software, ensuring you have the latest information.
sudo yum update -
Installs curl, a tool for transferring data from or to a server.
sudo yum install curl -
Downloads and executes the installation script for Node.js version 16.x.
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo -E bash - -
Installs Node.js without requiring user confirmation.
sudo yum install -y nodejs
-
-
-
run
sudo yum install gitto install git.
-
-
Run the commands in order:
-
Downloads Yarn's GPG key and adds it to the system's list of trusted keys, ensuring that packages from the Yarn repository can be verified.
curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo -
Updates the local package index, so the system is aware of the latest packages available from the newly added Yarn repository.
sudo yum update -
Installs Yarn, a package manager for JavaScript, using the APT package manager.
sudo yum install yarn
-
After install curl, node, git, and yarn, follow these steps:
-
Clone the Repository:
-
run
git clone https://github.com/Chayanon6409682538/ProjectCS360_foodadvisor.git -
Navigate to your project folder by running
cd ProjectCS360_foodadvisorfrom your command line.
-
-
Setup .env:
-
backend
-
To get PublicIPv4 use:
curl ipinfo.io/ip -
To get PREVIEW_SECRET use:
openssl rand -base64 32
-
Creat.env File
nano api/.env -
Copy this code and paste in .env file
HOST=0.0.0.0 PORT=1337 STRAPI_ADMIN_CLIENT_URL=http://<Your public IPv4 EC2 instance>:3000 STRAPI_ADMIN_CLIENT_PREVIEW_SECRET=<Your PREVIEW_SECRET>
-
-
frontend
-
Creat.env File
nano client/.env -
Copy this code and paste in .env file
NEXT_PUBLIC_API_URL=http://<Your public IPv4 EC2 instance>:1337 PREVIEW_SECRET=<Your PREVIEW_SECRET>
-
-
-
Start Strapi:
-
Navigate to your
./foodadvisor/apifolder by runningcd apifrom your command line.
-
then run
yarn && yarn seed && yarn build && yarn start &
-
-
Start Next.js:
-
Navigate to your
./foodadvisor/clientfolder by runningcd clientfrom your command line.
-
then run
yarn && yarn build && yarn start
-
-
Accessing Strapi Backend on EC2:
- http://[EC2 public ip]:1337 for strapi backend
-
Accessing Next.js Frontend on EC2:
- http://[EC2 public ip]:3000 next.js frontend
How to deploy and run the project using the provided bash script [Specify the bash script path in the repo]
After launching your EC2 instance and downloading the key pair for SSH access, follow this steps:
-
-
run
sudo yum install gitto install git.
-
After install git, follow these steps:
-
Clone the Repository:
-
run
git clone https://github.com/Chayanon6409682538/ProjectCS360_foodadvisor.git -
Navigate to your project folder by running
cd ProjectCS360_foodadvisorfrom your command line.
-
-
Run the Deployment Script:
-
Ensure the script is executable by running
chmod +x install_strapi_foodadvisor.sh -
Execute the script by running
./install_strapi_foodadvisor.sh
-
In the FoodAdvisor project, which is a food menu application using Strapi for the backend and Next.js for the frontend.
-
Testing Tools
-
Jest: For both Unit and Integration Testing, to validate functions and module interactions.
-
Supertest: For validating API endpoints.
-
Strapi Testing Utils: For streamlining testing of the Strapi backend APIs.
-
Install Testing Dependencies by running
yarn add jest supertest --save-dev
-
Navigate to your
./foodadvisor/clientfolder by runningcd clientfrom your command line.
-
Install dependencies by running
yarn -
To run all Unit and Integration tests across both frontend and backend components, use the following command below
yarn test
client/
├── _tests_/
│ ├── menu.test.js/
describe('createMenu', () => {
it('should create a menu item and return it', async () => {
const mockResponse = {
data: {
id: 1,
name: 'New Menu',
price: 0.00,
type: 'Main Menu',
isAvailable: false,
photo: {
data: {
id: 1,
},
},
},
};
fetch.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce(mockResponse),
});
const params = {
name: 'New Menu',
price: 0.00,
type: 'Main Menu',
isAvailable: false,
photo: {
data: {
id: 1,
},
},
};
const result = await createMenu(params);
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/menus`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
expect(result).toEqual(mockResponse);
});
it('should throw an error when the response is not ok', async () => {
fetch.mockResolvedValueOnce({ ok: false });
await expect(createMenu({ name: 'Pizza', price: 10 })).rejects.toThrow('Failed to create menu item');
});
});
describe('connectRelation', () => {
it('should connect menu to restaurant and return updated menu', async () => {
const mockResponse = { data: { id: 1, name: 'Pizza' } };
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce(mockResponse) });
const result = await connectRelation(1, 1);
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/restaurants/1`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data:
{ menus:
{ connect: [1] }
}
}),
});
expect(result).toEqual(mockResponse);
});
it('should handle fetch errors in connectRelation', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
await expect(connectRelation(1, 1)).rejects.toThrow('Network error');
});
});
describe('updateMenu', () => {
it('should update a menu item and return it', async () => {
const mockResponse = { data: { id: 1, name: 'Not Updated Menu' } };
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce(mockResponse) });
const result = await updateMenu(1, { name: 'Updated Menu' });
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/menus/1`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Updated Menu' }),
});
expect(result).toEqual(mockResponse);
});
it('should throw an error when updating fails', async () => {
fetch.mockResolvedValueOnce({ ok: false });
await expect(updateMenu(1, { name: 'Updated Menu' })).rejects.toThrow('Failed to update menu item');
});
});
describe('deleteMenu', () => {
it('should delete a menu item and return it', async () => {
const mockResponse = { data: { id: 1 } };
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce(mockResponse) });
const result = await deleteMenu(1);
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/menus/1`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
expect(result).toEqual(mockResponse);
});
it('should throw an error when deletion fails', async () => {
fetch.mockResolvedValueOnce({ ok: false });
await expect(deleteMenu(1)).rejects.toThrow('Failed to delete menu item');
});
});
describe('changePhoto', () => {
const mockFile = new File([''], 'photo.jpg', { type: 'image/jpeg' });
it('should upload a new photo and update the menu item', async () => {
const mockResponse = { data: { id: 1, photo: { data: { id: 2 } } } };
// Mock responses for fetching, deleting, and uploading
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce({ data: { attributes: { photo: { data: { id: 1 } } } } } ) });
fetch.mockResolvedValueOnce({ ok: true }); // for delete photo
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce([{ id: 2 }]) }); // for upload new photo
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce(mockResponse) }); // for updating menu
const result = await changePhoto(mockFile, 1);
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/menus/1?populate=photo`);
expect(fetch).toHaveBeenCalledWith(`${process.env.NEXT_PUBLIC_API_URL}/api/upload`, expect.any(Object));
expect(result).toEqual(mockResponse);
});
it('should throw an error when retrieval fails', async () => {
fetch.mockResolvedValueOnce({ ok: false });
await expect(changePhoto(mockFile, 1)).rejects.toThrow('Failed to retrieve menu item. Status: undefined');
});
it('should throw an error when deletion of old photo fails', async () => {
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce({ data: { attributes: { photo: { data: { id: 1 } } } } }) });
fetch.mockResolvedValueOnce({ ok: false });
await expect(changePhoto(mockFile, 1)).rejects.toThrow('Failed to delete old photo. Status: undefined');
});
it('should throw an error when upload fails', async () => {
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce({ data: { attributes: { photo: { data: { id: 1 } } } } }) });
fetch.mockResolvedValueOnce({ ok: true });
fetch.mockResolvedValueOnce({ ok: false });
await expect(changePhoto(mockFile, 1)).rejects.toThrow('Failed to upload new photo. Status: undefined');
});
it('should handle the case where there is no existing photo', async () => {
const mockFile = {}; // Replace with actual file mock if needed
const mockResponse = { data: { attributes: { photo: { id: 2, url: "new-photo-url.jpg" } } } };
// 1. Check for an existing photo and return null (indicating no existing photo)
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce({ data: { attributes: { photo: null } } }) });
// 2. No deletion of old photo since there is no existing photo
// 3. Mock successful upload response for the new photo
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce([{ id: 2 }]) });
// 4. Mock response for updating the menu item with the new photo
fetch.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValueOnce(mockResponse) });
// Run the function and verify it returns the expected result
const result = await changePhoto(mockFile, 1);
expect(result).toEqual(mockResponse);
});
This is the test result:
Menu Services Tests
createMenu
√ should create a menu item and return it (3 ms)
√ should throw an error when the response is not ok (19 ms)
connectRelation
√ should connect menu to restaurant and return updated menu (2 ms)
√ should handle fetch errors in connectRelation (3 ms)
updateMenu
√ should update a menu item and return it (2 ms)
√ should throw an error when updating fails (1 ms)
deleteMenu
√ should delete a menu item and return it (1 ms)
√ should throw an error when deletion fails (1 ms)
changePhoto
√ should upload a new photo and update the menu item (40 ms)
√ should throw an error when retrieval fails (5 ms)
√ should throw an error when deletion of old photo fails (8 ms)
√ should throw an error when upload fails (7 ms)
√ should handle the case where there is no existing photo (5 ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
menu-services.js | 100 | 100 | 100 | 100 |
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 13 passed, 13 total
Snapshots: 0 total
Time: 7.556 s
Ran all test suites.
Done in 21.85s.
Create new files in _tests_ and following this pattern:
// xxxx.test.js
describe('Feature Name', () => {
it('should do something specific', () => {
// Test implementation
});
});
The workflow is triggered on:
-
Push events to
masteranddevbranches -
Pull request events to
masteranddevbranches
The workflow runs tests across the following combinations:
- ubuntu-latest
- 16.x
-
Check code
clone repository uses: actions/checkout@v4 -
Setup node.js
uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} -
Install yarn
run: npm install -g yarn -
Install dependencies in api
run: yarn working-directory: ./api -
Build project
run: yarn build working-directory: ./api -
Install dependencies in client
run: yarn working-directory: ./client -
Test unit test client
run: yarn test working-directory: ./client
- Go to
Actionpage. - Selected latest workflows.
- Selected passed job.
- Selected test unit and test client.
After completed, these steps will show the test results.
This workflow uses the following configuration (YAML file):
name: Github Actions Workflow
on:
push:
branches: [master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, redhat-lastest]
node-version: [16.x]
runs-on: ${{ matrix.os }}
env:
HOST: '0.0.0.0'
PORT: '1337'
APP_KEYS: 'mock'
API_TOKEN_SALT: 'mock'
#TRANSFER_TOKEN_SALT: 'mock'
#DATABASE_CLIENT: 'sqlite'
#DATABASE_FILENAME: '.tmp/data.db'
JWT_SECRET: ${{ secrets.JWT_SECRET }}
ADMIN_JWT_SECRET: ${{ secrets.ADMIN_JWT_SECRET }}
STRAPI_ADMIN_CLIENT_PREVIEW_SECRET: ${{ secrets.STRAPI_ADMIN_CLIENT_PREVIEW_SECRET }}
steps:
- name: Check code
uses: actions/checkout@v4
- name: Setup node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install
working-directory: ./api
- name: Build project
run: yarn build
working-directory: ./api
- name: Test
run:
yarn test
echo "my STRAPI_ADMIN_CLIENT_PREVIEW_SECRET is ${{ secrets.STRAPI_ADMIN_CLIENT_PREVIEW_SECRET }}"
working-directory: ./api


