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
15 changes: 14 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
Service ServiceType
Port int
Hostname string
APIKey string

Redis *redis.RedisConfig
OpenTelemetry *otel.OpenTelemetryConfig
Expand Down Expand Up @@ -80,7 +81,8 @@ func Parse(flags Flags) (*Config, error) {
config := &Config{
Hostname: hostname,
Service: service,
Port: mustInt(viper, "PORT"),
Port: getPort(viper),
APIKey: viper.GetString("API_KEY"),
Redis: &redis.RedisConfig{
Host: viper.GetString("REDIS_HOST"),
Port: mustInt(viper, "REDIS_PORT"),
Expand All @@ -100,3 +102,14 @@ func mustInt(viper *v.Viper, configName string) int {
}
return i
}

func getPort(viper *v.Viper) int {
port := mustInt(viper, "PORT")
if viper.GetString("API_PORT") != "" {
apiPort, err := strconv.Atoi(viper.GetString("API_PORT"))
if err == nil {
port = apiPort
}
}
return port
}
214 changes: 214 additions & 0 deletions internal/destination/destination_test/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package destination_test

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/hookdeck/EventKit/internal/destination"
"github.com/hookdeck/EventKit/internal/util/testutil"
"github.com/stretchr/testify/assert"
)

func setupRouter(destinationHandlers *destination.DestinationHandlers) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/destinations", destinationHandlers.List)
r.POST("/destinations", destinationHandlers.Create)
r.GET("/destinations/:destinationID", destinationHandlers.Retrieve)
r.PATCH("/destinations/:destinationID", destinationHandlers.Update)
r.DELETE("/destinations/:destinationID", destinationHandlers.Delete)
return r
}

func TestDestinationListHandler(t *testing.T) {
t.Parallel()

redisClient := testutil.CreateTestRedisClient(t)
handlers := destination.NewHandlers(redisClient)
router := setupRouter(handlers)

t.Run("should return 501", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/destinations", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotImplemented, w.Code)
})
}

func TestDestinationCreateHandler(t *testing.T) {
t.Parallel()

redisClient := testutil.CreateTestRedisClient(t)
handlers := destination.NewHandlers(redisClient)
router := setupRouter(handlers)

t.Run("should create", func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()

exampleDestination := destination.CreateDestinationRequest{
Name: "Test Destination",
}
destinationJSON, _ := json.Marshal(exampleDestination)
req, _ := http.NewRequest("POST", "/destinations", strings.NewReader(string(destinationJSON)))
router.ServeHTTP(w, req)

var destinationResponse map[string]any
json.Unmarshal(w.Body.Bytes(), &destinationResponse)

assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, exampleDestination.Name, destinationResponse["name"])
assert.NotEqual(t, "", destinationResponse["id"])
})
}

func TestDestinationRetrieveHandler(t *testing.T) {
t.Parallel()

redisClient := testutil.CreateTestRedisClient(t)
handlers := destination.NewHandlers(redisClient)
router := setupRouter(handlers)

t.Run("should return 404 when there's no destination", func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/destinations/invalid_id", nil)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("should retrieve when there's a destination", func(t *testing.T) {
t.Parallel()

// Setup test destination
exampleDestination := destination.Destination{
ID: uuid.New().String(),
Name: "Test Destination",
}
redisClient.Set(context.Background(), "destination:"+exampleDestination.ID, exampleDestination.Name, 0)

// Test HTTP request
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/destinations/"+exampleDestination.ID, nil)
router.ServeHTTP(w, req)

var destinationResponse map[string]any
json.Unmarshal(w.Body.Bytes(), &destinationResponse)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, exampleDestination.ID, destinationResponse["id"])
assert.Equal(t, exampleDestination.Name, destinationResponse["name"])

// Clean up
redisClient.Del(context.Background(), "destination:"+exampleDestination.ID)
})
}

func TestDestinationUpdateHandler(t *testing.T) {
t.Parallel()

redisClient := testutil.CreateTestRedisClient(t)
handlers := destination.NewHandlers(redisClient)
router := setupRouter(handlers)

initialDestination := destination.Destination{
Name: "Test Destination",
}

updateDestinationRequest := destination.UpdateDestinationRequest{
Name: "Updated Destination",
}
updateDestinationJSON, _ := json.Marshal(updateDestinationRequest)

t.Run("should validate", func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/destinations/invalid_id", nil)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
})

t.Run("should return 404 when there's no destination", func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/destinations/invalid_id", strings.NewReader(string(updateDestinationJSON)))
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("should update destination", func(t *testing.T) {
t.Parallel()

// Setup initial destination
newDestination := initialDestination
newDestination.ID = uuid.New().String()
redisClient.Set(context.Background(), "destination:"+newDestination.ID, newDestination.Name, 0)

// Test HTTP request
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/destinations/"+newDestination.ID, strings.NewReader(string(updateDestinationJSON)))
router.ServeHTTP(w, req)

var destinationResponse map[string]any
json.Unmarshal(w.Body.Bytes(), &destinationResponse)

assert.Equal(t, http.StatusAccepted, w.Code)
assert.Equal(t, newDestination.ID, destinationResponse["id"])
assert.Equal(t, updateDestinationRequest.Name, destinationResponse["name"])

// Clean up
redisClient.Del(context.Background(), "destination:"+newDestination.ID)
})
}

func TestDestinationDeleteHandler(t *testing.T) {
redisClient := testutil.CreateTestRedisClient(t)
handlers := destination.NewHandlers(redisClient)
router := setupRouter(handlers)

t.Run("should return 404 when there's no destination", func(t *testing.T) {
t.Parallel()

w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/destinations/invalid_id", nil)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("should delete destination", func(t *testing.T) {
t.Parallel()

// Setup initial destination
newDestination := destination.Destination{
ID: uuid.New().String(),
Name: "Test Destination",
}
redisClient.Set(context.Background(), "destination:"+newDestination.ID, newDestination.Name, 0)

w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/destinations/"+newDestination.ID, nil)
router.ServeHTTP(w, req)

var destinationResponse map[string]any
json.Unmarshal(w.Body.Bytes(), &destinationResponse)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, newDestination.ID, destinationResponse["id"])
assert.Equal(t, newDestination.Name, destinationResponse["name"])
})
}
67 changes: 67 additions & 0 deletions internal/destination/destination_test/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package destination_test

import (
"context"
"testing"

"github.com/google/uuid"
"github.com/hookdeck/EventKit/internal/destination"
"github.com/hookdeck/EventKit/internal/util/testutil"
"github.com/stretchr/testify/assert"
)

func TestDestinationModel(t *testing.T) {
t.Parallel()

redisClient := testutil.CreateTestRedisClient(t)
model := destination.NewDestinationModel(redisClient)

input := destination.Destination{
ID: uuid.New().String(),
Name: "Test Destination",
}

t.Run("gets empty", func(t *testing.T) {
actual, err := model.Get(context.Background(), input.ID)
assert.Nil(t, actual, "model.Get() should return nil when there's no value")
assert.Nil(t, err, "model.Get() should not return an error when there's no value")
})

t.Run("sets", func(t *testing.T) {
err := model.Set(context.Background(), input)
assert.Nil(t, err, "model.Set() should not return an error")

value, err := redisClient.Get(context.Background(), "destination:"+input.ID).Result()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, input.Name, value, "model.Set() should set destination name %s", input.Name)
})

t.Run("gets", func(t *testing.T) {
actual, err := model.Get(context.Background(), input.ID)
assert.Nil(t, err, "model.Get() should not return an error")
assert.Equal(t, input, *actual, "model.Get() should return %s", input)
})

t.Run("overrides", func(t *testing.T) {
input.Name = "Test Destination 2"

err := model.Set(context.Background(), input)
assert.Nil(t, err, "model.Set() should not return an error", input)

actual, err := model.Get(context.Background(), input.ID)
assert.Nil(t, err, "model.Get() should not return an error")
assert.Equal(t, input, *actual, "model.Get() should return %s", input)
})

t.Run("clears", func(t *testing.T) {
deleted, err := model.Clear(context.Background(), input.ID)
assert.Nil(t, err, "model.Clear() should not return an error")
assert.Equal(t, *deleted, input, "model.Clear() should return deleted value", input)

actual, err := model.Get(context.Background(), input.ID)
assert.Nil(t, actual, "model.Clear() should properly remove value")
assert.Nil(t, err, "model.Clear() should properly remove value")
})
}
41 changes: 41 additions & 0 deletions internal/services/api/auth_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api

import (
"errors"
"net/http"
"strings"

"github.com/gin-gonic/gin"
)

func apiKeyAuthMiddleware(apiKey string) gin.HandlerFunc {
if apiKey == "" {
return func(c *gin.Context) {
c.Next()
}
}

return func(c *gin.Context) {
authorizationToken, err := extractBearerToken(c.GetHeader("Authorization"))
if err != nil {
// TODO: Consider sending a more detailed error message.
// Currently we don't have clear specs on how to send back error message.
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if authorizationToken != apiKey {
// TODO: Consider sending a more detailed error message.
// Currently we don't have clear specs on how to send back error message.
c.AbortWithStatus(http.StatusUnauthorized)
return
}
Comment on lines +20 to +31
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to point this out as the spec hasn't mentioned errors yet, afaik. Do you want to circle back on this later?

Happy to sort this out now if you can share what you have in mind or provide the OpenAPI schema spec or something like that.

c.Next()
}
}

func extractBearerToken(header string) (string, error) {
if !strings.HasPrefix(header, "Bearer ") {
return "", errors.New("invalid bearer token")
}
return strings.TrimPrefix(header, "Bearer "), nil
}
Loading