Merge branch 'master' of github.com:aldy505/jokes-bapak2-api into client/docs
This commit is contained in:
commit
0b109d0bd2
|
@ -1,4 +1,4 @@
|
||||||
name: API
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -6,9 +6,10 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
api-build:
|
api-build:
|
||||||
name: Build
|
name: API
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: golang:1.16.6
|
container: golang:1.17-buster
|
||||||
|
timeout-minutes: 15
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13-alpine
|
image: postgres:13-alpine
|
||||||
|
@ -27,7 +28,7 @@ jobs:
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine
|
image: redis:6-alpine
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
defaults:
|
defaults:
|
||||||
|
@ -38,23 +39,14 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: cache go modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Installling dependencies
|
- name: Installling dependencies
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
run: go mod download
|
||||||
run: go mod vendor
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build main.go
|
run: go build main.go
|
||||||
|
|
||||||
- name: Run test & coverage
|
- name: Run test & coverage
|
||||||
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
|
||||||
env:
|
env:
|
||||||
ENV: development
|
ENV: development
|
||||||
PORT: 5000
|
PORT: 5000
|
||||||
|
@ -83,3 +75,48 @@ jobs:
|
||||||
- uses: codecov/codecov-action@v2
|
- uses: codecov/codecov-action@v2
|
||||||
with:
|
with:
|
||||||
flags: api
|
flags: api
|
||||||
|
|
||||||
|
client-build:
|
||||||
|
name: Client
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: node:14-buster
|
||||||
|
timeout-minutes: 15
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./client
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Installling dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build
|
||||||
|
env:
|
||||||
|
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
|
||||||
|
VITE_NODE_ENV: development
|
||||||
|
VITE_API_ENDPOINT: https://jokesbapak2.herokuapp.com/v1
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: javascript
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
||||||
|
- name: Create Sentry release
|
||||||
|
uses: getsentry/action-release@v1
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_CLIENT }}
|
||||||
|
with:
|
||||||
|
environment: production
|
||||||
|
set_commits: skip
|
||||||
|
version: ${{ github.sha }}
|
|
@ -1,68 +0,0 @@
|
||||||
name: Client
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
client-build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./client
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v2.1.5
|
|
||||||
with:
|
|
||||||
node-version: 14.x
|
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: cache node_modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Installling dependencies
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
run: yarn
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: yarn build
|
|
||||||
env:
|
|
||||||
VITE_SENTRY_DSN: https://examplePublicKey@o0.ingest.sentry.io/0
|
|
||||||
VITE_NODE_ENV: development
|
|
||||||
VITE_API_ENDPOINT: https://jokesbapak2.herokuapp.com/v1
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: javascript
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
|
|
||||||
- name: Create Sentry release
|
|
||||||
uses: getsentry/action-release@v1
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_CLIENT }}
|
|
||||||
with:
|
|
||||||
environment: production
|
|
||||||
set_commits: skip
|
|
||||||
version: ${{ github.sha }}
|
|
|
@ -8,7 +8,8 @@ jobs:
|
||||||
client-build:
|
client-build:
|
||||||
name: Client
|
name: Client
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: node:14-buster
|
||||||
|
timeout-minutes: 15
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
|
@ -17,26 +18,8 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v2.1.5
|
|
||||||
with:
|
|
||||||
node-version: 14.x
|
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- name: cache node_modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Installling dependencies
|
- name: Installling dependencies
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
run: yarn install
|
||||||
run: yarn
|
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: npx eslint --ext .svelte,.js,.ts --ignore-path .gitignore .
|
run: npx eslint --ext .svelte,.js,.ts --ignore-path .gitignore .
|
||||||
|
@ -62,7 +45,8 @@ jobs:
|
||||||
api-build:
|
api-build:
|
||||||
name: API
|
name: API
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: golang:1.16.6
|
container: golang:1.17-buster
|
||||||
|
timeout-minutes: 15
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13-alpine
|
image: postgres:13-alpine
|
||||||
|
@ -81,7 +65,7 @@ jobs:
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine
|
image: redis:6-alpine
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
defaults:
|
defaults:
|
||||||
|
@ -92,23 +76,14 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: cache go modules
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Installling dependencies
|
- name: Installling dependencies
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
run: go mod download
|
||||||
run: go mod vendor
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build main.go
|
run: go build main.go
|
||||||
|
|
||||||
- name: Run test & coverage
|
- name: Run test & coverage
|
||||||
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
|
||||||
env:
|
env:
|
||||||
ENV: development
|
ENV: development
|
||||||
PORT: 5000
|
PORT: 5000
|
||||||
|
|
33
README.md
33
README.md
|
@ -24,10 +24,10 @@ This is some kind of [icanhazdadjokes](https://icanhazdadjoke.com/) but it's Ind
|
||||||
|
|
||||||
You can consume this API via a website (linked in the front facing web) with a few endpoints:
|
You can consume this API via a website (linked in the front facing web) with a few endpoints:
|
||||||
|
|
||||||
* `/v1/` - Random jokes bapak2
|
* `/` - Random jokes bapak2
|
||||||
* `/v1/id/{number}` - Jokes bapak2 based on ID
|
* `/id/{number}` - Jokes bapak2 based on ID
|
||||||
* `/v1/today` - Jokes bapak2 of the day
|
* `/today` - Jokes bapak2 of the day
|
||||||
* `/v1/total` - Total available jokes bapak2
|
* `/total` - Total available jokes bapak2
|
||||||
|
|
||||||
Currently I'm (still) searching for an alternative for AWS S3 that I can use for free.
|
Currently I'm (still) searching for an alternative for AWS S3 that I can use for free.
|
||||||
|
|
||||||
|
@ -54,25 +54,26 @@ See [CONTRIBUTING](./CONTRIBUTING.md) or README files on each project directory
|
||||||
* [Ronny Gunawan](https://github.com/ronnygunawan) for the caching concept & ideas
|
* [Ronny Gunawan](https://github.com/ronnygunawan) for the caching concept & ideas
|
||||||
* [artileda](https://github.com/artileda) for the jokes submission
|
* [artileda](https://github.com/artileda) for the jokes submission
|
||||||
* [elianiva](https://github.com/elianiva) for solving my SvelteKit problems
|
* [elianiva](https://github.com/elianiva) for solving my SvelteKit problems
|
||||||
|
* [kokizzu](https://github.com/kokizzu) for the dependency injection concept & ideas
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Jokes Bapak2 API is licensed under [GNU GENERAL PUBLIC LICENSE v3 license](./LICENSE)
|
Jokes Bapak2 API is licensed under [GNU GENERAL PUBLIC LICENSE v3 license](./LICENSE)
|
||||||
|
|
||||||
```
|
```
|
||||||
Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
|
Jokes Bapak2 API is a free-to-use image API of Indonesian dad jokes.
|
||||||
Copyright (C) 2021-present Jokes Bapak2 Contributors
|
Copyright (C) 2021-present Jokes Bapak2 Contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
```
|
```
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.16.6-buster
|
FROM golang:1.17-buster
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"jokes-bapak2-api/app/v1/core"
|
|
||||||
"jokes-bapak2-api/app/v1/platform/cache"
|
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
|
||||||
"jokes-bapak2-api/app/v1/routes"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/etag"
|
|
||||||
)
|
|
||||||
|
|
||||||
var memory = cache.InMemory()
|
|
||||||
var db = database.New()
|
|
||||||
|
|
||||||
func New() *fiber.App {
|
|
||||||
app := fiber.New(fiber.Config{
|
|
||||||
DisableKeepalive: true,
|
|
||||||
CaseSensitive: true,
|
|
||||||
ErrorHandler: errorHandler,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
|
||||||
Dsn: os.Getenv("SENTRY_DSN"),
|
|
||||||
Environment: os.Getenv("ENV"),
|
|
||||||
AttachStacktrace: true,
|
|
||||||
// Enable printing of SDK debug messages.
|
|
||||||
// Useful when getting started or trying to figure something out.
|
|
||||||
Debug: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer sentry.Flush(2 * time.Second)
|
|
||||||
|
|
||||||
err = database.Setup()
|
|
||||||
if err != nil {
|
|
||||||
sentry.CaptureException(err)
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = core.SetAllJSONJoke(db, memory)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
err = core.SetTotalJoke(db, memory)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Use(cors.New())
|
|
||||||
app.Use(etag.New())
|
|
||||||
|
|
||||||
routes.Health(app)
|
|
||||||
routes.Joke(app)
|
|
||||||
routes.Submit(app)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorHandler(c *fiber.Ctx, err error) error {
|
|
||||||
log.Println(err)
|
|
||||||
sentry.CaptureException(err)
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
"error": "Something went wrong on our end",
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
|
|
||||||
"github.com/allegro/bigcache/v3"
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
"github.com/pquerna/ffjson/ffjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetAllJSONJoke fetches jokes data from GetAllJSONJokes then set it to memory cache.
|
|
||||||
func SetAllJSONJoke(db *pgxpool.Pool, memory *bigcache.BigCache) error {
|
|
||||||
jokes, err := GetAllJSONJokes(db)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = memory.Set("jokes", jokes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetTotalJoke(db *pgxpool.Pool, memory *bigcache.BigCache) error {
|
|
||||||
check, err := CheckJokesCache(memory)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !check {
|
|
||||||
err = SetAllJSONJoke(db, memory)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jokes, err := memory.Get("jokes")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []models.Joke
|
|
||||||
err = ffjson.Unmarshal(jokes, &data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var total = []byte{byte(len(data))}
|
|
||||||
err = memory.Set("total", total)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"jokes-bapak2-api/app/v1/platform/cache"
|
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
|
||||||
"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))
|
|
|
@ -1,31 +0,0 @@
|
||||||
package health
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"jokes-bapak2-api/app/v1/handler"
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Health(c *fiber.Ctx) error {
|
|
||||||
// Ping REDIS database
|
|
||||||
err := handler.Redis.Ping(context.Background()).Err()
|
|
||||||
if err != nil {
|
|
||||||
return c.
|
|
||||||
Status(fiber.StatusServiceUnavailable).
|
|
||||||
JSON(models.Error{
|
|
||||||
Error: "REDIS: " + err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = handler.Db.Query(context.Background(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
|
|
||||||
if err != nil {
|
|
||||||
return c.
|
|
||||||
Status(fiber.StatusServiceUnavailable).
|
|
||||||
JSON(models.Error{
|
|
||||||
Error: "POSTGRESQL: " + err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
package joke
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io/ioutil"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/core"
|
|
||||||
"jokes-bapak2-api/app/v1/handler"
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TodayJoke(c *fiber.Ctx) error {
|
|
||||||
// check from handler.Redis if today's joke already exists
|
|
||||||
// send the joke if exists
|
|
||||||
// get a new joke if it's not, then send it.
|
|
||||||
var joke models.Today
|
|
||||||
err := handler.Redis.MGet(context.Background(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
eq, err := utils.IsToday(joke.Date)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if eq {
|
|
||||||
c.Set("Content-Type", joke.ContentType)
|
|
||||||
return c.Status(fiber.StatusOK).Send([]byte(joke.Image))
|
|
||||||
} else {
|
|
||||||
var link string
|
|
||||||
err := handler.Db.QueryRow(context.Background(), "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := handler.Client.Get(link, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC().Format(time.RFC3339)
|
|
||||||
err = handler.Redis.MSet(context.Background(), map[string]interface{}{
|
|
||||||
"today:link": link,
|
|
||||||
"today:date": now,
|
|
||||||
"today:image": string(data),
|
|
||||||
"today:contentType": response.Header.Get("content-type"),
|
|
||||||
}).Err()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("Content-Type", response.Header.Get("content-type"))
|
|
||||||
return c.Status(fiber.StatusOK).Send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func SingleJoke(c *fiber.Ctx) error {
|
|
||||||
checkCache, err := core.CheckJokesCache(handler.Memory)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkCache {
|
|
||||||
jokes, err := core.GetAllJSONJokes(handler.Db)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = handler.Memory.Set("jokes", jokes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
link, err := core.GetRandomJokeFromCache(handler.Memory)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get image data
|
|
||||||
response, err := handler.Client.Get(link, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("Content-Type", response.Header.Get("content-type"))
|
|
||||||
return c.Status(fiber.StatusOK).Send(data)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func JokeByID(c *fiber.Ctx) error {
|
|
||||||
checkCache, err := core.CheckJokesCache(handler.Memory)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkCache {
|
|
||||||
jokes, err := core.GetAllJSONJokes(handler.Db)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = handler.Memory.Set("jokes", jokes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(c.Params("id"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
link, err := core.GetCachedJokeByID(handler.Memory, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if link == "" {
|
|
||||||
return c.
|
|
||||||
Status(fiber.StatusNotFound).
|
|
||||||
Send([]byte("Requested ID was not found."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get image data
|
|
||||||
response, err := handler.Client.Get(link, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("Content-Type", response.Header.Get("content-type"))
|
|
||||||
return c.Status(fiber.StatusOK).Send(data)
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
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])),
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
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")
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
package submit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"jokes-bapak2-api/app/v1/core"
|
|
||||||
"jokes-bapak2-api/app/v1/handler"
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/georgysavva/scany/pgxscan"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SubmitJoke(c *fiber.Ctx) error {
|
|
||||||
var body models.Submission
|
|
||||||
err := c.BodyParser(&body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image and/or Link should not be empty
|
|
||||||
if body.Image == "" && body.Link == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
|
|
||||||
Error: "a link or an image should be supplied in a form of multipart/form-data",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author should be supplied
|
|
||||||
if body.Author == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
|
|
||||||
Error: "an author key consisting on the format \"yourname <youremail@mail>\" must be supplied",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Validate format
|
|
||||||
valid := core.ValidateAuthor(body.Author)
|
|
||||||
if !valid {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
|
|
||||||
Error: "please stick to the format of \"yourname <youremail@mail>\" and within 200 characters",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url string
|
|
||||||
|
|
||||||
// Check link validity if link was provided
|
|
||||||
if body.Link != "" {
|
|
||||||
valid, err := core.CheckImageValidity(handler.Client, body.Link)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(models.Error{
|
|
||||||
Error: "URL provided is not a valid image",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
url = body.Link
|
|
||||||
}
|
|
||||||
|
|
||||||
// If image was provided
|
|
||||||
if body.Image != "" {
|
|
||||||
image := strings.NewReader(body.Image)
|
|
||||||
|
|
||||||
url, err = core.UploadImage(handler.Client, image)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC().Format(time.RFC3339)
|
|
||||||
|
|
||||||
sql, args, err := handler.Psql.
|
|
||||||
Insert("submission").
|
|
||||||
Columns("link", "created_at", "author").
|
|
||||||
Values(url, now, body.Author).
|
|
||||||
Suffix("RETURNING id,created_at,link,author,status").
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var submission []models.Submission
|
|
||||||
result, err := handler.Db.Query(context.Background(), sql, args...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer result.Close()
|
|
||||||
|
|
||||||
err = pgxscan.ScanAll(&submission, result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.
|
|
||||||
Status(fiber.StatusCreated).
|
|
||||||
JSON(models.ResponseSubmission{
|
|
||||||
Message: "Joke submitted. Please wait for a few days for admin to approve your submission.",
|
|
||||||
Data: submission[0],
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package submit_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io/ioutil"
|
|
||||||
v1 "jokes-bapak2-api/app/v1"
|
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var db *pgxpool.Pool = database.New()
|
|
||||||
var submissionData = []interface{}{1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test <test@example.com>", 0, 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test <test@example.com>", 1}
|
|
||||||
var app *fiber.App = v1.New()
|
|
||||||
|
|
||||||
func cleanup() {
|
|
||||||
s, err := db.Query(context.Background(), "DROP TABLE \"submission\"")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer s.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() error {
|
|
||||||
err := database.Setup()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := db.Query(context.Background(), "INSERT INTO \"submission\" (id, link, created_at, author, status) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10);", submissionData...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func TestGetSubmission_200(t *testing.T) {
|
|
||||||
err := setup()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "/submit", nil)
|
|
||||||
res, err := app.Test(req, -1)
|
|
||||||
|
|
||||||
assert.Equalf(t, false, err != nil, "get submission")
|
|
||||||
assert.Equalf(t, 200, res.StatusCode, "get submission")
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
assert.Nilf(t, err, "get submission")
|
|
||||||
assert.Equalf(t, "{\"count\":2,\"jokes\":[{\"id\":1,\"link\":\"https://via.placeholder.com/300/01f/fff.png\",\"created_at\":\"2021-08-03T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":0},{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSubmission_Params(t *testing.T) {
|
|
||||||
err := setup()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "/submit?page=1&limit=5&approved=true", nil)
|
|
||||||
res, err := app.Test(req, -1)
|
|
||||||
|
|
||||||
assert.Equalf(t, false, err != nil, "get submission")
|
|
||||||
assert.Equalf(t, 200, res.StatusCode, "get submission")
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
assert.Nilf(t, err, "get submission")
|
|
||||||
assert.Equalf(t, "{\"count\":1,\"jokes\":[{\"id\":2,\"link\":\"https://via.placeholder.com/300/02f/fff.png\",\"created_at\":\"2021-08-04T18:20:38Z\",\"author\":\"Test \\u003ctest@example.com\\u003e\",\"status\":1}]}", string(body), "get submission")
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
"jokes-bapak2-api/app/v1/platform/database"
|
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
|
||||||
phccrypto "github.com/aldy505/phc-crypto"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
|
||||||
var db = database.New()
|
|
||||||
|
|
||||||
func RequireAuth() fiber.Handler {
|
|
||||||
return func(c *fiber.Ctx) error {
|
|
||||||
var auth models.Auth
|
|
||||||
err := c.BodyParser(&auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if key exists
|
|
||||||
sql, args, err := psql.
|
|
||||||
Select("token").
|
|
||||||
From("administrators").
|
|
||||||
Where(squirrel.Eq{"key": auth.Key}).
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var token string
|
|
||||||
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 err
|
|
||||||
}
|
|
||||||
|
|
||||||
crypto, err := phccrypto.Use(phccrypto.Argon2, phccrypto.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
verify, err := crypto.Verify(token, auth.Token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if verify {
|
|
||||||
sql, args, err = psql.
|
|
||||||
Update("administrators").
|
|
||||||
Set("last_used", time.Now().UTC().Format(time.RFC3339)).
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Query(context.Background(), sql, args...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sql, args, err = psql.
|
|
||||||
Select("id").
|
|
||||||
From("administrators").
|
|
||||||
Where(squirrel.Eq{"key": auth.Key}).
|
|
||||||
ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var id int
|
|
||||||
err = db.QueryRow(context.Background(), sql, args...).Scan(&id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Locals("userID", id)
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.
|
|
||||||
Status(fiber.StatusForbidden).
|
|
||||||
JSON(models.Error{
|
|
||||||
Error: "Invalid key",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var ErrNoRows = errors.New("no rows in result set")
|
|
||||||
var ErrConnDone = errors.New("connection is already closed")
|
|
||||||
var ErrTxDone = errors.New("transaction has already been committed or rolled back")
|
|
||||||
|
|
||||||
var ErrNotFound = errors.New("record not found")
|
|
||||||
var ErrEmpty = errors.New("record is empty")
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/allegro/bigcache/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InMemory() *bigcache.BigCache {
|
|
||||||
cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
return cache
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Connect to the database
|
|
||||||
func New() *redis.Client {
|
|
||||||
opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
rdb := redis.NewClient(opt)
|
|
||||||
return rdb
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
-- Access the data from your HTTP Request software (Postman or Insomnia)
|
|
||||||
-- with this auth:
|
|
||||||
-- key: test
|
|
||||||
-- token: password
|
|
||||||
|
|
||||||
INSERT INTO "administrators" ("id", "key", "token", "last_used") VALUES
|
|
||||||
(1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');
|
|
||||||
|
|
||||||
-- 10 jokes is enough right?
|
|
||||||
|
|
||||||
INSERT INTO "jokesbapak2" ("id", "link", "creator") VALUES
|
|
||||||
(1, 'https://picsum.photos/id/1000/500/500', 1),
|
|
||||||
(2, 'https://picsum.photos/id/1001/500/500', 1),
|
|
||||||
(3, 'https://picsum.photos/id/1002/500/500', 1),
|
|
||||||
(4, 'https://picsum.photos/id/1003/500/500', 1),
|
|
||||||
(5, 'https://picsum.photos/id/1004/500/500', 1),
|
|
||||||
(6, 'https://picsum.photos/id/1005/500/500', 1),
|
|
||||||
(7, 'https://picsum.photos/id/1006/500/500', 1),
|
|
||||||
(8, 'https://picsum.photos/id/1010/500/500', 1),
|
|
||||||
(9, 'https://picsum.photos/id/1008/500/500', 1),
|
|
||||||
(10, 'https://picsum.photos/id/1009/500/500', 1);
|
|
|
@ -1,27 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Connect to the database
|
|
||||||
func New() *pgxpool.Pool {
|
|
||||||
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Unable to create pool config", err)
|
|
||||||
}
|
|
||||||
poolConfig.MaxConns = 18
|
|
||||||
poolConfig.MinConns = 2
|
|
||||||
|
|
||||||
conn, err := pgxpool.ConnectConfig(context.Background(), poolConfig)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Unable to create connection", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"jokes-bapak2-api/app/v1/handler/health"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Health(app *fiber.App) *fiber.App {
|
|
||||||
// Health check
|
|
||||||
app.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), health.Health)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"jokes-bapak2-api/app/v1/handler/joke"
|
|
||||||
"jokes-bapak2-api/app/v1/middleware"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Joke(app *fiber.App) *fiber.App {
|
|
||||||
// Single route
|
|
||||||
app.Get("/", joke.SingleJoke)
|
|
||||||
|
|
||||||
// Today's joke
|
|
||||||
app.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), joke.TodayJoke)
|
|
||||||
|
|
||||||
// Joke by ID
|
|
||||||
app.Get("/id/:id", middleware.OnlyIntegerAsID(), joke.JokeByID)
|
|
||||||
|
|
||||||
// Count total jokes
|
|
||||||
app.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), joke.TotalJokes)
|
|
||||||
|
|
||||||
// Add new joke
|
|
||||||
app.Put("/", middleware.RequireAuth(), joke.AddNewJoke)
|
|
||||||
|
|
||||||
// Update a joke
|
|
||||||
app.Patch("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.UpdateJoke)
|
|
||||||
|
|
||||||
// Delete a joke
|
|
||||||
app.Delete("/id/:id", middleware.RequireAuth(), middleware.OnlyIntegerAsID(), joke.DeleteJoke)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package administrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetUserID(db *pgxpool.Pool, ctx context.Context, key string) (int, error) {
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
tx, err := c.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
sql, args, err := query.
|
||||||
|
Update("administrators").
|
||||||
|
Set("last_used", time.Now().UTC().Format(time.RFC3339)).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args, err = query.
|
||||||
|
Select("id").
|
||||||
|
From("administrators").
|
||||||
|
Where(squirrel.Eq{"key": key}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int
|
||||||
|
err = tx.QueryRow(ctx, sql, args...).Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package administrator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/administrator"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetUserID_Success(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
_, err = c.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := administrator.GetUserID(db, ctx, "very secure")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != 1 {
|
||||||
|
t.Error("id is not correct, want: 1, got:", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserID_Failed(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
id, err := administrator.GetUserID(db, ctx, "very secure")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("an error was expected, got:", id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package administrator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *pgxpool.Pool
|
||||||
|
|
||||||
|
var administratorsData = []interface{}{
|
||||||
|
1, "very secure", "not the real one", time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
defer Teardown()
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup() {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err = pgxpool.ConnectConfig(ctx, poolConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`CREATE TABLE IF NOT EXISTS administrators (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
token TEXT,
|
||||||
|
last_used VARCHAR(255)
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Teardown() (err error) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
tx, err := c.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Flush() error {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
_, err = conn.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package administrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckKeyExists(db *pgxpool.Pool, ctx context.Context, key string) (string, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
|
||||||
|
// Check if key exists
|
||||||
|
sql, args, err := query.
|
||||||
|
Select("token").
|
||||||
|
From("administrators").
|
||||||
|
Where(squirrel.Eq{"key": key}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
err = conn.QueryRow(ctx, sql, args...).Scan(&token)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package administrator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/administrator"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckKeyExists_Success(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
_, err = c.Exec(
|
||||||
|
ctx,
|
||||||
|
"INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)",
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := administrator.CheckKeyExists(db, ctx, "very secure")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != "not the real one" {
|
||||||
|
t.Error("key isn't not the real one, got:", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckKeyExists_Failing(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
_, err = c.Exec(
|
||||||
|
ctx,
|
||||||
|
"INSERT INTO administrators (id, key, token, last_used) VALUES ($1, $2, $3, $4)",
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := administrator.CheckKeyExists(db, ctx, "others")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != "" {
|
||||||
|
t.Error("key is not empty, got:", key)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,34 @@
|
||||||
package core
|
package joke
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"errors"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/allegro/bigcache/v3"
|
"github.com/allegro/bigcache/v3"
|
||||||
"github.com/georgysavva/scany/pgxscan"
|
"github.com/georgysavva/scany/pgxscan"
|
||||||
|
"github.com/jackc/pgx"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
"github.com/pquerna/ffjson/ffjson"
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte.
|
// GetAllJSONJokes fetch the database for all the jokes then output it as a JSON []byte.
|
||||||
// Keep in mind, you will need to store it to memory yourself.
|
// Keep in mind, you will need to store it to memory yourself.
|
||||||
func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
|
func GetAllJSONJokes(db *pgxpool.Pool, ctx context.Context) ([]byte, error) {
|
||||||
var jokes []models.Joke
|
conn, err := db.Acquire(ctx)
|
||||||
results, err := db.Query(context.Background(), "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"")
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var jokes []schema.Joke
|
||||||
|
results, err := conn.Query(ctx, "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer results.Close()
|
defer results.Close()
|
||||||
|
|
||||||
err = pgxscan.ScanAll(&jokes, results)
|
err = pgxscan.ScanAll(&jokes, results)
|
||||||
|
@ -35,17 +44,34 @@ func GetAllJSONJokes(db *pgxpool.Pool) ([]byte, error) {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only return a link
|
||||||
|
func GetRandomJokeFromDB(db *pgxpool.Pool, ctx context.Context) (string, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var link string
|
||||||
|
err = conn.QueryRow(ctx, "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetRandomJokeFromCache returns a link string of a random joke from cache.
|
// GetRandomJokeFromCache returns a link string of a random joke from cache.
|
||||||
func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
|
func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
|
||||||
jokes, err := memory.Get("jokes")
|
jokes, err := memory.Get("jokes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "Entry not found" {
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
return "", models.ErrNotFound
|
return "", schema.ErrNotFound
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []models.Joke
|
var data []schema.Joke
|
||||||
err = ffjson.Unmarshal(jokes, &data)
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
@ -54,7 +80,7 @@ func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
|
||||||
// Return an error if the database is empty
|
// Return an error if the database is empty
|
||||||
dataLength := len(data)
|
dataLength := len(data)
|
||||||
if dataLength == 0 {
|
if dataLength == 0 {
|
||||||
return "", models.ErrEmpty
|
return "", schema.ErrEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
random := rand.Intn(dataLength)
|
random := rand.Intn(dataLength)
|
||||||
|
@ -67,7 +93,7 @@ func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) {
|
||||||
func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
|
func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
|
||||||
_, err := memory.Get("jokes")
|
_, err := memory.Get("jokes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "Entry not found" {
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -80,7 +106,7 @@ func CheckJokesCache(memory *bigcache.BigCache) (bool, error) {
|
||||||
func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
|
func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
|
||||||
_, err := memory.Get("total")
|
_, err := memory.Get("total")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "Entry not found" {
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -93,16 +119,16 @@ func CheckTotalJokesCache(memory *bigcache.BigCache) (bool, error) {
|
||||||
func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
|
func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
|
||||||
jokes, err := memory.Get("jokes")
|
jokes, err := memory.Get("jokes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "Entry not found" {
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
return "", models.ErrNotFound
|
return "", schema.ErrNotFound
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []models.Joke
|
var data []schema.Joke
|
||||||
err = ffjson.Unmarshal(jokes, &data)
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a simple solution, might convert it to goroutines and channels sometime soon.
|
// This is a simple solution, might convert it to goroutines and channels sometime soon.
|
||||||
|
@ -119,11 +145,42 @@ func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) {
|
||||||
func GetCachedTotalJokes(memory *bigcache.BigCache) (int, error) {
|
func GetCachedTotalJokes(memory *bigcache.BigCache) (int, error) {
|
||||||
total, err := memory.Get("total")
|
total, err := memory.Get("total")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "Entry not found" {
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
return 0, models.ErrNotFound
|
return 0, schema.ErrNotFound
|
||||||
}
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
i, err := strconv.Atoi(string(total))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
return int(total[0]), nil
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckJokeExists(db *pgxpool.Pool, ctx context.Context, id string) (bool, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
|
||||||
|
sql, args, err := query.
|
||||||
|
Select("id").
|
||||||
|
From("jokesbapak2").
|
||||||
|
Where(squirrel.Eq{"id": id}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jokeID int
|
||||||
|
err = conn.QueryRow(ctx, sql, args...).Scan(&jokeID)
|
||||||
|
if err != nil && errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Itoa(jokeID) == id, nil
|
||||||
}
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
package joke_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetAllJSONJokes(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.GetAllJSONJokes(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(j) == "" {
|
||||||
|
t.Error("j should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRandomJokeFromDB(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.GetRandomJokeFromDB(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == "" {
|
||||||
|
t.Error("j should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRandomJokeFromCache(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
jokes := []schema.Joke{
|
||||||
|
{ID: 1, Link: "link1", Creator: 1},
|
||||||
|
{ID: 2, Link: "link2", Creator: 1},
|
||||||
|
{ID: 3, Link: "link3", Creator: 1},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(jokes)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = memory.Set("jokes", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.GetRandomJokeFromCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == "" {
|
||||||
|
t.Error("j should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckJokesCache_True(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
jokes := []schema.Joke{
|
||||||
|
{ID: 1, Link: "link1", Creator: 1},
|
||||||
|
{ID: 2, Link: "link2", Creator: 1},
|
||||||
|
{ID: 3, Link: "link3", Creator: 1},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(jokes)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = memory.Set("jokes", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == false {
|
||||||
|
t.Error("j should not be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckJokesCache_False(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
j, err := joke.CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == true {
|
||||||
|
t.Error("j should not be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckTotalJokesCache_True(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
err := memory.Set("total", []byte("10"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.CheckTotalJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == false {
|
||||||
|
t.Error("j should not be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckTotalJokesCache_False(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
j, err := joke.CheckTotalJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == true {
|
||||||
|
t.Error("j should not be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCachedJokeByID(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
jokes := []schema.Joke{
|
||||||
|
{ID: 1, Link: "link1", Creator: 1},
|
||||||
|
{ID: 2, Link: "link2", Creator: 1},
|
||||||
|
{ID: 3, Link: "link3", Creator: 1},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(jokes)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = memory.Set("jokes", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.GetCachedJokeByID(memory, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j != "link1" {
|
||||||
|
t.Error("j should be link1, got:", j)
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := joke.GetCachedJokeByID(memory, 4)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k != "" {
|
||||||
|
t.Error("k should be empty, got:", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCachedTotalJokes(t *testing.T) {
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
err := memory.Set("total", []byte("10"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.GetCachedTotalJokes(memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j != 10 {
|
||||||
|
t.Error("j should be 10, got:", j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckJokeExists(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := joke.CheckJokeExists(db, ctx, "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == false {
|
||||||
|
t.Error("j should not be false")
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := joke.CheckJokeExists(db, ctx, "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == true {
|
||||||
|
t.Error("k should not be true")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package joke_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *pgxpool.Pool
|
||||||
|
var cache *redis.Client
|
||||||
|
var memory *bigcache.BigCache
|
||||||
|
|
||||||
|
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 administratorsData = []interface{}{
|
||||||
|
1, "very secure", "not the real one", time.Now().Format(time.RFC3339), 2, "test", "$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106", "",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
defer Teardown()
|
||||||
|
Setup()
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup() {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err = pgxpool.ConnectConfig(ctx, poolConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = redis.NewClient(opt)
|
||||||
|
|
||||||
|
memory, err = bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`CREATE TABLE IF NOT EXISTS administrators (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
token TEXT,
|
||||||
|
last_used VARCHAR(255)
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`CREATE TABLE IF NOT EXISTS jokesbapak2 (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
link TEXT UNIQUE,
|
||||||
|
creator INT NOT NULL REFERENCES "administrators" ("id")
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`CREATE TABLE IF NOT EXISTS submission (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
link VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
created_at VARCHAR(255),
|
||||||
|
author VARCHAR(255) NOT NULL,
|
||||||
|
status SMALLINT DEFAULT 0
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Teardown() (err error) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cache.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = memory.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Flush() error {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
_, err = conn.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = conn.Exec(ctx, "TRUNCATE TABLE jokesbapak2 RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = conn.Exec(ctx, "TRUNCATE TABLE administrators RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cache.FlushAll(ctx).Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = memory.Reset()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetAllJSONJoke fetches jokes data from GetAllJSONJokes then set it to memory cache.
|
||||||
|
func SetAllJSONJoke(db *pgxpool.Pool, ctx context.Context, memory *bigcache.BigCache) error {
|
||||||
|
jokes, err := GetAllJSONJokes(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTotalJoke(db *pgxpool.Pool, ctx context.Context, memory *bigcache.BigCache) error {
|
||||||
|
check, err := CheckJokesCache(memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !check {
|
||||||
|
err = SetAllJSONJoke(db, ctx, memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jokes, err := memory.Get("jokes")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []schema.Joke
|
||||||
|
err = ffjson.Unmarshal(jokes, &data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = []byte{byte(len(data))}
|
||||||
|
err = memory.Set("total", total)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertJokeIntoDB(db *pgxpool.Pool, ctx context.Context, joke schema.Joke) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sql, args, err := query.
|
||||||
|
Insert("jokesbapak2").
|
||||||
|
Columns("link", "creator").
|
||||||
|
Values(joke.Link, joke.Creator).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSingleJoke(db *pgxpool.Pool, ctx context.Context, id int) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sql, args, err := query.
|
||||||
|
Delete("jokesbapak2").
|
||||||
|
Where(squirrel.Eq{"id": id}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateJoke(db *pgxpool.Pool, ctx context.Context, newJoke schema.Joke) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sql, args, err := query.
|
||||||
|
Update("jokesbapak2").
|
||||||
|
Set("link", newJoke.Link).
|
||||||
|
Set("creator", newJoke.Creator).
|
||||||
|
Where(squirrel.Eq{"id": newJoke.ID}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package joke_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetAllJSONJoke(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = joke.SetAllJSONJoke(db, ctx, memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetTotalJoke(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = joke.SetTotalJoke(db, ctx, memory)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertJokeIntoDB(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
data := schema.Joke{
|
||||||
|
ID: 1,
|
||||||
|
Link: "link1",
|
||||||
|
Creator: 1,
|
||||||
|
}
|
||||||
|
err := joke.InsertJokeIntoDB(db, ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSingleJoke(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = joke.DeleteSingleJoke(db, ctx, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateJoke(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
err = conn.BeginFunc(ctx, func(t pgx.Tx) error {
|
||||||
|
_, err := t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "administrators"
|
||||||
|
(id, key, token, last_used)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4),
|
||||||
|
($5, $6, $7, $8)`,
|
||||||
|
administratorsData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = t.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO "jokesbapak2"
|
||||||
|
(id, link, creator)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3),
|
||||||
|
($4, $5, $6),
|
||||||
|
($7, $8, $9)`,
|
||||||
|
jokesData...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newJoke := schema.Joke{
|
||||||
|
ID: 1,
|
||||||
|
Link: "link1",
|
||||||
|
Creator: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = joke.UpdateJoke(db, ctx, newJoke)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("record not found")
|
||||||
|
var ErrEmpty = errors.New("record is empty")
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
type Joke struct {
|
||||||
|
ID int `json:"id" form:"id" db:"id"`
|
||||||
|
Link string `json:"link" form:"link" db:"link"`
|
||||||
|
Creator int `json:"creator" form:"creator" db:"creator"`
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package models
|
package schema
|
||||||
|
|
||||||
type Submission struct {
|
type Submission struct {
|
||||||
ID int `json:"id,omitempty" db:"id"`
|
ID int `json:"id,omitempty" db:"id"`
|
||||||
|
@ -17,21 +17,8 @@ type SubmissionQuery struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponseSubmission struct {
|
type ResponseSubmission struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Data Submission `json:"data,omitempty"`
|
Submission Submission `json:"submission,omitempty"`
|
||||||
}
|
AuthorPage string `json:"author_page,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"`
|
|
||||||
}
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package submit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aldy505/bob"
|
||||||
|
"github.com/georgysavva/scany/pgxscan"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSubmittedItems(db *pgxpool.Pool, ctx context.Context, queries schema.SubmissionQuery) ([]schema.Submission, error) {
|
||||||
|
var err error
|
||||||
|
var limit int
|
||||||
|
var offset int
|
||||||
|
var approved bool
|
||||||
|
|
||||||
|
if queries.Limit != "" {
|
||||||
|
limit, err = strconv.Atoi(queries.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if queries.Page != "" {
|
||||||
|
page, err := strconv.Atoi(queries.Page)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
|
||||||
|
}
|
||||||
|
offset = (page - 1) * 20
|
||||||
|
}
|
||||||
|
|
||||||
|
if queries.Approved != "" {
|
||||||
|
approved, err = strconv.ParseBool(queries.Approved)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var status int
|
||||||
|
|
||||||
|
if approved {
|
||||||
|
status = 1
|
||||||
|
} else {
|
||||||
|
status = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sql, args, err := GetterQueryBuilder(queries, status, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var submissions []schema.Submission
|
||||||
|
results, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
}
|
||||||
|
defer results.Close()
|
||||||
|
|
||||||
|
err = pgxscan.ScanAll(&submissions, results)
|
||||||
|
if err != nil {
|
||||||
|
return []schema.Submission{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetterQueryBuilder(queries schema.SubmissionQuery, status, limit, offset int) (string, []interface{}, error) {
|
||||||
|
var sql string
|
||||||
|
var args []interface{}
|
||||||
|
var sqlQuery strings.Builder
|
||||||
|
|
||||||
|
sqlQuery.WriteString("SELECT * FROM submission WHERE TRUE")
|
||||||
|
|
||||||
|
if queries.Author != "" {
|
||||||
|
sqlQuery.WriteString(" AND author = ?")
|
||||||
|
escapedAuthor, err := url.QueryUnescape(queries.Author)
|
||||||
|
if err != nil {
|
||||||
|
return sql, args, err
|
||||||
|
|
||||||
|
}
|
||||||
|
args = append(args, escapedAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if queries.Approved != "" {
|
||||||
|
sqlQuery.WriteString(" AND status = ?")
|
||||||
|
args = append(args, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
sqlQuery.WriteString(" LIMIT " + strconv.Itoa(limit))
|
||||||
|
} else {
|
||||||
|
sqlQuery.WriteString(" LIMIT 20")
|
||||||
|
}
|
||||||
|
|
||||||
|
if queries.Page != "" {
|
||||||
|
sqlQuery.WriteString(" OFFSET " + strconv.Itoa(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar)
|
||||||
|
|
||||||
|
return sql, args, nil
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package submit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"jokes-bapak2-api/core/submit"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSubmittedItems(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
c, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer c.Release()
|
||||||
|
|
||||||
|
tx, err := c.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO submission
|
||||||
|
(id, link, created_at, author, status)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4, $5),
|
||||||
|
($6, $7, $8, $9, $10)`,
|
||||||
|
submissionData...)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := submit.GetSubmittedItems(db, ctx, schema.SubmissionQuery{})
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) != 2 {
|
||||||
|
t.Error("expected 2 items, got", len(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetterQueryBuilder(t *testing.T) {
|
||||||
|
s, _, err := submit.GetterQueryBuilder(schema.SubmissionQuery{}, 0, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != "SELECT * FROM submission WHERE TRUE LIMIT 20" {
|
||||||
|
t.Error("expected query to be", "SELECT * FROM submission WHERE TRUE LIMIT 20", "got", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, i, err := submit.GetterQueryBuilder(schema.SubmissionQuery{
|
||||||
|
Author: "Test <example@test.com>",
|
||||||
|
Approved: "true",
|
||||||
|
Page: "2",
|
||||||
|
}, 2, 15, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != "SELECT * FROM submission WHERE TRUE AND author = $1 AND status = $2 LIMIT 15 OFFSET 10" {
|
||||||
|
t.Error(
|
||||||
|
"expected query to be",
|
||||||
|
"SELECT * FROM submission WHERE TRUE AND author = $1 AND status = $2 LIMIT 15 OFFSET 15",
|
||||||
|
"got:",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i[0].(string) != "Test <example@test.com>" {
|
||||||
|
t.Error("expected first arg to be Test <example@test.com>, got:", i[0].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if i[1].(int) != 2 {
|
||||||
|
t.Error("expected second arg to be 1, got:", i[1].(int))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
package submit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *pgxpool.Pool
|
||||||
|
|
||||||
|
var submissionData = []interface{}{
|
||||||
|
1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test <test@example.com>", 0,
|
||||||
|
2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test <test@example.com>", 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
defer Teardown()
|
||||||
|
Setup()
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup() {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err = pgxpool.ConnectConfig(ctx, poolConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`CREATE TABLE IF NOT EXISTS submission (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
link VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
created_at VARCHAR(255),
|
||||||
|
author VARCHAR(255) NOT NULL,
|
||||||
|
status SMALLINT DEFAULT 0
|
||||||
|
)`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Teardown() (err error) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, "DROP TABLE IF EXISTS submission CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "DROP TABLE IF EXISTS jokesbapak2 CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "DROP TABLE IF EXISTS administrators CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Flush() error {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
_, err = conn.Exec(ctx, "TRUNCATE TABLE submission RESTART IDENTITY CASCADE")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,17 +1,22 @@
|
||||||
package core
|
package submit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"jokes-bapak2-api/app/v1/models"
|
"jokes-bapak2-api/core/schema"
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/utils"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/georgysavva/scany/pgxscan"
|
||||||
"github.com/gojek/heimdall/v7/httpclient"
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
"github.com/pquerna/ffjson/ffjson"
|
"github.com/pquerna/ffjson/ffjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,7 +76,7 @@ func UploadImage(client *httpclient.Client, image io.Reader) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data models.ImageAPI
|
var data schema.ImageAPI
|
||||||
err = ffjson.Unmarshal(responseBody, &data)
|
err = ffjson.Unmarshal(responseBody, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -79,3 +84,39 @@ func UploadImage(client *httpclient.Client, image io.Reader) (string, error) {
|
||||||
|
|
||||||
return data.Data.URL, nil
|
return data.Data.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SubmitJoke(db *pgxpool.Pool, ctx context.Context, s schema.Submission, link string) (schema.Submission, error) {
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return schema.Submission{}, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
sql, args, err := query.
|
||||||
|
Insert("submission").
|
||||||
|
Columns("link", "created_at", "author").
|
||||||
|
Values(link, now, s.Author).
|
||||||
|
Suffix("RETURNING id,created_at,link,author,status").
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return schema.Submission{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var submission schema.Submission
|
||||||
|
result, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return schema.Submission{}, err
|
||||||
|
}
|
||||||
|
defer result.Close()
|
||||||
|
|
||||||
|
err = pgxscan.ScanOne(&submission, result)
|
||||||
|
if err != nil {
|
||||||
|
return schema.Submission{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return submission, nil
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package submit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"jokes-bapak2-api/core/submit"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubmitJoke(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer Flush()
|
||||||
|
|
||||||
|
s, err := submit.SubmitJoke(db, ctx, schema.Submission{Author: "Test <example@test.com>"}, "https://example.net/img.png")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("an error was thrown:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Link != "https://example.net/img.png" {
|
||||||
|
t.Error("link is not correct, got:", s.Link)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
|
@ -0,0 +1,40 @@
|
||||||
|
package validator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateAuthor_False(t *testing.T) {
|
||||||
|
v := validator.ValidateAuthor("Test Author")
|
||||||
|
if v != false {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
v = validator.ValidateAuthor("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec.")
|
||||||
|
if v != false {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
v = validator.ValidateAuthor("")
|
||||||
|
if v != false {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
v = validator.ValidateAuthor("Test <Author")
|
||||||
|
if v != false {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
v = validator.ValidateAuthor("Test <Author>")
|
||||||
|
if v != false {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAuthor_True(t *testing.T) {
|
||||||
|
v := validator.ValidateAuthor("Test Author <author@mail.com>")
|
||||||
|
if v != true {
|
||||||
|
t.Error("Expected true, got false")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package core
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package validator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckImageValidity_Error(t *testing.T) {
|
||||||
|
client := httpclient.NewClient()
|
||||||
|
b, err := validator.CheckImageValidity(client, "http://lorem-ipsum")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if b {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != "URL must use HTTPS protocol" {
|
||||||
|
t.Error("Expected error to be URL must use HTTPS protocol, got:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckImageValidity_False(t *testing.T) {
|
||||||
|
client := httpclient.NewClient()
|
||||||
|
|
||||||
|
b, err := validator.CheckImageValidity(client, "https://www.youtube.com/watch?v=yTJV6T37Reo")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Expected nil, got error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if b {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckImageValidity_True(t *testing.T) {
|
||||||
|
client := httpclient.NewClient()
|
||||||
|
|
||||||
|
b, err := validator.CheckImageValidity(client, "https://i.ytimg.com/vi/yTJV6T37Reo/maxresdefault.jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Expected nil, got error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b {
|
||||||
|
t.Error("Expected true, got false")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate if link already exists
|
||||||
|
func JokeLinkExists(db *pgxpool.Pool, ctx context.Context, link string) (bool, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
|
||||||
|
sql, args, err := query.
|
||||||
|
Select("link").
|
||||||
|
From("jokesbapak2").
|
||||||
|
Where(squirrel.Eq{"link": link}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var validateLink string
|
||||||
|
err = conn.QueryRow(ctx, sql, args...).Scan(&validateLink)
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateLink != "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the joke exists
|
||||||
|
func JokeIDExists(db *pgxpool.Pool, ctx context.Context, id int) (bool, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||||
|
sql, args, err := query.
|
||||||
|
Select("id").
|
||||||
|
From("jokesbapak2").
|
||||||
|
Where(squirrel.Eq{"id": id}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jokeID int
|
||||||
|
err = conn.QueryRow(ctx, sql, args...).Scan(&jokeID)
|
||||||
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SubmitLinkExists(db *pgxpool.Pool, ctx context.Context, query squirrel.StatementBuilderType, link string) (bool, error) {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
sql, args, err := query.
|
||||||
|
Select("link").
|
||||||
|
From("submission").
|
||||||
|
Where(squirrel.Eq{"link": link}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var validateLink string
|
||||||
|
err = conn.QueryRow(ctx, sql, args...).Scan(&validateLink)
|
||||||
|
if err != nil && err != pgx.ErrNoRows {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && validateLink != "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
|
@ -19,6 +19,10 @@
|
||||||
"url": "https://jokesbapak2.herokuapp.com/v1",
|
"url": "https://jokesbapak2.herokuapp.com/v1",
|
||||||
"description": "Production"
|
"description": "Production"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://jokesbapak2.herokuapp.com",
|
||||||
|
"description": "Production"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "http://localhost:5000",
|
"url": "http://localhost:5000",
|
||||||
"description": "Development"
|
"description": "Development"
|
|
@ -16,6 +16,8 @@ info:
|
||||||
servers:
|
servers:
|
||||||
- url: "https://jokesbapak2.herokuapp.com/v1"
|
- url: "https://jokesbapak2.herokuapp.com/v1"
|
||||||
description: Production
|
description: Production
|
||||||
|
- url: "https://jokesbapak2.herokuapp.com"
|
||||||
|
description: Production
|
||||||
- url: "http://localhost:5000"
|
- url: "http://localhost:5000"
|
||||||
description: Development
|
description: Development
|
||||||
paths:
|
paths:
|
34
api/go.mod
34
api/go.mod
|
@ -1,6 +1,6 @@
|
||||||
module jokes-bapak2-api
|
module jokes-bapak2-api
|
||||||
|
|
||||||
go 1.16
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/squirrel v1.5.0
|
github.com/Masterminds/squirrel v1.5.0
|
||||||
|
@ -13,14 +13,44 @@ require (
|
||||||
github.com/go-redis/redis/v8 v8.11.0
|
github.com/go-redis/redis/v8 v8.11.0
|
||||||
github.com/gofiber/fiber/v2 v2.15.0
|
github.com/gofiber/fiber/v2 v2.15.0
|
||||||
github.com/gojek/heimdall/v7 v7.0.2
|
github.com/gojek/heimdall/v7 v7.0.2
|
||||||
|
github.com/jackc/pgx v3.6.2+incompatible
|
||||||
github.com/jackc/pgx/v4 v4.12.0
|
github.com/jackc/pgx/v4 v4.12.0
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7
|
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
|
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
|
||||||
|
github.com/jackc/pgconn v1.9.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/pgtype v1.8.0 // indirect
|
||||||
|
github.com/jackc/puddle v1.1.3 // indirect
|
||||||
|
github.com/klauspost/compress v1.12.2 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.3.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.26.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
|
||||||
|
golang.org/x/text v0.3.6 // indirect
|
||||||
|
)
|
||||||
|
|
|
@ -98,8 +98,9 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
|
||||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||||
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||||
github.com/georgysavva/scany v0.2.9 h1:Xt6rjYpHnMClTm/g+oZTnoSxUwiln5GqMNU+QeLNHQU=
|
github.com/georgysavva/scany v0.2.9 h1:Xt6rjYpHnMClTm/g+oZTnoSxUwiln5GqMNU+QeLNHQU=
|
||||||
github.com/georgysavva/scany v0.2.9/go.mod h1:yeOeC1BdIdl6hOwy8uefL2WNSlseFzbhlG/frrh65SA=
|
github.com/georgysavva/scany v0.2.9/go.mod h1:yeOeC1BdIdl6hOwy8uefL2WNSlseFzbhlG/frrh65SA=
|
||||||
|
@ -214,6 +215,8 @@ github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9
|
||||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependencies struct {
|
||||||
|
DB *pgxpool.Pool
|
||||||
|
Redis *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dependencies) Health(c *fiber.Ctx) error {
|
||||||
|
conn, err := d.DB.Acquire(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
// Ping REDIS database
|
||||||
|
err = d.Redis.Ping(c.Context()).Err()
|
||||||
|
if err != nil {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusServiceUnavailable).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "REDIS: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Query(c.Context(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1")
|
||||||
|
if err != nil {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusServiceUnavailable).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "POSTGRESQL: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package health
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependencies struct {
|
||||||
|
DB *pgxpool.Pool
|
||||||
|
Redis *redis.Client
|
||||||
|
Memory *bigcache.BigCache
|
||||||
|
HTTP *httpclient.Client
|
||||||
|
Query squirrel.StatementBuilderType
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
core "jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) AddNewJoke(c *fiber.Ctx) error {
|
||||||
|
var body schema.Joke
|
||||||
|
err := c.BodyParser(&body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check link validity
|
||||||
|
valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusBadRequest).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "URL provided is not a valid image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
validateLink, err := validator.JokeLinkExists(d.DB, c.Context(), body.Link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validateLink {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(Error{
|
||||||
|
Error: "Given link is already on the jokesbapak2 database",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.InsertJokeIntoDB(
|
||||||
|
d.DB,
|
||||||
|
c.Context(),
|
||||||
|
schema.Joke{
|
||||||
|
Link: body.Link,
|
||||||
|
Creator: c.Locals("userID").(int),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusCreated).
|
||||||
|
JSON(ResponseJoke{
|
||||||
|
Link: body.Link,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
core "jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) DeleteJoke(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
validate, err := validator.JokeIDExists(d.DB, c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if validate {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusNotAcceptable).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "specified joke id does not exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.DeleteSingleJoke(d.DB, c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusOK).
|
||||||
|
JSON(ResponseJoke{
|
||||||
|
Message: "specified joke id has been deleted",
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
core "jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"jokes-bapak2-api/utils"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) TodayJoke(c *fiber.Ctx) error {
|
||||||
|
// check from handler.Redis if today's joke already exists
|
||||||
|
// send the joke if exists
|
||||||
|
// get a new joke if it's not, then send it.
|
||||||
|
var joke Today
|
||||||
|
err := d.Redis.MGet(c.Context(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eq, err := utils.IsToday(joke.Date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if eq {
|
||||||
|
c.Set("Content-Type", joke.ContentType)
|
||||||
|
return c.Status(fiber.StatusOK).Send([]byte(joke.Image))
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := core.GetRandomJokeFromDB(d.DB, c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := d.HTTP.Get(link, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
err = d.Redis.MSet(c.Context(), map[string]interface{}{
|
||||||
|
"today:link": link,
|
||||||
|
"today:date": now,
|
||||||
|
"today:image": string(data),
|
||||||
|
"today:contentType": response.Header.Get("content-type"),
|
||||||
|
}).Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", response.Header.Get("content-type"))
|
||||||
|
return c.Status(fiber.StatusOK).Send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dependencies) SingleJoke(c *fiber.Ctx) error {
|
||||||
|
checkCache, err := core.CheckJokesCache(d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkCache {
|
||||||
|
jokes, err := core.GetAllJSONJokes(d.DB, c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := core.GetRandomJokeFromCache(d.Memory)
|
||||||
|
if err != nil && !errors.Is(err, schema.ErrEmpty) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image data
|
||||||
|
response, err := d.HTTP.Get(link, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", response.Header.Get("content-type"))
|
||||||
|
return c.Status(fiber.StatusOK).Send(data)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dependencies) JokeByID(c *fiber.Ctx) error {
|
||||||
|
checkCache, err := core.CheckJokesCache(d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkCache {
|
||||||
|
jokes, err := core.GetAllJSONJokes(d.DB, c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Memory.Set("jokes", jokes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := core.GetCachedJokeByID(d.Memory, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if link == "" {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusNotFound).
|
||||||
|
Send([]byte("Requested ID was not found."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image data
|
||||||
|
response, err := d.HTTP.Get(link, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", response.Header.Get("content-type"))
|
||||||
|
return c.Status(fiber.StatusOK).Send(data)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
core "jokes-bapak2-api/core/joke"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) TotalJokes(c *fiber.Ctx) error {
|
||||||
|
checkTotal, err := core.CheckTotalJokesCache(d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkTotal {
|
||||||
|
err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total, err := d.Memory.Get("total")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, bigcache.ErrEntryNotFound) {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusInternalServerError).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "no data found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusOK).
|
||||||
|
JSON(ResponseJoke{
|
||||||
|
Message: strconv.Itoa(int(total[0])),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package joke
|
||||||
|
|
||||||
|
import (
|
||||||
|
core "jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) UpdateJoke(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
// Check if the joke exists
|
||||||
|
|
||||||
|
jokeExists, err := core.CheckJokeExists(d.DB, c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jokeExists {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusNotAcceptable).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "specified joke id does not exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
body := new(schema.Joke)
|
||||||
|
err = c.BodyParser(&body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check link validity
|
||||||
|
valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusBadRequest).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "URL provided is not a valid image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
newID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newCreator, err := strconv.Atoi(c.Locals("userID").(string))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedJoke := schema.Joke{
|
||||||
|
Link: body.Link,
|
||||||
|
Creator: newCreator,
|
||||||
|
ID: newID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.UpdateJoke(d.DB, c.Context(), updatedJoke)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.SetTotalJoke(d.DB, c.Context(), d.Memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusOK).
|
||||||
|
JSON(ResponseJoke{
|
||||||
|
Message: "specified joke id has been updated",
|
||||||
|
Link: body.Link,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
package models
|
package joke
|
||||||
|
|
||||||
type Joke struct {
|
type ResponseJoke struct {
|
||||||
ID int `json:"id" form:"id" db:"id"`
|
Link string `json:"link,omitempty"`
|
||||||
Link string `json:"link" form:"link" db:"link"`
|
Message string `json:"message,omitempty"`
|
||||||
Creator int `json:"creator" form:"creator" db:"creator"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Today struct {
|
type Today struct {
|
||||||
|
@ -12,7 +11,6 @@ type Today struct {
|
||||||
ContentType string `redis:"today:contentType"`
|
ContentType string `redis:"today:contentType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponseJoke struct {
|
type Error struct {
|
||||||
Link string `json:"link,omitempty"`
|
Error string `json:"error"`
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package submit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependencies struct {
|
||||||
|
DB *pgxpool.Pool
|
||||||
|
Redis *redis.Client
|
||||||
|
Memory *bigcache.BigCache
|
||||||
|
HTTP *httpclient.Client
|
||||||
|
Query squirrel.StatementBuilderType
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package submit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
core "jokes-bapak2-api/core/submit"
|
||||||
|
"jokes-bapak2-api/core/validator"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error {
|
||||||
|
conn, err := d.DB.Acquire(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
var body schema.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(schema.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(schema.Error{
|
||||||
|
Error: "An author key consisting on the format \"yourname <youremail@mail>\" must be supplied",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Validate format
|
||||||
|
valid := validator.ValidateAuthor(body.Author)
|
||||||
|
if !valid {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
|
||||||
|
Error: "Please stick to the format of \"yourname <youremail@mail>\" and within 200 characters",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var link string
|
||||||
|
|
||||||
|
// Check link validity if link was provided
|
||||||
|
if body.Link != "" {
|
||||||
|
valid, err := validator.CheckImageValidity(d.HTTP, body.Link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(schema.Error{
|
||||||
|
Error: "URL provided is not a valid image",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
link = body.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
// If image was provided
|
||||||
|
if body.Image != "" {
|
||||||
|
image := strings.NewReader(body.Image)
|
||||||
|
|
||||||
|
link, err = core.UploadImage(d.HTTP, image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if link already exists
|
||||||
|
validateLink, err := validator.SubmitLinkExists(d.DB, c.Context(), d.Query, link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if validateLink {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(schema.Error{
|
||||||
|
Error: "Given link is already on the submission queue.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
submission, err := core.SubmitJoke(d.DB, c.Context(), body, link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusCreated).
|
||||||
|
JSON(schema.ResponseSubmission{
|
||||||
|
Message: "Joke submitted. Please wait for a few days for admin to approve your submission.",
|
||||||
|
Submission: submission,
|
||||||
|
AuthorPage: "/submit?author=" + url.QueryEscape(body.Author),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package submit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/core/schema"
|
||||||
|
core "jokes-bapak2-api/core/submit"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) GetSubmission(c *fiber.Ctx) error {
|
||||||
|
query := new(schema.SubmissionQuery)
|
||||||
|
err := c.QueryParser(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
submissions, err := core.GetSubmittedItems(d.DB, c.Context(), *query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusOK).
|
||||||
|
JSON(fiber.Map{
|
||||||
|
"count": len(submissions),
|
||||||
|
"jokes": submissions,
|
||||||
|
})
|
||||||
|
}
|
118
api/main.go
118
api/main.go
|
@ -4,22 +4,100 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"jokes-bapak2-api/core/joke"
|
||||||
|
"jokes-bapak2-api/platform/database"
|
||||||
|
"jokes-bapak2-api/routes"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "jokes-bapak2-api/app/v1"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/etag"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
timeoutDefault, _ := time.ParseDuration("1m")
|
// Setup PostgreSQL
|
||||||
|
poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln("Unable to create pool config", err)
|
||||||
|
}
|
||||||
|
poolConfig.MaxConnIdleTime = time.Minute * 3
|
||||||
|
poolConfig.MaxConnLifetime = time.Minute * 5
|
||||||
|
poolConfig.MaxConns = 15
|
||||||
|
poolConfig.MinConns = 4
|
||||||
|
|
||||||
|
db, err := pgxpool.ConnectConfig(context.Background(), poolConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln("Unable to create connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Setup Redis
|
||||||
|
opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
rdb := redis.NewClient(opt)
|
||||||
|
defer rdb.Close()
|
||||||
|
|
||||||
|
// Setup In Memory
|
||||||
|
memory, err := bigcache.NewBigCache(bigcache.DefaultConfig(6 * time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
defer memory.Close()
|
||||||
|
|
||||||
|
// Setup Sentry
|
||||||
|
err = sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: os.Getenv("SENTRY_DSN"),
|
||||||
|
Environment: os.Getenv("ENV"),
|
||||||
|
AttachStacktrace: true,
|
||||||
|
// Enable printing of SDK debug messages.
|
||||||
|
// Useful when getting started or trying to figure something out.
|
||||||
|
Debug: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
defer sentry.Flush(2 * time.Second)
|
||||||
|
|
||||||
|
setupCtx, setupCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute*4))
|
||||||
|
defer setupCancel()
|
||||||
|
|
||||||
|
err = database.Populate(db, setupCtx)
|
||||||
|
if err != nil {
|
||||||
|
sentry.CaptureException(err)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = joke.SetAllJSONJoke(db, setupCtx, memory)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
err = joke.SetTotalJoke(db, setupCtx, memory)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutDefault := time.Minute * 1
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
ReadTimeout: timeoutDefault,
|
ReadTimeout: timeoutDefault,
|
||||||
WriteTimeout: timeoutDefault,
|
WriteTimeout: timeoutDefault,
|
||||||
|
CaseSensitive: true,
|
||||||
|
DisableKeepalive: true,
|
||||||
|
ErrorHandler: errorHandler,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use(limiter.New(limiter.Config{
|
app.Use(limiter.New(limiter.Config{
|
||||||
|
@ -27,11 +105,21 @@ func main() {
|
||||||
Expiration: 1 * time.Minute,
|
Expiration: 1 * time.Minute,
|
||||||
LimitReached: limitHandler,
|
LimitReached: limitHandler,
|
||||||
}))
|
}))
|
||||||
app.Use(favicon.New(favicon.Config{
|
|
||||||
File: "./favicon.png",
|
|
||||||
}))
|
|
||||||
|
|
||||||
app.Mount("/v1", v1.New())
|
app.Use(cors.New())
|
||||||
|
app.Use(etag.New())
|
||||||
|
|
||||||
|
route := routes.Dependencies{
|
||||||
|
DB: db,
|
||||||
|
Redis: rdb,
|
||||||
|
Memory: memory,
|
||||||
|
HTTP: httpclient.NewClient(httpclient.WithHTTPTimeout(10 * time.Second)),
|
||||||
|
Query: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar),
|
||||||
|
App: app,
|
||||||
|
}
|
||||||
|
route.Health()
|
||||||
|
route.Joke()
|
||||||
|
route.Submit()
|
||||||
|
|
||||||
// Start server (with or without graceful shutdown).
|
// Start server (with or without graceful shutdown).
|
||||||
if os.Getenv("ENV") == "development" {
|
if os.Getenv("ENV") == "development" {
|
||||||
|
@ -47,6 +135,14 @@ func limitHandler(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errorHandler(c *fiber.Ctx, err error) error {
|
||||||
|
log.Println(err)
|
||||||
|
sentry.CaptureException(err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Something went wrong on our end",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.
|
// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.
|
||||||
func StartServerWithGracefulShutdown(a *fiber.App) {
|
func StartServerWithGracefulShutdown(a *fiber.App) {
|
||||||
// Create channel for idle connections.
|
// Create channel for idle connections.
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/core/administrator"
|
||||||
|
|
||||||
|
phccrypto "github.com/aldy505/phc-crypto"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequireAuth(db *pgxpool.Pool) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
var auth Auth
|
||||||
|
err := c.BodyParser(&auth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := administrator.CheckKeyExists(db, c.Context(), auth.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusForbidden).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "Invalid key",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto, err := phccrypto.Use(phccrypto.Argon2, phccrypto.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
verify, err := crypto.Verify(token, auth.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if verify {
|
||||||
|
id, err := administrator.GetUserID(db, c.Context(), auth.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("userID", id)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.
|
||||||
|
Status(fiber.StatusForbidden).
|
||||||
|
JSON(Error{
|
||||||
|
Error: "Invalid key",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package models
|
package middleware
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
ID int `json:"id" form:"id" db:"id"`
|
ID int `json:"id" form:"id" db:"id"`
|
||||||
|
@ -6,3 +6,7 @@ type Auth struct {
|
||||||
Token string `json:"token" form:"token" db:"token"`
|
Token string `json:"token" form:"token" db:"token"`
|
||||||
LastUsed string `json:"last_used" form:"last_used" db:"last_used"`
|
LastUsed string `json:"last_used" form:"last_used" db:"last_used"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"jokes-bapak2-api/app/v1/models"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
@ -21,7 +20,7 @@ func OnlyIntegerAsID() fiber.Handler {
|
||||||
|
|
||||||
return c.
|
return c.
|
||||||
Status(fiber.StatusBadRequest).
|
Status(fiber.StatusBadRequest).
|
||||||
JSON(models.Error{
|
JSON(Error{
|
||||||
Error: "only numbers are allowed as ID",
|
Error: "only numbers are allowed as ID",
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -2,24 +2,46 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/aldy505/bob"
|
"github.com/aldy505/bob"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup the table connection, create table if not exists
|
// Setup the table connection, create table if not exists
|
||||||
func Setup() error {
|
func Populate(db *pgxpool.Pool, ctx context.Context) error {
|
||||||
db := New()
|
err := setupAuthTable(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// administrators table
|
err = setupJokesTable(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupSubmissionTable(db, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAuthTable(db *pgxpool.Pool, ctx context.Context) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
|
// Check if table exists
|
||||||
var tableAuthExists bool
|
var tableAuthExists bool
|
||||||
err := db.QueryRow(context.Background(), `SELECT EXISTS (
|
err = conn.QueryRow(ctx, `SELECT EXISTS (
|
||||||
SELECT FROM information_schema.tables
|
SELECT FROM information_schema.tables
|
||||||
WHERE table_schema = 'public'
|
WHERE table_schema = 'public'
|
||||||
AND table_name = 'administrators'
|
AND table_name = 'administrators'
|
||||||
);`).Scan(&tableAuthExists)
|
);`).Scan(&tableAuthExists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("16 - failed on checking table: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,33 +49,37 @@ func Setup() error {
|
||||||
sql, _, err := bob.
|
sql, _, err := bob.
|
||||||
CreateTable("administrators").
|
CreateTable("administrators").
|
||||||
AddColumn(bob.ColumnDef{Name: "id", Type: "SERIAL", Extras: []string{"PRIMARY KEY"}}).
|
AddColumn(bob.ColumnDef{Name: "id", Type: "SERIAL", Extras: []string{"PRIMARY KEY"}}).
|
||||||
StringColumn("key", "UNIQUE").
|
StringColumn("key", "NOT NULL", "UNIQUE").
|
||||||
TextColumn("token").
|
TextColumn("token").
|
||||||
StringColumn("last_used").
|
StringColumn("last_used").
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("17 - failed on table creation: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Query(context.Background(), sql)
|
_, err = conn.Exec(ctx, sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("18 - failed on table creation: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Jokesbapak2 table
|
func setupJokesTable(db *pgxpool.Pool, ctx context.Context) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
// Check if table exists
|
// Check if table exists
|
||||||
var tableJokesExists bool
|
var tableJokesExists bool
|
||||||
err = db.QueryRow(context.Background(), `SELECT EXISTS (
|
err = conn.QueryRow(ctx, `SELECT EXISTS (
|
||||||
SELECT FROM information_schema.tables
|
SELECT FROM information_schema.tables
|
||||||
WHERE table_schema = 'public'
|
WHERE table_schema = 'public'
|
||||||
AND table_name = 'jokesbapak2'
|
AND table_name = 'jokesbapak2'
|
||||||
);`).Scan(&tableJokesExists)
|
);`).Scan(&tableJokesExists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("10 - failed on checking table: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,28 +91,33 @@ func Setup() error {
|
||||||
AddColumn(bob.ColumnDef{Name: "creator", Type: "INT", Extras: []string{"NOT NULL", "REFERENCES \"administrators\" (\"id\")"}}).
|
AddColumn(bob.ColumnDef{Name: "creator", Type: "INT", Extras: []string{"NOT NULL", "REFERENCES \"administrators\" (\"id\")"}}).
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("11 - failed on table creation: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Query(context.Background(), sql)
|
_, err = conn.Exec(ctx, sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("12 - failed on table creation: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submission table
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSubmissionTable(db *pgxpool.Pool, ctx context.Context) error {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Release()
|
||||||
|
|
||||||
//Check if table exists
|
//Check if table exists
|
||||||
var tableSubmissionExists bool
|
var tableSubmissionExists bool
|
||||||
err = db.QueryRow(context.Background(), `SELECT EXISTS (
|
err = conn.QueryRow(ctx, `SELECT EXISTS (
|
||||||
SELECT FROM information_schema.tables
|
SELECT FROM information_schema.tables
|
||||||
WHERE table_schema = 'public'
|
WHERE table_schema = 'public'
|
||||||
AND table_name = 'submission'
|
AND table_name = 'submission'
|
||||||
);`).Scan(&tableJokesExists)
|
);`).Scan(&tableSubmissionExists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("13 - failed on checking table: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,12 +131,12 @@ func Setup() error {
|
||||||
AddColumn(bob.ColumnDef{Name: "status", Type: "SMALLINT", Extras: []string{"DEFAULT 0"}}).
|
AddColumn(bob.ColumnDef{Name: "status", Type: "SMALLINT", Extras: []string{"DEFAULT 0"}}).
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("14 - failed on table creation: ", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Query(context.Background(), sql)
|
_, err = conn.Exec(ctx, sql)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("15 - failed on table creation: ", err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Access the data from your HTTP Request software (Postman or Insomnia)
|
||||||
|
-- with this auth:
|
||||||
|
-- key: test
|
||||||
|
-- token: password
|
||||||
|
|
||||||
|
INSERT INTO "administrators" ("id", "key", "token", "last_used") VALUES
|
||||||
|
(1, 'test', '$argon2id$v=19$m=65536,t=16,p=4$3a08c79fbf2222467a623df9a9ebf75802c65a4f9be36eb1df2f5d2052d53cb7$ce434bd38f7ba1fc1f2eb773afb8a1f7f2dad49140803ac6cb9d7256ce9826fb3b4afa1e2488da511c852fc6c33a76d5657eba6298a8e49d617b9972645b7106', '');
|
||||||
|
|
||||||
|
-- 10 jokes is enough right?
|
||||||
|
|
||||||
|
INSERT INTO "jokesbapak2" ("link", "creator") VALUES
|
||||||
|
('https://picsum.photos/id/1000/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1001/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1002/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1003/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1004/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1005/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1006/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1010/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1008/500/500', 1),
|
||||||
|
('https://picsum.photos/id/1009/500/500', 1);
|
|
@ -0,0 +1,19 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Masterminds/squirrel"
|
||||||
|
"github.com/allegro/bigcache/v3"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gojek/heimdall/v7/httpclient"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dependencies struct {
|
||||||
|
DB *pgxpool.Pool
|
||||||
|
Redis *redis.Client
|
||||||
|
Memory *bigcache.BigCache
|
||||||
|
HTTP *httpclient.Client
|
||||||
|
Query squirrel.StatementBuilderType
|
||||||
|
App *fiber.App
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/handler/health"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) Health() {
|
||||||
|
// Health check
|
||||||
|
deps := health.Dependencies{
|
||||||
|
DB: d.DB,
|
||||||
|
Redis: d.Redis,
|
||||||
|
}
|
||||||
|
|
||||||
|
d.App.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health)
|
||||||
|
d.App.Get("/v1/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"jokes-bapak2-api/handler/joke"
|
||||||
|
"jokes-bapak2-api/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dependencies) Joke() {
|
||||||
|
deps := joke.Dependencies{
|
||||||
|
DB: d.DB,
|
||||||
|
Redis: d.Redis,
|
||||||
|
Memory: d.Memory,
|
||||||
|
HTTP: d.HTTP,
|
||||||
|
Query: d.Query,
|
||||||
|
}
|
||||||
|
// Single route
|
||||||
|
d.App.Get("/", deps.SingleJoke)
|
||||||
|
d.App.Get("/v1", deps.SingleJoke)
|
||||||
|
|
||||||
|
// Today's joke
|
||||||
|
d.App.Get("/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke)
|
||||||
|
d.App.Get("/v1/today", cache.New(cache.Config{Expiration: 6 * time.Hour}), deps.TodayJoke)
|
||||||
|
|
||||||
|
// Joke by ID
|
||||||
|
d.App.Get("/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID)
|
||||||
|
d.App.Get("/v1/id/:id", middleware.OnlyIntegerAsID(), deps.JokeByID)
|
||||||
|
|
||||||
|
// Count total jokes
|
||||||
|
d.App.Get("/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes)
|
||||||
|
d.App.Get("/v1/total", cache.New(cache.Config{Expiration: 15 * time.Minute}), deps.TotalJokes)
|
||||||
|
|
||||||
|
// Add new joke
|
||||||
|
d.App.Put("/", middleware.RequireAuth(d.DB), deps.AddNewJoke)
|
||||||
|
|
||||||
|
// Update a joke
|
||||||
|
d.App.Patch("/id/:id", middleware.RequireAuth(d.DB), middleware.OnlyIntegerAsID(), deps.UpdateJoke)
|
||||||
|
|
||||||
|
// Delete a joke
|
||||||
|
d.App.Delete("/id/:id", middleware.RequireAuth(d.DB), middleware.OnlyIntegerAsID(), deps.DeleteJoke)
|
||||||
|
}
|
|
@ -1,16 +1,24 @@
|
||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"jokes-bapak2-api/app/v1/handler/submit"
|
"jokes-bapak2-api/handler/submit"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Submit(app *fiber.App) *fiber.App {
|
func (d *Dependencies) Submit() {
|
||||||
|
deps := submit.Dependencies{
|
||||||
|
DB: d.DB,
|
||||||
|
Redis: d.Redis,
|
||||||
|
Memory: d.Memory,
|
||||||
|
HTTP: d.HTTP,
|
||||||
|
Query: d.Query,
|
||||||
|
}
|
||||||
|
|
||||||
// Get pending submitted joke
|
// Get pending submitted joke
|
||||||
app.Get(
|
d.App.Get(
|
||||||
"/submit",
|
"/submit",
|
||||||
cache.New(cache.Config{
|
cache.New(cache.Config{
|
||||||
Expiration: 5 * time.Minute,
|
Expiration: 5 * time.Minute,
|
||||||
|
@ -18,10 +26,8 @@ func Submit(app *fiber.App) *fiber.App {
|
||||||
return c.OriginalURL()
|
return c.OriginalURL()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
submit.GetSubmission)
|
deps.GetSubmission)
|
||||||
|
|
||||||
// Add a joke
|
// Add a joke
|
||||||
app.Post("/submit", submit.SubmitJoke)
|
d.App.Post("/submit", deps.SubmitJoke)
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/utils"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"jokes-bapak2-api/utils"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsToday_Today(t *testing.T) {
|
func TestIsToday_Today(t *testing.T) {
|
|
@ -1,10 +1,9 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"jokes-bapak2-api/utils"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseToJSONBody(t *testing.T) {
|
func TestParseToJSONBody(t *testing.T) {
|
|
@ -1,7 +1,7 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
"jokes-bapak2-api/utils"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"jokes-bapak2-api/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"jokes-bapak2-api/app/v1/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequest_Get(t *testing.T) {
|
func TestRequest_Get(t *testing.T) {
|
Loading…
Reference in New Issue