Skip to content

Commit

Permalink
Added tests and cleaned code
Browse files Browse the repository at this point in the history
  • Loading branch information
SartajBhuvaji committed Nov 23, 2024
1 parent a90acf7 commit ca2dc81
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 20 deletions.
4 changes: 4 additions & 0 deletions .dummy_env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Create a .env file with the following variables
REDIS_HOST = "REDIS_HOST"
REDIS_PASSWORD = "REDIS_PASSWORD"
REDIS_DB = REDIS_DB
7 changes: 1 addition & 6 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: '1.23.2'

- name: Test
run: go test -v ./tests/...

- name: Build
run: go build ./main.go


run: go build ./main.go
99 changes: 97 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,97 @@
# goURLShortnet
Go url shortner
# goURLShortner 🔗

A lightning-fast URL shortening service built with Go and Redis, designed for high performance and reliability.

[![Go](https://github.com/SartajBhuvaji/goURLShortnet/actions/workflows/go.yml/badge.svg)](https://github.com/SartajBhuvaji/goURLShortnet/actions/workflows/go.yml)

[![Go Report Card](https://goreportcard.com/badge/github.com/SartajBhuvaji/goURLShortnet)](https://goreportcard.com/report/github.com/SartajBhuvaji/goURLShortnet)

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

## 🚀 Features

- **Fast URL Shortening**: Efficient Base62 encoding for short, memorable URLs
- **Redis Backend**: High-performance storage and retrieval
- **RESTful API**: Clean and intuitive API endpoints
- **Analytics**: Track creation time, last access, and usage count
- **Concurrent Processing**: Handle multiple requests efficiently
- **Production Ready**: Includes tests, CI/CD, and comprehensive error handling

## 🛠️ Tech Stack

- **Go** - Core programming language
- **Redis** - Primary database
- **godotenv** - Environment configuration
- **go-redis** - Redis client for Go

## 📋 Prerequisites

- Go 1.23.2 or higher
- Redis server
- Git

## 🔧 Installation

1. **Clone the repository**
```bash
git clone https://github.com/YourUsername/goURLShortner.git
cd goURLShortner
```

2. **Install dependencies**
```bash
go mod download
```

3. **Configure environment**
Create a `.env` file in the root directory:
```env
REDIS_HOST=localhost:6379
REDIS_PASSWORD=your_redis_password
REDIS_DB=0
```

## 🚦 Usage

1. **Start the server**
```bash
go run main.go
```
Server starts on `http://localhost:8080`

2. **API Endpoints**

### Shorten URL
```http
POST /shorten
Content-Type: application/json
{
"url": "https://www.example.com/very/long/url/that/needs/shortening"
}
```
Response:
```json
{
"short_url": "www.goURLShortner/abc123"
}
```

### Redirect to Original URL
```http
GET /redirect?url=abc123
```
Response:
```json
{
"long_url": "https://www.example.com/very/long/url/that/needs/shortening"
}
```

## 🧪 Testing

Run the test suite:

```bash
go test ./tests -v
```
18 changes: 7 additions & 11 deletions api/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,25 @@ type RedirectURLRequest struct {
}

// handle the short --> long URL redirection

func RedirectHandler(w http.ResponseWriter, r *http.Request, redisClient *database.RedisClient) {
if r.Method != http.MethodPost {
if r.Method != http.MethodGet {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

var req ShortenURLRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
// Get the short URL from query parameters
shortUrl := r.URL.Query().Get("url")
if shortUrl == "" {
http.Error(w, "URL parameter is required", http.StatusBadRequest)
return
}

if req.URL == "" {
http.Error(w, "URL is required", http.StatusBadRequest)
return
}
shortUrl := req.URL
longUrl, err := redisClient.Get(shortUrl)
if err != nil {
http.Error(w, "Error fetching URL from Reddis", http.StatusInternalServerError)
http.Error(w, "Error fetching URL from Redis", http.StatusInternalServerError)
return
}
//http.Redirect(w, r, longUrl, http.StatusMovedPermanently)

response := RedirectURLRequest{
LongURL: longUrl,
Expand Down
3 changes: 2 additions & 1 deletion api/shorten.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func ShortenURLHandler(w http.ResponseWriter, r *http.Request, redisClient *data
return
}

counter, err := redisClient.GetCounter() // flag
counter, err := redisClient.GetCounter()
if err != nil {
http.Error(w, "Error fetching counter from Reddis", http.StatusInternalServerError)
return
Expand All @@ -60,6 +60,7 @@ func ShortenURLHandler(w http.ResponseWriter, r *http.Request, redisClient *data
return
}

// Return the short URL
resp := ShortenURLResponse{ShortURL: shortURL}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ func main() {
log.Fatalf("Could not start server: %s\n", err)
}
}

// RUN: go run main.go
127 changes: 127 additions & 0 deletions tests/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestShortenURLHandler(t *testing.T) {
if err != nil {
t.Fatalf("could not set up Redis: %v", err)
}
defer redisClient.Close()

// create a handler function that matches http.HandlerFunc signature
handler := func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -55,3 +56,129 @@ func TestShortenURLHandler(t *testing.T) {
t.Fatalf("response body does not have short_url field")
}
}

func TestRedirectHandler(t *testing.T) {
// First, create a shortened URL
redisClient, err := utils.SetupRedis()
if err != nil {
t.Fatalf("could not set up Redis: %v", err)
}
defer redisClient.Close()

// Store a test URL in Redis
testShortURL := "testcode"
testLongURL := "https://www.example.com"
err = redisClient.Set(testShortURL, testLongURL)
if err != nil {
t.Fatalf("could not set test URL in Redis: %v", err)
}

// Create a request to redirect
req, err := http.NewRequest(http.MethodGet, "/redirect?url="+testShortURL, nil)
if err != nil {
t.Fatalf("could not create request: %v", err)
}

rec := httptest.NewRecorder()

handler := func(w http.ResponseWriter, r *http.Request) {
api.RedirectHandler(w, r, redisClient)
}

http.HandlerFunc(handler).ServeHTTP(rec, req)

// Check status code
if rec.Code != http.StatusOK {
t.Errorf("expected status OK; got %v", rec.Code)
}

// Check response body
var respBody api.RedirectURLRequest
if err := json.NewDecoder(rec.Body).Decode(&respBody); err != nil {
t.Fatalf("could not decode JSON: %v", err)
}

if respBody.LongURL != testLongURL {
t.Errorf("expected long URL %s; got %s", testLongURL, respBody.LongURL)
}
}

func TestRedirectHandlerInvalidMethod(t *testing.T) {
redisClient, err := utils.SetupRedis()
if err != nil {
t.Fatalf("could not set up Redis: %v", err)
}
defer redisClient.Close()

// Try with POST method instead of GET
req, err := http.NewRequest(http.MethodPost, "/redirect?url=test", nil)
if err != nil {
t.Fatalf("could not create request: %v", err)
}

rec := httptest.NewRecorder()

handler := func(w http.ResponseWriter, r *http.Request) {
api.RedirectHandler(w, r, redisClient)
}

http.HandlerFunc(handler).ServeHTTP(rec, req)

if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected status Method Not Allowed; got %v", rec.Code)
}
}

func TestRedirectHandlerMissingURL(t *testing.T) {
redisClient, err := utils.SetupRedis()
if err != nil {
t.Fatalf("could not set up Redis: %v", err)
}
defer redisClient.Close()

// Create request without URL parameter
req, err := http.NewRequest(http.MethodGet, "/redirect", nil)
if err != nil {
t.Fatalf("could not create request: %v", err)
}

rec := httptest.NewRecorder()

handler := func(w http.ResponseWriter, r *http.Request) {
api.RedirectHandler(w, r, redisClient)
}

http.HandlerFunc(handler).ServeHTTP(rec, req)

if rec.Code != http.StatusBadRequest {
t.Errorf("expected status Bad Request; got %v", rec.Code)
}
}

func TestShortenURLHandlerInvalidJSON(t *testing.T) {
// Create invalid JSON request body
invalidJSON := []byte(`{"url": invalid}`)

req, err := http.NewRequest(http.MethodPost, "/shorten", bytes.NewBuffer(invalidJSON))
if err != nil {
t.Fatalf("could not create request: %v", err)
}

rec := httptest.NewRecorder()

redisClient, err := utils.SetupRedis()
if err != nil {
t.Fatalf("could not set up Redis: %v", err)
}
defer redisClient.Close()

handler := func(w http.ResponseWriter, r *http.Request) {
api.ShortenURLHandler(w, r, redisClient)
}

http.HandlerFunc(handler).ServeHTTP(rec, req)

if rec.Code != http.StatusBadRequest {
t.Errorf("expected status Bad Request; got %v", rec.Code)
}
}
File renamed without changes.

0 comments on commit ca2dc81

Please sign in to comment.