Merge pull request #8 from aldy505/api/submit

Let people submit their jokes to the API
This commit is contained in:
Reinaldy Rafli 2021-08-04 17:22:27 +07:00 committed by GitHub
commit 27c0c82b40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2253 additions and 760 deletions

View File

@ -28,6 +28,7 @@ func New() *fiber.App {
err := sentry.Init(sentry.ClientOptions{
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
}

View File

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

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

@ -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": {
"201": {
"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 &lt;email&gt;\".\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"
}
}
}
}
}
}
}
},
"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"
}
}
}
}
}
}

View File

@ -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:
201:
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 &lt;email&gt;".
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'
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

View File

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

View File

@ -1,7 +1,8 @@
package handler
package health
import (
"context"
"jokes-bapak2-api/app/v1/handler"
"jokes-bapak2-api/app/v1/models"
"github.com/gofiber/fiber/v2"
@ -9,7 +10,7 @@ import (
func Health(c *fiber.Ctx) error {
// Ping REDIS database
err := redis.Ping(context.Background()).Err()
err := handler.Redis.Ping(context.Background()).Err()
if err != nil {
return c.
Status(fiber.StatusServiceUnavailable).
@ -18,7 +19,7 @@ func Health(c *fiber.Ctx) error {
})
}
_, err = db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
_, err = handler.Db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
if err != nil {
return c.
Status(fiber.StatusServiceUnavailable).

View File

@ -0,0 +1,72 @@
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/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 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() {
j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
if err != nil {
panic(err)
}
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 := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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,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

@ -0,0 +1,65 @@
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.
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
}
err = core.SetTotalJoke(handler.Db, handler.Memory)
if err != nil {
return err
}
return c.
Status(fiber.StatusCreated).
JSON(models.ResponseJoke{
Link: body.Link,
})
}

View File

@ -0,0 +1,57 @@
package joke_test
import (
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddNewJoke_201(t *testing.T) {
// TODO: Remove this line below, make this test works
t.SkipNow()
err := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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")
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) {
// TODO: Remove this line below, make this test works
t.SkipNow()
err := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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")
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

@ -0,0 +1,73 @@
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
}
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
}
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",
})
}

View File

@ -0,0 +1,64 @@
package joke_test
import (
"context"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDeleteJoke_200(t *testing.T) {
// TODO: Remove this line below, make this test works
t.SkipNow()
err := setup()
if err != nil {
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/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")
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) {
// TODO: Remove this line below, make this test works
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)
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")
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,4 +1,4 @@
package handler
package joke
import (
"context"
@ -7,6 +7,7 @@ import (
"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"
@ -14,11 +15,11 @@ import (
)
func TodayJoke(c *fiber.Ctx) error {
// check from redis if today's joke already exists
// 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 := redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
err := handler.Redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
if err != nil {
return err
}
@ -33,12 +34,12 @@ func TodayJoke(c *fiber.Ctx) error {
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)
err := handler.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)
response, err := handler.Client.Get(link, nil)
if err != nil {
return err
}
@ -49,7 +50,7 @@ func TodayJoke(c *fiber.Ctx) error {
}
now := time.Now().UTC().Format(time.RFC3339)
err = redis.MSet(context.Background(), map[string]interface{}{
err = handler.Redis.MSet(context.Background(), map[string]interface{}{
"today:link": link,
"today:date": now,
"today:image": string(data),
@ -66,29 +67,29 @@ func TodayJoke(c *fiber.Ctx) error {
}
func SingleJoke(c *fiber.Ctx) error {
checkCache, err := core.CheckJokesCache(memory)
checkCache, err := core.CheckJokesCache(handler.Memory)
if err != nil {
return err
}
if !checkCache {
jokes, err := core.GetAllJSONJokes(db)
jokes, err := core.GetAllJSONJokes(handler.Db)
if err != nil {
return err
}
err = memory.Set("jokes", jokes)
err = handler.Memory.Set("jokes", jokes)
if err != nil {
return err
}
}
link, err := core.GetRandomJokeFromCache(memory)
link, err := core.GetRandomJokeFromCache(handler.Memory)
if err != nil {
return err
}
// Get image data
response, err := client.Get(link, nil)
response, err := handler.Client.Get(link, nil)
if err != nil {
return err
}
@ -104,17 +105,17 @@ func SingleJoke(c *fiber.Ctx) error {
}
func JokeByID(c *fiber.Ctx) error {
checkCache, err := core.CheckJokesCache(memory)
checkCache, err := core.CheckJokesCache(handler.Memory)
if err != nil {
return err
}
if !checkCache {
jokes, err := core.GetAllJSONJokes(db)
jokes, err := core.GetAllJSONJokes(handler.Db)
if err != nil {
return err
}
err = memory.Set("jokes", jokes)
err = handler.Memory.Set("jokes", jokes)
if err != nil {
return err
}
@ -125,17 +126,19 @@ func JokeByID(c *fiber.Ctx) error {
return err
}
link, err := core.GetCachedJokeByID(memory, id)
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."))
return c.
Status(fiber.StatusNotFound).
Send([]byte("Requested ID was not found."))
}
// Get image data
response, err := client.Get(link, nil)
response, err := handler.Client.Get(link, nil)
if err != nil {
return err
}

View File

@ -0,0 +1,131 @@
package joke_test
import (
"context"
"io/ioutil"
"net/http"
"testing"
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 *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() {
j, err := db.Query(context.Background(), "DROP TABLE \"jokesbapak2\"")
if err != nil {
panic(err)
}
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, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');")
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 TestTodayJoke(t *testing.T) {
err := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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")
}
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")
}

View File

@ -0,0 +1,43 @@
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])),
})
}

View File

@ -0,0 +1,30 @@
package joke_test
import (
"io/ioutil"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTotalJokes(t *testing.T) {
err := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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")
}

View File

@ -0,0 +1,91 @@
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
}
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
}
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",
})
}

View File

@ -0,0 +1,55 @@
package joke_test
import (
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpdateJoke_200(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\":\"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")
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) {
// TODO: Remove this line below, make this test works
t.SkipNow()
err := setup()
if err != nil {
t.Fatal(err)
}
defer cleanup()
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")
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

@ -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,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.StatusCreated).
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,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 <test@example.com>", 0, 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test <test@example.com>", 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")
}

View File

@ -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,7 +37,9 @@ 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{
return c.
Status(fiber.StatusForbidden).
JSON(models.Error{
Error: "Invalid key",
})
}
@ -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,7 +88,9 @@ func RequireAuth() fiber.Handler {
return c.Next()
}
return c.Status(fiber.StatusForbidden).JSON(models.Error{
return c.
Status(fiber.StatusForbidden).
JSON(models.Error{
Error: "Invalid key",
})
}

View File

@ -19,7 +19,9 @@ func OnlyIntegerAsID() fiber.Handler {
return c.Next()
}
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
return c.
Status(fiber.StatusBadRequest).
JSON(models.Error{
Error: "only numbers are allowed as ID",
})
}

View File

@ -8,3 +8,7 @@ var ErrTxDone = errors.New("transaction has already been committed or rolled bac
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"`
}
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"`
}

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

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

View File

@ -1,14 +1,16 @@
package routes
import (
"jokes-bapak2-api/app/v1/handler"
"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", handler.Health)
app.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), health.Health)
return app
}

View File

@ -1,33 +1,35 @@
package routes
import (
"jokes-bapak2-api/app/v1/handler"
"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 {
// Single route
app.Get("/", handler.SingleJoke)
app.Get("/", joke.SingleJoke)
// Today's joke
app.Get("/today", handler.TodayJoke)
app.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), 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", cache.New(cache.Config{Expiration: 15 * time.Minute}), 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
}

View File

@ -0,0 +1,27 @@
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",
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)
return app
}

View File

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

View File

@ -7,8 +7,7 @@ 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) {
func TestIsToday_Today(t *testing.T) {
today, err := utils.IsToday(time.Now().Format(time.RFC3339))
if err != nil {
t.Error(err.Error())
@ -16,9 +15,9 @@ func TestIsToday(t *testing.T) {
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) {
func TestIsToday_NotToday(t *testing.T) {
today, err := utils.IsToday("2021-01-01T11:48:24Z")
if err != nil {
t.Error(err.Error())
@ -26,9 +25,9 @@ func TestIsToday(t *testing.T) {
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) {
func TestIsToday_ErrorIfEmpty(t *testing.T) {
today, err := utils.IsToday("")
if err != nil {
t.Error(err.Error())
@ -36,5 +35,14 @@ func TestIsToday(t *testing.T) {
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)
}
}

View File

@ -8,7 +8,6 @@ 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,
@ -22,11 +21,9 @@ func TestParseToJSONBody(t *testing.T) {
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,
@ -40,5 +37,4 @@ func TestParseToFormBody(t *testing.T) {
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))
}
})
}

View File

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

View File

@ -0,0 +1,26 @@
package utils_test
import (
"jokes-bapak2-api/app/v1/utils"
"testing"
)
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")
}
}

View File

@ -7,8 +7,7 @@ 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) {
func TestRequest_Get(t *testing.T) {
res, err := utils.Request(utils.RequestConfig{
URL: "https://jsonplaceholder.typicode.com/todos/1",
Method: http.MethodGet,
@ -23,5 +22,4 @@ func TestRequest(t *testing.T) {
if res.StatusCode != 200 {
t.Error("response does not have 200 status", res.Status)
}
})
}

View File

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

View File

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

View File

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