From 84cfab08ef0fdbb8dba0e8486b692ccfc89e66b7 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 01:14:32 +0700 Subject: [PATCH 01/10] refactor: moving each routes to new directory --- api/app/v1/handler/health/health.go | 31 ++++ api/app/v1/handler/health/health_test.go | 43 ++++++ api/app/v1/handler/joke/joke_add.go | 55 +++++++ api/app/v1/handler/joke/joke_add_test.go | 62 ++++++++ api/app/v1/handler/joke/joke_delete.go | 60 ++++++++ api/app/v1/handler/joke/joke_delete_test.go | 64 +++++++++ api/app/v1/handler/joke/joke_get.go | 151 ++++++++++++++++++++ api/app/v1/handler/joke/joke_get_test.go | 94 ++++++++++++ api/app/v1/handler/joke/joke_total.go | 39 +++++ api/app/v1/handler/joke/joke_total_test.go | 46 ++++++ api/app/v1/handler/joke/joke_update.go | 75 ++++++++++ api/app/v1/handler/joke/joke_update_test.go | 62 ++++++++ api/app/v1/routes/health.go | 4 +- api/app/v1/routes/joke.go | 16 +-- 14 files changed, 792 insertions(+), 10 deletions(-) create mode 100644 api/app/v1/handler/health/health.go create mode 100644 api/app/v1/handler/health/health_test.go create mode 100644 api/app/v1/handler/joke/joke_add.go create mode 100644 api/app/v1/handler/joke/joke_add_test.go create mode 100644 api/app/v1/handler/joke/joke_delete.go create mode 100644 api/app/v1/handler/joke/joke_delete_test.go create mode 100644 api/app/v1/handler/joke/joke_get.go create mode 100644 api/app/v1/handler/joke/joke_get_test.go create mode 100644 api/app/v1/handler/joke/joke_total.go create mode 100644 api/app/v1/handler/joke/joke_total_test.go create mode 100644 api/app/v1/handler/joke/joke_update.go create mode 100644 api/app/v1/handler/joke/joke_update_test.go diff --git a/api/app/v1/handler/health/health.go b/api/app/v1/handler/health/health.go new file mode 100644 index 0000000..128cda8 --- /dev/null +++ b/api/app/v1/handler/health/health.go @@ -0,0 +1,31 @@ +package health + +import ( + "context" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + + "github.com/gofiber/fiber/v2" +) + +func Health(c *fiber.Ctx) error { + // Ping REDIS database + err := handler.Redis.Ping(context.Background()).Err() + if err != nil { + return c. + Status(fiber.StatusServiceUnavailable). + JSON(models.Error{ + Error: "REDIS: " + err.Error(), + }) + } + + _, err = handler.Db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") + if err != nil { + return c. + Status(fiber.StatusServiceUnavailable). + JSON(models.Error{ + Error: "POSTGRESQL: " + err.Error(), + }) + } + return c.SendStatus(fiber.StatusOK) +} diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go new file mode 100644 index 0000000..d4e21dd --- /dev/null +++ b/api/app/v1/handler/health/health_test.go @@ -0,0 +1,43 @@ +package health_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") + }) +} diff --git a/api/app/v1/handler/joke/joke_add.go b/api/app/v1/handler/joke/joke_add.go new file mode 100644 index 0000000..b72bf58 --- /dev/null +++ b/api/app/v1/handler/joke/joke_add.go @@ -0,0 +1,55 @@ +package joke + +import ( + "context" + + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + + "github.com/gofiber/fiber/v2" +) + +func AddNewJoke(c *fiber.Ctx) error { + var body models.Joke + err := c.BodyParser(&body) + if err != nil { + return err + } + + // Check link validity + valid, err := core.CheckImageValidity(handler.Client, body.Link) + if err != nil { + return err + } + + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "URL provided is not a valid image", + }) + } + + sql, args, err := handler.Psql.Insert("jokesbapak2").Columns("link", "creator").Values(body.Link, c.Locals("userID")).ToSql() + if err != nil { + return err + } + + // TODO: Implement solution if the link provided already exists. + _, err = handler.Db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + + err = core.SetAllJSONJoke(handler.Db, handler.Memory) + if err != nil { + return err + } + err = core.SetTotalJoke(handler.Db, handler.Memory) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{ + Link: body.Link, + }) +} diff --git a/api/app/v1/handler/joke/joke_add_test.go b/api/app/v1/handler/joke/joke_add_test.go new file mode 100644 index 0000000..48a6cb4 --- /dev/null +++ b/api/app/v1/handler/joke/joke_add_test.go @@ -0,0 +1,62 @@ +package joke_test + +import ( + "context" + "io/ioutil" + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/platform/database" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAddNewJoke(t *testing.T) { + // t.SkipNow() + err := database.Setup() + if err != nil { + t.Fatal(err) + } + hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" + _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(cleanup) + + app := v1.New() + + t.Run("Add - should return 201", func(t *testing.T) { + reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PUT", "/", reqBody) + req.Header.Set("content-type", "application/json") + req.Header.Add("accept", "application/json") + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke add") + assert.Equalf(t, 201, res.StatusCode, "joke add") + assert.NotEqualf(t, 0, res.ContentLength, "joke add") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke add") + assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add") + }) + + t.Run("Add - should not be a valid image", func(t *testing.T) { + reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PUT", "/", reqBody) + req.Header.Set("content-type", "application/json") + req.Header.Add("accept", "application/json") + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke add") + assert.Equalf(t, 400, res.StatusCode, "joke add") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke add") + assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") + }) + +} diff --git a/api/app/v1/handler/joke/joke_delete.go b/api/app/v1/handler/joke/joke_delete.go new file mode 100644 index 0000000..96a0a2b --- /dev/null +++ b/api/app/v1/handler/joke/joke_delete.go @@ -0,0 +1,60 @@ +package joke + +import ( + "context" + "strconv" + + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + + "github.com/Masterminds/squirrel" + "github.com/gofiber/fiber/v2" +) + +func DeleteJoke(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return err + } + + // Check if the joke exists + sql, args, err := handler.Psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + var jokeID int + err = handler.Db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) + if err != nil { + return err + } + + if jokeID == id { + sql, args, err = handler.Psql.Delete("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + _, err = handler.Db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + + err = core.SetAllJSONJoke(handler.Db, handler.Memory) + if err != nil { + return err + } + err = core.SetTotalJoke(handler.Db, handler.Memory) + 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.Error{ + Error: "specified joke id does not exists", + }) +} diff --git a/api/app/v1/handler/joke/joke_delete_test.go b/api/app/v1/handler/joke/joke_delete_test.go new file mode 100644 index 0000000..d8ffad3 --- /dev/null +++ b/api/app/v1/handler/joke/joke_delete_test.go @@ -0,0 +1,64 @@ +package joke_test + +import ( + "context" + "io/ioutil" + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/platform/database" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDeleteJoke(t *testing.T) { + // TODO: Remove this line below, make this test works + t.SkipNow() + + err := database.Setup() + if err != nil { + t.Fatal(err) + } + hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" + _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) + if err != nil { + t.Fatal(err) + } + _, err = handler.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("Delete - should return 200", func(t *testing.T) { + reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("DELETE", "/id/1", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke delete") + assert.Equalf(t, 200, res.StatusCode, "joke delete") + assert.NotEqualf(t, 0, res.ContentLength, "joke delete") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke delete") + assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") + }) + + t.Run("Delete - id doesn't exists", func(t *testing.T) { + reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("DELETE", "/id/100", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke delete") + assert.Equalf(t, 406, res.StatusCode, "joke delete") + assert.NotEqualf(t, 0, res.ContentLength, "joke delete") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke delete") + assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete") + }) +} diff --git a/api/app/v1/handler/joke/joke_get.go b/api/app/v1/handler/joke/joke_get.go new file mode 100644 index 0000000..47563ec --- /dev/null +++ b/api/app/v1/handler/joke/joke_get.go @@ -0,0 +1,151 @@ +package joke + +import ( + "context" + "io/ioutil" + "strconv" + "time" + + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "jokes-bapak2-api/app/v1/utils" + + "github.com/gofiber/fiber/v2" +) + +func TodayJoke(c *fiber.Ctx) error { + // check from handler.Redis if today's joke already exists + // send the joke if exists + // get a new joke if it's not, then send it. + var joke models.Today + err := handler.Redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke) + if err != nil { + return err + } + + eq, err := utils.IsToday(joke.Date) + if err != nil { + return err + } + + if eq { + c.Set("Content-Type", joke.ContentType) + return c.Status(fiber.StatusOK).Send([]byte(joke.Image)) + } else { + var link string + err := handler.Db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link) + if err != nil { + return err + } + + response, err := handler.Client.Get(link, nil) + if err != nil { + return err + } + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + now := time.Now().UTC().Format(time.RFC3339) + err = handler.Redis.MSet(context.Background(), map[string]interface{}{ + "today:link": link, + "today:date": now, + "today:image": string(data), + "today:contentType": response.Header.Get("content-type"), + }).Err() + if err != nil { + return err + } + + c.Set("Content-Type", response.Header.Get("content-type")) + return c.Status(fiber.StatusOK).Send(data) + } + +} + +func SingleJoke(c *fiber.Ctx) error { + checkCache, err := core.CheckJokesCache(handler.Memory) + if err != nil { + return err + } + + if !checkCache { + jokes, err := core.GetAllJSONJokes(handler.Db) + if err != nil { + return err + } + err = handler.Memory.Set("jokes", jokes) + if err != nil { + return err + } + } + + link, err := core.GetRandomJokeFromCache(handler.Memory) + if err != nil { + return err + } + + // Get image data + response, err := handler.Client.Get(link, nil) + if err != nil { + return err + } + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + c.Set("Content-Type", response.Header.Get("content-type")) + return c.Status(fiber.StatusOK).Send(data) + +} + +func JokeByID(c *fiber.Ctx) error { + checkCache, err := core.CheckJokesCache(handler.Memory) + if err != nil { + return err + } + + if !checkCache { + jokes, err := core.GetAllJSONJokes(handler.Db) + if err != nil { + return err + } + err = handler.Memory.Set("jokes", jokes) + if err != nil { + return err + } + } + + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return err + } + + link, err := core.GetCachedJokeByID(handler.Memory, id) + if err != nil { + return err + } + + if link == "" { + return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found.")) + } + + // Get image data + response, err := handler.Client.Get(link, nil) + if err != nil { + return err + } + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + c.Set("Content-Type", response.Header.Get("content-type")) + return c.Status(fiber.StatusOK).Send(data) +} diff --git a/api/app/v1/handler/joke/joke_get_test.go b/api/app/v1/handler/joke/joke_get_test.go new file mode 100644 index 0000000..18d2d1f --- /dev/null +++ b/api/app/v1/handler/joke/joke_get_test.go @@ -0,0 +1,94 @@ +package joke_test + +import ( + "context" + "io/ioutil" + "net/http" + "testing" + "time" + + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/platform/database" + + _ "github.com/joho/godotenv/autoload" + "github.com/stretchr/testify/assert" +) + +var db = database.New() +var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} + +func cleanup() { + _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") + if err != nil { + panic(err) + } + _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") + if err != nil { + panic(err) + } +} + +/// Need to find some workaround for this test +func TestJokeGet(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("TodayJoke - should return 200", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/today", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "today joke") + assert.Equalf(t, 200, res.StatusCode, "today joke") + assert.NotEqualf(t, 0, res.ContentLength, "today joke") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "today joke") + }) + + t.Run("SingleJoke - should return 200", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "single joke") + assert.Equalf(t, 200, res.StatusCode, "single joke") + assert.NotEqualf(t, 0, res.ContentLength, "single joke") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "single joke") + }) + + t.Run("JokeByID - should return 200", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/id/1", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke by id") + assert.Equalf(t, 200, res.StatusCode, "joke by id") + assert.NotEqualf(t, 0, res.ContentLength, "joke by id") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke by id") + }) + + t.Run("JokeByID - should return 404", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/id/300", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke by id") + assert.Equalf(t, 404, res.StatusCode, "joke by id") + assert.NotEqualf(t, 0, res.ContentLength, "joke by id") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke by id") + assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") + }) +} diff --git a/api/app/v1/handler/joke/joke_total.go b/api/app/v1/handler/joke/joke_total.go new file mode 100644 index 0000000..c6bb933 --- /dev/null +++ b/api/app/v1/handler/joke/joke_total.go @@ -0,0 +1,39 @@ +package joke + +import ( + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +func TotalJokes(c *fiber.Ctx) error { + checkTotal, err := core.CheckTotalJokesCache(handler.Memory) + if err != nil { + return err + } + + if !checkTotal { + err = core.SetTotalJoke(handler.Db, handler.Memory) + if err != nil { + return err + } + } + + total, err := handler.Memory.Get("total") + + if err != nil { + if err.Error() == "Entry not found" { + return c.Status(fiber.StatusInternalServerError).JSON(models.Error{ + Error: "no data found", + }) + } + return err + } + + return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ + Message: strconv.Itoa(int(total[0])), + }) +} diff --git a/api/app/v1/handler/joke/joke_total_test.go b/api/app/v1/handler/joke/joke_total_test.go new file mode 100644 index 0000000..10364bd --- /dev/null +++ b/api/app/v1/handler/joke/joke_total_test.go @@ -0,0 +1,46 @@ +package joke_test + +import ( + "context" + "io/ioutil" + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/handler" + "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 = handler.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 = handler.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") + // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM. + assert.Equalf(t, "{\"message\":\"1\"}", string(body), "joke total") + }) +} diff --git a/api/app/v1/handler/joke/joke_update.go b/api/app/v1/handler/joke/joke_update.go new file mode 100644 index 0000000..4ac89f9 --- /dev/null +++ b/api/app/v1/handler/joke/joke_update.go @@ -0,0 +1,75 @@ +package joke + +import ( + "context" + + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + + "github.com/Masterminds/squirrel" + "github.com/gofiber/fiber/v2" +) + +func UpdateJoke(c *fiber.Ctx) error { + id := c.Params("id") + // Check if the joke exists + sql, args, err := handler.Psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + if err != nil { + return err + } + + var jokeID string + err = handler.Db.QueryRow(context.Background(), sql, args...).Scan(&jokeID) + if err != nil && err != models.ErrNoRows { + return err + } + + if jokeID == id { + body := new(models.Joke) + err = c.BodyParser(&body) + if err != nil { + return err + } + + // Check link validity + valid, err := core.CheckImageValidity(handler.Client, body.Link) + if err != nil { + return err + } + + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "URL provided is not a valid image", + }) + } + + sql, args, err = handler.Psql.Update("jokesbapak2").Set("link", body.Link).Set("creator", c.Locals("userID")).ToSql() + if err != nil { + return err + } + + _, err = handler.Db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + + err = core.SetAllJSONJoke(handler.Db, handler.Memory) + if err != nil { + return err + } + err = core.SetTotalJoke(handler.Db, handler.Memory) + 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.Error{ + Error: "specified joke id does not exists", + }) +} diff --git a/api/app/v1/handler/joke/joke_update_test.go b/api/app/v1/handler/joke/joke_update_test.go new file mode 100644 index 0000000..2ec9076 --- /dev/null +++ b/api/app/v1/handler/joke/joke_update_test.go @@ -0,0 +1,62 @@ +package joke_test + +import ( + "context" + "io/ioutil" + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/platform/database" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestUpdateJoke(t *testing.T) { + t.SkipNow() + err := database.Setup() + if err != nil { + t.Fatal(err) + } + hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" + _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) + if err != nil { + t.Fatal(err) + } + _, err = handler.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("Update - should return 200", func(t *testing.T) { + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PATCH", "/id/1", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke update") + assert.Equalf(t, 200, res.StatusCode, "joke update") + assert.NotEqualf(t, 0, res.ContentLength, "joke update") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke update") + assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update") + }) + + t.Run("Update - id doesn't exists", func(t *testing.T) { + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PATCH", "/id/100", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke update") + assert.Equalf(t, 406, res.StatusCode, "joke update") + assert.NotEqualf(t, 0, res.ContentLength, "joke update") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke update") + assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update") + }) +} diff --git a/api/app/v1/routes/health.go b/api/app/v1/routes/health.go index 7c4dfdd..969cb88 100644 --- a/api/app/v1/routes/health.go +++ b/api/app/v1/routes/health.go @@ -1,14 +1,14 @@ package routes import ( - "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/handler/health" "github.com/gofiber/fiber/v2" ) func Health(app *fiber.App) *fiber.App { // Health check - app.Get("/health", handler.Health) + app.Get("/health", health.Health) return app } diff --git a/api/app/v1/routes/joke.go b/api/app/v1/routes/joke.go index 79a90c3..54cc5c7 100644 --- a/api/app/v1/routes/joke.go +++ b/api/app/v1/routes/joke.go @@ -1,7 +1,7 @@ package routes import ( - "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/handler/joke" "jokes-bapak2-api/app/v1/middleware" "github.com/gofiber/fiber/v2" @@ -9,25 +9,25 @@ import ( func Joke(app *fiber.App) *fiber.App { // Single route - app.Get("/", handler.SingleJoke) + app.Get("/", joke.SingleJoke) // Today's joke - app.Get("/today", handler.TodayJoke) + app.Get("/today", joke.TodayJoke) // Joke by ID - app.Get("/id/:id", middleware.OnlyIntegerAsID(), handler.JokeByID) + app.Get("/id/:id", middleware.OnlyIntegerAsID(), joke.JokeByID) // Count total jokes - app.Get("/total", handler.TotalJokes) + app.Get("/total", joke.TotalJokes) // Add new joke - app.Put("/", middleware.RequireAuth(), handler.AddNewJoke) + app.Put("/", middleware.RequireAuth(), joke.AddNewJoke) // Update a joke - app.Patch("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), handler.UpdateJoke) + app.Patch("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.UpdateJoke) // Delete a joke - app.Delete("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), handler.DeleteJoke) + app.Delete("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.DeleteJoke) return app } From 9d9311e90c95cb02d271de568127c7f5a81c2563 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 01:15:35 +0700 Subject: [PATCH 02/10] feat: generate random string for ID usage --- api/app/v1/utils/array.go | 2 +- api/app/v1/utils/array_test.go | 2 +- api/app/v1/utils/random.go | 21 +++++++++++++++++++++ api/app/v1/utils/random_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 api/app/v1/utils/random.go create mode 100644 api/app/v1/utils/random_test.go diff --git a/api/app/v1/utils/array.go b/api/app/v1/utils/array.go index 6b70eaa..ba953b1 100644 --- a/api/app/v1/utils/array.go +++ b/api/app/v1/utils/array.go @@ -8,4 +8,4 @@ func IsIn(arr []string, value string) bool { } } return false -} \ No newline at end of file +} diff --git a/api/app/v1/utils/array_test.go b/api/app/v1/utils/array_test.go index 3868a40..506932c 100644 --- a/api/app/v1/utils/array_test.go +++ b/api/app/v1/utils/array_test.go @@ -20,4 +20,4 @@ func TestIsIn(t *testing.T) { t.Error("check should be false: ", check) } }) -} \ No newline at end of file +} diff --git a/api/app/v1/utils/random.go b/api/app/v1/utils/random.go new file mode 100644 index 0000000..374870b --- /dev/null +++ b/api/app/v1/utils/random.go @@ -0,0 +1,21 @@ +package utils + +import ( + "crypto/rand" + "encoding/hex" +) + +// RandomString generates a random string with p bytes of length. +// Specifying 10 in the p parameter will result in the length of 20. +func RandomString(p int) (string, error) { + if p <= 0 { + p = 10 + } + arr := make([]byte, p) + _, err := rand.Read(arr) + if err != nil { + return "", err + } + + return hex.EncodeToString(arr), nil +} diff --git a/api/app/v1/utils/random_test.go b/api/app/v1/utils/random_test.go new file mode 100644 index 0000000..8e62792 --- /dev/null +++ b/api/app/v1/utils/random_test.go @@ -0,0 +1,27 @@ +package utils_test + +import ( + "jokes-bapak2-api/app/v1/utils" + "testing" +) + +func TestRandomString(t *testing.T) { + t.Run("should create a random string with param", func(t *testing.T) { + random, err := utils.RandomString(10) + if err != nil { + t.Error(err) + } + if len(random) != 20 { + t.Error("result is not within the length of 10") + } + }) + t.Run("should create a random string with invalid params", func(t *testing.T) { + random, err := utils.RandomString(10) + if err != nil { + t.Error(err) + } + if len(random) != 20 { + t.Error("result is not within the length of 10") + } + }) +} From c265da52a18b5a504ee3576da909a38297c6256e Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 09:51:22 +0700 Subject: [PATCH 03/10] feat: joke submission --- api/app/v1/core/submit_setter.go | 81 ++++++++++++ api/app/v1/core/submit_validation.go | 25 ++++ api/app/v1/handler/builder.go | 10 +- api/app/v1/handler/health.go | 30 ----- api/app/v1/handler/health/health_test.go | 14 ++ api/app/v1/handler/health_test.go | 43 ------- api/app/v1/handler/joke_add.go | 54 -------- api/app/v1/handler/joke_add_test.go | 61 --------- api/app/v1/handler/joke_delete.go | 59 --------- api/app/v1/handler/joke_delete_test.go | 63 --------- api/app/v1/handler/joke_get.go | 150 ---------------------- api/app/v1/handler/joke_get_test.go | 94 -------------- api/app/v1/handler/joke_total.go | 38 ------ api/app/v1/handler/joke_total_test.go | 45 ------- api/app/v1/handler/joke_update.go | 74 ----------- api/app/v1/handler/joke_update_test.go | 61 --------- api/app/v1/handler/submit/submit_add.go | 101 +++++++++++++++ api/app/v1/handler/submit/submit_get.go | 104 +++++++++++++++ api/app/v1/models/errors.go | 14 ++ api/app/v1/models/general.go | 8 ++ api/app/v1/models/{request.go => joke.go} | 12 +- api/app/v1/models/response.go | 10 -- api/app/v1/models/submit.go | 37 ++++++ api/app/v1/routes/submit.go | 17 +++ 24 files changed, 411 insertions(+), 794 deletions(-) create mode 100644 api/app/v1/core/submit_setter.go create mode 100644 api/app/v1/core/submit_validation.go delete mode 100644 api/app/v1/handler/health.go delete mode 100644 api/app/v1/handler/health_test.go delete mode 100644 api/app/v1/handler/joke_add.go delete mode 100644 api/app/v1/handler/joke_add_test.go delete mode 100644 api/app/v1/handler/joke_delete.go delete mode 100644 api/app/v1/handler/joke_delete_test.go delete mode 100644 api/app/v1/handler/joke_get.go delete mode 100644 api/app/v1/handler/joke_get_test.go delete mode 100644 api/app/v1/handler/joke_total.go delete mode 100644 api/app/v1/handler/joke_total_test.go delete mode 100644 api/app/v1/handler/joke_update.go delete mode 100644 api/app/v1/handler/joke_update_test.go create mode 100644 api/app/v1/handler/submit/submit_add.go create mode 100644 api/app/v1/handler/submit/submit_get.go create mode 100644 api/app/v1/models/errors.go create mode 100644 api/app/v1/models/general.go rename api/app/v1/models/{request.go => joke.go} (59%) delete mode 100644 api/app/v1/models/response.go create mode 100644 api/app/v1/models/submit.go create mode 100644 api/app/v1/routes/submit.go diff --git a/api/app/v1/core/submit_setter.go b/api/app/v1/core/submit_setter.go new file mode 100644 index 0000000..231c704 --- /dev/null +++ b/api/app/v1/core/submit_setter.go @@ -0,0 +1,81 @@ +package core + +import ( + "bytes" + "io" + "io/ioutil" + "jokes-bapak2-api/app/v1/models" + "jokes-bapak2-api/app/v1/utils" + "mime/multipart" + "net/http" + "net/url" + "os" + + "github.com/gojek/heimdall/v7/httpclient" + "github.com/pquerna/ffjson/ffjson" +) + +// UploadImage process the image from the user to be uploaded to the cloud storage. +// Returns the image URL. +func UploadImage(client *httpclient.Client, image io.Reader) (string, error) { + hostURL := os.Getenv("IMAGE_API_URL") + fileName, err := utils.RandomString(10) + if err != nil { + return "", err + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + fw, err := writer.CreateFormField("image") + if err != nil { + return "", err + } + + _, err = io.Copy(fw, image) + if err != nil { + return "", err + } + + err = writer.Close() + if err != nil { + return "", err + } + + headers := http.Header{ + "Content-Type": []string{writer.FormDataContentType()}, + "User-Agent": []string{"JokesBapak2 API"}, + "Accept": []string{"application/json"}, + } + + requestURL, err := url.Parse(hostURL) + if err != nil { + return "", err + } + + params := url.Values{} + params.Add("key", os.Getenv("IMAGE_API_KEY")) + params.Add("name", fileName) + + requestURL.RawQuery = params.Encode() + + res, err := client.Post(requestURL.String(), bytes.NewReader(body.Bytes()), headers) + if err != nil { + return "", err + } + + defer res.Body.Close() + + responseBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + var data models.ImageAPI + err = ffjson.Unmarshal(responseBody, &data) + if err != nil { + return "", err + } + + return data.Data.URL, nil +} diff --git a/api/app/v1/core/submit_validation.go b/api/app/v1/core/submit_validation.go new file mode 100644 index 0000000..a1e23af --- /dev/null +++ b/api/app/v1/core/submit_validation.go @@ -0,0 +1,25 @@ +package core + +import ( + "regexp" + "strings" +) + +func ValidateAuthor(author string) bool { + if len(author) > 200 { + return false + } + + split := strings.Split(author, " ") + if strings.HasPrefix(split[0], "<") && strings.HasSuffix(split[0], ">") { + return false + } + if !strings.HasPrefix(split[len(split)-1], "<") && !strings.HasSuffix(split[len(split)-1], ">") { + return false + } + + email := strings.Replace(split[len(split)-1], "<", "", 1) + email = strings.Replace(email, ">", "", 1) + pattern := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + return pattern.MatchString(email) +} diff --git a/api/app/v1/handler/builder.go b/api/app/v1/handler/builder.go index 59cb394..6121335 100644 --- a/api/app/v1/handler/builder.go +++ b/api/app/v1/handler/builder.go @@ -9,8 +9,8 @@ import ( "github.com/gojek/heimdall/v7/httpclient" ) -var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) -var db = database.New() -var redis = cache.New() -var memory = cache.InMemory() -var client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)) +var Psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) +var Db = database.New() +var Redis = cache.New() +var Memory = cache.InMemory() +var Client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)) diff --git a/api/app/v1/handler/health.go b/api/app/v1/handler/health.go deleted file mode 100644 index 8f1942f..0000000 --- a/api/app/v1/handler/health.go +++ /dev/null @@ -1,30 +0,0 @@ -package handler - -import ( - "context" - "jokes-bapak2-api/app/v1/models" - - "github.com/gofiber/fiber/v2" -) - -func Health(c *fiber.Ctx) error { - // Ping REDIS database - err := redis.Ping(context.Background()).Err() - if err != nil { - return c. - Status(fiber.StatusServiceUnavailable). - JSON(models.Error{ - Error: "REDIS: " + err.Error(), - }) - } - - _, err = db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") - if err != nil { - return c. - Status(fiber.StatusServiceUnavailable). - JSON(models.Error{ - Error: "POSTGRESQL: " + err.Error(), - }) - } - return c.SendStatus(fiber.StatusOK) -} diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go index d4e21dd..9e5f325 100644 --- a/api/app/v1/handler/health/health_test.go +++ b/api/app/v1/handler/health/health_test.go @@ -12,6 +12,20 @@ import ( "github.com/stretchr/testify/assert" ) +var db = database.New() +var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} + +func cleanup() { + _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") + if err != nil { + panic(err) + } + _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") + if err != nil { + panic(err) + } +} + func TestHealth(t *testing.T) { err := database.Setup() if err != nil { diff --git a/api/app/v1/handler/health_test.go b/api/app/v1/handler/health_test.go deleted file mode 100644 index f6cca84..0000000 --- a/api/app/v1/handler/health_test.go +++ /dev/null @@ -1,43 +0,0 @@ -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") - }) -} diff --git a/api/app/v1/handler/joke_add.go b/api/app/v1/handler/joke_add.go deleted file mode 100644 index 208ef53..0000000 --- a/api/app/v1/handler/joke_add.go +++ /dev/null @@ -1,54 +0,0 @@ -package handler - -import ( - "context" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/gofiber/fiber/v2" -) - -func AddNewJoke(c *fiber.Ctx) error { - var body models.Joke - err := c.BodyParser(&body) - if err != nil { - return err - } - - // Check link validity - valid, err := core.CheckImageValidity(client, body.Link) - if err != nil { - return err - } - - if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) - } - - sql, args, err := psql.Insert("jokesbapak2").Columns("link", "creator").Values(body.Link, c.Locals("userID")).ToSql() - if err != nil { - return err - } - - // TODO: Implement solution if the link provided already exists. - _, err = db.Query(context.Background(), sql, args...) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - - return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{ - Link: body.Link, - }) -} diff --git a/api/app/v1/handler/joke_add_test.go b/api/app/v1/handler/joke_add_test.go deleted file mode 100644 index 760ab38..0000000 --- a/api/app/v1/handler/joke_add_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestAddNewJoke(t *testing.T) { - // t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(cleanup) - - app := v1.New() - - t.Run("Add - should return 201", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 201, res.StatusCode, "joke add") - assert.NotEqualf(t, 0, res.ContentLength, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add") - }) - - t.Run("Add - should not be a valid image", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 400, res.StatusCode, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") - }) - -} diff --git a/api/app/v1/handler/joke_delete.go b/api/app/v1/handler/joke_delete.go deleted file mode 100644 index e7e2d74..0000000 --- a/api/app/v1/handler/joke_delete.go +++ /dev/null @@ -1,59 +0,0 @@ -package handler - -import ( - "context" - "strconv" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/Masterminds/squirrel" - "github.com/gofiber/fiber/v2" -) - -func DeleteJoke(c *fiber.Ctx) error { - id, err := strconv.Atoi(c.Params("id")) - if err != nil { - return err - } - - // 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 int - 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 - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - 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.Error{ - Error: "specified joke id does not exists", - }) -} diff --git a/api/app/v1/handler/joke_delete_test.go b/api/app/v1/handler/joke_delete_test.go deleted file mode 100644 index 5e4d9ec..0000000 --- a/api/app/v1/handler/joke_delete_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestDeleteJoke(t *testing.T) { - // TODO: Remove this line below, make this test works - t.SkipNow() - - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, 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("Delete - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 200, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") - }) - - t.Run("Delete - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 406, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete") - }) -} diff --git a/api/app/v1/handler/joke_get.go b/api/app/v1/handler/joke_get.go deleted file mode 100644 index 463b028..0000000 --- a/api/app/v1/handler/joke_get.go +++ /dev/null @@ -1,150 +0,0 @@ -package handler - -import ( - "context" - "io/ioutil" - "strconv" - "time" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - "jokes-bapak2-api/app/v1/utils" - - "github.com/gofiber/fiber/v2" -) - -func TodayJoke(c *fiber.Ctx) error { - // check from redis if today's joke already exists - // send the joke if exists - // get a new joke if it's not, then send it. - var joke models.Today - err := redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke) - if err != nil { - return err - } - - eq, err := utils.IsToday(joke.Date) - if err != nil { - return err - } - - if eq { - c.Set("Content-Type", joke.ContentType) - return c.Status(fiber.StatusOK).Send([]byte(joke.Image)) - } else { - var link string - err := db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link) - if err != nil { - return err - } - - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - now := time.Now().UTC().Format(time.RFC3339) - err = redis.MSet(context.Background(), map[string]interface{}{ - "today:link": link, - "today:date": now, - "today:image": string(data), - "today:contentType": response.Header.Get("content-type"), - }).Err() - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) - } - -} - -func SingleJoke(c *fiber.Ctx) error { - checkCache, err := core.CheckJokesCache(memory) - if err != nil { - return err - } - - if !checkCache { - jokes, err := core.GetAllJSONJokes(db) - if err != nil { - return err - } - err = memory.Set("jokes", jokes) - if err != nil { - return err - } - } - - link, err := core.GetRandomJokeFromCache(memory) - if err != nil { - return err - } - - // Get image data - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) - -} - -func JokeByID(c *fiber.Ctx) error { - checkCache, err := core.CheckJokesCache(memory) - if err != nil { - return err - } - - if !checkCache { - jokes, err := core.GetAllJSONJokes(db) - if err != nil { - return err - } - err = memory.Set("jokes", jokes) - if err != nil { - return err - } - } - - id, err := strconv.Atoi(c.Params("id")) - if err != nil { - return err - } - - link, err := core.GetCachedJokeByID(memory, id) - if err != nil { - return err - } - - if link == "" { - return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found.")) - } - - // Get image data - response, err := client.Get(link, nil) - if err != nil { - return err - } - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - c.Set("Content-Type", response.Header.Get("content-type")) - return c.Status(fiber.StatusOK).Send(data) -} diff --git a/api/app/v1/handler/joke_get_test.go b/api/app/v1/handler/joke_get_test.go deleted file mode 100644 index 0f491b6..0000000 --- a/api/app/v1/handler/joke_get_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - "net/http" - "testing" - "time" - - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - - _ "github.com/joho/godotenv/autoload" - "github.com/stretchr/testify/assert" -) - -var db = database.New() -var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} - -func cleanup() { - _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") - if err != nil { - panic(err) - } - _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") - if err != nil { - panic(err) - } -} - -/// Need to find some workaround for this test -func TestJokeGet(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("TodayJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/today", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "today joke") - assert.Equalf(t, 200, res.StatusCode, "today joke") - assert.NotEqualf(t, 0, res.ContentLength, "today joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "today joke") - }) - - t.Run("SingleJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "single joke") - assert.Equalf(t, 200, res.StatusCode, "single joke") - assert.NotEqualf(t, 0, res.ContentLength, "single joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "single joke") - }) - - t.Run("JokeByID - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/1", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 200, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - }) - - t.Run("JokeByID - should return 404", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/300", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 404, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") - }) -} diff --git a/api/app/v1/handler/joke_total.go b/api/app/v1/handler/joke_total.go deleted file mode 100644 index 9e473d7..0000000 --- a/api/app/v1/handler/joke_total.go +++ /dev/null @@ -1,38 +0,0 @@ -package handler - -import ( - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - "strconv" - - "github.com/gofiber/fiber/v2" -) - -func TotalJokes(c *fiber.Ctx) error { - checkTotal, err := core.CheckTotalJokesCache(memory) - if err != nil { - return err - } - - if !checkTotal { - err = core.SetTotalJoke(db, memory) - if err != nil { - return err - } - } - - total, err := memory.Get("total") - - if err != nil { - if err.Error() == "Entry not found" { - return c.Status(fiber.StatusInternalServerError).JSON(models.Error{ - Error: "no data found", - }) - } - return err - } - - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: strconv.Itoa(int(total[0])), - }) -} diff --git a/api/app/v1/handler/joke_total_test.go b/api/app/v1/handler/joke_total_test.go deleted file mode 100644 index 3b4744a..0000000 --- a/api/app/v1/handler/joke_total_test.go +++ /dev/null @@ -1,45 +0,0 @@ -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") - // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM. - assert.Equalf(t, "{\"message\":\"1\"}", string(body), "joke total") - }) -} diff --git a/api/app/v1/handler/joke_update.go b/api/app/v1/handler/joke_update.go deleted file mode 100644 index 00e3853..0000000 --- a/api/app/v1/handler/joke_update.go +++ /dev/null @@ -1,74 +0,0 @@ -package handler - -import ( - "context" - - "jokes-bapak2-api/app/v1/core" - "jokes-bapak2-api/app/v1/models" - - "github.com/Masterminds/squirrel" - "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 && err != models.ErrNoRows { - return err - } - - if jokeID == id { - body := new(models.Joke) - err = c.BodyParser(&body) - if err != nil { - return err - } - - // Check link validity - valid, err := core.CheckImageValidity(client, body.Link) - if err != nil { - return err - } - - if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) - } - - sql, args, err = psql.Update("jokesbapak2").Set("link", body.Link).Set("creator", c.Locals("userID")).ToSql() - if err != nil { - return err - } - - _, err = db.Query(context.Background(), sql, args...) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(db, memory) - if err != nil { - return err - } - err = core.SetTotalJoke(db, memory) - 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.Error{ - Error: "specified joke id does not exists", - }) -} diff --git a/api/app/v1/handler/joke_update_test.go b/api/app/v1/handler/joke_update_test.go deleted file mode 100644 index 85eade7..0000000 --- a/api/app/v1/handler/joke_update_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package handler_test - -import ( - "context" - "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/platform/database" - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestUpdateJoke(t *testing.T) { - t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, 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("Update - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 200, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update") - }) - - t.Run("Update - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 406, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update") - }) -} diff --git a/api/app/v1/handler/submit/submit_add.go b/api/app/v1/handler/submit/submit_add.go new file mode 100644 index 0000000..ae3d267 --- /dev/null +++ b/api/app/v1/handler/submit/submit_add.go @@ -0,0 +1,101 @@ +package submit + +import ( + "context" + "jokes-bapak2-api/app/v1/core" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "strings" + "time" + + "github.com/georgysavva/scany/pgxscan" + "github.com/gofiber/fiber/v2" +) + +func SubmitJoke(c *fiber.Ctx) error { + var body models.Submission + err := c.BodyParser(&body) + if err != nil { + return err + } + + // Image and/or Link should not be empty + if body.Image == "" && body.Link == "" { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "a link or an image should be supplied in a form of multipart/form-data", + }) + } + + // Author should be supplied + if body.Author == "" { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "an author key consisting on the format \"yourname \" must be supplied", + }) + } else { + // Validate format + valid := core.ValidateAuthor(body.Author) + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "please stick to the format of \"yourname \" and within 200 characters", + }) + } + } + + var url string + + // Check link validity if link was provided + if body.Link != "" { + valid, err := core.CheckImageValidity(handler.Client, body.Link) + if err != nil { + return err + } + if !valid { + return c.Status(fiber.StatusBadRequest).JSON(models.Error{ + Error: "URL provided is not a valid image", + }) + } + + url = body.Link + } + + // If image was provided + if body.Image != "" { + image := strings.NewReader(body.Image) + + url, err = core.UploadImage(handler.Client, image) + if err != nil { + return err + } + } + + now := time.Now().UTC().Format(time.RFC3339) + + sql, args, err := handler.Psql. + Insert("submission"). + Columns("link", "created_at", "author"). + Values(url, now, body.Author). + Suffix("RETURNING id,created_at,link,author,status"). + ToSql() + if err != nil { + return err + } + + var submission []models.Submission + result, err := handler.Db.Query(context.Background(), sql, args...) + if err != nil { + return err + } + defer result.Close() + + err = pgxscan.ScanAll(&submission, result) + if err != nil { + return err + } + + return c. + Status(fiber.StatusOK). + JSON(models.ResponseSubmission{ + Message: "Joke submitted. Please wait for a few days for admin to approve your submission.", + Data: submission[0], + }) +} diff --git a/api/app/v1/handler/submit/submit_get.go b/api/app/v1/handler/submit/submit_get.go new file mode 100644 index 0000000..f3a3fa7 --- /dev/null +++ b/api/app/v1/handler/submit/submit_get.go @@ -0,0 +1,104 @@ +package submit + +import ( + "bytes" + "context" + "jokes-bapak2-api/app/v1/handler" + "jokes-bapak2-api/app/v1/models" + "log" + "strconv" + + "github.com/aldy505/bob" + "github.com/georgysavva/scany/pgxscan" + "github.com/gofiber/fiber/v2" +) + +func GetSubmission(c *fiber.Ctx) error { + query := new(models.SubmissionQuery) + err := c.QueryParser(query) + if err != nil { + return err + } + + var limit int + var offset int + var approved bool + + if query.Limit != "" { + limit, err = strconv.Atoi(query.Limit) + if err != nil { + return err + } + } + if query.Page != "" { + page, err := strconv.Atoi(query.Page) + if err != nil { + return err + } + offset = (page - 1) * 20 + } + + if query.Approved != "" { + approved, err = strconv.ParseBool(query.Approved) + if err != nil { + return err + } + } + + var status int + + if approved { + status = 1 + } else { + status = 0 + } + + var sql string + var args []interface{} + + var sqlQuery *bytes.Buffer = &bytes.Buffer{} + sqlQuery.WriteString("SELECT * FROM submission WHERE TRUE") + + if query.Author != "" { + sqlQuery.WriteString(" AND author = ?") + args = append(args, query.Author) + } + + if query.Approved != "" { + sqlQuery.WriteString(" AND status = ?") + args = append(args, status) + } + + if limit > 0 { + sqlQuery.WriteString(" LIMIT " + strconv.Itoa(limit)) + } else { + sqlQuery.WriteString(" LIMIT 20") + } + + if query.Page != "" { + sqlQuery.WriteString(" OFFSET " + strconv.Itoa(offset)) + } + + sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar) + + var submissions []models.Submission + results, err := handler.Db.Query(context.Background(), sql, args...) + if err != nil { + log.Println(err) + return err + } + + defer results.Close() + + err = pgxscan.ScanAll(&submissions, results) + if err != nil { + return err + } + + return c. + Status(fiber.StatusOK). + JSON(fiber.Map{ + "count": len(submissions), + "jokes": submissions, + }) +} diff --git a/api/app/v1/models/errors.go b/api/app/v1/models/errors.go new file mode 100644 index 0000000..f7060e0 --- /dev/null +++ b/api/app/v1/models/errors.go @@ -0,0 +1,14 @@ +package models + +import "errors" + +var ErrNoRows = errors.New("no rows in result set") +var ErrConnDone = errors.New("connection is already closed") +var ErrTxDone = errors.New("transaction has already been committed or rolled back") + +var ErrNotFound = errors.New("record not found") +var ErrEmpty = errors.New("record is empty") + +type Error struct { + Error string `json:"error"` +} diff --git a/api/app/v1/models/general.go b/api/app/v1/models/general.go new file mode 100644 index 0000000..11eae1e --- /dev/null +++ b/api/app/v1/models/general.go @@ -0,0 +1,8 @@ +package models + +type Auth struct { + ID int `json:"id" form:"id" db:"id"` + Key string `json:"key" form:"key" db:"key"` + Token string `json:"token" form:"token" db:"token"` + LastUsed string `json:"last_used" form:"last_used" db:"last_used"` +} diff --git a/api/app/v1/models/request.go b/api/app/v1/models/joke.go similarity index 59% rename from api/app/v1/models/request.go rename to api/app/v1/models/joke.go index 6b24ac0..f2b66c4 100644 --- a/api/app/v1/models/request.go +++ b/api/app/v1/models/joke.go @@ -6,15 +6,13 @@ type Joke struct { Creator int `json:"creator" form:"creator" db:"creator"` } -type Auth struct { - ID int `json:"id" form:"id" db:"id"` - Key string `json:"key" form:"key" db:"key"` - Token string `json:"token" form:"token" db:"token"` - LastUsed string `json:"last_used" form:"last_used" db:"last_used"` -} - type Today struct { Date string `redis:"today:date"` Image string `redis:"today:image"` ContentType string `redis:"today:contentType"` } + +type ResponseJoke struct { + Link string `json:"link,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/api/app/v1/models/response.go b/api/app/v1/models/response.go deleted file mode 100644 index 590536c..0000000 --- a/api/app/v1/models/response.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -type Error struct { - Error string `json:"error"` -} - -type ResponseJoke struct { - Link string `json:"link,omitempty"` - Message string `json:"message,omitempty"` -} diff --git a/api/app/v1/models/submit.go b/api/app/v1/models/submit.go new file mode 100644 index 0000000..ded4eef --- /dev/null +++ b/api/app/v1/models/submit.go @@ -0,0 +1,37 @@ +package models + +type Submission struct { + ID int `json:"id,omitempty" db:"id"` + Link string `json:"link" form:"link" db:"link"` + Image string `json:"image,omitempty" form:"image"` + CreatedAt string `json:"created_at" db:"created_at"` + Author string `json:"author" form:"author" db:"author"` + Status int `json:"status" db:"status"` +} + +type SubmissionQuery struct { + Author string `query:"author"` + Limit string `query:"limit"` + Page string `query:"page"` + Approved string `query:"approved"` +} + +type ResponseSubmission struct { + ID string `json:"id,omitempty"` + Message string `json:"message,omitempty"` + Data Submission `json:"data,omitempty"` +} + +type ImageAPI struct { + Data ImageAPIData `json:"data"` + Success bool `json:"success"` + Status int `json:"status"` +} + +type ImageAPIData struct { + ID string `json:"id"` + Title string `json:"title"` + URLViewer string `json:"url_viewer"` + URL string `json:"url"` + DisplayURL string `json:"display_url"` +} diff --git a/api/app/v1/routes/submit.go b/api/app/v1/routes/submit.go new file mode 100644 index 0000000..6084980 --- /dev/null +++ b/api/app/v1/routes/submit.go @@ -0,0 +1,17 @@ +package routes + +import ( + "jokes-bapak2-api/app/v1/handler/submit" + + "github.com/gofiber/fiber/v2" +) + +func Submit(app *fiber.App) *fiber.App { + // Get pending submitted joke + app.Get("/submit", submit.GetSubmission) + + // Add a joke + app.Post("/submit", submit.SubmitJoke) + + return app +} From 8f570f99dbd679cbc07bbbb47cc33a0818051b2a Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 12:56:14 +0700 Subject: [PATCH 04/10] test: refactor and add submit test --- api/app/v1/app.go | 6 +- api/app/v1/core/joke_getter.go | 2 + api/app/v1/core/joke_validation.go | 4 +- api/app/v1/handler/health/health_test.go | 63 +++++--- api/app/v1/handler/joke/joke_add.go | 4 +- api/app/v1/handler/joke/joke_add_test.go | 81 +++++----- api/app/v1/handler/joke/joke_delete.go | 4 +- api/app/v1/handler/joke/joke_delete_test.go | 78 ++++----- api/app/v1/handler/joke/joke_get_test.go | 158 ++++++++++++------- api/app/v1/handler/joke/joke_total_test.go | 38 ++--- api/app/v1/handler/joke/joke_update.go | 4 +- api/app/v1/handler/joke/joke_update_test.go | 76 ++++----- api/app/v1/handler/submit/submit_get_test.go | 79 ++++++++++ api/app/v1/models/sql.go | 10 -- api/app/v1/platform/database/create.go | 35 +++- api/app/v1/utils/array_test.go | 27 ++-- api/app/v1/utils/date_test.go | 68 ++++---- api/app/v1/utils/parse_test.go | 56 +++---- api/app/v1/utils/random_test.go | 37 +++-- api/app/v1/utils/request_test.go | 30 ++-- api/go.mod | 6 + api/go.sum | 19 ++- api/main.go | 4 +- 23 files changed, 511 insertions(+), 378 deletions(-) create mode 100644 api/app/v1/handler/submit/submit_get_test.go delete mode 100644 api/app/v1/models/sql.go diff --git a/api/app/v1/app.go b/api/app/v1/app.go index 328e107..bccbe98 100644 --- a/api/app/v1/app.go +++ b/api/app/v1/app.go @@ -26,8 +26,9 @@ func New() *fiber.App { }) err := sentry.Init(sentry.ClientOptions{ - Dsn: os.Getenv("SENTRY_DSN"), - Environment: os.Getenv("ENV"), + Dsn: os.Getenv("SENTRY_DSN"), + Environment: os.Getenv("ENV"), + AttachStacktrace: true, // Enable printing of SDK debug messages. // Useful when getting started or trying to figure something out. Debug: true, @@ -58,6 +59,7 @@ func New() *fiber.App { routes.Health(app) routes.Joke(app) + routes.Submit(app) return app } diff --git a/api/app/v1/core/joke_getter.go b/api/app/v1/core/joke_getter.go index 9d55b37..6297293 100644 --- a/api/app/v1/core/joke_getter.go +++ b/api/app/v1/core/joke_getter.go @@ -20,6 +20,8 @@ func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) { return nil, err } + defer results.Close() + err = pgxscan.ScanAll(&jokes, results) if err != nil { return nil, err diff --git a/api/app/v1/core/joke_validation.go b/api/app/v1/core/joke_validation.go index 95275c8..9d47236 100644 --- a/api/app/v1/core/joke_validation.go +++ b/api/app/v1/core/joke_validation.go @@ -23,8 +23,8 @@ func CheckImageValidity(client *httpclient.Client, link string) (bool, error) { if res.StatusCode == 200 && utils.IsIn(ValidContentType, res.Header.Get("content-type")) { return true, nil } - + return false, nil } return false, errors.New("URL must use HTTPS protocol") -} \ No newline at end of file +} diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go index 9e5f325..9cd990a 100644 --- a/api/app/v1/handler/health/health_test.go +++ b/api/app/v1/handler/health/health_test.go @@ -9,49 +9,64 @@ import ( "testing" "time" + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v4/pgxpool" + _ "github.com/joho/godotenv/autoload" "github.com/stretchr/testify/assert" ) -var db = database.New() +var db *pgxpool.Pool = database.New() var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} +var app *fiber.App = v1.New() func cleanup() { - _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") + j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") if err != nil { panic(err) } - _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") + a, err := db.Query(context.Background(), "DROP TABLE \"administrators\"") if err != nil { panic(err) } + + defer j.Close() + defer a.Close() +} + +func setup() error { + err := database.Setup() + if err != nil { + return err + } + a, 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 { + return err + } + j, 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 { + return err + } + + defer a.Close() + defer j.Close() + + return nil } 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...) + err := setup() if err != nil { t.Fatal(err) } - t.Cleanup(cleanup) + defer cleanup() - app := v1.New() + req, _ := http.NewRequest("GET", "/health", nil) + res, err := app.Test(req, -1) - 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") - }) + 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") } diff --git a/api/app/v1/handler/joke/joke_add.go b/api/app/v1/handler/joke/joke_add.go index b72bf58..c569357 100644 --- a/api/app/v1/handler/joke/joke_add.go +++ b/api/app/v1/handler/joke/joke_add.go @@ -35,11 +35,13 @@ func AddNewJoke(c *fiber.Ctx) error { } // TODO: Implement solution if the link provided already exists. - _, err = handler.Db.Query(context.Background(), sql, args...) + r, err := handler.Db.Query(context.Background(), sql, args...) if err != nil { return err } + defer r.Close() + err = core.SetAllJSONJoke(handler.Db, handler.Memory) if err != nil { return err diff --git a/api/app/v1/handler/joke/joke_add_test.go b/api/app/v1/handler/joke/joke_add_test.go index 48a6cb4..4e39332 100644 --- a/api/app/v1/handler/joke/joke_add_test.go +++ b/api/app/v1/handler/joke/joke_add_test.go @@ -1,62 +1,55 @@ package joke_test import ( - "context" "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/handler" - "jokes-bapak2-api/app/v1/platform/database" "net/http" "strings" "testing" - "time" "github.com/stretchr/testify/assert" ) -func TestAddNewJoke(t *testing.T) { - // t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) +func TestAddNewJoke_201(t *testing.T) { + t.SkipNow() + err := setup() if err != nil { t.Fatal(err) } - t.Cleanup(cleanup) + defer cleanup() - app := v1.New() - - t.Run("Add - should return 201", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 201, res.StatusCode, "joke add") - assert.NotEqualf(t, 0, res.ContentLength, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add") - }) - - t.Run("Add - should not be a valid image", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PUT", "/", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 400, res.StatusCode, "joke add") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") - }) + reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PUT", "/", reqBody) + req.Header.Set("content-type", "application/json") + req.Header.Add("accept", "application/json") + res, err := app.Test(req, -1) + assert.Equalf(t, false, err != nil, "joke add") + assert.Equalf(t, 201, res.StatusCode, "joke add") + assert.NotEqualf(t, 0, res.ContentLength, "joke add") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke add") + assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\"}", string(body), "joke add") +} + +func TestAddNewJoke_NotValidImage(t *testing.T) { + t.SkipNow() + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PUT", "/", reqBody) + req.Header.Set("content-type", "application/json") + req.Header.Add("accept", "application/json") + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke add") + assert.Equalf(t, 400, res.StatusCode, "joke add") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke add") + assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") } diff --git a/api/app/v1/handler/joke/joke_delete.go b/api/app/v1/handler/joke/joke_delete.go index 96a0a2b..4b8e63d 100644 --- a/api/app/v1/handler/joke/joke_delete.go +++ b/api/app/v1/handler/joke/joke_delete.go @@ -36,11 +36,13 @@ func DeleteJoke(c *fiber.Ctx) error { return err } - _, err = handler.Db.Query(context.Background(), sql, args...) + r, err := handler.Db.Query(context.Background(), sql, args...) if err != nil { return err } + defer r.Close() + err = core.SetAllJSONJoke(handler.Db, handler.Memory) if err != nil { return err diff --git a/api/app/v1/handler/joke/joke_delete_test.go b/api/app/v1/handler/joke/joke_delete_test.go index d8ffad3..ff81e02 100644 --- a/api/app/v1/handler/joke/joke_delete_test.go +++ b/api/app/v1/handler/joke/joke_delete_test.go @@ -1,64 +1,52 @@ package joke_test import ( - "context" "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/handler" - "jokes-bapak2-api/app/v1/platform/database" "net/http" "strings" "testing" - "time" "github.com/stretchr/testify/assert" ) -func TestDeleteJoke(t *testing.T) { +func TestDeleteJoke_200(t *testing.T) { // TODO: Remove this line below, make this test works t.SkipNow() - - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = handler.Db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) + err := setup() if err != nil { t.Fatal(err) } - t.Cleanup(cleanup) + defer cleanup() + + reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("DELETE", "/id/1", reqBody) + res, err := app.Test(req, -1) - app := v1.New() - - t.Run("Delete - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 200, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") - }) - - t.Run("Delete - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke delete") - assert.Equalf(t, 406, res.StatusCode, "joke delete") - assert.NotEqualf(t, 0, res.ContentLength, "joke delete") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke delete") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete") - }) + assert.Equalf(t, false, err != nil, "joke delete") + assert.Equalf(t, 200, res.StatusCode, "joke delete") + assert.NotEqualf(t, 0, res.ContentLength, "joke delete") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke delete") + assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") +} +func TestDeleteJoke_NotExists(t *testing.T) { + t.SkipNow() + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("DELETE", "/id/100", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke delete") + assert.Equalf(t, 406, res.StatusCode, "joke delete") + assert.NotEqualf(t, 0, res.ContentLength, "joke delete") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke delete") + assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke delete") } diff --git a/api/app/v1/handler/joke/joke_get_test.go b/api/app/v1/handler/joke/joke_get_test.go index 18d2d1f..53933ac 100644 --- a/api/app/v1/handler/joke/joke_get_test.go +++ b/api/app/v1/handler/joke/joke_get_test.go @@ -10,85 +10,125 @@ import ( v1 "jokes-bapak2-api/app/v1" "jokes-bapak2-api/app/v1/platform/database" + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v4/pgxpool" _ "github.com/joho/godotenv/autoload" "github.com/stretchr/testify/assert" ) -var db = database.New() +var db *pgxpool.Pool = database.New() var jokesData = []interface{}{1, "https://via.placeholder.com/300/06f/fff.png", 1, 2, "https://via.placeholder.com/300/07f/fff.png", 1, 3, "https://via.placeholder.com/300/08f/fff.png", 1} +var app *fiber.App = v1.New() func cleanup() { - _, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") + j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"") if err != nil { panic(err) } - _, err = db.Query(context.Background(), "DROP TABLE \"administrators\"") + a, err := db.Query(context.Background(), "DROP TABLE \"administrators\"") if err != nil { panic(err) } + + defer j.Close() + defer a.Close() +} + +func setup() error { + err := database.Setup() + if err != nil { + return err + } + + hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" + var args []interface{} = []interface{}{1, "very secure", hashedToken, time.Now().Format(time.RFC3339)} + a, err := db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", args...) + if err != nil { + return err + } + + defer a.Close() + + j, 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 { + return err + } + + defer j.Close() + + return nil } /// Need to find some workaround for this test -func TestJokeGet(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...) +func TestTodayJoke(t *testing.T) { + err := setup() if err != nil { t.Fatal(err) } - t.Cleanup(cleanup) + defer cleanup() - app := v1.New() + req, _ := http.NewRequest("GET", "/today", nil) + res, err := app.Test(req, -1) - t.Run("TodayJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/today", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "today joke") - assert.Equalf(t, 200, res.StatusCode, "today joke") - assert.NotEqualf(t, 0, res.ContentLength, "today joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "today joke") - }) - - t.Run("SingleJoke - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "single joke") - assert.Equalf(t, 200, res.StatusCode, "single joke") - assert.NotEqualf(t, 0, res.ContentLength, "single joke") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "single joke") - }) - - t.Run("JokeByID - should return 200", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/1", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 200, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - _, err = ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - }) - - t.Run("JokeByID - should return 404", func(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/300", nil) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke by id") - assert.Equalf(t, 404, res.StatusCode, "joke by id") - assert.NotEqualf(t, 0, res.ContentLength, "joke by id") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke by id") - assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") - }) + assert.Equalf(t, false, err != nil, "today joke") + assert.Equalf(t, 200, res.StatusCode, "today joke") + assert.NotEqualf(t, 0, res.ContentLength, "today joke") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "today joke") +} + +func TestSingleJoke(t *testing.T) { + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + req, _ := http.NewRequest("GET", "/", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "single joke") + assert.Equalf(t, 200, res.StatusCode, "single joke") + assert.NotEqualf(t, 0, res.ContentLength, "single joke") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "single joke") +} + +func TestJokeByID_200(t *testing.T) { + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + req, _ := http.NewRequest("GET", "/id/1", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke by id") + assert.Equalf(t, 200, res.StatusCode, "joke by id") + assert.NotEqualf(t, 0, res.ContentLength, "joke by id") + _, err = ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke by id") +} + +func TestJokeByID_404(t *testing.T) { + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + req, _ := http.NewRequest("GET", "/id/300", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke by id") + assert.Equalf(t, 404, res.StatusCode, "joke by id") + assert.NotEqualf(t, 0, res.ContentLength, "joke by id") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke by id") + assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") } diff --git a/api/app/v1/handler/joke/joke_total_test.go b/api/app/v1/handler/joke/joke_total_test.go index 10364bd..ea907c9 100644 --- a/api/app/v1/handler/joke/joke_total_test.go +++ b/api/app/v1/handler/joke/joke_total_test.go @@ -1,46 +1,30 @@ package joke_test import ( - "context" "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/handler" - "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 = handler.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 = handler.Db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) + err := setup() if err != nil { t.Fatal(err) } - t.Cleanup(cleanup) + defer cleanup() - app := v1.New() + req, _ := http.NewRequest("GET", "/total", nil) + res, err := app.Test(req, -1) - 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") + // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM. + assert.Equalf(t, "{\"message\":\"3\"}", string(body), "joke total") - 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") - // FIXME: This should be "message": "3", not one. I don't know what's wrong as it's 1 AM. - assert.Equalf(t, "{\"message\":\"1\"}", string(body), "joke total") - }) } diff --git a/api/app/v1/handler/joke/joke_update.go b/api/app/v1/handler/joke/joke_update.go index 4ac89f9..36bef5d 100644 --- a/api/app/v1/handler/joke/joke_update.go +++ b/api/app/v1/handler/joke/joke_update.go @@ -49,11 +49,13 @@ func UpdateJoke(c *fiber.Ctx) error { return err } - _, err = handler.Db.Query(context.Background(), sql, args...) + r, err := handler.Db.Query(context.Background(), sql, args...) if err != nil { return err } + defer r.Close() + err = core.SetAllJSONJoke(handler.Db, handler.Memory) if err != nil { return err diff --git a/api/app/v1/handler/joke/joke_update_test.go b/api/app/v1/handler/joke/joke_update_test.go index 2ec9076..52a0019 100644 --- a/api/app/v1/handler/joke/joke_update_test.go +++ b/api/app/v1/handler/joke/joke_update_test.go @@ -1,62 +1,50 @@ package joke_test import ( - "context" "io/ioutil" - v1 "jokes-bapak2-api/app/v1" - "jokes-bapak2-api/app/v1/handler" - "jokes-bapak2-api/app/v1/platform/database" "net/http" "strings" "testing" - "time" "github.com/stretchr/testify/assert" ) -func TestUpdateJoke(t *testing.T) { +func TestUpdateJoke_200(t *testing.T) { t.SkipNow() - err := database.Setup() - if err != nil { - t.Fatal(err) - } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - _, err = handler.Db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", 1, "very secure", hashedToken, time.Now().Format(time.RFC3339)) - if err != nil { - t.Fatal(err) - } - _, err = handler.Db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) + err := setup() if err != nil { t.Fatal(err) } + defer cleanup() - t.Cleanup(cleanup) + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PATCH", "/id/1", reqBody) + res, err := app.Test(req, -1) - app := v1.New() - - t.Run("Update - should return 200", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/1", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 200, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update") - }) - - t.Run("Update - id doesn't exists", func(t *testing.T) { - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("PATCH", "/id/100", reqBody) - res, err := app.Test(req, -1) - - assert.Equalf(t, false, err != nil, "joke update") - assert.Equalf(t, 406, res.StatusCode, "joke update") - assert.NotEqualf(t, 0, res.ContentLength, "joke update") - body, err := ioutil.ReadAll(res.Body) - assert.Nilf(t, err, "joke update") - assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update") - }) + assert.Equalf(t, false, err != nil, "joke update") + assert.Equalf(t, 200, res.StatusCode, "joke update") + assert.NotEqualf(t, 0, res.ContentLength, "joke update") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke update") + assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke update") +} + +func TestUpdateJoke_NotExists(t *testing.T) { + t.SkipNow() + err := setup() + if err != nil { + t.Fatal(err) + } + defer cleanup() + + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + req, _ := http.NewRequest("PATCH", "/id/100", reqBody) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "joke update") + assert.Equalf(t, 406, res.StatusCode, "joke update") + assert.NotEqualf(t, 0, res.ContentLength, "joke update") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "joke update") + assert.Equalf(t, "{\"message\":\"specified joke id does not exists\"}", string(body), "joke update") } diff --git a/api/app/v1/handler/submit/submit_get_test.go b/api/app/v1/handler/submit/submit_get_test.go new file mode 100644 index 0000000..3a9dd18 --- /dev/null +++ b/api/app/v1/handler/submit/submit_get_test.go @@ -0,0 +1,79 @@ +package submit_test + +import ( + "context" + "io/ioutil" + v1 "jokes-bapak2-api/app/v1" + "jokes-bapak2-api/app/v1/platform/database" + "net/http" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v4/pgxpool" + _ "github.com/joho/godotenv/autoload" + + "github.com/stretchr/testify/assert" +) + +var db *pgxpool.Pool = database.New() +var submissionData = []interface{}{1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test ", 0, 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test ", 1} +var app *fiber.App = v1.New() + +func cleanup() { + s, err := db.Query(context.Background(), "DROP TABLE \"submission\"") + if err != nil { + panic(err) + } + defer s.Close() +} + +func setup() error { + err := database.Setup() + if err != nil { + return err + } + + s, err := db.Query(context.Background(), "INSERT INTO \"submission\" (id, link, created_at, author, status) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10);", submissionData...) + if err != nil { + return err + } + + defer s.Close() + + return nil +} +func TestGetSubmission_200(t *testing.T) { + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + req, _ := http.NewRequest("GET", "/submit", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "get submission") + assert.Equalf(t, 200, res.StatusCode, "get submission") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "get submission") + assert.Equalf(t, "{\"count\":2,\"jokes\":[{\"id\":1,\"link\":\"https://via.placeholder.com/300/01f/fff.png\",\"created_at\":\"2021-08-03T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":0},{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission") +} + +func TestGetSubmission_Params(t *testing.T) { + err := setup() + if err != nil { + t.Fatal(err) + } + + defer cleanup() + + req, _ := http.NewRequest("GET", "/submit?page=1&limit=5&approved=true", nil) + res, err := app.Test(req, -1) + + assert.Equalf(t, false, err != nil, "get submission") + assert.Equalf(t, 200, res.StatusCode, "get submission") + body, err := ioutil.ReadAll(res.Body) + assert.Nilf(t, err, "get submission") + assert.Equalf(t, "{\"count\":1,\"jokes\":[{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission") +} \ No newline at end of file diff --git a/api/app/v1/models/sql.go b/api/app/v1/models/sql.go deleted file mode 100644 index 6938ef1..0000000 --- a/api/app/v1/models/sql.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "errors" - -var ErrNoRows = errors.New("no rows in result set") -var ErrConnDone = errors.New("connection is already closed") -var ErrTxDone = errors.New("transaction has already been committed or rolled back") - -var ErrNotFound = errors.New("record not found") -var ErrEmpty = errors.New("record is empty") diff --git a/api/app/v1/platform/database/create.go b/api/app/v1/platform/database/create.go index 9bfaf45..0611f39 100644 --- a/api/app/v1/platform/database/create.go +++ b/api/app/v1/platform/database/create.go @@ -35,7 +35,7 @@ func Setup() error { log.Fatalln("17 - failed on table creation: ", err) return err } - + _, err = db.Query(context.Background(), sql) if err != nil { log.Fatalln("18 - failed on table creation: ", err) @@ -76,5 +76,38 @@ func Setup() error { } } + // Submission table + + //Check if table exists + var tableSubmissionExists bool + err = db.QueryRow(context.Background(), `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'submission' + );`).Scan(&tableJokesExists) + if err != nil { + log.Fatalln("13 - failed on checking table: ", err) + return err + } + + if !tableSubmissionExists { + sql, _, err := bob. + CreateTable("submission"). + AddColumn(bob.ColumnDef{Name: "id", Type: "SERIAL", Extras: []string{"PRIMARY KEY"}}). + TextColumn("link", "UNIQUE", "NOT NULL"). + StringColumn("created_at"). + StringColumn("author", "NOT NULL"). + AddColumn(bob.ColumnDef{Name: "status", Type: "SMALLINT", Extras: []string{"DEFAULT 0"}}). + ToSql() + if err != nil { + log.Fatalln("14 - failed on table creation: ", err) + } + + _, err = db.Query(context.Background(), sql) + if err != nil { + log.Fatalln("15 - failed on table creation: ", err) + } + } + return nil } diff --git a/api/app/v1/utils/array_test.go b/api/app/v1/utils/array_test.go index 506932c..3f57c03 100644 --- a/api/app/v1/utils/array_test.go +++ b/api/app/v1/utils/array_test.go @@ -5,19 +5,18 @@ import ( "testing" ) -func TestIsIn(t *testing.T) { +func TestIsIn_True(t *testing.T) { arr := []string{"John", "Matthew", "Thomas", "Adam"} - t.Run("should return true", func(t *testing.T) { - check := utils.IsIn(arr, "Thomas") - if !check { - t.Error("check should be true: ", check) - } - }) - - t.Run("should return false", func(t *testing.T) { - check := utils.IsIn(arr, "James") - if check { - t.Error("check should be false: ", check) - } - }) + check := utils.IsIn(arr, "Thomas") + if !check { + t.Error("check should be true: ", check) + } +} + +func TestIsIn_False(t *testing.T) { + arr := []string{"John", "Matthew", "Thomas", "Adam"} + check := utils.IsIn(arr, "James") + if check { + t.Error("check should be false: ", check) + } } diff --git a/api/app/v1/utils/date_test.go b/api/app/v1/utils/date_test.go index 821ce55..2e328b8 100644 --- a/api/app/v1/utils/date_test.go +++ b/api/app/v1/utils/date_test.go @@ -7,34 +7,42 @@ import ( "jokes-bapak2-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().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) - } - }) - - t.Run("should return false with no error if no date is supplied", func(t *testing.T) { - today, err := utils.IsToday("") - if err != nil { - t.Error(err.Error()) - } - if today != false { - t.Error("it should be false:", today) - } - }) +func TestIsToday_Today(t *testing.T) { + today, err := utils.IsToday(time.Now().Format(time.RFC3339)) + if err != nil { + t.Error(err.Error()) + } + if today == false { + t.Error("today should be true:", today) + } } + +func TestIsToday_NotToday(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) + } +} + +func TestIsToday_ErrorIfEmpty(t *testing.T) { + today, err := utils.IsToday("") + if err != nil { + t.Error(err.Error()) + } + if today != false { + t.Error("it should be false:", today) + } +} + +func TestIsToday_ErrorIfInvalid(t *testing.T) { + today, err := utils.IsToday("asdfghjkl") + if err == nil { + t.Error("it should be error:", today, err) + } + if today != false { + t.Error("it should be false:", today) + } +} \ No newline at end of file diff --git a/api/app/v1/utils/parse_test.go b/api/app/v1/utils/parse_test.go index 1728c9b..db1dd0c 100644 --- a/api/app/v1/utils/parse_test.go +++ b/api/app/v1/utils/parse_test.go @@ -8,37 +8,33 @@ import ( ) 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)) - } - }) + 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{}{ - "age": 32, - "fat": true, - "name": "Scott", - } - parsed, err := utils.ParseToFormBody(body) - if err != nil { - t.Error(err.Error()) - } - result := [3]string{"age=32&", "fat=true&", "name=Scott&"} - 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)) - } - }) + body := map[string]interface{}{ + "age": 32, + "fat": true, + "name": "Scott", + } + parsed, err := utils.ParseToFormBody(body) + if err != nil { + t.Error(err.Error()) + } + result := [3]string{"age=32&", "fat=true&", "name=Scott&"} + 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)) + } } diff --git a/api/app/v1/utils/random_test.go b/api/app/v1/utils/random_test.go index 8e62792..d549507 100644 --- a/api/app/v1/utils/random_test.go +++ b/api/app/v1/utils/random_test.go @@ -5,23 +5,22 @@ import ( "testing" ) -func TestRandomString(t *testing.T) { - t.Run("should create a random string with param", func(t *testing.T) { - random, err := utils.RandomString(10) - if err != nil { - t.Error(err) - } - if len(random) != 20 { - t.Error("result is not within the length of 10") - } - }) - t.Run("should create a random string with invalid params", func(t *testing.T) { - random, err := utils.RandomString(10) - if err != nil { - t.Error(err) - } - if len(random) != 20 { - t.Error("result is not within the length of 10") - } - }) +func TestRandomString_Valid(t *testing.T) { + random, err := utils.RandomString(10) + if err != nil { + t.Error(err) + } + if len(random) != 20 { + t.Error("result is not within the length of 10") + } } + +func TestRandomString_Invalid(t *testing.T) { + random, err := utils.RandomString(10) + if err != nil { + t.Error(err) + } + if len(random) != 20 { + t.Error("result is not within the length of 10") + } +} \ No newline at end of file diff --git a/api/app/v1/utils/request_test.go b/api/app/v1/utils/request_test.go index fa80d72..6e51489 100644 --- a/api/app/v1/utils/request_test.go +++ b/api/app/v1/utils/request_test.go @@ -7,21 +7,19 @@ import ( "jokes-bapak2-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) - } +func TestRequest_Get(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 7b04a56..49da1b8 100644 --- a/api/go.mod +++ b/api/go.mod @@ -15,6 +15,12 @@ require ( github.com/gojek/heimdall/v7 v7.0.2 github.com/jackc/pgx/v4 v4.12.0 github.com/joho/godotenv v1.3.0 + github.com/kr/text v0.2.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 github.com/stretchr/testify v1.7.0 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/api/go.sum b/api/go.sum index b272a8d..baaf628 100644 --- a/api/go.sum +++ b/api/go.sum @@ -70,6 +70,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -308,12 +309,12 @@ github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -372,6 +373,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= @@ -623,8 +626,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -688,8 +692,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -708,11 +713,13 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/api/main.go b/api/main.go index 02c395e..08b959b 100644 --- a/api/main.go +++ b/api/main.go @@ -67,7 +67,7 @@ func StartServerWithGracefulShutdown(a *fiber.App) { }() // Run server. - if err := a.Listen(":" + os.Getenv("PORT")); err != nil { + if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil { log.Printf("Oops... Server is not running! Reason: %v", err) } @@ -77,7 +77,7 @@ func StartServerWithGracefulShutdown(a *fiber.App) { // StartServer func for starting a simple server. func StartServer(a *fiber.App) { // Run server. - if err := a.Listen(":" + os.Getenv("PORT")); err != nil { + if err := a.Listen(os.Getenv("HOST") + ":" + os.Getenv("PORT")); err != nil { log.Printf("Oops... Server is not running! Reason: %v", err) } } From c0f817722d6b098397f262286430cf1f735d7dc2 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 15:52:18 +0700 Subject: [PATCH 05/10] docs: swagger/openapi documentation --- api/app/v1/documentation.json | 507 ++++++++++++++++++++++++++++++++++ api/app/v1/documentation.yaml | 320 +++++++++++++++++++++ 2 files changed, 827 insertions(+) create mode 100644 api/app/v1/documentation.json create mode 100644 api/app/v1/documentation.yaml diff --git a/api/app/v1/documentation.json b/api/app/v1/documentation.json new file mode 100644 index 0000000..2cdaf14 --- /dev/null +++ b/api/app/v1/documentation.json @@ -0,0 +1,507 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Jokesbapak2 Image API", + "description": "Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought, why not make it as an API?\n", + "version": "0.0.1", + "contact": { + "name": "Reinaldy Rafli", + "url": "https://github.com/aldy505", + "email": "aldy505@tutanota.com" + }, + "license": { + "name": "GNU General Public License v3.0", + "url": "https://github.com/aldy505/jokes-bapak2/blob/master/LICENSE" + } + }, + "servers": [ + { + "url": "https://jokesbapak2.herokuapp.com/v1", + "description": "Production" + }, + { + "url": "http://localhost:5000", + "description": "Development" + } + ], + "paths": { + "/": { + "get": { + "tags": [ + "Jokes" + ], + "summary": "Get random Jokes Bapak2 image", + "description": "Returns a different image (PNG, JPG, or GIF) for every call.", + "responses": { + "200": { + "description": "Image data", + "content": { + "image/gif": {}, + "image/png": {}, + "image/jpeg": {} + } + } + } + }, + "put": { + "summary": "Add a new joke into database", + "description": "asd", + "tags": [ + "Jokes" + ], + "requestBody": { + "description": "asds", + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/request.auth" + }, + { + "$ref": "#/components/schemas/request.joke" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Image has been added", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/request.joke" + }, + "example": { + "link": "https://link.to/image.jpg" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + }, + "example": { + "error": "URL provided is not a valid image" + } + } + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + }, + "/id/{id}": { + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "number" + }, + "required": true, + "description": "A number that represents image's ID" + } + ], + "get": { + "summary": "Get random Jokes Bapak2 image by ID", + "description": "Returns consistent image for every call.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Image data", + "content": { + "image/jpeg": {}, + "image/png": {}, + "image/gif": {} + } + }, + "404": { + "description": "Provided image ID was not found", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Requested ID was not found." + } + } + } + } + }, + "patch": { + "summary": "Update a Joke with certain image ID", + "description": "Returns consistent image for every call.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Sucessfully updated an image item", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/response.confirmation" + }, + { + "$ref": "#/components/schemas/request.joke" + } + ] + } + } + } + }, + "400": { + "description": "Link provided is not a valid image", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "406": { + "description": "If the Joke ID does not exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + }, + "delete": { + "summary": "Delete a Joke with certain image ID", + "description": "hi", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Sucessfully deleted an image item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.confirmation" + } + } + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "406": { + "description": "If the Joke ID does not exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + }, + "/today": { + "get": { + "summary": "Get the joke of the day", + "description": "A joke a day makes more of a dad out of you.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Image data", + "content": { + "image/jpeg": {}, + "image/png": {}, + "image/gif": {} + } + } + } + } + }, + "/total": { + "get": { + "summary": "Get total amount of jokes in database", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Total jokes", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.confirmation" + }, + "example": { + "message": "154" + } + } + } + } + } + } + }, + "/submit": { + "get": { + "summary": "Get submitted Jokes", + "tags": [ + "Submission" + ], + "parameters": [ + { + "name": "author", + "in": "query", + "required": false, + "description": "Author to be queried", + "schema": { + "type": "string" + } + }, + { + "name": "approved", + "in": "query", + "required": false, + "description": "Whether query just approved jokes or not", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "number" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "asd", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number" + }, + "jokes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/response.submission" + } + } + } + } + } + } + } + } + }, + "post": { + "summary": "Submit a joke", + "description": "Must be in multipart/form-data format. Author must be in the format of \"Name <email>\".\n", + "tags": [ + "Submission" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "link": { + "description": "Image link", + "type": "string" + }, + "image": { + "description": "Image data", + "type": "string" + }, + "author": { + "description": "Person who submitted this", + "type": "string" + } + }, + "required": [ + "author", + "image", + "link" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Joke successfully submitted", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/response.confirmation" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/response.submission" + } + } + } + ] + } + } + } + }, + "400": { + "description": "Invalid data was sent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + }, + "/health": { + "get": { + "summary": "Health check", + "description": "Ping the databases to make sure everything's alright", + "tags": [ + "Health" + ], + "responses": { + "200": { + "description": "Everything is okay" + }, + "403": { + "description": "Something is not okay", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "request.auth": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "request.joke": { + "type": "object", + "properties": { + "link": { + "type": "string" + } + } + }, + "response.confirmation": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "response.error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "response.submission": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "link": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "author": { + "type": "string" + }, + "status": { + "type": "number" + } + } + } + } + } +} \ No newline at end of file diff --git a/api/app/v1/documentation.yaml b/api/app/v1/documentation.yaml new file mode 100644 index 0000000..ca7e228 --- /dev/null +++ b/api/app/v1/documentation.yaml @@ -0,0 +1,320 @@ +openapi: 3.0.0 +info: + title: Jokesbapak2 Image API + description: > + Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, + Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. + But I thought, why not make it as an API? + version: 0.0.1 + contact: + name: Reinaldy Rafli + url: https://github.com/aldy505 + email: aldy505@tutanota.com + license: + name: GNU General Public License v3.0 + url: https://github.com/aldy505/jokes-bapak2/blob/master/LICENSE +servers: + - url: "https://jokesbapak2.herokuapp.com/v1" + description: Production + - url: "http://localhost:5000" + description: Development +paths: + /: + get: + tags: + - Jokes + summary: Get random Jokes Bapak2 image + description: Returns a different image (PNG, JPG, or GIF) for every call. + responses: + 200: + description: Image data + content: + 'image/gif': {} + 'image/png': {} + 'image/jpeg': {} + put: + summary: Add a new joke into database + description: asd + tags: + - Jokes + requestBody: + description: asds + required: true + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/request.auth' + - $ref: '#/components/schemas/request.joke' + responses: + 200: + description: Image has been added + content: + application/json: + schema: + $ref: '#/components/schemas/request.joke' + example: + link: https://link.to/image.jpg + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + example: + error: URL provided is not a valid image + 403: + description: Must be authenticated to submit a joke + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + /id/{id}: + parameters: + - in: path + name: id + schema: + type: number + required: true + description: A number that represents image's ID + get: + summary: Get random Jokes Bapak2 image by ID + description: Returns consistent image for every call. + tags: + - Jokes + responses: + 200: + description: Image data + content: + 'image/jpeg': {} + 'image/png': {} + 'image/gif': {} + 404: + description: Provided image ID was not found + content: + text/plain: + schema: + type: string + example: + Requested ID was not found. + patch: + summary: Update a Joke with certain image ID + description: Returns consistent image for every call. + tags: + - Jokes + responses: + 200: + description: Sucessfully updated an image item + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/response.confirmation' + - $ref: '#/components/schemas/request.joke' + 400: + description: Link provided is not a valid image + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + 403: + description: Must be authenticated to submit a joke + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + 406: + description: If the Joke ID does not exists + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + delete: + summary: Delete a Joke with certain image ID + description: hi + tags: + - Jokes + responses: + 200: + description: Sucessfully deleted an image item + content: + application/json: + schema: + $ref: '#/components/schemas/response.confirmation' + 403: + description: Must be authenticated to submit a joke + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + 406: + description: If the Joke ID does not exists + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + /today: + get: + summary: Get the joke of the day + description: A joke a day makes more of a dad out of you. + tags: + - Jokes + responses: + 200: + description: Image data + content: + 'image/jpeg': {} + 'image/png': {} + 'image/gif': {} + /total: + get: + summary: Get total amount of jokes in database + tags: + - Jokes + responses: + 200: + description: Total jokes + content: + application/json: + schema: + $ref: '#/components/schemas/response.confirmation' + example: + message: "154" + /submit: + get: + summary: Get submitted Jokes + tags: + - Submission + parameters: + - name: author + in: query + required: false + description: Author to be queried + schema: + type: string + - name: approved + in: query + required: false + description: Whether query just approved jokes or not + schema: + type: boolean + - name: limit + in: query + required: false + schema: + type: number + - name: page + in: query + required: false + schema: + type: number + responses: + 200: + description: asd + content: + application/json: + schema: + type: object + properties: + count: + type: number + jokes: + type: array + items: + $ref: '#/components/schemas/response.submission' + post: + summary: Submit a joke + description: > + Must be in multipart/form-data format. + Author must be in the format of "Name <email>". + tags: + - Submission + requestBody: + content: + multipart/form-data: + schema: + properties: + link: + description: Image link + type: string + image: + description: Image data + type: string + author: + description: Person who submitted this + type: string + required: + - author + - image + - link + responses: + 200: + description: Joke successfully submitted + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/response.confirmation' + - type: object + properties: + data: + $ref: '#/components/schemas/response.submission' + 400: + description: Invalid data was sent + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + /health: + get: + summary: Health check + description: Ping the databases to make sure everything's alright + tags: + - Health + responses: + 200: + description: Everything is okay + 403: + description: Something is not okay + content: + application/json: + schema: + $ref: '#/components/schemas/response.error' + +components: + schemas: + request.auth: + type: object + properties: + key: + type: string + token: + type: string + request.joke: + type: object + properties: + link: + type: string + response.confirmation: + type: object + properties: + message: + type: string + response.error: + type: object + properties: + error: + type: string + response.submission: + type: object + properties: + id: + type: number + link: + type: string + created_at: + type: string + author: + type: string + status: + type: number \ No newline at end of file From e3e178087c08a0f87b60cb0fa9a8a7ea0963e4b0 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 16:03:53 +0700 Subject: [PATCH 06/10] perf: caching certain routes --- api/app/v1/handler/health/health_test.go | 2 +- api/app/v1/handler/joke/joke_delete_test.go | 2 +- api/app/v1/handler/submit/submit_get_test.go | 4 ++-- api/app/v1/routes/health.go | 4 +++- api/app/v1/routes/joke.go | 6 ++++-- api/app/v1/routes/submit.go | 12 +++++++++++- api/app/v1/utils/date_test.go | 2 +- api/app/v1/utils/random_test.go | 2 +- 8 files changed, 24 insertions(+), 10 deletions(-) diff --git a/api/app/v1/handler/health/health_test.go b/api/app/v1/handler/health/health_test.go index 9cd990a..cf55b70 100644 --- a/api/app/v1/handler/health/health_test.go +++ b/api/app/v1/handler/health/health_test.go @@ -49,7 +49,7 @@ func setup() error { defer a.Close() defer j.Close() - + return nil } diff --git a/api/app/v1/handler/joke/joke_delete_test.go b/api/app/v1/handler/joke/joke_delete_test.go index ff81e02..2685a22 100644 --- a/api/app/v1/handler/joke/joke_delete_test.go +++ b/api/app/v1/handler/joke/joke_delete_test.go @@ -18,7 +18,7 @@ func TestDeleteJoke_200(t *testing.T) { } defer cleanup() - + reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") req, _ := http.NewRequest("DELETE", "/id/1", reqBody) res, err := app.Test(req, -1) diff --git a/api/app/v1/handler/submit/submit_get_test.go b/api/app/v1/handler/submit/submit_get_test.go index 3a9dd18..2d6eb47 100644 --- a/api/app/v1/handler/submit/submit_get_test.go +++ b/api/app/v1/handler/submit/submit_get_test.go @@ -32,7 +32,7 @@ func setup() error { if err != nil { return err } - + s, err := db.Query(context.Background(), "INSERT INTO \"submission\" (id, link, created_at, author, status) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10);", submissionData...) if err != nil { return err @@ -76,4 +76,4 @@ func TestGetSubmission_Params(t *testing.T) { body, err := ioutil.ReadAll(res.Body) assert.Nilf(t, err, "get submission") assert.Equalf(t, "{\"count\":1,\"jokes\":[{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission") -} \ No newline at end of file +} diff --git a/api/app/v1/routes/health.go b/api/app/v1/routes/health.go index 969cb88..c1dc52b 100644 --- a/api/app/v1/routes/health.go +++ b/api/app/v1/routes/health.go @@ -2,13 +2,15 @@ package routes import ( "jokes-bapak2-api/app/v1/handler/health" + "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" ) func Health(app *fiber.App) *fiber.App { // Health check - app.Get("/health", health.Health) + app.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), health.Health) return app } diff --git a/api/app/v1/routes/joke.go b/api/app/v1/routes/joke.go index 54cc5c7..1bddc1d 100644 --- a/api/app/v1/routes/joke.go +++ b/api/app/v1/routes/joke.go @@ -3,8 +3,10 @@ package routes import ( "jokes-bapak2-api/app/v1/handler/joke" "jokes-bapak2-api/app/v1/middleware" + "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" ) func Joke(app *fiber.App) *fiber.App { @@ -12,13 +14,13 @@ func Joke(app *fiber.App) *fiber.App { app.Get("/", joke.SingleJoke) // Today's joke - app.Get("/today", joke.TodayJoke) + app.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), joke.TodayJoke) // Joke by ID app.Get("/id/:id", middleware.OnlyIntegerAsID(), joke.JokeByID) // Count total jokes - app.Get("/total", joke.TotalJokes) + app.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), joke.TotalJokes) // Add new joke app.Put("/", middleware.RequireAuth(), joke.AddNewJoke) diff --git a/api/app/v1/routes/submit.go b/api/app/v1/routes/submit.go index 6084980..24b9274 100644 --- a/api/app/v1/routes/submit.go +++ b/api/app/v1/routes/submit.go @@ -2,13 +2,23 @@ package routes import ( "jokes-bapak2-api/app/v1/handler/submit" + "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" ) func Submit(app *fiber.App) *fiber.App { // Get pending submitted joke - app.Get("/submit", submit.GetSubmission) + app.Get( + "/submit", + cache.New(cache.Config{ + Expiration: 5 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return c.OriginalURL() + }, + }), + submit.GetSubmission) // Add a joke app.Post("/submit", submit.SubmitJoke) diff --git a/api/app/v1/utils/date_test.go b/api/app/v1/utils/date_test.go index 2e328b8..f96f077 100644 --- a/api/app/v1/utils/date_test.go +++ b/api/app/v1/utils/date_test.go @@ -45,4 +45,4 @@ func TestIsToday_ErrorIfInvalid(t *testing.T) { if today != false { t.Error("it should be false:", today) } -} \ No newline at end of file +} diff --git a/api/app/v1/utils/random_test.go b/api/app/v1/utils/random_test.go index d549507..65a2be1 100644 --- a/api/app/v1/utils/random_test.go +++ b/api/app/v1/utils/random_test.go @@ -23,4 +23,4 @@ func TestRandomString_Invalid(t *testing.T) { if len(random) != 20 { t.Error("result is not within the length of 10") } -} \ No newline at end of file +} From 98c15d2e6d93a688c9bfb3bb889f89039d21fa81 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 16:46:48 +0700 Subject: [PATCH 07/10] chore: codefactor changes --- api/app/v1/documentation.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/app/v1/documentation.yaml b/api/app/v1/documentation.yaml index ca7e228..57a3dda 100644 --- a/api/app/v1/documentation.yaml +++ b/api/app/v1/documentation.yaml @@ -2,8 +2,8 @@ openapi: 3.0.0 info: title: Jokesbapak2 Image API description: > - Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, - Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. + Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, + Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought, why not make it as an API? version: 0.0.1 contact: @@ -169,7 +169,7 @@ paths: /total: get: summary: Get total amount of jokes in database - tags: + tags: - Jokes responses: 200: From 79a8dccd9ca5dc423a0cd2f515ebf100d9e99bf4 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 17:05:58 +0700 Subject: [PATCH 08/10] test: refactor & clean up --- api/app/v1/handler/joke/joke_add_test.go | 6 ++++-- api/app/v1/handler/joke/joke_delete_test.go | 14 +++++++++++++- api/app/v1/handler/joke/joke_get_test.go | 5 +---- api/app/v1/handler/joke/joke_update_test.go | 9 +++++++-- api/app/v1/utils/random_test.go | 2 +- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/api/app/v1/handler/joke/joke_add_test.go b/api/app/v1/handler/joke/joke_add_test.go index 4e39332..ada5cd0 100644 --- a/api/app/v1/handler/joke/joke_add_test.go +++ b/api/app/v1/handler/joke/joke_add_test.go @@ -10,6 +10,7 @@ import ( ) func TestAddNewJoke_201(t *testing.T) { + // TODO: Remove this line below, make this test works t.SkipNow() err := setup() if err != nil { @@ -18,7 +19,7 @@ func TestAddNewJoke_201(t *testing.T) { defer cleanup() - reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"very secure\",\"token\":\"password\"}") + reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/07f/ff0000.png\",\"key\":\"test\",\"token\":\"password\"}") req, _ := http.NewRequest("PUT", "/", reqBody) req.Header.Set("content-type", "application/json") req.Header.Add("accept", "application/json") @@ -33,6 +34,7 @@ func TestAddNewJoke_201(t *testing.T) { } func TestAddNewJoke_NotValidImage(t *testing.T) { + // TODO: Remove this line below, make this test works t.SkipNow() err := setup() if err != nil { @@ -41,7 +43,7 @@ func TestAddNewJoke_NotValidImage(t *testing.T) { defer cleanup() - reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"very secure\",\"token\":\"password\"}") + reqBody := strings.NewReader("{\"link\":\"https://google.com/\",\"key\":\"test\",\"token\":\"password\"}") req, _ := http.NewRequest("PUT", "/", reqBody) req.Header.Set("content-type", "application/json") req.Header.Add("accept", "application/json") diff --git a/api/app/v1/handler/joke/joke_delete_test.go b/api/app/v1/handler/joke/joke_delete_test.go index 2685a22..3a9531f 100644 --- a/api/app/v1/handler/joke/joke_delete_test.go +++ b/api/app/v1/handler/joke/joke_delete_test.go @@ -1,6 +1,7 @@ package joke_test import ( + "context" "io/ioutil" "net/http" "strings" @@ -17,10 +18,18 @@ func TestDeleteJoke_200(t *testing.T) { t.Fatal(err) } + j, err := db.Query(context.Background(), "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3);", 100, "https://via.placeholder.com/300/01f/fff.png", 1) + if err != nil { + t.Fatal(err) + } + + defer j.Close() defer cleanup() reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") - req, _ := http.NewRequest("DELETE", "/id/1", reqBody) + req, _ := http.NewRequest("DELETE", "/id/100", reqBody) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") res, err := app.Test(req, -1) assert.Equalf(t, false, err != nil, "joke delete") @@ -31,6 +40,7 @@ func TestDeleteJoke_200(t *testing.T) { assert.Equalf(t, "{\"message\":\"specified joke id has been deleted\"}", string(body), "joke delete") } func TestDeleteJoke_NotExists(t *testing.T) { + // TODO: Remove this line below, make this test works t.SkipNow() err := setup() if err != nil { @@ -41,6 +51,8 @@ func TestDeleteJoke_NotExists(t *testing.T) { reqBody := strings.NewReader("{\"key\":\"very secure\",\"token\":\"password\"}") req, _ := http.NewRequest("DELETE", "/id/100", reqBody) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") res, err := app.Test(req, -1) assert.Equalf(t, false, err != nil, "joke delete") diff --git a/api/app/v1/handler/joke/joke_get_test.go b/api/app/v1/handler/joke/joke_get_test.go index 53933ac..fe136bf 100644 --- a/api/app/v1/handler/joke/joke_get_test.go +++ b/api/app/v1/handler/joke/joke_get_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "net/http" "testing" - "time" v1 "jokes-bapak2-api/app/v1" "jokes-bapak2-api/app/v1/platform/database" @@ -40,9 +39,7 @@ func setup() error { return err } - hashedToken := "$argon2id$v=19$m=65536,t=16,p=4$48beb241490caa57fbca8e63df1e1b5fba8934baf78205ee775f96a85f45b889$e6dfca3f69adbe7653dbb353f366d741a3640313c45e33eabaca0c217c16417de80d70ac67f217c9ca46634b0abaad5f4ea2b064caa44ce218fb110b4cba9d36" - var args []interface{} = []interface{}{1, "very secure", hashedToken, time.Now().Format(time.RFC3339)} - a, err := db.Query(context.Background(), "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4);", args...) + a, err := db.Query(context.Background(), "INSERT INTO \"administrators\" (\"id\", \"key\", \"token\", \"last_used\") VALUES (1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');") if err != nil { return err } diff --git a/api/app/v1/handler/joke/joke_update_test.go b/api/app/v1/handler/joke/joke_update_test.go index 52a0019..34518cf 100644 --- a/api/app/v1/handler/joke/joke_update_test.go +++ b/api/app/v1/handler/joke/joke_update_test.go @@ -17,8 +17,10 @@ func TestUpdateJoke_200(t *testing.T) { } defer cleanup() - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"test\",\"token\":\"password\"}") req, _ := http.NewRequest("PATCH", "/id/1", reqBody) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") res, err := app.Test(req, -1) assert.Equalf(t, false, err != nil, "joke update") @@ -30,6 +32,7 @@ func TestUpdateJoke_200(t *testing.T) { } func TestUpdateJoke_NotExists(t *testing.T) { + // TODO: Remove this line below, make this test works t.SkipNow() err := setup() if err != nil { @@ -37,8 +40,10 @@ func TestUpdateJoke_NotExists(t *testing.T) { } defer cleanup() - reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"very secure\",\"token\":\"password\"}") + reqBody := strings.NewReader("{\"link\":\"https://picsum.photos/id/9/200/300\",\"key\":\"test\",\"token\":\"password\"}") req, _ := http.NewRequest("PATCH", "/id/100", reqBody) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") res, err := app.Test(req, -1) assert.Equalf(t, false, err != nil, "joke update") diff --git a/api/app/v1/utils/random_test.go b/api/app/v1/utils/random_test.go index 65a2be1..9b1b642 100644 --- a/api/app/v1/utils/random_test.go +++ b/api/app/v1/utils/random_test.go @@ -16,7 +16,7 @@ func TestRandomString_Valid(t *testing.T) { } func TestRandomString_Invalid(t *testing.T) { - random, err := utils.RandomString(10) + random, err := utils.RandomString(-10) if err != nil { t.Error(err) } From e38f36c5799655fd1b257bd0ed189950b0364c2b Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 17:08:06 +0700 Subject: [PATCH 09/10] fix: correct status code --- api/app/v1/documentation.json | 702 ++++++++++++------------ api/app/v1/documentation.yaml | 4 +- api/app/v1/handler/submit/submit_add.go | 2 +- 3 files changed, 354 insertions(+), 354 deletions(-) diff --git a/api/app/v1/documentation.json b/api/app/v1/documentation.json index 2cdaf14..827aded 100644 --- a/api/app/v1/documentation.json +++ b/api/app/v1/documentation.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Jokesbapak2 Image API", - "description": "Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought, why not make it as an API?\n", + "description": "Jokes Bapak2 is an image API that you can use for free! I've been seeing lots and lots of Indonesian dad jokes on Twitter, Facebook and Instagram on early 2020. In a month, I made a Discord bot that provides the jokes. But I thought, why not make it as an API?\n", "version": "0.0.1", "contact": { "name": "Reinaldy Rafli", @@ -68,382 +68,382 @@ } }, "responses": { - "200": { - "description": "Image has been added", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/request.joke" - }, - "example": { - "link": "https://link.to/image.jpg" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - }, - "example": { - "error": "URL provided is not a valid image" - } - } - } - }, - "403": { - "description": "Must be authenticated to submit a joke", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - } - } - } - }, - "/id/{id}": { - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "number" - }, - "required": true, - "description": "A number that represents image's ID" - } - ], - "get": { - "summary": "Get random Jokes Bapak2 image by ID", - "description": "Returns consistent image for every call.", - "tags": [ - "Jokes" - ], - "responses": { - "200": { - "description": "Image data", - "content": { - "image/jpeg": {}, - "image/png": {}, - "image/gif": {} - } - }, - "404": { - "description": "Provided image ID was not found", - "content": { - "text/plain": { - "schema": { - "type": "string" - }, - "example": "Requested ID was not found." - } - } - } - } - }, - "patch": { - "summary": "Update a Joke with certain image ID", - "description": "Returns consistent image for every call.", - "tags": [ - "Jokes" - ], - "responses": { - "200": { - "description": "Sucessfully updated an image item", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/response.confirmation" - }, - { + "201": { + "description": "Image has been added", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/request.joke" - } - ] - } - } - } - }, - "400": { - "description": "Link provided is not a valid image", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - }, - "403": { - "description": "Must be authenticated to submit a joke", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - }, - "406": { - "description": "If the Joke ID does not exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - } - } - }, - "delete": { - "summary": "Delete a Joke with certain image ID", - "description": "hi", - "tags": [ - "Jokes" - ], - "responses": { - "200": { - "description": "Sucessfully deleted an image item", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.confirmation" - } - } - } - }, - "403": { - "description": "Must be authenticated to submit a joke", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - }, - "406": { - "description": "If the Joke ID does not exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - } - } - } - }, - "/today": { - "get": { - "summary": "Get the joke of the day", - "description": "A joke a day makes more of a dad out of you.", - "tags": [ - "Jokes" - ], - "responses": { - "200": { - "description": "Image data", - "content": { - "image/jpeg": {}, - "image/png": {}, - "image/gif": {} - } - } - } - } - }, - "/total": { - "get": { - "summary": "Get total amount of jokes in database", - "tags": [ - "Jokes" - ], - "responses": { - "200": { - "description": "Total jokes", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.confirmation" - }, - "example": { - "message": "154" - } - } - } - } - } - } - }, - "/submit": { - "get": { - "summary": "Get submitted Jokes", - "tags": [ - "Submission" - ], - "parameters": [ - { - "name": "author", - "in": "query", - "required": false, - "description": "Author to be queried", - "schema": { - "type": "string" - } - }, - { - "name": "approved", - "in": "query", - "required": false, - "description": "Whether query just approved jokes or not", - "schema": { - "type": "boolean" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "number" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "asd", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { - "type": "number" }, - "jokes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/response.submission" - } + "example": { + "link": "https://link.to/image.jpg" } } } - } - } - } - } - }, - "post": { - "summary": "Submit a joke", - "description": "Must be in multipart/form-data format. Author must be in the format of \"Name <email>\".\n", - "tags": [ - "Submission" - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "properties": { - "link": { - "description": "Image link", - "type": "string" - }, - "image": { - "description": "Image data", - "type": "string" - }, - "author": { - "description": "Person who submitted this", - "type": "string" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + }, + "example": { + "error": "URL provided is not a valid image" + } } - }, - "required": [ - "author", - "image", - "link" - ] + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } } } } }, - "responses": { - "200": { - "description": "Joke successfully submitted", - "content": { - "application/json": { - "schema": { - "allOf": [ - { + "/id/{id}": { + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "number" + }, + "required": true, + "description": "A number that represents image's ID" + } + ], + "get": { + "summary": "Get random Jokes Bapak2 image by ID", + "description": "Returns consistent image for every call.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Image data", + "content": { + "image/jpeg": {}, + "image/png": {}, + "image/gif": {} + } + }, + "404": { + "description": "Provided image ID was not found", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "Requested ID was not found." + } + } + } + } + }, + "patch": { + "summary": "Update a Joke with certain image ID", + "description": "Returns consistent image for every call.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Sucessfully updated an image item", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/response.confirmation" + }, + { + "$ref": "#/components/schemas/request.joke" + } + ] + } + } + } + }, + "400": { + "description": "Link provided is not a valid image", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "406": { + "description": "If the Joke ID does not exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + }, + "delete": { + "summary": "Delete a Joke with certain image ID", + "description": "hi", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Sucessfully deleted an image item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.confirmation" + } + } + } + }, + "403": { + "description": "Must be authenticated to submit a joke", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + }, + "406": { + "description": "If the Joke ID does not exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + }, + "/today": { + "get": { + "summary": "Get the joke of the day", + "description": "A joke a day makes more of a dad out of you.", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Image data", + "content": { + "image/jpeg": {}, + "image/png": {}, + "image/gif": {} + } + } + } + } + }, + "/total": { + "get": { + "summary": "Get total amount of jokes in database", + "tags": [ + "Jokes" + ], + "responses": { + "200": { + "description": "Total jokes", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/response.confirmation" }, - { + "example": { + "message": "154" + } + } + } + } + } + } + }, + "/submit": { + "get": { + "summary": "Get submitted Jokes", + "tags": [ + "Submission" + ], + "parameters": [ + { + "name": "author", + "in": "query", + "required": false, + "description": "Author to be queried", + "schema": { + "type": "string" + } + }, + { + "name": "approved", + "in": "query", + "required": false, + "description": "Whether query just approved jokes or not", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "number" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "asd", + "content": { + "application/json": { + "schema": { "type": "object", "properties": { - "data": { - "$ref": "#/components/schemas/response.submission" + "count": { + "type": "number" + }, + "jokes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/response.submission" + } } } } - ] + } } } } }, - "400": { - "description": "Invalid data was sent", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" + "post": { + "summary": "Submit a joke", + "description": "Must be in multipart/form-data format. Author must be in the format of \"Name <email>\".\n", + "tags": [ + "Submission" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "link": { + "description": "Image link", + "type": "string" + }, + "image": { + "description": "Image data", + "type": "string" + }, + "author": { + "description": "Person who submitted this", + "type": "string" + } + }, + "required": [ + "author", + "image", + "link" + ] + } + } + } + }, + "responses": { + "201": { + "description": "Joke successfully submitted", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/response.confirmation" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/response.submission" + } + } + } + ] + } + } + } + }, + "400": { + "description": "Invalid data was sent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } + } + } + } + } + }, + "/health": { + "get": { + "summary": "Health check", + "description": "Ping the databases to make sure everything's alright", + "tags": [ + "Health" + ], + "responses": { + "200": { + "description": "Everything is okay" + }, + "403": { + "description": "Something is not okay", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/response.error" + } + } } } } } } - } - }, - "/health": { - "get": { - "summary": "Health check", - "description": "Ping the databases to make sure everything's alright", - "tags": [ - "Health" - ], - "responses": { - "200": { - "description": "Everything is okay" - }, - "403": { - "description": "Something is not okay", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/response.error" - } - } - } - } - } - } - } }, "components": { "schemas": { diff --git a/api/app/v1/documentation.yaml b/api/app/v1/documentation.yaml index 57a3dda..d15d255 100644 --- a/api/app/v1/documentation.yaml +++ b/api/app/v1/documentation.yaml @@ -47,7 +47,7 @@ paths: - $ref: '#/components/schemas/request.auth' - $ref: '#/components/schemas/request.joke' responses: - 200: + 201: description: Image has been added content: application/json: @@ -248,7 +248,7 @@ paths: - image - link responses: - 200: + 201: description: Joke successfully submitted content: application/json: diff --git a/api/app/v1/handler/submit/submit_add.go b/api/app/v1/handler/submit/submit_add.go index ae3d267..65be308 100644 --- a/api/app/v1/handler/submit/submit_add.go +++ b/api/app/v1/handler/submit/submit_add.go @@ -93,7 +93,7 @@ func SubmitJoke(c *fiber.Ctx) error { } return c. - Status(fiber.StatusOK). + Status(fiber.StatusCreated). JSON(models.ResponseSubmission{ Message: "Joke submitted. Please wait for a few days for admin to approve your submission.", Data: submission[0], From b6788b33c616d0d15ea4dc703cc9fd88e09ebe08 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Aug 2021 17:14:33 +0700 Subject: [PATCH 10/10] style: chained function line breaks --- api/app/v1/handler/joke/joke_add.go | 22 ++++++++++----- api/app/v1/handler/joke/joke_delete.go | 27 ++++++++++++------ api/app/v1/handler/joke/joke_get.go | 4 ++- api/app/v1/handler/joke/joke_total.go | 16 +++++++---- api/app/v1/handler/joke/joke_update.go | 38 ++++++++++++++++++-------- api/app/v1/middleware/auth.go | 33 ++++++++++++++++------ api/app/v1/middleware/validation.go | 8 ++++-- 7 files changed, 102 insertions(+), 46 deletions(-) diff --git a/api/app/v1/handler/joke/joke_add.go b/api/app/v1/handler/joke/joke_add.go index c569357..30c0989 100644 --- a/api/app/v1/handler/joke/joke_add.go +++ b/api/app/v1/handler/joke/joke_add.go @@ -24,12 +24,18 @@ func AddNewJoke(c *fiber.Ctx) error { } if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) + return c. + Status(fiber.StatusBadRequest). + JSON(models.Error{ + Error: "URL provided is not a valid image", + }) } - sql, args, err := handler.Psql.Insert("jokesbapak2").Columns("link", "creator").Values(body.Link, c.Locals("userID")).ToSql() + sql, args, err := handler.Psql. + Insert("jokesbapak2"). + Columns("link", "creator"). + Values(body.Link, c.Locals("userID")). + ToSql() if err != nil { return err } @@ -51,7 +57,9 @@ func AddNewJoke(c *fiber.Ctx) error { return err } - return c.Status(fiber.StatusCreated).JSON(models.ResponseJoke{ - Link: body.Link, - }) + return c. + Status(fiber.StatusCreated). + JSON(models.ResponseJoke{ + Link: body.Link, + }) } diff --git a/api/app/v1/handler/joke/joke_delete.go b/api/app/v1/handler/joke/joke_delete.go index 4b8e63d..d3b7c69 100644 --- a/api/app/v1/handler/joke/joke_delete.go +++ b/api/app/v1/handler/joke/joke_delete.go @@ -19,7 +19,11 @@ func DeleteJoke(c *fiber.Ctx) error { } // Check if the joke exists - sql, args, err := handler.Psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + sql, args, err := handler.Psql. + Select("id"). + From("jokesbapak2"). + Where(squirrel.Eq{"id": id}). + ToSql() if err != nil { return err } @@ -31,7 +35,10 @@ func DeleteJoke(c *fiber.Ctx) error { } if jokeID == id { - sql, args, err = handler.Psql.Delete("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + sql, args, err = handler.Psql. + Delete("jokesbapak2"). + Where(squirrel.Eq{"id": id}). + ToSql() if err != nil { return err } @@ -52,11 +59,15 @@ func DeleteJoke(c *fiber.Ctx) error { return err } - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: "specified joke id has been deleted", - }) + return c. + Status(fiber.StatusOK). + JSON(models.ResponseJoke{ + Message: "specified joke id has been deleted", + }) } - return c.Status(fiber.StatusNotAcceptable).JSON(models.Error{ - Error: "specified joke id does not exists", - }) + return c. + Status(fiber.StatusNotAcceptable). + JSON(models.Error{ + Error: "specified joke id does not exists", + }) } diff --git a/api/app/v1/handler/joke/joke_get.go b/api/app/v1/handler/joke/joke_get.go index 47563ec..cab33a8 100644 --- a/api/app/v1/handler/joke/joke_get.go +++ b/api/app/v1/handler/joke/joke_get.go @@ -132,7 +132,9 @@ func JokeByID(c *fiber.Ctx) error { } if link == "" { - return c.Status(fiber.StatusNotFound).Send([]byte("Requested ID was not found.")) + return c. + Status(fiber.StatusNotFound). + Send([]byte("Requested ID was not found.")) } // Get image data diff --git a/api/app/v1/handler/joke/joke_total.go b/api/app/v1/handler/joke/joke_total.go index c6bb933..6fc1874 100644 --- a/api/app/v1/handler/joke/joke_total.go +++ b/api/app/v1/handler/joke/joke_total.go @@ -26,14 +26,18 @@ func TotalJokes(c *fiber.Ctx) error { if err != nil { if err.Error() == "Entry not found" { - return c.Status(fiber.StatusInternalServerError).JSON(models.Error{ - Error: "no data found", - }) + return c. + Status(fiber.StatusInternalServerError). + JSON(models.Error{ + Error: "no data found", + }) } return err } - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: strconv.Itoa(int(total[0])), - }) + return c. + Status(fiber.StatusOK). + JSON(models.ResponseJoke{ + Message: strconv.Itoa(int(total[0])), + }) } diff --git a/api/app/v1/handler/joke/joke_update.go b/api/app/v1/handler/joke/joke_update.go index 36bef5d..7a5ee83 100644 --- a/api/app/v1/handler/joke/joke_update.go +++ b/api/app/v1/handler/joke/joke_update.go @@ -14,7 +14,11 @@ import ( func UpdateJoke(c *fiber.Ctx) error { id := c.Params("id") // Check if the joke exists - sql, args, err := handler.Psql.Select("id").From("jokesbapak2").Where(squirrel.Eq{"id": id}).ToSql() + sql, args, err := handler.Psql. + Select("id"). + From("jokesbapak2"). + Where(squirrel.Eq{"id": id}). + ToSql() if err != nil { return err } @@ -39,12 +43,18 @@ func UpdateJoke(c *fiber.Ctx) error { } if !valid { - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "URL provided is not a valid image", - }) + return c. + Status(fiber.StatusBadRequest). + JSON(models.Error{ + Error: "URL provided is not a valid image", + }) } - sql, args, err = handler.Psql.Update("jokesbapak2").Set("link", body.Link).Set("creator", c.Locals("userID")).ToSql() + sql, args, err = handler.Psql. + Update("jokesbapak2"). + Set("link", body.Link). + Set("creator", c.Locals("userID")). + ToSql() if err != nil { return err } @@ -65,13 +75,17 @@ func UpdateJoke(c *fiber.Ctx) error { return err } - return c.Status(fiber.StatusOK).JSON(models.ResponseJoke{ - Message: "specified joke id has been updated", - Link: body.Link, - }) + 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.Error{ - Error: "specified joke id does not exists", - }) + return c. + Status(fiber.StatusNotAcceptable). + JSON(models.Error{ + Error: "specified joke id does not exists", + }) } diff --git a/api/app/v1/middleware/auth.go b/api/app/v1/middleware/auth.go index 1c1157b..7ef7768 100644 --- a/api/app/v1/middleware/auth.go +++ b/api/app/v1/middleware/auth.go @@ -24,7 +24,11 @@ func RequireAuth() fiber.Handler { } // Check if key exists - sql, args, err := psql.Select("token").From("administrators").Where(squirrel.Eq{"key": auth.Key}).ToSql() + sql, args, err := psql. + Select("token"). + From("administrators"). + Where(squirrel.Eq{"key": auth.Key}). + ToSql() if err != nil { return err } @@ -33,9 +37,11 @@ func RequireAuth() fiber.Handler { err = db.QueryRow(context.Background(), sql, args...).Scan(&token) if err != nil { if err.Error() == "no rows in result set" { - return c.Status(fiber.StatusForbidden).JSON(models.Error{ - Error: "Invalid key", - }) + return c. + Status(fiber.StatusForbidden). + JSON(models.Error{ + Error: "Invalid key", + }) } return err } @@ -51,7 +57,10 @@ func RequireAuth() fiber.Handler { } if verify { - sql, args, err = psql.Update("administrators").Set("last_used", time.Now().UTC().Format(time.RFC3339)).ToSql() + sql, args, err = psql. + Update("administrators"). + Set("last_used", time.Now().UTC().Format(time.RFC3339)). + ToSql() if err != nil { return err } @@ -61,7 +70,11 @@ func RequireAuth() fiber.Handler { return err } - sql, args, err = psql.Select("id").From("administrators").Where(squirrel.Eq{"key": auth.Key}).ToSql() + sql, args, err = psql. + Select("id"). + From("administrators"). + Where(squirrel.Eq{"key": auth.Key}). + ToSql() if err != nil { return err } @@ -75,8 +88,10 @@ func RequireAuth() fiber.Handler { return c.Next() } - return c.Status(fiber.StatusForbidden).JSON(models.Error{ - Error: "Invalid key", - }) + return c. + Status(fiber.StatusForbidden). + JSON(models.Error{ + Error: "Invalid key", + }) } } diff --git a/api/app/v1/middleware/validation.go b/api/app/v1/middleware/validation.go index c1f913f..0156d81 100644 --- a/api/app/v1/middleware/validation.go +++ b/api/app/v1/middleware/validation.go @@ -19,8 +19,10 @@ func OnlyIntegerAsID() fiber.Handler { return c.Next() } - return c.Status(fiber.StatusBadRequest).JSON(models.Error{ - Error: "only numbers are allowed as ID", - }) + return c. + Status(fiber.StatusBadRequest). + JSON(models.Error{ + Error: "only numbers are allowed as ID", + }) } }