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