Skip to content
Merged
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
160 changes: 160 additions & 0 deletions internal/http/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package http

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/maniac-en/req/internal/log"
)

func NewHTTPManager() *HTTPManager {
client := &http.Client{
Timeout: 30 * time.Second,
}
return &HTTPManager{
Client: client,
}
}

func validateMethod(method string) error {
method = strings.ToUpper(strings.TrimSpace(method))
validMethods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
for _, valid := range validMethods {
if method == valid {
return nil
}
}
return fmt.Errorf("invalid HTTP method: %s", method)
}

func validateURL(url string) error {
url = strings.TrimSpace(url)
if url == "" {
return fmt.Errorf("URL cannot be empty")
}
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return fmt.Errorf("URL must start with http:// or https://")
}
return nil
}

func (h *HTTPManager) ValidateRequest(req *Request) error {
if err := validateMethod(req.Method); err != nil {
log.Error("invalid method", "method", req.Method, "error", err)
return err
}
if err := validateURL(req.URL); err != nil {
log.Error("invalid URL", "url", req.URL, "error", err)
return err
}
return nil
}

func (h *HTTPManager) ExecuteRequest(req *Request) (*Response, error) {
if err := h.ValidateRequest(req); err != nil {
return nil, err
}

log.Debug("executing HTTP request", "method", req.Method, "url", req.URL)

requestURL, err := h.buildURL(req.URL, req.QueryParams)
if err != nil {
log.Error("failed to build URL", "error", err)
return nil, fmt.Errorf("failed to build URL: %w", err)
}

start := time.Now()

var body io.Reader
if req.Body != "" && (strings.ToUpper(req.Method) == "POST" || strings.ToUpper(req.Method) == "PUT" || strings.ToUpper(req.Method) == "PATCH") {
body = strings.NewReader(req.Body)
}

httpReq, err := http.NewRequest(strings.ToUpper(req.Method), requestURL, body)
if err != nil {
log.Error("failed to create HTTP request", "error", err)
return nil, fmt.Errorf("failed to create request: %w", err)
}

if body != nil {
h.setContentType(httpReq, req.Body)
}

if err := h.setHeaders(httpReq, req.Headers); err != nil {
log.Error("failed to set headers", "error", err)
return nil, fmt.Errorf("failed to set headers: %w", err)
}

resp, err := h.Client.Do(httpReq)
if err != nil {
log.Error("HTTP request failed", "error", err)
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
log.Error("failed to close response body", "error", closeErr)
}
}()

duration := time.Since(start)

response := &Response{
StatusCode: resp.StatusCode,
Status: resp.Status,
Headers: resp.Header,
Duration: duration,
}

log.Info("HTTP request completed", "status", resp.StatusCode, "duration", duration)
return response, nil
}

func (h *HTTPManager) buildURL(baseURL string, queryParams map[string]string) (string, error) {
if len(queryParams) == 0 {
return baseURL, nil
}

parsedURL, err := url.Parse(baseURL)
if err != nil {
return "", err
}

values := parsedURL.Query()
for key, value := range queryParams {
values.Set(key, value)
}
parsedURL.RawQuery = values.Encode()

return parsedURL.String(), nil
}

func (h *HTTPManager) setHeaders(req *http.Request, headers map[string]string) error {
for key, value := range headers {
if strings.TrimSpace(key) == "" {
return fmt.Errorf("header key cannot be empty")
}
req.Header.Set(key, value)
}
return nil
}

func (h *HTTPManager) setContentType(req *http.Request, body string) {
if req.Header.Get("Content-Type") != "" {
return
}

body = strings.TrimSpace(body)
if strings.HasPrefix(body, "{") || strings.HasPrefix(body, "[") {
if json.Valid([]byte(body)) {
req.Header.Set("Content-Type", "application/json")
return
}
}

req.Header.Set("Content-Type", "text/plain")
}
155 changes: 155 additions & 0 deletions internal/http/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package http

import (
"net/http"
"testing"
"time"
)

func TestNewHTTPManager(t *testing.T) {
manager := NewHTTPManager()
if manager == nil {
t.Fatal("NewHTTPManager returned nil")
}
if manager.Client == nil {
t.Fatal("HTTPManager client is nil")
}
if manager.Client.Timeout != 30*time.Second {
t.Errorf("expected timeout 30s, got %v", manager.Client.Timeout)
}
}

func TestValidateMethod(t *testing.T) {
tests := []struct {
method string
valid bool
}{
{"GET", true},
{"POST", true},
{"put", true},
{"delete", true},
{"INVALID", false},
{"", false},
}

for _, test := range tests {
err := validateMethod(test.method)
if test.valid && err != nil {
t.Errorf("expected %s to be valid, got error: %v", test.method, err)
}
if !test.valid && err == nil {
t.Errorf("expected %s to be invalid, got no error", test.method)
}
}
}

func TestValidateURL(t *testing.T) {
tests := []struct {
url string
valid bool
}{
{"https://example.com", true},
{"http://localhost:8080", true},
{"ftp://invalid.com", false},
{"", false},
{"not-a-url", false},
}

for _, test := range tests {
err := validateURL(test.url)
if test.valid && err != nil {
t.Errorf("expected %s to be valid, got error: %v", test.url, err)
}
if !test.valid && err == nil {
t.Errorf("expected %s to be invalid, got no error", test.url)
}
}
}

func TestValidateRequest(t *testing.T) {
manager := NewHTTPManager()

validReq := &Request{
Method: "GET",
URL: "https://example.com",
}

if err := manager.ValidateRequest(validReq); err != nil {
t.Errorf("expected valid request to pass validation, got: %v", err)
}

invalidReq := &Request{
Method: "INVALID",
URL: "not-a-url",
}

if err := manager.ValidateRequest(invalidReq); err == nil {
t.Error("expected invalid request to fail validation")
}
}

func TestBuildURL(t *testing.T) {
manager := NewHTTPManager()

tests := []struct {
baseURL string
queryParams map[string]string
expected string
}{
{"https://example.com", nil, "https://example.com"},
{"https://example.com", map[string]string{}, "https://example.com"},
{"https://example.com", map[string]string{"foo": "bar"}, "https://example.com?foo=bar"},
}

for _, test := range tests {
result, err := manager.buildURL(test.baseURL, test.queryParams)
if err != nil {
t.Errorf("buildURL failed: %v", err)
}
if result != test.expected {
t.Errorf("expected %s, got %s", test.expected, result)
}
}
}

func TestSetHeaders(t *testing.T) {
manager := NewHTTPManager()
req, _ := http.NewRequest("GET", "https://example.com", nil)

headers := map[string]string{
"Content-Type": "application/json",
"User-Agent": "req-cli",
}

err := manager.setHeaders(req, headers)
if err != nil {
t.Errorf("setHeaders failed: %v", err)
}

if req.Header.Get("Content-Type") != "application/json" {
t.Error("Content-Type header not set correctly")
}
}

func TestSetContentType(t *testing.T) {
manager := NewHTTPManager()

tests := []struct {
body string
expected string
}{
{`{"key": "value"}`, "application/json"},
{`[1, 2, 3]`, "application/json"},
{"plain text", "text/plain"},
}

for _, test := range tests {
req, _ := http.NewRequest("POST", "https://example.com", nil)
manager.setContentType(req, test.body)

if req.Header.Get("Content-Type") != test.expected {
t.Errorf("for body %q, expected %q, got %q",
test.body, test.expected, req.Header.Get("Content-Type"))
}
}
}
27 changes: 27 additions & 0 deletions internal/http/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Package http provides HTTP client functionality for making HTTP requests.
package http

import (
"net/http"
"time"
)

type HTTPManager struct {
Client *http.Client
}

type Request struct {
Method string
URL string
Headers map[string]string
QueryParams map[string]string
Body string
}

type Response struct {
StatusCode int
Status string
Headers map[string][]string
Comment thread
maniac-en marked this conversation as resolved.
Body string
Duration time.Duration
}
10 changes: 7 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/maniac-en/req/internal/collections"
"github.com/maniac-en/req/internal/database"
"github.com/maniac-en/req/internal/http"
"github.com/maniac-en/req/internal/log"
_ "github.com/mattn/go-sqlite3"
"github.com/pressly/goose/v3"
Expand All @@ -34,6 +35,7 @@ var (
type Config struct {
DB *database.Queries
Collections *collections.CollectionsManager
HTTP *http.HTTPManager
}

func initPaths() error {
Expand Down Expand Up @@ -124,16 +126,18 @@ func main() {
log.Fatal("failed to run migrations", "error", err)
}

// create database client and collections manager
// create database client and managers
db := database.New(DB)
collectionsManager := collections.NewCollectionsManager(db)
httpManager := http.NewHTTPManager()

config := &Config{
DB: db,
Collections: collectionsManager,
HTTP: httpManager,
}

log.Info("application initialized", "components", []string{"database", "collections", "logging"})
log.Debug("configuration loaded", "collections_manager", config.Collections != nil, "database", config.DB != nil)
log.Info("application initialized", "components", []string{"database", "collections", "http", "logging"})
log.Debug("configuration loaded", "collections_manager", config.Collections != nil, "database", config.DB != nil, "http_manager", config.HTTP != nil)
log.Info("application started successfully")
}