Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 13 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,17 @@
# ![Scale-Up Velocity](./readme-files/logo-main.png) Final 1 - URL shortner 📎
# Shawty 🐫 URL Shortener

In this project you will create your own [URL shortener](https://en.wikipedia.org/wiki/URL_shortening)!
A simple and responsive URL shortening service.

This repository includes a basic template for starting the project:
- [Fire up the demo](https://shawty.davidbinneun.repl.co/) at repl.it

## Instructions
## Features
- Fully responsive design
- Random ID generation
- Copy your new link easily
- Dedicated statistic page for your URL at /api/statistic/:id
- Custom error pages

- Fork this repository to your account as a **public** repo
- Clone your new repository to your computer 🖥
- Install the project dependencies by running `npm install` from the vscode terminal `ctrl + j` (make sure you are in the correct directory) 📂
- [Create a new branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for the development process
- Make changes to the code to meet the project requirements 📝
- [Commit Early, Push Often](https://www.worklytics.co/commit-early-push-often/) - your work will be evaluated by your git flow and overall github usage 🏄‍♂️
- Before submitting, create a pull request from the development branch into the main branch. **Leave the PR open and do not merge the branches**. The open PR will be used to review and mark your code
- Good Luck! 🤘

## Testing your project

In this assignment, you will have to create your own tests, as learned in class. Your grade will be calculated by your test coverage.

Optionally, You can create a github [action](https://docs.github.com/en/actions) that runs your tests on each commit:

![Commits test](./readme-files/commit-tests.png)

## Guidelines

- Create a route `/api/shorturl/` in your `express` app that will handle all url shortening requests. (We recommend using [express Router](https://expressjs.com/en/guide/routing.html))

- Write/read **Asynchronously** a single JSON file as your DB

- [Serve](https://expressjs.com/en/starter/static-files.html) your client files from your server at route `/`

- Style and change your front-end as you wish. You can take inspiration from this [example](https://www.shorturl.at/)

## Requirements

- Examine thoroughly and copy all functionality of [this](https://url-shortener-microservice.freecodecamp.rocks/) FCC example

- Use a `class DataBase{}` to read/write (**Asynchronously**) all data in your back-end (you can use a json file as persistent layer)

- Add another functionality to your service: a statistics route (`api/statistic/:shorturl-id`) that will respond with the following data per `shorturl-id`:
- `creationDate` - a SQLDate format
- `redirectCount` - the amount of times this url was used for redirection
- `originalUrl`
- `shorturl-id`

- Fully test your `express` app with `jest` and `supertest`. Test each end point response **including** error responses.

Use a separate DB file for your tests. _Hint: use [Environment variables](https://jestjs.io/docs/en/environment-variables)_

## Bonus

- Add any feature you desire. Some ideas worth extra points:
- Custom short URL. Support optional `shorturl-id` parameter in your `POST` request. Pay attention to error handling.
- Serve a styled statistics dashboard instead of the default JSON statistics
- Use the [`JSONBIN.io`](https://jsonbin.io/) service bin as your persistent layer in your back-end DB class (use CRUD operations to read write bins)
- Try implementing user management
- Use supertest/puppeteer test to test any bonus feature you implemented

**Add an explanation in `README.md` for each bonus feature you add and a link to any resource you used**

## Grading policy

- Using jsonbin.io with/instead of writing to files
- Correct DB class usage
- Code quality and style: indentation, Meaningful and non-disambiguate variable names, Comments and documentation, file and directory structure
- Visual creativity, style your front-end to make it look awesome 💅🏿
- Division to reusable functions, no code duplication
- Git usage: meaningful commit messages, small commits, folder and file structures, README file, issues, etc...

## Submitting
- Submit your solution repo link - an open PR from your dev branch to the main one
- Your readme should have a [`repl.it`](https://repl.it/) link with your solutions.
- Submit a link to your repo to the CRM.

GOOD LUCK!
## Resources used
- [Pug rendering engine](https://pugjs.org/api/getting-started.html)
- [shortid](https://www.npmjs.com/package/shortid)
- [is-valid-http-url](https://www.npmjs.com/package/is-valid-http-url)
55 changes: 52 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,62 @@ require("dotenv").config();
const express = require("express");
const cors = require("cors");
const app = express();
const shortid = require('shortid');
const DataBase = require('./backend/database.js');

app.set('view engine', 'pug');
app.use(cors());

app.use(express.urlencoded({extended: false}));
app.use("/public", express.static(`./public`));

// Access the website
app.get("/", (req, res) => {
res.sendFile(__dirname + "/views/index.html");
res.render('index');
});

// Create new shortened URL
app.post("/api/shorturl/new", async (req, res) => {
try {
let newID = await DataBase.addURL(req.body.url);
if (newID === null) return sendStatus(400);
res.status(201).render('new', { id: "http://" + req.get('host') + "/" + newID});
} catch {
res.sendStatus(500);
}
});

// Access shortened URL
app.get("/:id", async (req, res) => {
if (!shortid.isValid(req.params.id))
return res.status(400).render('error', {statusCode: 400, message: "Illegal request."});

try {
let redirectUrl = await DataBase.getOriginalUrl(req.params.id);

if (redirectUrl === null)
return res.status(404).render('error', {statusCode: 404, message: "We don't have it here."});

res.redirect(redirectUrl);
} catch {
res.status(500).render('error', {statusCode: 500, message: "Internal server error."});
}
});

// Access statistics about a shortened URL
app.get("/api/statistic/:id", async (req, res) => {
if (!shortid.isValid(req.params.id))
return res.status(400).render('error', {statusCode: 400, message: "Illegal request."});

try {
let item = await DataBase.getItem(req.params.id);

if (item === null)
return res.status(404).render('error', {statusCode: 404, message: "We don't have it here."});

else res.status(200).render('statistic', {creationDate: item.creationDate, originalUrl: item.originalUrl, redirectCount: item.redirectCount, id: item.id });
} catch {
res.status(500).render('error', {statusCode: 500, message: "Internal server error."});
}
});

module.exports = app;
module.exports = app;
1 change: 1 addition & 0 deletions backend/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"creationDate":"04/03/2021","redirectCount":0,"originalUrl":"https://www.w3schools.com/java/java_try_catch.asp","id":"A6-kJvHUa"},{"creationDate":"04/03/2021","redirectCount":0,"originalUrl":"https://github.com/","id":"RKY26Idoy"},{"creationDate":"04/03/2021","redirectCount":0,"originalUrl":"https://github.com/davidbinneun/json-server/blob/main/index.test.js","id":"YJPY1mhqt"}]
62 changes: 62 additions & 0 deletions backend/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs').promises;
const Item = require('./item.js');
const databaseFile = process.env.NODE_ENV === 'test' ? './backend/testdata.json':'./backend/data.json';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

अति उत्कृष्ट

const isUrl = require("is-valid-http-url");

// Performs actions on the database file
class DataBase {
static items = [];

// Gets all data from the JSON file into the items array
static async readAllData(){
const data = await fs.readFile(databaseFile, 'utf8' , err => { if (err) return;}); // TODO remove callback
this.items = JSON.parse(data);
}

// Receives URL, adds it to database and returns the id given to it
static async addURL(url){
await this.readAllData();

// Check if URL is legal
if (!isUrl(url)) return null; // TODO take this out

// Check if URL exists in database
for(let item of this.items){
if (url === item.originalUrl) {
return item.id; // URL exists, returns its id
}
}
// If URL is new, add to database and return id
let newItem = new Item(url);
this.items.push(newItem);
fs.writeFile(databaseFile, JSON.stringify(this.items));
return newItem.id;
}

// Receives id, returns the URL it has
static async getOriginalUrl(id){
await this.readAllData();
for (let item of this.items){
if (item.id === id){
item.redirectCount += 1;
fs.writeFile(databaseFile, JSON.stringify(this.items));
return item.originalUrl;
}
}

return null;
}

// Receives id, returns the full item object
static async getItem(id){
await this.readAllData();
for (let item of this.items){
if (item.id === id){
return item;
}
}
return null; // TODO throw error incase of error
}
}

module.exports = DataBase;
23 changes: 23 additions & 0 deletions backend/item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const shortid = require('shortid');

class Item {
constructor(url){
this.creationDate = this.getIsraeliDate(new Date());
this.redirectCount = 0;
this.originalUrl = url;
this.id = shortid.generate();
}

getIsraeliDate(date){
return addZero(date.getDate()) + "/" + addZero(date.getMonth() + 1) + "/" + date.getFullYear();

function addZero(number){
if (number < 10)
return "0" + number;
else
return number;
}
}
}

module.exports = Item;
1 change: 1 addition & 0 deletions backend/testdata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ const app = require("./app");
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
console.log(`We are live on port ${PORT}`);
});
69 changes: 69 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const supertest = require("supertest");
const app = require("./app");
const request = supertest(app);
const Item = require('./backend/item.js');
const fs = require("fs").promises;

beforeAll(async () => {
await fs.writeFile("./backend/testdata.json", "[]");
});

describe("Sending URL to the server", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a test for posting an existing url

it("The URL is legal", async () => {
const response = await request.post('/api/shorturl/new').type('form').send({url:"https://www.youtube.com/"});
expect(response.status).toBe(201);
});
it("The URL is illegal", async () => {
const response = await request.post('/api/shorturl/new').type('form').send({url:"utubecom/?hl=iw&gl=IL"});
expect(response.status).toBe(400);
});
it("Corrupted file in server", async () => {
await fs.writeFile("./backend/testdata.json", "[");
const response = await request.post('/api/shorturl/new').type('form').send({url:"https://www.youtube.com/"});
expect(response.status).toBe(500);
});
});

describe("Redirect to URL by ID", () => {
it("Send Legal ID", async () => {
let newItem = new Item('https://www.youtube.com/');
await fs.writeFile("./backend/testdata.json", JSON.stringify([newItem]));
const response = await request.get(`/api/statistic/${newItem.id}`);
expect(response.status).toBe(200);
});
it("Send Illegal ID", async () => {
const response = await request.get(`/api/statistic/abc`);
expect(response.status).toBe(400);
});
it("Legal ID but ID not found", async () => {
const response = await request.get(`/api/statistic/abcdefg`);
expect(response.status).toBe(404);
});
it("Corrupted file in server", async () => {
await fs.writeFile("./backend/testdata.json", "[");
const response = await request.get(`/api/statistic/abcdefg`);
expect(response.status).toBe(500);
});
});

describe("Get statistics by ID", () => {
it("Send Legal ID", async () => {
let newItem = new Item('https://www.youtube.com/');
await fs.writeFile("./backend/testdata.json", JSON.stringify([newItem]));
const response = await request.get(`/${newItem.id}`);
expect(response.status).toBe(302);
});
it("Send Illegal ID", async () => {
const response = await request.get(`/abc`);
expect(response.status).toBe(400);
});
it("Legal ID but ID not found", async () => {
const response = await request.get(`/abcdefg`);
expect(response.status).toBe(404);
});
it("Corrupted file in server", async () => {
await fs.writeFile("./backend/testdata.json", "[");
const response = await request.get(`/abcdefg`);
expect(response.status).toBe(500);
});
Comment on lines +11 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job and good tests. Covering almost every case.
However naming is a bit confusing. We usually write what we expect the server to do in each case, what we are about to test.

});
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"env": {
"NODE_ENV": "development"
},
"ext": "js,json,html,css",
"ext": "js,html,css,pug",
"delay": "2500"
}
Loading