refactor: swap package, clean up, added test
This commit is contained in:
parent
80d9e047a7
commit
60431d3e38
|
@ -17,6 +17,8 @@ $ go run main.go
|
||||||
$ go build main.go
|
$ go build main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is a placeholder data ready for you to query it manually in `/app/v1/platform/database/placeholder.sql`. Have a good time developing!
|
||||||
|
|
||||||
## Used packages
|
## Used packages
|
||||||
|
|
||||||
| Name | Version | Type |
|
| Name | Version | Type |
|
||||||
|
|
|
@ -6,9 +6,13 @@ import (
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
"jokes-bapak2-api/app/v1/platform/database"
|
||||||
"jokes-bapak2-api/app/v1/routes"
|
"jokes-bapak2-api/app/v1/routes"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
gocache "github.com/patrickmn/go-cache"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var memory = cache.InMemory()
|
var memory = cache.InMemory()
|
||||||
|
@ -18,23 +22,57 @@ func New() *fiber.App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
DisableKeepalive: true,
|
DisableKeepalive: true,
|
||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
|
ErrorHandler: errorHandler,
|
||||||
})
|
})
|
||||||
|
|
||||||
checkCache := core.CheckJokesCache(memory)
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: os.Getenv("SENTRY_DSN"),
|
||||||
|
Environment: os.Getenv("ENV"),
|
||||||
|
// Enable printing of SDK debug messages.
|
||||||
|
// Useful when getting started or trying to figure something out.
|
||||||
|
Debug: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sentry.Flush(2 * time.Second)
|
||||||
|
|
||||||
|
err = database.Setup()
|
||||||
|
if err != nil {
|
||||||
|
sentry.CaptureException(err)
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCache, err := core.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
if !checkCache {
|
if !checkCache {
|
||||||
jokes, err := core.GetAllJSONJokes(db)
|
jokes, err := core.GetAllJSONJokes(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, gocache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Use(cors.New())
|
||||||
|
app.Use(etag.New())
|
||||||
|
|
||||||
routes.Health(app)
|
routes.Health(app)
|
||||||
routes.Joke(app)
|
routes.Joke(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(c *fiber.Ctx, err error) error {
|
||||||
|
log.Println(err)
|
||||||
|
sentry.CaptureException(err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Something went wrong on our end",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"jokes-bapak2-api/app/v1/models"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
"github.com/georgysavva/scany/pgxscan"
|
"github.com/georgysavva/scany/pgxscan"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte.
|
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte.
|
||||||
|
@ -25,7 +25,7 @@ func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(jokes)
|
data, err := ffjson.Marshal(jokes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -34,18 +34,22 @@ func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRandomJokeFromCache returns a link string of a random joke from cache.
|
// GetRandomJokeFromCache returns a link string of a random joke from cache.
|
||||||
func GetRandomJokeFromCache(memory *cache.Cache) (string, error) {
|
func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
|
||||||
jokes, found := memory.Get("jokes")
|
jokes, err := memory.Get("jokes")
|
||||||
if !found {
|
if err != nil {
|
||||||
return "", models.ErrNotFound
|
if err.Error() == "Entry not found" {
|
||||||
|
return "", models.ErrNotFound
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []models.Joke
|
var data []models.Joke
|
||||||
err := json.Unmarshal(jokes.([]byte), &data)
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return an error if the database is empty
|
||||||
dataLength := len(data)
|
dataLength := len(data)
|
||||||
if dataLength == 0 {
|
if dataLength == 0 {
|
||||||
return "", models.ErrEmpty
|
return "", models.ErrEmpty
|
||||||
|
@ -58,20 +62,30 @@ func GetRandomJokeFromCache(memory *cache.Cache) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckJokesCache checks if there is some value inside jokes cache.
|
// CheckJokesCache checks if there is some value inside jokes cache.
|
||||||
func CheckJokesCache(memory *cache.Cache) bool {
|
func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
|
||||||
_, found := memory.Get("jokes")
|
_, err := memory.Get("jokes")
|
||||||
return found
|
if err != nil {
|
||||||
|
if err.Error() == "Entry not found" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCachedJokeByID returns a link string of a certain ID from cache.
|
// GetCachedJokeByID returns a link string of a certain ID from cache.
|
||||||
func GetCachedJokeByID(memory *cache.Cache, id int) (string, error) {
|
func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
|
||||||
jokes, found := memory.Get("jokes")
|
jokes, err := memory.Get("jokes")
|
||||||
if !found {
|
if err != nil {
|
||||||
return "", models.ErrNotFound
|
if err.Error() == "Entry not found" {
|
||||||
|
return "", models.ErrNotFound
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []models.Joke
|
var data []models.Joke
|
||||||
err := json.Unmarshal(jokes.([]byte), &data)
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package handler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
v1 "jokes-bapak2-api/app/v1"
|
||||||
|
"jokes-bapak2-api/app/v1/platform/database"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealth(t *testing.T) {
|
||||||
|
err := database.Setup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
|
app := v1.New()
|
||||||
|
|
||||||
|
t.Run("Health - should return 200", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", "/health", nil)
|
||||||
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
|
assert.Equalf(t, false, err != nil, "health")
|
||||||
|
assert.Equalf(t, 200, res.StatusCode, "health")
|
||||||
|
assert.NotEqualf(t, 0, res.ContentLength, "health")
|
||||||
|
_, err = ioutil.ReadAll(res.Body)
|
||||||
|
assert.Nilf(t, err, "health")
|
||||||
|
})
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"jokes-bapak2-api/app/v1/models"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddNewJoke(c *fiber.Ctx) error {
|
func AddNewJoke(c *fiber.Ctx) error {
|
||||||
|
@ -32,7 +31,10 @@ func AddNewJoke(c *fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{
|
return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{
|
||||||
Link: body.Link,
|
Link: body.Link,
|
||||||
|
|
|
@ -2,17 +2,20 @@ package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/core"
|
"jokes-bapak2-api/app/v1/core"
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"jokes-bapak2-api/app/v1/models"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteJoke(c *fiber.Ctx) error {
|
func DeleteJoke(c *fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the joke exists
|
// Check if the joke exists
|
||||||
sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql()
|
sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql()
|
||||||
|
@ -20,7 +23,7 @@ func DeleteJoke(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var jokeID string
|
var jokeID int
|
||||||
err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
|
err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -41,7 +44,10 @@ func DeleteJoke(c *fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
||||||
Message: "specified joke id has been deleted",
|
Message: "specified joke id has been deleted",
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/app/v1/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TodayJoke(c *fiber.Ctx) error {
|
func TodayJoke(c *fiber.Ctx) error {
|
||||||
|
@ -67,14 +66,20 @@ func TodayJoke(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SingleJoke(c *fiber.Ctx) error {
|
func SingleJoke(c *fiber.Ctx) error {
|
||||||
checkCache := core.CheckJokesCache(memory)
|
checkCache, err := core.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if !checkCache {
|
if !checkCache {
|
||||||
jokes, err := core.GetAllJSONJokes(db)
|
jokes, err := core.GetAllJSONJokes(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := core.GetRandomJokeFromCache(memory)
|
link, err := core.GetRandomJokeFromCache(memory)
|
||||||
|
@ -99,14 +104,17 @@ func SingleJoke(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func JokeByID(c *fiber.Ctx) error {
|
func JokeByID(c *fiber.Ctx) error {
|
||||||
checkCache := core.CheckJokesCache(memory)
|
checkCache, err := core.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if !checkCache {
|
if !checkCache {
|
||||||
jokes, err := core.GetAllJSONJokes(db)
|
jokes, err := core.GetAllJSONJokes(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var db = database.New()
|
var db = database.New()
|
||||||
var jokesData = []interface{}{1, "https://loremflickr.com/320/240", 1, 2, "https://loremflickr.com/320/240", 1, 3, "https://loremflickr.com/320/240", 1}
|
var jokesData = []interface{}{1, "https://picsum.photos/id/1/200/300", 1, 2, "https://picsum.photos/id/2/200/300", 1, 3, "https://picsum.photos/id/3/200/300", 1}
|
||||||
|
|
||||||
func cleanup() {
|
func cleanup() {
|
||||||
_, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
|
_, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
|
||||||
|
@ -34,7 +34,7 @@ func TestJokeGet(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (key, token, last_used) VALUES ($1, $2, $3);", "very secure", "not the real one", time.Now().Format(time.RFC3339))
|
_, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,6 @@ func TestJokeGet(t *testing.T) {
|
||||||
app := v1.New()
|
app := v1.New()
|
||||||
|
|
||||||
t.Run("TodayJoke - should return 200", func(t *testing.T) {
|
t.Run("TodayJoke - should return 200", func(t *testing.T) {
|
||||||
t.SkipNow()
|
|
||||||
req, _ := http.NewRequest("GET", "/today", nil)
|
req, _ := http.NewRequest("GET", "/today", nil)
|
||||||
res, err := app.Test(req, -1)
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
|
@ -60,7 +59,6 @@ func TestJokeGet(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SingleJoke - should return 200", func(t *testing.T) {
|
t.Run("SingleJoke - should return 200", func(t *testing.T) {
|
||||||
t.SkipNow()
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
req, _ := http.NewRequest("GET", "/", nil)
|
||||||
res, err := app.Test(req, -1)
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
|
@ -72,8 +70,7 @@ func TestJokeGet(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("JokeByID - should return 200", func(t *testing.T) {
|
t.Run("JokeByID - should return 200", func(t *testing.T) {
|
||||||
t.SkipNow()
|
req, _ := http.NewRequest("GET", "/id/1", nil)
|
||||||
req, _ := http.NewRequest("GET", "/2", nil)
|
|
||||||
res, err := app.Test(req, -1)
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
assert.Equalf(t, false, err != nil, "joke by id")
|
assert.Equalf(t, false, err != nil, "joke by id")
|
||||||
|
@ -84,8 +81,7 @@ func TestJokeGet(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("JokeByID - should return 404", func(t *testing.T) {
|
t.Run("JokeByID - should return 404", func(t *testing.T) {
|
||||||
t.SkipNow()
|
req, _ := http.NewRequest("GET", "/id/300", nil)
|
||||||
req, _ := http.NewRequest("GET", "/300", nil)
|
|
||||||
res, err := app.Test(req, -1)
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
assert.Equalf(t, false, err != nil, "joke by id")
|
assert.Equalf(t, false, err != nil, "joke by id")
|
||||||
|
|
|
@ -1,35 +1,43 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"jokes-bapak2-api/app/v1/core"
|
"jokes-bapak2-api/app/v1/core"
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"jokes-bapak2-api/app/v1/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TotalJokes(c *fiber.Ctx) error {
|
func TotalJokes(c *fiber.Ctx) error {
|
||||||
checkCache := core.CheckJokesCache(memory)
|
checkCache, err := core.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if !checkCache {
|
if !checkCache {
|
||||||
jokes, err := core.GetAllJSONJokes(db)
|
jokes, err := core.GetAllJSONJokes(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jokes, found := memory.Get("jokes")
|
jokes, err := memory.Get("jokes")
|
||||||
if !found {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(models.Error{
|
if err.Error() == "Entry not found" {
|
||||||
Error: "no data found",
|
return c.Status(fiber.StatusInternalServerError).JSON(models.Error{
|
||||||
})
|
Error: "no data found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []models.Joke
|
var data []models.Joke
|
||||||
err := json.Unmarshal(jokes.([]byte), &data)
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package handler_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
v1 "jokes-bapak2-api/app/v1"
|
||||||
|
"jokes-bapak2-api/app/v1/platform/database"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTotalJokes(t *testing.T) {
|
||||||
|
err := database.Setup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", "not the real one", time.Now().Format(time.RFC3339))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
|
app := v1.New()
|
||||||
|
|
||||||
|
t.Run("Total - should return 200", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", "/total", nil)
|
||||||
|
res, err := app.Test(req, -1)
|
||||||
|
|
||||||
|
assert.Equalf(t, false, err != nil, "joke total")
|
||||||
|
assert.Equalf(t, 200, res.StatusCode, "joke total")
|
||||||
|
assert.NotEqualf(t, 0, res.ContentLength, "joke total")
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
assert.Nilf(t, err, "joke total")
|
||||||
|
assert.Equalf(t, "{\"message\":\"3\"}", string(body), "joke total")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateJoke(c *fiber.Ctx) error {
|
func UpdateJoke(c *fiber.Ctx) error {
|
||||||
|
@ -46,7 +45,11 @@ func UpdateJoke(c *fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
memory.Set("jokes", jokes, cache.NoExpiration)
|
|
||||||
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{
|
||||||
Message: "specified joke id has been updated",
|
Message: "specified joke id has been updated",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/app/v1/models"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OnlyIntegerAsID() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
regex, err := regexp.Compile(`([0-9]+)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := regex.FindStringIndex(c.Params("id"))
|
||||||
|
if loc[1] == len(c.Params("id")) {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
|
||||||
|
Error: "only numbers are allowed as ID",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gocache "github.com/patrickmn/go-cache"
|
"github.com/allegro/bigcache/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InMemory() *gocache.Cache {
|
func InMemory() *bigcache.BigCache {
|
||||||
cache := gocache.New(6*time.Hour, 6*time.Hour)
|
cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Access the data from your HTTP Request software (Postman or Insomnia)
|
||||||
|
-- with this auth:
|
||||||
|
-- key: test
|
||||||
|
-- token: password
|
||||||
|
|
||||||
|
INSERT INTO "administrators" ("id", "key", "token", "last_used") VALUES
|
||||||
|
(1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');
|
||||||
|
|
||||||
|
-- 10 jokes is enough right?
|
||||||
|
|
||||||
|
INSERT INTO "jokesbapak2" ("id", "link", "creator") VALUES
|
||||||
|
(1, 'https://picsum.photos/id/1000/500/500', 1),
|
||||||
|
(2, 'https://picsum.photos/id/1001/500/500', 1),
|
||||||
|
(3, 'https://picsum.photos/id/1002/500/500', 1),
|
||||||
|
(4, 'https://picsum.photos/id/1003/500/500', 1),
|
||||||
|
(5, 'https://picsum.photos/id/1004/500/500', 1),
|
||||||
|
(6, 'https://picsum.photos/id/1005/500/500', 1),
|
||||||
|
(7, 'https://picsum.photos/id/1006/500/500', 1),
|
||||||
|
(8, 'https://picsum.photos/id/1010/500/500', 1),
|
||||||
|
(9, 'https://picsum.photos/id/1008/500/500', 1),
|
||||||
|
(10, 'https://picsum.photos/id/1009/500/500', 1);
|
|
@ -15,7 +15,7 @@ func Joke(app *fiber.App) *fiber.App {
|
||||||
app.Get("/today", handler.TodayJoke)
|
app.Get("/today", handler.TodayJoke)
|
||||||
|
|
||||||
// Joke by ID
|
// Joke by ID
|
||||||
app.Get("/id/:id", handler.JokeByID)
|
app.Get("/id/:id", middleware.OnlyIntegerAsID(), handler.JokeByID)
|
||||||
|
|
||||||
// Count total jokes
|
// Count total jokes
|
||||||
app.Get("/total", handler.TotalJokes)
|
app.Get("/total", handler.TotalJokes)
|
||||||
|
@ -24,10 +24,10 @@ func Joke(app *fiber.App) *fiber.App {
|
||||||
app.Put("/", middleware.RequireAuth(), handler.AddNewJoke)
|
app.Put("/", middleware.RequireAuth(), handler.AddNewJoke)
|
||||||
|
|
||||||
// Update a joke
|
// Update a joke
|
||||||
app.Patch("/:id", middleware.RequireAuth(), handler.UpdateJoke)
|
app.Patch("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), handler.UpdateJoke)
|
||||||
|
|
||||||
// Delete a joke
|
// Delete a joke
|
||||||
app.Delete("/:id", middleware.RequireAuth(), handler.DeleteJoke)
|
app.Delete("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), handler.DeleteJoke)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseToFormBody converts a body to form data type
|
// ParseToFormBody converts a body to form data type
|
||||||
|
@ -25,7 +26,7 @@ func ParseToFormBody(body map[string]interface{}) ([]byte, error) {
|
||||||
|
|
||||||
// ParseToJSONBody converts a body to json data type
|
// ParseToJSONBody converts a body to json data type
|
||||||
func ParseToJSONBody(body map[string]interface{}) ([]byte, error) {
|
func ParseToJSONBody(body map[string]interface{}) ([]byte, error) {
|
||||||
b, err := json.Marshal(body)
|
b, err := ffjson.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/app/v1/utils"
|
||||||
|
@ -27,16 +28,16 @@ func TestParseToJSONBody(t *testing.T) {
|
||||||
func TestParseToFormBody(t *testing.T) {
|
func TestParseToFormBody(t *testing.T) {
|
||||||
t.Run("should be able to parse a form body", func(t *testing.T) {
|
t.Run("should be able to parse a form body", func(t *testing.T) {
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"name": "Scott",
|
|
||||||
"age": 32,
|
"age": 32,
|
||||||
"fat": true,
|
"fat": true,
|
||||||
|
"name": "Scott",
|
||||||
}
|
}
|
||||||
parsed, err := utils.ParseToFormBody(body)
|
parsed, err := utils.ParseToFormBody(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err.Error())
|
t.Error(err.Error())
|
||||||
}
|
}
|
||||||
result := "name=Scott&age=32&fat=true&"
|
result := [3]string{"age=32&", "fat=true&", "name=Scott&"}
|
||||||
if string(parsed) != result {
|
if !strings.Contains(string(parsed), result[0]) && !strings.Contains(string(parsed), result[1]) && !strings.Contains(string(parsed), result[2]) {
|
||||||
t.Error("parsed string is not the same as result:", string(parsed))
|
t.Error("parsed string is not the same as result:", string(parsed))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/Masterminds/squirrel v1.5.0
|
github.com/Masterminds/squirrel v1.5.0
|
||||||
github.com/aldy505/bob v0.0.1
|
github.com/aldy505/bob v0.0.1
|
||||||
github.com/aldy505/phc-crypto v1.1.0
|
github.com/aldy505/phc-crypto v1.1.0
|
||||||
|
github.com/allegro/bigcache/v3 v3.0.0
|
||||||
github.com/georgysavva/scany v0.2.9
|
github.com/georgysavva/scany v0.2.9
|
||||||
github.com/getsentry/sentry-go v0.11.0
|
github.com/getsentry/sentry-go v0.11.0
|
||||||
github.com/go-redis/redis/v8 v8.11.0
|
github.com/go-redis/redis/v8 v8.11.0
|
||||||
|
@ -14,5 +15,6 @@ require (
|
||||||
github.com/jackc/pgx/v4 v4.11.0
|
github.com/jackc/pgx/v4 v4.11.0
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7
|
||||||
github.com/stretchr/testify v1.5.1
|
github.com/stretchr/testify v1.5.1
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/allegro/bigcache/v3 v3.0.0 h1:5Hxq+GTy8gHEeQccCZZDCfZRTydUfErdUf0iVDcMAFg=
|
||||||
|
github.com/allegro/bigcache/v3 v3.0.0/go.mod h1:t5TAJn1B9qvf/VlJrSM1r6NlFAYoFDubYUsCuIO9nUQ=
|
||||||
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
|
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
|
||||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
@ -404,6 +406,8 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20=
|
||||||
|
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
|
34
api/main.go
34
api/main.go
|
@ -7,12 +7,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "jokes-bapak2-api/app/v1"
|
v1 "jokes-bapak2-api/app/v1"
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
@ -20,37 +16,17 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
timeoutDefault, _ := time.ParseDuration("1m")
|
timeoutDefault, _ := time.ParseDuration("1m")
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
|
||||||
Dsn: os.Getenv("SENTRY_DSN"),
|
|
||||||
|
|
||||||
// Enable printing of SDK debug messages.
|
|
||||||
// Useful when getting started or trying to figure something out.
|
|
||||||
Debug: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer sentry.Flush(2 * time.Second)
|
|
||||||
|
|
||||||
err = database.Setup()
|
|
||||||
if err != nil {
|
|
||||||
sentry.CaptureException(err)
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
ReadTimeout: timeoutDefault,
|
ReadTimeout: timeoutDefault,
|
||||||
WriteTimeout: timeoutDefault,
|
WriteTimeout: timeoutDefault,
|
||||||
ErrorHandler: errorHandler,
|
|
||||||
})
|
})
|
||||||
app.Use(cors.New())
|
|
||||||
app.Use(limiter.New(limiter.Config{
|
app.Use(limiter.New(limiter.Config{
|
||||||
Max: 15,
|
Max: 15,
|
||||||
Expiration: 1 * time.Minute,
|
Expiration: 1 * time.Minute,
|
||||||
LimitReached: limitHandler,
|
LimitReached: limitHandler,
|
||||||
}))
|
}))
|
||||||
app.Use(etag.New())
|
|
||||||
app.Use(favicon.New(favicon.Config{
|
app.Use(favicon.New(favicon.Config{
|
||||||
File: "./favicon.png",
|
File: "./favicon.png",
|
||||||
}))
|
}))
|
||||||
|
@ -65,14 +41,6 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(c *fiber.Ctx, err error) error {
|
|
||||||
log.Println(err)
|
|
||||||
sentry.CaptureException(err)
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
"error": "Something went wrong on our end",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func limitHandler(c *fiber.Ctx) error {
|
func limitHandler(c *fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||||
"message": "we only allow up to 15 request per minute",
|
"message": "we only allow up to 15 request per minute",
|
||||||
|
|
Loading…
Reference in New Issue