feat: joke submission

This commit is contained in:
Reinaldy Rafli 2021-08-04 09:51:22 +07:00
parent 9d9311e90c
commit c265da52a1
24 changed files with 411 additions and 794 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -9,8 +9,8 @@ import (
"github.com/gojek/heimdall/v7/httpclient" "github.com/gojek/heimdall/v7/httpclient"
) )
var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) var Psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
var db = database.New() var Db = database.New()
var redis = cache.New() var Redis = cache.New()
var memory = cache.InMemory() var Memory = cache.InMemory()
var client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)) var Client = httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second))

View File

@ -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)
}

View File

@ -12,6 +12,20 @@ import (
"github.com/stretchr/testify/assert" "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) { func TestHealth(t *testing.T) {
err := database.Setup() err := database.Setup()
if err != nil { if err != nil {

View File

@ -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")
})
}

View File

@ -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,
})
}

View File

@ -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")
})
}

View File

@ -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",
})
}

View File

@ -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")
})
}

View File

@ -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)
}

View File

@ -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")
})
}

View File

@ -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])),
})
}

View File

@ -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")
})
}

View File

@ -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",
})
}

View File

@ -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")
})
}

View File

@ -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 <youremail@mail>\" 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 <youremail@mail>\" 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],
})
}

View File

@ -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,
})
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -6,15 +6,13 @@ type Joke struct {
Creator int `json:"creator" form:"creator" db:"creator"` 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 { type Today struct {
Date string `redis:"today:date"` Date string `redis:"today:date"`
Image string `redis:"today:image"` Image string `redis:"today:image"`
ContentType string `redis:"today:contentType"` ContentType string `redis:"today:contentType"`
} }
type ResponseJoke struct {
Link string `json:"link,omitempty"`
Message string `json:"message,omitempty"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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
}