From 96931551aa4c622732bd21191ddf420ef0596739 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 9 Jul 2021 19:13:19 +0700 Subject: [PATCH] feat: nearly finished --- api/.gitignore | 8 +- api/README.md | 48 +++++++++-- api/app/v1/app.go | 18 ++++ api/app/v1/handler/health.go | 7 ++ api/app/v1/handler/joke_add.go | 6 +- api/app/v1/handler/joke_delete.go | 42 ++++++++++ api/app/v1/handler/joke_get.go | 13 ++- api/app/v1/handler/joke_update.go | 50 ++++++++++++ api/app/v1/middleware/auth.go | 11 +-- api/app/v1/models/auth.go | 6 -- api/app/v1/models/joke.go | 6 -- api/app/v1/models/request.go | 11 +++ api/app/v1/models/response.go | 10 +++ api/app/v1/platform/cache/redis.go | 1 + api/app/v1/platform/database/create.go | 100 ++++++----------------- api/app/v1/platform/database/postgres.go | 1 + api/app/v1/routes/health.go | 12 +++ api/app/v1/routes/joke.go | 21 ++--- api/app/v1/utils/date.go | 10 +-- api/app/v1/utils/date_test.go | 30 +++++++ api/app/v1/utils/parse.go | 33 ++++++++ api/app/v1/utils/parse_test.go | 43 ++++++++++ api/app/v1/utils/request.go | 55 +++++++++++++ api/app/v1/utils/request_test.go | 27 ++++++ api/go.mod | 3 +- api/go.sum | 4 + api/main.go | 50 +++++++++++- 27 files changed, 490 insertions(+), 136 deletions(-) create mode 100644 api/app/v1/app.go create mode 100644 api/app/v1/handler/health.go delete mode 100644 api/app/v1/models/auth.go delete mode 100644 api/app/v1/models/joke.go create mode 100644 api/app/v1/models/request.go create mode 100644 api/app/v1/models/response.go create mode 100644 api/app/v1/routes/health.go create mode 100644 api/app/v1/utils/date_test.go create mode 100644 api/app/v1/utils/parse.go create mode 100644 api/app/v1/utils/parse_test.go create mode 100644 api/app/v1/utils/request.go create mode 100644 api/app/v1/utils/request_test.go diff --git a/api/.gitignore b/api/.gitignore index 942f1f5..94859b2 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -12,4 +12,10 @@ *.out # Dependency directories (remove the comment below to include it) -vendor/ \ No newline at end of file +vendor/ + +# Environment variable +.env + +# Heroku bin directory +bin \ No newline at end of file diff --git a/api/README.md b/api/README.md index f374aaf..e1b35d5 100644 --- a/api/README.md +++ b/api/README.md @@ -6,20 +6,52 @@ Still work in progress ```bash # Install modules -$ go install +$ go mod download # or $ go mod vendor # run the local server -$ go run ./ +$ go run main.go # build everything -$ go build ./ +$ go build main.go ``` -## Modules +## Used packages -- https://github.com/Masterminds/squirrel -- https://github.com/jackc/pgx -- https://github.com/go-redis/redis/v8 -- https://github.com/unrolled/secure \ No newline at end of file +| Name | Version | Type | +| --- | --- | --- | +| gofiber/fiber | v2.14.0 | Framework | +| jackc/pgx | v4.11.0 | Database | +| go-redis/redis | v8.11.0 | Cache | +| joho/godotenv | v1.3.0 | Config | +| getsentry/sentry-go | v0.11.0 | Logging | +| aldy505/phc-crypto | v1.1.0 | Utils | +| Masterminds/squirrel | v1.5.0 | Utils | +| aldy505/bob | v0.0.1 | Utils | + +## Directory structure + +``` +└-- /app + └---- /v1 + └---- /handler + └---- /middleware folder for add middleware + └---- /models + └---- /platform + └--------- /cache folder with in-memory cache setup functions + └--------- /database folder with database setup functions + └---- /routes folder for describe routes + └---- /utils folder with utility functions +``` +## `.env` configuration + +```ini +ENV=development +PORT=5000 + +DATABASE_URL=postgres://postgres:password@localhost:5432/jokesbapak2 +REDIS_URL=redis://@localhost:6379 + +SENTRY_DSN= +``` \ No newline at end of file diff --git a/api/app/v1/app.go b/api/app/v1/app.go new file mode 100644 index 0000000..fe7a857 --- /dev/null +++ b/api/app/v1/app.go @@ -0,0 +1,18 @@ +package v1 + +import ( + "github.com/aldy505/jokes-bapak2-api/api/app/v1/routes" + "github.com/gofiber/fiber/v2" +) + +func New() *fiber.App { + app := fiber.New(fiber.Config{ + DisableKeepalive: true, + CaseSensitive: true, + }) + + routes.Health(app) + routes.Joke(app) + + return app +} diff --git a/api/app/v1/handler/health.go b/api/app/v1/handler/health.go new file mode 100644 index 0000000..dfec28e --- /dev/null +++ b/api/app/v1/handler/health.go @@ -0,0 +1,7 @@ +package handler + +import "github.com/gofiber/fiber/v2" + +func Health(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/api/app/v1/handler/joke_add.go b/api/app/v1/handler/joke_add.go index bd1f534..f67b422 100644 --- a/api/app/v1/handler/joke_add.go +++ b/api/app/v1/handler/joke_add.go @@ -8,7 +8,7 @@ import ( ) func AddNewJoke(c *fiber.Ctx) error { - var body models.JokePost + var body models.RequestJokePost err := c.BodyParser(&body) if err != nil { return err @@ -24,7 +24,7 @@ func AddNewJoke(c *fiber.Ctx) error { return err } - return c.Status(fiber.StatusCreated).JSON(fiber.Map{ - "link": body.Link, + return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{ + Link: body.Link, }) } diff --git a/api/app/v1/handler/joke_delete.go b/api/app/v1/handler/joke_delete.go index abeebd1..3b1601e 100644 --- a/api/app/v1/handler/joke_delete.go +++ b/api/app/v1/handler/joke_delete.go @@ -1 +1,43 @@ package handler + +import ( + "context" + + "github.com/Masterminds/squirrel" + "github.com/aldy505/jokes-bapak2-api/api/app/v1/models" + "github.com/gofiber/fiber/v2" +) + +func DeleteJoke(c *fiber.Ctx) error { + id := c.Params("id") + + // Check if the joke exists + sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + var jokeID string + err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) + if err != nil { + return err + } + + if jokeID == id { + sql, args, err = psql.Delete("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + _, err = db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ + Message: "specified joke id has been deleted", + }) + } + return c.Status(fiber.StatusNotAcceptable).JSON(models.ResponseError{ + Error: "specified joke id does not exists", + }) +} diff --git a/api/app/v1/handler/joke_get.go b/api/app/v1/handler/joke_get.go index 49c80e4..142d3ce 100644 --- a/api/app/v1/handler/joke_get.go +++ b/api/app/v1/handler/joke_get.go @@ -2,7 +2,6 @@ package handler import ( "context" - "strconv" "time" "github.com/Masterminds/squirrel" @@ -38,20 +37,20 @@ func TodayJoke(c *fiber.Ctx) error { if eq { c.Attachment(joke.link) - return c.SendStatus(200) + return c.SendStatus(fiber.StatusOK) } else { var link string err := db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 WHERE random() < 0.01 LIMIT 1").Scan(&link) if err != nil { return err } - now := strconv.Itoa(int(time.Now().Unix())) + now := time.Now().UTC().Format(time.RFC3339) err = redis.MSet(context.Background(), "today:link", link, "today:date", now).Err() if err != nil { return err } c.Attachment(link) - return c.SendStatus(200) + return c.SendStatus(fiber.StatusOK) } } @@ -66,7 +65,7 @@ func SingleJoke(c *fiber.Ctx) error { return err } c.Attachment(link) - return c.SendStatus(200) + return c.SendStatus(fiber.StatusOK) } func JokeByID(c *fiber.Ctx) error { @@ -79,8 +78,8 @@ func JokeByID(c *fiber.Ctx) error { return err } if link == "" { - return c.Status(404).Send([]byte("Requested ID was not found.")) + return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found.")) } c.Attachment(link) - return c.SendStatus(200) + return c.SendStatus(fiber.StatusOK) } diff --git a/api/app/v1/handler/joke_update.go b/api/app/v1/handler/joke_update.go index abeebd1..63bd5db 100644 --- a/api/app/v1/handler/joke_update.go +++ b/api/app/v1/handler/joke_update.go @@ -1 +1,51 @@ package handler + +import ( + "context" + + "github.com/Masterminds/squirrel" + "github.com/aldy505/jokes-bapak2-api/api/app/v1/models" + "github.com/gofiber/fiber/v2" +) + +func UpdateJoke(c *fiber.Ctx) error { + id := c.Params("id") + // Check if the joke exists + sql, args, err := psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + var jokeID string + err = db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) + if err != nil { + return err + } + + if jokeID == id { + body := new(models.RequestJokePost) + err = c.BodyParser(&body) + if err != nil { + return err + } + + sql, args, err = psql.Update("jokesbapak2").Set("link", body.Link).Set("key", body.Key).ToSql() + if err != nil { + return err + } + + _, err := db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ + Message: "specified joke id has been updated", + Link: body.Link, + }) + } + + return c.Status(fiber.StatusNotAcceptable).JSON(models.ResponseError{ + Error: "specified joke id does not exists", + }) +} diff --git a/api/app/v1/middleware/auth.go b/api/app/v1/middleware/auth.go index a347c06..8215dc8 100644 --- a/api/app/v1/middleware/auth.go +++ b/api/app/v1/middleware/auth.go @@ -15,7 +15,7 @@ var db = database.New() func RequireAuth() fiber.Handler { return func(c *fiber.Ctx) error { - var auth models.Auth + var auth models.RequestAuth err := c.BodyParser(&auth) if err != nil { return err @@ -33,8 +33,8 @@ func RequireAuth() fiber.Handler { } if token == "" { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Invalid token", + return c.Status(fiber.StatusForbidden).JSON(models.ResponseError{ + Error: "Invalid token", }) } @@ -61,8 +61,9 @@ func RequireAuth() fiber.Handler { if verify { c.Next() } - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Invalid key", + + return c.Status(fiber.StatusForbidden).JSON(models.ResponseError{ + Error: "Invalid key", }) } } diff --git a/api/app/v1/models/auth.go b/api/app/v1/models/auth.go deleted file mode 100644 index a64f2b7..0000000 --- a/api/app/v1/models/auth.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type Auth struct { - Key string `json:"key"` - Token string `json:"token"` -} diff --git a/api/app/v1/models/joke.go b/api/app/v1/models/joke.go deleted file mode 100644 index 2a4c79d..0000000 --- a/api/app/v1/models/joke.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type JokePost struct { - Key string `json:"string"` - Link string `json:"link"` -} diff --git a/api/app/v1/models/request.go b/api/app/v1/models/request.go new file mode 100644 index 0000000..471b019 --- /dev/null +++ b/api/app/v1/models/request.go @@ -0,0 +1,11 @@ +package models + +type RequestJokePost struct { + Key string `json:"string"` + Link string `json:"link"` +} + +type RequestAuth struct { + Key string `json:"key"` + Token string `json:"token"` +} diff --git a/api/app/v1/models/response.go b/api/app/v1/models/response.go new file mode 100644 index 0000000..4f13263 --- /dev/null +++ b/api/app/v1/models/response.go @@ -0,0 +1,10 @@ +package models + +type ResponseError struct { + Error string `json:"error"` +} + +type ResponseJoke struct { + Link string `json:"link"` + Message string `json:"message"` +} diff --git a/api/app/v1/platform/cache/redis.go b/api/app/v1/platform/cache/redis.go index 707002e..2a6eab8 100644 --- a/api/app/v1/platform/cache/redis.go +++ b/api/app/v1/platform/cache/redis.go @@ -5,6 +5,7 @@ import ( "os" "github.com/go-redis/redis/v8" + _ "github.com/joho/godotenv/autoload" ) // Connect to the database diff --git a/api/app/v1/platform/database/create.go b/api/app/v1/platform/database/create.go index bd3ede4..57edd1e 100644 --- a/api/app/v1/platform/database/create.go +++ b/api/app/v1/platform/database/create.go @@ -5,105 +5,55 @@ import ( "log" "strings" - sq "github.com/Masterminds/squirrel" "github.com/aldy505/bob" ) // Set up the table connection, create table if not exists func Setup() error { - psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) db := New() // Jokesbapak2 table & data - // Check if table exists - sql, args, err := bob.HasTable("jokesbapak2").PlaceholderFormat(bob.Dollar).ToSQL() + sql, _, err := bob.CreateTableIfNotExists("jokesbapak2"). + Columns("id", "link", "key"). + Types("SERIAL", "TEXT", "VARCHAR(255)"). + Primary("id").ToSql() if err != nil { - - log.Fatalln("failed on checking database table:", err) + log.Fatalln("10 - failed on table creation: ", err) } - var hasTableJokes bool - err = db.QueryRow(context.Background(), sql, args...).Scan(&hasTableJokes) - if err != nil { - if err.Error() == "no rows in result set" { - hasTableJokes = false - } else { - log.Fatalln("failed on checking database table:", err) - } - } - - if !hasTableJokes { - sql, _, err = bob.CreateTable("jokesbapak2"). - Columns("id", "link"). - Types("SERIAL", "VARCHAR(255)"). - Primary("id").ToSQL() + splitSql := strings.Split(sql, ";") + for i := range splitSql { + _, err = db.Query(context.Background(), splitSql[i]) if err != nil { - log.Fatalln("failed on table creation:", err) - } - - splitSql := strings.Split(sql, ";") - for i := range splitSql { - _, err = db.Query(context.Background(), splitSql[i]) - if err != nil { - log.Println(sql) - log.Fatalln("Failed on table creation: ", err) - return err - } - } - - insertQuery, args, err := psql.Insert("jokesbapak2"). - Columns("link"). - Values("https://i.ibb.co/19pntdQ/Ea-p8-BWU8-AAtbjp.jpg"). - ToSql() - if err != nil { - log.Fatalln("Failed on query creation: ", err) - return err - } - _, err = db.Query(context.Background(), insertQuery, args...) - if err != nil { - log.Fatalln("Failed on table insertion: ", err) + log.Fatalln("11 - failed on table creation: ", err) return err } } // Authorization - // Check if table exists - sql, args, err = bob.HasTable("authorization").PlaceholderFormat(bob.Dollar).ToSQL() + sql, _, err = bob.CreateTableIfNotExists("authorization"). + Columns("id", "token", "key"). + Types("SERIAL", "TEXT", "VARCHAR(255)"). + Primary("id"). + Unique("token"). + ToSql() if err != nil { - log.Fatalln("failed on checking database table:", err) + log.Fatalln("14 - failed on table creation: ", err) + return err } - var hasTableAuth bool - err = db.QueryRow(context.Background(), sql, args...).Scan(&hasTableAuth) - if err != nil { - if err.Error() == "no rows in result set" { - hasTableAuth = false - } else { - log.Fatalln("failed on checking database table:", err) - } - } - - if !hasTableAuth { - sql, _, err = bob.CreateTable("authorization"). - Columns("id", "token", "key"). - Types("SERIAL", "VARCHAR(255)", "VARCHAR(255)"). - Primary("id"). - Unique("token"). - ToSQL() + splitSql = strings.Split(sql, ";") + for i := range splitSql { + _, err = db.Query(context.Background(), splitSql[i]) if err != nil { - log.Fatalln("Failed on table creation: ", err) + log.Fatalln("15 - failed on table creation: ", err) return err } - - splitSql := strings.Split(sql, ";") - for i := range splitSql { - _, err = db.Query(context.Background(), splitSql[i]) - if err != nil { - log.Fatalln("Failed on table creation: ", err) - return err - } - } } + _, err = db.Query(context.Background(), "ALTER TABLE jokesbapak2 ADD CONSTRAINT fk_jokesbapak2_key FOREIGN KEY (key) REFERENCES authorization (id)") + if err != nil { + log.Fatalln("16 - failed on foreign key iteration: ", err) + } return nil } diff --git a/api/app/v1/platform/database/postgres.go b/api/app/v1/platform/database/postgres.go index c1a6aa9..eb7b926 100644 --- a/api/app/v1/platform/database/postgres.go +++ b/api/app/v1/platform/database/postgres.go @@ -6,6 +6,7 @@ import ( "os" "github.com/jackc/pgx/v4/pgxpool" + _ "github.com/joho/godotenv/autoload" ) // Connect to the database diff --git a/api/app/v1/routes/health.go b/api/app/v1/routes/health.go new file mode 100644 index 0000000..f92eaa1 --- /dev/null +++ b/api/app/v1/routes/health.go @@ -0,0 +1,12 @@ +package routes + +import ( + "github.com/aldy505/jokes-bapak2-api/api/app/v1/handler" + "github.com/gofiber/fiber/v2" +) + +func Health(app *fiber.App) *fiber.App { + // Health check + app.Get("/", handler.Health) + return app +} diff --git a/api/app/v1/routes/joke.go b/api/app/v1/routes/joke.go index b09fd07..70bb687 100644 --- a/api/app/v1/routes/joke.go +++ b/api/app/v1/routes/joke.go @@ -12,31 +12,24 @@ import ( var db = database.New() var redis = cache.New() -func New() *fiber.App { - app := fiber.New(fiber.Config{ - ETag: true, - DisableKeepalive: true, - CaseSensitive: true, - }) - - v1 := app.Group("/v1") +func Joke(app *fiber.App) *fiber.App { // Single route - v1.Get("/", handler.SingleJoke) + app.Get("/", handler.SingleJoke) // Today's joke - v1.Get("/today", handler.TodayJoke) + app.Get("/today", handler.TodayJoke) // Joke by ID - v1.Get("/:id", handler.JokeByID) + app.Get("/:id", handler.JokeByID) // Add new joke - v1.Put("/", middleware.RequireAuth(), handler.AddNewJoke) + app.Put("/", middleware.RequireAuth(), handler.AddNewJoke) // Update a joke - v1.Patch("/:id") + app.Patch("/:id", middleware.RequireAuth(), handler.UpdateJoke) // Delete a joke - v1.Delete("/:id") + app.Delete("/:id", middleware.RequireAuth(), handler.DeleteJoke) return app } diff --git a/api/app/v1/utils/date.go b/api/app/v1/utils/date.go index 5db7987..563947f 100644 --- a/api/app/v1/utils/date.go +++ b/api/app/v1/utils/date.go @@ -4,15 +4,13 @@ import "time" // IsToday checks if a date is in fact today or not. func IsToday(date string) (bool, error) { - parse, err := time.Parse(time.UnixDate, date) + parse, err := time.Parse(time.RFC3339, date) if err != nil { return false, err } - now := time.Now() + y1, m1, d1 := parse.Date() + y2, m2, d2 := time.Now().Date() - if parse.Equal(now) { - return true, nil - } - return false, nil + return y1 == y2 && m1 == m2 && d1 == d2, nil } diff --git a/api/app/v1/utils/date_test.go b/api/app/v1/utils/date_test.go new file mode 100644 index 0000000..0a0f1b7 --- /dev/null +++ b/api/app/v1/utils/date_test.go @@ -0,0 +1,30 @@ +package utils_test + +import ( + "testing" + "time" + + "github.com/aldy505/jokes-bapak2-api/api/app/v1/utils" +) + +func TestIsToday(t *testing.T) { + t.Run("should be able to tell if it's today", func(t *testing.T) { + today, err := utils.IsToday(time.Now().UTC().Format(time.RFC3339)) + if err != nil { + t.Error(err.Error()) + } + if today == false { + t.Error("today should be true:", today) + } + }) + + t.Run("should be able to tell if it's not today", func(t *testing.T) { + today, err := utils.IsToday("2021-01-01T11:48:24Z") + if err != nil { + t.Error(err.Error()) + } + if today == true { + t.Error("today should be false:", today) + } + }) +} diff --git a/api/app/v1/utils/parse.go b/api/app/v1/utils/parse.go new file mode 100644 index 0000000..44a1b06 --- /dev/null +++ b/api/app/v1/utils/parse.go @@ -0,0 +1,33 @@ +package utils + +import ( + "encoding/json" + "strconv" +) + +// ParseToFormBody converts a body to form data type +func ParseToFormBody(body map[string]interface{}) ([]byte, error) { + var form string + for key, value := range body { + form += key + "=" + switch v := value.(type) { + case string: + form += v + case int: + form += strconv.Itoa(v) + case bool: + form += strconv.FormatBool(v) + } + form += "&" + } + return []byte(form), nil +} + +// ParseToJSONBody converts a body to json data type +func ParseToJSONBody(body map[string]interface{}) ([]byte, error) { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + return b, nil +} diff --git a/api/app/v1/utils/parse_test.go b/api/app/v1/utils/parse_test.go new file mode 100644 index 0000000..1b525cc --- /dev/null +++ b/api/app/v1/utils/parse_test.go @@ -0,0 +1,43 @@ +package utils_test + +import ( + "testing" + + "github.com/aldy505/jokes-bapak2-api/api/app/v1/utils" +) + +func TestParseToJSONBody(t *testing.T) { + t.Run("should be able to parse a json string", func(t *testing.T) { + body := map[string]interface{}{ + "name": "Scott", + "age": 32, + "fat": true, + } + parsed, err := utils.ParseToJSONBody(body) + if err != nil { + t.Error(err.Error()) + } + result := "{\"age\":32,\"fat\":true,\"name\":\"Scott\"}" + if string(parsed) != result { + t.Error("parsed string is not the same as result:", string(parsed)) + } + }) +} + +func TestParseToFormBody(t *testing.T) { + t.Run("should be able to parse a form body", func(t *testing.T) { + body := map[string]interface{}{ + "name": "Scott", + "age": 32, + "fat": true, + } + parsed, err := utils.ParseToFormBody(body) + if err != nil { + t.Error(err.Error()) + } + result := "name=Scott&age=32&fat=true&" + if string(parsed) != result { + t.Error("parsed string is not the same as result:", string(parsed)) + } + }) +} diff --git a/api/app/v1/utils/request.go b/api/app/v1/utils/request.go new file mode 100644 index 0000000..8a73af5 --- /dev/null +++ b/api/app/v1/utils/request.go @@ -0,0 +1,55 @@ +package utils + +import ( + "bytes" + "net/http" +) + +type ContentType int + +const ( + JSON ContentType = iota + Form +) + +type RequestConfig struct { + URL string + Method string + Headers map[string]interface{} + Body map[string]interface{} + ContentType ContentType +} + +// Request is a simple wrapper around http.NewRequest +func Request(config RequestConfig) (response *http.Response, err error) { + client := &http.Client{} + + var body []byte + if config.ContentType == JSON { + parsed, err := ParseToJSONBody(config.Body) + if err != nil { + return &http.Response{}, err + } + + body = parsed + } else if config.ContentType == Form { + parsed, err := ParseToFormBody(config.Body) + if err != nil { + return &http.Response{}, err + } + + body = parsed + } + + request, err := http.NewRequest(config.Method, config.URL, bytes.NewReader(body)) + if err != nil { + return + } + + response, err = client.Do(request) + if err != nil { + return + } + + return +} diff --git a/api/app/v1/utils/request_test.go b/api/app/v1/utils/request_test.go new file mode 100644 index 0000000..54d0f47 --- /dev/null +++ b/api/app/v1/utils/request_test.go @@ -0,0 +1,27 @@ +package utils_test + +import ( + "net/http" + "testing" + + "github.com/aldy505/jokes-bapak2-api/api/app/v1/utils" +) + +func TestRequest(t *testing.T) { + t.Run("should be able to do a get request", func(t *testing.T) { + res, err := utils.Request(utils.RequestConfig{ + URL: "https://jsonplaceholder.typicode.com/todos/1", + Method: http.MethodGet, + Headers: map[string]interface{}{ + "User-Agent": "Jokesbapak2 Test API", + "Accept": "application/json", + }, + }) + if err != nil { + t.Error(err.Error()) + } + if res.StatusCode != 200 { + t.Error("response does not have 200 status", res.Status) + } + }) +} diff --git a/api/go.mod b/api/go.mod index efa582e..45bd976 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,10 +4,11 @@ go 1.16 require ( github.com/Masterminds/squirrel v1.5.0 - github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54 + github.com/aldy505/bob v0.0.1 github.com/aldy505/phc-crypto v1.1.0 github.com/getsentry/sentry-go v0.11.0 github.com/go-redis/redis/v8 v8.11.0 github.com/gofiber/fiber/v2 v2.14.0 github.com/jackc/pgx/v4 v4.11.0 + github.com/joho/godotenv v1.3.0 ) diff --git a/api/go.sum b/api/go.sum index 128b045..866b4e1 100644 --- a/api/go.sum +++ b/api/go.sum @@ -17,6 +17,8 @@ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54 h1:U9GFaqa00Ft1kqmUxoNz+CugEmXGdX6bz2L+O/dbB/8= github.com/aldy505/bob v0.0.0-20210630160113-75547d606a54/go.mod h1:/8HuD17XXgzuaFw5j4oDyB8O+NlW8mKWd0QCCbeoLVE= +github.com/aldy505/bob v0.0.1 h1:L/nvD9+ViLJaWbgbeBes/4xQfz7YtQLJtk8OjSa9L2k= +github.com/aldy505/bob v0.0.1/go.mod h1:/8HuD17XXgzuaFw5j4oDyB8O+NlW8mKWd0QCCbeoLVE= github.com/aldy505/phc-crypto v1.1.0 h1:BagRKCrB7FOYy5vnuXR6xs6ml2gJD/CvSJkX/Ozo63w= github.com/aldy505/phc-crypto v1.1.0/go.mod h1:LJugClOkOWKnpLrWhSaIDRN/5ftvZPD48S5oXsT7iTg= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -243,6 +245,8 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/api/main.go b/api/main.go index 24cb889..1c76a3c 100644 --- a/api/main.go +++ b/api/main.go @@ -3,16 +3,18 @@ package main import ( "log" "os" + "os/signal" "time" + v1 "github.com/aldy505/jokes-bapak2-api/api/app/v1" "github.com/aldy505/jokes-bapak2-api/api/app/v1/platform/database" - "github.com/aldy505/jokes-bapak2-api/api/app/v1/routes" "github.com/getsentry/sentry-go" "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/limiter" + _ "github.com/joho/godotenv/autoload" ) func main() { @@ -45,14 +47,54 @@ func main() { app.Use(limiter.New()) app.Use(etag.New()) - app.Mount("/", routes.New()) + app.Mount("/v1", v1.New()) - log.Fatal(app.Listen(":" + os.Getenv("PORT"))) + // Start server (with or without graceful shutdown). + if os.Getenv("ENV") == "development" { + StartServer(app) + } else { + StartServerWithGracefulShutdown(app) + } } func errorHandler(c *fiber.Ctx, err error) error { sentry.CaptureException(err) - return c.Status(500).JSON(fiber.Map{ + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Something went wrong on our end", }) } + +// StartServerWithGracefulShutdown function for starting server with a graceful shutdown. +func StartServerWithGracefulShutdown(a *fiber.App) { + // Create channel for idle connections. + idleConnsClosed := make(chan struct{}) + + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) // Catch OS signals. + <-sigint + + // Received an interrupt signal, shutdown. + if err := a.Shutdown(); err != nil { + // Error from closing listeners, or context timeout: + log.Printf("Oops... Server is not shutting down! Reason: %v", err) + } + + close(idleConnsClosed) + }() + + // Run server. + if err := a.Listen(":" + os.Getenv("PORT")); err != nil { + log.Printf("Oops... Server is not running! Reason: %v", err) + } + + <-idleConnsClosed +} + +// StartServer func for starting a simple server. +func StartServer(a *fiber.App) { + // Run server. + if err := a.Listen(":" + os.Getenv("PORT")); err != nil { + log.Printf("Oops... Server is not running! Reason: %v", err) + } +}