Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iter4 #4

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ vendor/
# IDEs directories
.idea
.vscode
cmd/.DS_Store
14 changes: 13 additions & 1 deletion cmd/shortener/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
package main

func main() {}
import (
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/server"
"github.com/shilin-anton/urlreducer/internal/app/storage"
)

func main() {
config.ParseFlags()

myStorage := storage.New()
myServer := server.New(myStorage)
myServer.Start()
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/shilin-anton/urlreducer

go 1.21.3

require github.com/go-chi/chi/v5 v5.0.11
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
14 changes: 14 additions & 0 deletions internal/app/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package config

import (
"flag"
)

var RunAddr string
var BaseAddr string

func ParseFlags() {
flag.StringVar(&RunAddr, "a", "localhost:8080", "address and port to run server")
flag.StringVar(&BaseAddr, "b", "http://localhost:8080", "base URL before short link")
flag.Parse()
}
76 changes: 76 additions & 0 deletions internal/app/handlers/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package handlers

import (
"crypto/md5"
"encoding/hex"
"github.com/go-chi/chi/v5"
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/storage"
"io"
"net/http"
)

type Storage interface {
Add(short string, url string)
Get(short string) (string, bool)
}

type Server struct {
data Storage
handler http.Handler
}

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handler.ServeHTTP(w, r)
}

func New() *Server {
r := chi.NewRouter()

S := &Server{
data: make(storage.Storage),
handler: r,
}
r.Get("/{short}", S.GetHandler)
r.Post("/", S.PostHandler)

return S
}

func shortenURL(url string) string {
// Решил использовать хэширование и первые символы результата, как короткую форму URL
hash := md5.Sum([]byte(url))
hashString := hex.EncodeToString(hash[:])
shortURL := hashString[:8]
return shortURL
}

func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(res, "Error reading request body", http.StatusInternalServerError)
return
}
defer req.Body.Close()

url := string(body)
short := shortenURL(url)

s.data.Add(short, url)

res.Header().Set("Content-Type", "text/plain")
res.WriteHeader(http.StatusCreated)
res.Write([]byte(config.BaseAddr + "/" + short))
}

func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) {
short := chi.URLParam(req, "short")

url, ok := s.data.Get(short)
if !ok {
http.NotFound(res, req)
return
}
res.Header().Set("Location", url)
res.WriteHeader(http.StatusTemporaryRedirect)
}
94 changes: 94 additions & 0 deletions internal/app/handlers/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package handlers

import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestPostHandler(t *testing.T) {
srv := New()

tests := []struct {
name string
method string
url string
requestBody string
wantStatusCode int
}{
{
name: "Valid POST request",
method: http.MethodPost,
url: "/",
requestBody: "http://example.com",
wantStatusCode: http.StatusCreated,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody))
w := httptest.NewRecorder()

srv.handler.ServeHTTP(w, req)

resp := w.Result()
defer resp.Body.Close()

if resp.StatusCode != test.wantStatusCode {
t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode)
}
})
}
}

func TestGetHandler(t *testing.T) {
srv := New()
srv.data.Add("test_short", "https://smth.ru")

tests := []struct {
name string
method string
url string
wantStatusCode int
wantLocationHeader string
}{
{
name: "Valid GET request with existing short link",
method: http.MethodGet,
url: "/test_short",
wantStatusCode: http.StatusTemporaryRedirect,
wantLocationHeader: "https://smth.ru",
},
{
name: "Invalid GET request with non-existing short link",
method: http.MethodGet,
url: "/non_existing_short_link",
wantStatusCode: http.StatusNotFound,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, test.url, nil)
w := httptest.NewRecorder()

srv.handler.ServeHTTP(w, req)

resp := w.Result()
defer resp.Body.Close()

if resp.StatusCode != test.wantStatusCode {
t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode)
}

if test.wantLocationHeader != "" {
location := resp.Header.Get("Location")
if location != test.wantLocationHeader {
t.Errorf("unexpected Location header: got %s, want %s", location, test.wantLocationHeader)
}
}
})
}
}
31 changes: 31 additions & 0 deletions internal/app/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package server

import (
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/handlers"
"log"
"net/http"
)

type Storage interface {
Add(short string, url string)
Get(short string) (string, bool)
}

type server struct {
handler http.Handler
storage Storage
}

func New(storage Storage) *server {
handler := handlers.New()
S := &server{
handler: handler,
storage: storage,
}
return S
}

func (s server) Start() {
log.Fatal(http.ListenAndServe(config.RunAddr, s.handler))
}
17 changes: 17 additions & 0 deletions internal/app/storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package storage

type Storage map[string]string

func (s Storage) Add(short string, url string) {
s[short] = url
}

func (s Storage) Get(short string) (string, bool) {
url, ok := s[short]
return url, ok
}

func New() Storage {
storage := make(map[string]string)
return storage
}
Loading