From c18a81a7aaab2022fada7e850ca804fbe6875727 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 18 Oct 2021 15:31:17 +0700 Subject: [PATCH] refactor: move everything to their own packages --- api/app/app.go | 27 +- api/app/core/administrator/id.go | 56 ++++ api/app/core/administrator/verify.go | 41 +++ .../core/{joke_getter.go => joke/getter.go} | 71 ++++- api/app/core/joke/getter_test.go | 73 +++++ api/app/core/joke/init_test.go | 153 ++++++++++ api/app/core/joke/setter.go | 133 +++++++++ api/app/core/joke_setter.go | 55 ---- api/app/core/schema/err.go | 6 + .../core/{schema.go => schema/image_api.go} | 13 +- api/app/core/schema/joke.go | 7 + .../{submit_setter.go => submit/setter.go} | 5 +- .../author.go} | 2 +- .../image.go} | 2 +- api/app/core/validator/joke.go | 68 +++++ api/app/handler/health/health.go | 13 +- api/app/handler/joke/dependencies.go | 13 +- api/app/handler/joke/joke_add.go | 66 ++--- api/app/handler/joke/joke_delete.go | 78 ++---- api/app/handler/joke/joke_get.go | 24 +- api/app/handler/joke/joke_total.go | 4 +- api/app/handler/joke/joke_update.go | 121 ++++---- api/app/handler/submit/dependencies.go | 13 +- api/app/handler/submit/submit_add.go | 23 +- api/app/handler/submit/submit_get.go | 2 +- api/app/middleware/auth.go | 58 +--- api/app/platform/database/create.go | 61 ++-- api/app/platform/database/placeholder.sql | 2 + api/app/routes/dependencies.go | 16 +- api/app/routes/health.go | 5 +- api/app/routes/joke.go | 17 +- api/app/routes/submit.go | 11 +- api/go.mod | 4 +- api/go.sum | 2 + api/main_test.go | 261 ------------------ 35 files changed, 821 insertions(+), 685 deletions(-) create mode 100644 api/app/core/administrator/id.go create mode 100644 api/app/core/administrator/verify.go rename api/app/core/{joke_getter.go => joke/getter.go} (62%) create mode 100644 api/app/core/joke/getter_test.go create mode 100644 api/app/core/joke/init_test.go create mode 100644 api/app/core/joke/setter.go delete mode 100644 api/app/core/joke_setter.go create mode 100644 api/app/core/schema/err.go rename api/app/core/{schema.go => schema/image_api.go} (52%) create mode 100644 api/app/core/schema/joke.go rename api/app/core/{submit_setter.go => submit/setter.go} (95%) rename api/app/core/{submit_validation.go => validator/author.go} (97%) rename api/app/core/{joke_validation.go => validator/image.go} (97%) create mode 100644 api/app/core/validator/joke.go delete mode 100644 api/main_test.go diff --git a/api/app/app.go b/api/app/app.go index 79aef00..9e02c8a 100644 --- a/api/app/app.go +++ b/api/app/app.go @@ -2,7 +2,7 @@ package app import ( "context" - "jokes-bapak2-api/app/core" + "jokes-bapak2-api/app/core/joke" "jokes-bapak2-api/app/platform/database" "jokes-bapak2-api/app/routes" "log" @@ -23,8 +23,6 @@ import ( ) func New() *fiber.App { - // Setup Context - ctx, cancel := context.WithCancel(context.Background()) // Setup PostgreSQL poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) @@ -36,7 +34,7 @@ func New() *fiber.App { poolConfig.MaxConns = 15 poolConfig.MinConns = 4 - db, err := pgxpool.ConnectConfig(ctx, poolConfig) + db, err := pgxpool.ConnectConfig(context.Background(), poolConfig) if err != nil { log.Panicln("Unable to create connection", err) } @@ -68,17 +66,18 @@ func New() *fiber.App { } defer sentry.Flush(2 * time.Second) - err = database.Setup(db, &ctx) + // TODO: These sequence below might be better wrapped as a Populate() function. + err = database.Setup(db) if err != nil { sentry.CaptureException(err) log.Panicln(err) } - err = core.SetAllJSONJoke(db, memory, &ctx) + err = joke.SetAllJSONJoke(db, context.Background(), memory) if err != nil { log.Panicln(err) } - err = core.SetTotalJoke(db, memory, &ctx) + err = joke.SetTotalJoke(db, context.Background(), memory) if err != nil { log.Panicln(err) } @@ -103,14 +102,12 @@ func New() *fiber.App { 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, - Context: &ctx, - Cancel: &cancel, + 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() diff --git a/api/app/core/administrator/id.go b/api/app/core/administrator/id.go new file mode 100644 index 0000000..8ff3f46 --- /dev/null +++ b/api/app/core/administrator/id.go @@ -0,0 +1,56 @@ +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) + + c1, err := db.Acquire(ctx) + if err != nil { + return 0, err + } + defer c1.Release() + + sql, args, err := query. + Update("administrators"). + Set("last_used", time.Now().UTC().Format(time.RFC3339)). + ToSql() + if err != nil { + return 0, err + } + + r, err := c1.Query(context.Background(), sql, args...) + if err != nil { + return 0, err + } + defer r.Close() + + c2, err := db.Acquire(ctx) + if err != nil { + return 0, err + } + defer c2.Release() + + sql, args, err = query. + Select("id"). + From("administrators"). + Where(squirrel.Eq{"key": key}). + ToSql() + if err != nil { + return 0, err + } + + var id int + err = c2.QueryRow(context.Background(), sql, args...).Scan(&id) + if err != nil { + return 0, err + } + + return id, nil +} diff --git a/api/app/core/administrator/verify.go b/api/app/core/administrator/verify.go new file mode 100644 index 0000000..9f6e93d --- /dev/null +++ b/api/app/core/administrator/verify.go @@ -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(context.Background(), sql, args...).Scan(&token) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return "", nil + } + return "", err + } + + return token, nil +} diff --git a/api/app/core/joke_getter.go b/api/app/core/joke/getter.go similarity index 62% rename from api/app/core/joke_getter.go rename to api/app/core/joke/getter.go index cdb913a..78a27a3 100644 --- a/api/app/core/joke_getter.go +++ b/api/app/core/joke/getter.go @@ -1,27 +1,31 @@ -package core +package joke import ( "context" "errors" + "jokes-bapak2-api/app/core/schema" "math/rand" + "strconv" + "github.com/Masterminds/squirrel" "github.com/allegro/bigcache/v3" "github.com/georgysavva/scany/pgxscan" + "github.com/jackc/pgx" "github.com/jackc/pgx/v4/pgxpool" "github.com/pquerna/ffjson/ffjson" ) // 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. -func GetAllJSONJokes(db *pgxpool.Pool, ctx *context.Context) ([]byte, error) { - conn, err := db.Acquire(*ctx) +func GetAllJSONJokes(db *pgxpool.Pool, ctx context.Context) ([]byte, error) { + conn, err := db.Acquire(ctx) if err != nil { - return nil, err + return []byte{}, err } defer conn.Release() - var jokes []Joke - results, err := conn.Query(*ctx, "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"") + var jokes []schema.Joke + results, err := conn.Query(context.Background(), "SELECT \"id\",\"link\" FROM \"jokesbapak2\" ORDER BY \"id\"") if err != nil { return nil, err } @@ -40,17 +44,33 @@ func GetAllJSONJokes(db *pgxpool.Pool, ctx *context.Context) ([]byte, error) { 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 + } + + var link string + err = conn.QueryRow(context.Background(), "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. func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) { jokes, err := memory.Get("jokes") if err != nil { if errors.Is(err, bigcache.ErrEntryNotFound) { - return "", ErrNotFound + return "", schema.ErrNotFound } return "", err } - var data []Joke + var data []schema.Joke err = ffjson.Unmarshal(jokes, &data) if err != nil { return "", nil @@ -59,7 +79,7 @@ func GetRandomJokeFromCache(memory *bigcache.BigCache) (string, error) { // Return an error if the database is empty dataLength := len(data) if dataLength == 0 { - return "", ErrEmpty + return "", schema.ErrEmpty } random := rand.Intn(dataLength) @@ -99,12 +119,12 @@ func GetCachedJokeByID(memory *bigcache.BigCache, id int) (string, error) { jokes, err := memory.Get("jokes") if err != nil { if errors.Is(err, bigcache.ErrEntryNotFound) { - return "", ErrNotFound + return "", schema.ErrNotFound } return "", err } - var data []Joke + var data []schema.Joke err = ffjson.Unmarshal(jokes, &data) if err != nil { return "", nil @@ -125,10 +145,37 @@ func GetCachedTotalJokes(memory *bigcache.BigCache) (int, error) { total, err := memory.Get("total") if err != nil { if errors.Is(err, bigcache.ErrEntryNotFound) { - return 0, ErrNotFound + return 0, schema.ErrNotFound } return 0, err } return int(total[0]), 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(context.Background(), sql, args...).Scan(&jokeID) + if err != nil && err != pgx.ErrNoRows { + return false, err + } + + return strconv.Itoa(jokeID) == id, nil +} diff --git a/api/app/core/joke/getter_test.go b/api/app/core/joke/getter_test.go new file mode 100644 index 0000000..5bd4be7 --- /dev/null +++ b/api/app/core/joke/getter_test.go @@ -0,0 +1,73 @@ +package joke_test + +import ( + "context" + "jokes-bapak2-api/app/core/joke" + "testing" + + "github.com/jackc/pgx/v4" +) + +func TestGetAllJSONJokes(t *testing.T) { + defer Teardown() + conn, err := db.Acquire(context.Background()) + if err != nil { + t.Error("an error was thrown:", err) + } + + err = conn.BeginFunc(context.Background(), func(t pgx.Tx) error { + _, err := t.Exec(context.Background(), "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(context.Background(), "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, context.Background()) + if err != nil { + t.Error("an error was thrown:", err) + } + + if string(j) == "" { + t.Error("j should not be empty") + } + +} + +func TestGetRandomJokeFromCache(t *testing.T) { + defer Teardown() + // +} + +func TestCheckJokesCache(t *testing.T) { + defer Teardown() + // +} + +func TestCheckTotalJokesCache(t *testing.T) { + defer Teardown() + // +} + +func TestGetCachedJokeByID(t *testing.T) { + defer Teardown() + // +} + +func TestGetCachedTotalJokes(t *testing.T) { + defer Teardown() + // +} + +func TestCheckJokeExists(t *testing.T) { + defer Teardown() + // +} diff --git a/api/app/core/joke/init_test.go b/api/app/core/joke/init_test.go new file mode 100644 index 0000000..20b608d --- /dev/null +++ b/api/app/core/joke/init_test.go @@ -0,0 +1,153 @@ +package joke_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/allegro/bigcache/v3" + "github.com/go-redis/redis/v8" + "github.com/jackc/pgx/v4" + "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 submissionData = []interface{}{ + 1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test ", 0, + 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test ", 1, +} +var 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) { + Setup() + defer Teardown() + + os.Exit(m.Run()) +} + +func Setup() { + poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) + if err != nil { + panic(err) + } + + db, err = pgxpool.ConnectConfig(context.Background(), 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(context.Background()) + if err != nil { + panic(err) + } + defer conn.Release() + + err = conn.BeginFunc(context.Background(), func(tx pgx.Tx) error { + _, err := tx.Exec( + context.Background(), + `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 { + return err + } + _, err = tx.Exec( + context.Background(), + `CREATE TABLE IF NOT EXISTS jokesbapak2 ( + id SERIAL PRIMARY KEY, + link TEXT UNIQUE, + creator INT NOT NULL REFERENCES "administrators" ("id") + );`, + ) + if err != nil { + return err + } + _, err = tx.Exec( + context.Background(), + `CREATE TABLE IF NOT EXISTS submission ( + id SERIAL PRIMARY KEY, + link UNIQUE NOT NULL, + created_at VARCHAR(255), + author VARCHAR(255) NOT NULL, + status SMALLINT DEFAULT 0 + );`, + ) + return err + }) + if err != nil { + panic(err) + } +} + +func Teardown() (err error) { + db.Close() + err = cache.Close() + if err != nil { + return + } + err = memory.Close() + return +} + +func TruncateTable(db *pgxpool.Pool, cache *redis.Client, memory *bigcache.BigCache) error { + conn, err := db.Acquire(context.Background()) + if err != nil { + return err + } + defer conn.Release() + + err = conn.BeginFunc(context.Background(), func(tx pgx.Tx) error { + _, err := tx.Exec(context.Background(), "TRUNCATE TABLE submission;") + if err != nil { + return err + } + _, err = tx.Exec(context.Background(), "TRUNCATE TABLE jokesbapak2;") + if err != nil { + return err + } + _, err = tx.Exec(context.Background(), "TRUNCATE TABLE administrators;") + return err + }) + if err != nil { + return err + } + + err = cache.FlushAll(context.Background()).Err() + if err != nil { + return err + } + + err = memory.Reset() + if err != nil { + return err + } + + return nil +} diff --git a/api/app/core/joke/setter.go b/api/app/core/joke/setter.go new file mode 100644 index 0000000..3068ec4 --- /dev/null +++ b/api/app/core/joke/setter.go @@ -0,0 +1,133 @@ +package joke + +import ( + "context" + "jokes-bapak2-api/app/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(context.Background(), 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(context.Background(), sql, args...) + if err != nil { + return err + } + defer r.Close() + + return nil +} + +func UpdateJoke(db *pgxpool.Pool, ctx context.Context, link, creator string) 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", link). + Set("creator", creator). + ToSql() + if err != nil { + return err + } + + r, err := conn.Query(context.Background(), sql, args...) + if err != nil { + return err + } + defer r.Close() + + return nil +} diff --git a/api/app/core/joke_setter.go b/api/app/core/joke_setter.go deleted file mode 100644 index cc4ac25..0000000 --- a/api/app/core/joke_setter.go +++ /dev/null @@ -1,55 +0,0 @@ -package core - -import ( - "context" - - "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, ctx *context.Context) 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, memory *bigcache.BigCache, ctx *context.Context) error { - check, err := CheckJokesCache(memory) - if err != nil { - return err - } - - if !check { - err = SetAllJSONJoke(db, memory, ctx) - if err != nil { - return err - } - } - - jokes, err := memory.Get("jokes") - if err != nil { - return err - } - - var data []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 -} diff --git a/api/app/core/schema/err.go b/api/app/core/schema/err.go new file mode 100644 index 0000000..41cc86c --- /dev/null +++ b/api/app/core/schema/err.go @@ -0,0 +1,6 @@ +package schema + +import "errors" + +var ErrNotFound = errors.New("record not found") +var ErrEmpty = errors.New("record is empty") diff --git a/api/app/core/schema.go b/api/app/core/schema/image_api.go similarity index 52% rename from api/app/core/schema.go rename to api/app/core/schema/image_api.go index b66d86b..6c3c25f 100644 --- a/api/app/core/schema.go +++ b/api/app/core/schema/image_api.go @@ -1,12 +1,4 @@ -package core - -import "errors" - -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"` -} +package schema type ImageAPI struct { Data ImageAPIData `json:"data"` @@ -21,6 +13,3 @@ type ImageAPIData struct { URL string `json:"url"` DisplayURL string `json:"display_url"` } - -var ErrNotFound = errors.New("record not found") -var ErrEmpty = errors.New("record is empty") diff --git a/api/app/core/schema/joke.go b/api/app/core/schema/joke.go new file mode 100644 index 0000000..69a9914 --- /dev/null +++ b/api/app/core/schema/joke.go @@ -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"` +} diff --git a/api/app/core/submit_setter.go b/api/app/core/submit/setter.go similarity index 95% rename from api/app/core/submit_setter.go rename to api/app/core/submit/setter.go index e490d83..49bcff2 100644 --- a/api/app/core/submit_setter.go +++ b/api/app/core/submit/setter.go @@ -1,9 +1,10 @@ -package core +package submit import ( "bytes" "io" "io/ioutil" + "jokes-bapak2-api/app/core/schema" "jokes-bapak2-api/app/utils" "mime/multipart" "net/http" @@ -70,7 +71,7 @@ func UploadImage(client *httpclient.Client, image io.Reader) (string, error) { return "", err } - var data ImageAPI + var data schema.ImageAPI err = ffjson.Unmarshal(responseBody, &data) if err != nil { return "", err diff --git a/api/app/core/submit_validation.go b/api/app/core/validator/author.go similarity index 97% rename from api/app/core/submit_validation.go rename to api/app/core/validator/author.go index a1e23af..afc7d43 100644 --- a/api/app/core/submit_validation.go +++ b/api/app/core/validator/author.go @@ -1,4 +1,4 @@ -package core +package validator import ( "regexp" diff --git a/api/app/core/joke_validation.go b/api/app/core/validator/image.go similarity index 97% rename from api/app/core/joke_validation.go rename to api/app/core/validator/image.go index b0ff300..2f5dda0 100644 --- a/api/app/core/joke_validation.go +++ b/api/app/core/validator/image.go @@ -1,4 +1,4 @@ -package core +package validator import ( "errors" diff --git a/api/app/core/validator/joke.go b/api/app/core/validator/joke.go new file mode 100644 index 0000000..ceddab1 --- /dev/null +++ b/api/app/core/validator/joke.go @@ -0,0 +1,68 @@ +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 LinkAlreadyExists(db *pgxpool.Pool, ctx context.Context, link string) (bool, error) { + conn, err := db.Acquire(ctx) + if err != nil { + return false, err + } + var query = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + defer conn.Release() + + 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(context.Background(), sql, args...).Scan(&validateLink) + if err != nil && err != pgx.ErrNoRows { + return false, err + } + + return validateLink != "", nil +} + +// Check if the joke exists +func IDAlreadyExists(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(context.Background(), 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 +} diff --git a/api/app/handler/health/health.go b/api/app/handler/health/health.go index 48c1b08..fd8d767 100644 --- a/api/app/handler/health/health.go +++ b/api/app/handler/health/health.go @@ -1,28 +1,25 @@ package health import ( - "context" - "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 - Context *context.Context + DB *pgxpool.Pool + Redis *redis.Client } func (d *Dependencies) Health(c *fiber.Ctx) error { - conn, err := d.DB.Acquire(*d.Context) + conn, err := d.DB.Acquire(c.Context()) if err != nil { return err } defer conn.Release() // Ping REDIS database - err = d.Redis.Ping(*d.Context).Err() + err = d.Redis.Ping(c.Context()).Err() if err != nil { return c. Status(fiber.StatusServiceUnavailable). @@ -31,7 +28,7 @@ func (d *Dependencies) Health(c *fiber.Ctx) error { }) } - _, err = conn.Query(*d.Context, "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") + _, err = conn.Query(c.Context(), "SELECT \"id\" FROM \"jokesbapak2\" LIMIT 1") if err != nil { return c. Status(fiber.StatusServiceUnavailable). diff --git a/api/app/handler/joke/dependencies.go b/api/app/handler/joke/dependencies.go index 013f3a7..75d0b4c 100644 --- a/api/app/handler/joke/dependencies.go +++ b/api/app/handler/joke/dependencies.go @@ -1,8 +1,6 @@ package joke import ( - "context" - "github.com/Masterminds/squirrel" "github.com/allegro/bigcache/v3" "github.com/go-redis/redis/v8" @@ -11,10 +9,9 @@ import ( ) type Dependencies struct { - DB *pgxpool.Pool - Redis *redis.Client - Memory *bigcache.BigCache - HTTP *httpclient.Client - Query squirrel.StatementBuilderType - Context *context.Context + DB *pgxpool.Pool + Redis *redis.Client + Memory *bigcache.BigCache + HTTP *httpclient.Client + Query squirrel.StatementBuilderType } diff --git a/api/app/handler/joke/joke_add.go b/api/app/handler/joke/joke_add.go index a5dbb62..841c83b 100644 --- a/api/app/handler/joke/joke_add.go +++ b/api/app/handler/joke/joke_add.go @@ -1,38 +1,25 @@ package joke import ( - "jokes-bapak2-api/app/core" + core "jokes-bapak2-api/app/core/joke" + "jokes-bapak2-api/app/core/schema" + "jokes-bapak2-api/app/core/validator" - "github.com/Masterminds/squirrel" "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v4" ) func (d *Dependencies) AddNewJoke(c *fiber.Ctx) error { - conn, err := d.DB.Acquire(*d.Context) - if err != nil { - return err - } - defer conn.Release() - - tx, err := conn.Begin(*d.Context) - if err != nil { - return err - } - defer tx.Rollback(*d.Context) - - var body core.Joke - err = c.BodyParser(&body) + var body schema.Joke + err := c.BodyParser(&body) if err != nil { return err } // Check link validity - valid, err := core.CheckImageValidity(d.HTTP, body.Link) + valid, err := validator.CheckImageValidity(d.HTTP, body.Link) if err != nil { return err } - if !valid { return c. Status(fiber.StatusBadRequest). @@ -40,51 +27,36 @@ func (d *Dependencies) AddNewJoke(c *fiber.Ctx) error { Error: "URL provided is not a valid image", }) } - // Validate if link already exists - sql, args, err := d.Query. - Select("link"). - From("jokesbapak2"). - Where(squirrel.Eq{"link": body.Link}). - ToSql() + + validateLink, err := validator.LinkAlreadyExists(d.DB, c.Context(), body.Link) if err != nil { return err } - var validateLink string - err = conn.QueryRow(*d.Context, sql, args...).Scan(&validateLink) - if err != nil && err != pgx.ErrNoRows { - return err - } - if err == nil && validateLink != "" { + if !validateLink { return c.Status(fiber.StatusConflict).JSON(Error{ Error: "Given link is already on the jokesbapak2 database", }) } - sql, args, err = d.Query. - Insert("jokesbapak2"). - Columns("link", "creator"). - Values(body.Link, c.Locals("userID")). - ToSql() + err = core.InsertJokeIntoDB( + d.DB, + c.Context(), + schema.Joke{ + Link: body.Link, + Creator: c.Locals("userID").(int), + }, + ) if err != nil { return err } - _, err = tx.Exec(*d.Context, sql, args...) + err = core.SetAllJSONJoke(d.DB, c.Context(), d.Memory) if err != nil { return err } - err = tx.Commit(*d.Context) - if err != nil { - return err - } - - err = core.SetAllJSONJoke(d.DB, d.Memory, d.Context) - if err != nil { - return err - } - err = core.SetTotalJoke(d.DB, d.Memory, d.Context) + err = core.SetTotalJoke(d.DB, c.Context(), d.Memory) if err != nil { return err } diff --git a/api/app/handler/joke/joke_delete.go b/api/app/handler/joke/joke_delete.go index 64b3b0f..3aad05d 100644 --- a/api/app/handler/joke/joke_delete.go +++ b/api/app/handler/joke/joke_delete.go @@ -1,75 +1,51 @@ package joke import ( - "jokes-bapak2-api/app/core" + core "jokes-bapak2-api/app/core/joke" + "jokes-bapak2-api/app/core/validator" "strconv" - "github.com/Masterminds/squirrel" "github.com/gofiber/fiber/v2" ) func (d *Dependencies) DeleteJoke(c *fiber.Ctx) error { - conn, err := d.DB.Acquire(*d.Context) - if err != nil { - return err - } - defer conn.Release() - id, err := strconv.Atoi(c.Params("id")) if err != nil { return err } - // Check if the joke exists - sql, args, err := d.Query. - Select("id"). - From("jokesbapak2"). - Where(squirrel.Eq{"id": id}). - ToSql() + validate, err := validator.IDAlreadyExists(d.DB, c.Context(), id) if err != nil { return err } - var jokeID int - err = conn.QueryRow(*d.Context, sql, args...).Scan(&jokeID) - if err != nil { - return err - } - - if jokeID == id { - sql, args, err = d.Query. - Delete("jokesbapak2"). - Where(squirrel.Eq{"id": id}). - ToSql() - if err != nil { - return err - } - - r, err := conn.Query(*d.Context, sql, args...) - if err != nil { - return err - } - - defer r.Close() - - err = core.SetAllJSONJoke(d.DB, d.Memory, d.Context) - if err != nil { - return err - } - err = core.SetTotalJoke(d.DB, d.Memory, d.Context) - if err != nil { - return err - } - + if validate { return c. - Status(fiber.StatusOK). - JSON(ResponseJoke{ - Message: "specified joke id has been deleted", + 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.StatusNotAcceptable). - JSON(Error{ - Error: "specified joke id does not exists", + Status(fiber.StatusOK). + JSON(ResponseJoke{ + Message: "specified joke id has been deleted", }) + } diff --git a/api/app/handler/joke/joke_get.go b/api/app/handler/joke/joke_get.go index 0e57e68..33efa43 100644 --- a/api/app/handler/joke/joke_get.go +++ b/api/app/handler/joke/joke_get.go @@ -1,8 +1,10 @@ package joke import ( + "errors" "io/ioutil" - "jokes-bapak2-api/app/core" + core "jokes-bapak2-api/app/core/joke" + "jokes-bapak2-api/app/core/schema" "jokes-bapak2-api/app/utils" "strconv" "time" @@ -15,7 +17,7 @@ func (d *Dependencies) TodayJoke(c *fiber.Ctx) error { // send the joke if exists // get a new joke if it's not, then send it. var joke Today - err := d.Redis.MGet(*d.Context, "today:link", "today:date", "today:image", "today:contentType").Scan(&joke) + err := d.Redis.MGet(c.Context(), "today:link", "today:date", "today:image", "today:contentType").Scan(&joke) if err != nil { return err } @@ -30,13 +32,7 @@ func (d *Dependencies) TodayJoke(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).Send([]byte(joke.Image)) } - conn, err := d.DB.Acquire(*d.Context) - if err != nil { - return err - } - defer conn.Release() - var link string - err = conn.QueryRow(*d.Context, "SELECT link FROM jokesbapak2 ORDER BY random() LIMIT 1").Scan(&link) + link, err := core.GetRandomJokeFromDB(d.DB, c.Context()) if err != nil { return err } @@ -52,7 +48,7 @@ func (d *Dependencies) TodayJoke(c *fiber.Ctx) error { } now := time.Now().UTC().Format(time.RFC3339) - err = d.Redis.MSet(*d.Context, map[string]interface{}{ + err = d.Redis.MSet(c.Context(), map[string]interface{}{ "today:link": link, "today:date": now, "today:image": string(data), @@ -73,10 +69,11 @@ func (d *Dependencies) SingleJoke(c *fiber.Ctx) error { } if !checkCache { - jokes, err := core.GetAllJSONJokes(d.DB, d.Context) + jokes, err := core.GetAllJSONJokes(d.DB, c.Context()) if err != nil { return err } + err = d.Memory.Set("jokes", jokes) if err != nil { return err @@ -84,7 +81,7 @@ func (d *Dependencies) SingleJoke(c *fiber.Ctx) error { } link, err := core.GetRandomJokeFromCache(d.Memory) - if err != nil { + if err != nil && !errors.Is(err, schema.ErrEmpty) { return err } @@ -111,10 +108,11 @@ func (d *Dependencies) JokeByID(c *fiber.Ctx) error { } if !checkCache { - jokes, err := core.GetAllJSONJokes(d.DB, d.Context) + jokes, err := core.GetAllJSONJokes(d.DB, c.Context()) if err != nil { return err } + err = d.Memory.Set("jokes", jokes) if err != nil { return err diff --git a/api/app/handler/joke/joke_total.go b/api/app/handler/joke/joke_total.go index be24a32..ea38489 100644 --- a/api/app/handler/joke/joke_total.go +++ b/api/app/handler/joke/joke_total.go @@ -2,7 +2,7 @@ package joke import ( "errors" - "jokes-bapak2-api/app/core" + core "jokes-bapak2-api/app/core/joke" "strconv" "github.com/allegro/bigcache/v3" @@ -16,7 +16,7 @@ func (d *Dependencies) TotalJokes(c *fiber.Ctx) error { } if !checkTotal { - err = core.SetTotalJoke(d.DB, d.Memory, d.Context) + err = core.SetTotalJoke(d.DB, c.Context(), d.Memory) if err != nil { return err } diff --git a/api/app/handler/joke/joke_update.go b/api/app/handler/joke/joke_update.go index 6ab0cd3..a275aed 100644 --- a/api/app/handler/joke/joke_update.go +++ b/api/app/handler/joke/joke_update.go @@ -1,94 +1,69 @@ package joke import ( - "jokes-bapak2-api/app/core" - "strconv" + core "jokes-bapak2-api/app/core/joke" + "jokes-bapak2-api/app/core/schema" + "jokes-bapak2-api/app/core/validator" - "github.com/Masterminds/squirrel" "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v4" ) func (d *Dependencies) UpdateJoke(c *fiber.Ctx) error { - conn, err := d.DB.Acquire(*d.Context) - if err != nil { - return err - } - defer conn.Release() - id := c.Params("id") // Check if the joke exists - sql, args, err := d.Query. - Select("id"). - From("jokesbapak2"). - Where(squirrel.Eq{"id": id}). - ToSql() + + jokeExists, err := core.CheckJokeExists(d.DB, c.Context(), id) if err != nil { return err } - var jokeID int - err = conn.QueryRow(*d.Context, sql, args...).Scan(&jokeID) - if err != nil && err != pgx.ErrNoRows { - return err - } - - if strconv.Itoa(jokeID) == id { - body := new(core.Joke) - err = c.BodyParser(&body) - if err != nil { - return err - } - - // Check link validity - valid, err := core.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", - }) - } - - sql, args, err = d.Query. - Update("jokesbapak2"). - Set("link", body.Link). - Set("creator", c.Locals("userID")). - ToSql() - if err != nil { - return err - } - - r, err := conn.Query(*d.Context, sql, args...) - if err != nil { - return err - } - defer r.Close() - - err = core.SetAllJSONJoke(d.DB, d.Memory, d.Context) - if err != nil { - return err - } - err = core.SetTotalJoke(d.DB, d.Memory, d.Context) - if err != nil { - return err - } - + if !jokeExists { return c. - Status(fiber.StatusOK). - JSON(ResponseJoke{ - Message: "specified joke id has been updated", - Link: body.Link, + 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", + }) + } + + err = core.UpdateJoke(d.DB, c.Context(), body.Link, c.Locals("userID").(string)) + 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.StatusNotAcceptable). - JSON(Error{ - Error: "specified joke id does not exists", + Status(fiber.StatusOK). + JSON(ResponseJoke{ + Message: "specified joke id has been updated", + Link: body.Link, }) } diff --git a/api/app/handler/submit/dependencies.go b/api/app/handler/submit/dependencies.go index 370f616..5f73c1b 100644 --- a/api/app/handler/submit/dependencies.go +++ b/api/app/handler/submit/dependencies.go @@ -1,8 +1,6 @@ package submit import ( - "context" - "github.com/Masterminds/squirrel" "github.com/allegro/bigcache/v3" "github.com/go-redis/redis/v8" @@ -11,10 +9,9 @@ import ( ) type Dependencies struct { - DB *pgxpool.Pool - Redis *redis.Client - Memory *bigcache.BigCache - HTTP *httpclient.Client - Query squirrel.StatementBuilderType - Context *context.Context + DB *pgxpool.Pool + Redis *redis.Client + Memory *bigcache.BigCache + HTTP *httpclient.Client + Query squirrel.StatementBuilderType } diff --git a/api/app/handler/submit/submit_add.go b/api/app/handler/submit/submit_add.go index 234a5cf..d3e4a6c 100644 --- a/api/app/handler/submit/submit_add.go +++ b/api/app/handler/submit/submit_add.go @@ -2,7 +2,8 @@ package submit import ( "context" - "jokes-bapak2-api/app/core" + core "jokes-bapak2-api/app/core/submit" + "jokes-bapak2-api/app/core/validator" "net/url" "strings" "time" @@ -15,7 +16,7 @@ import ( ) func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { - conn, err := d.DB.Acquire(*d.Context) + conn, err := d.DB.Acquire(c.Context()) if err != nil { return err } @@ -41,7 +42,7 @@ func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { }) } else { // Validate format - valid := core.ValidateAuthor(body.Author) + valid := validator.ValidateAuthor(body.Author) if !valid { return c.Status(fiber.StatusBadRequest).JSON(Error{ Error: "Please stick to the format of \"yourname \" and within 200 characters", @@ -53,7 +54,7 @@ func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { // Check link validity if link was provided if body.Link != "" { - valid, err := core.CheckImageValidity(d.HTTP, body.Link) + valid, err := validator.CheckImageValidity(d.HTTP, body.Link) if err != nil { return err } @@ -77,7 +78,7 @@ func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { } // Validate if link already exists - validateLink, err := validateIfLinkExists(conn, d.Context, d.Query, link) + validateLink, err := validateIfLinkExists(d.DB, c.Context(), d.Query, link) if err != nil { return err } @@ -101,7 +102,7 @@ func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { } var submission []Submission - result, err := conn.Query(*d.Context, sql, args...) + result, err := conn.Query(c.Context(), sql, args...) if err != nil { return err } @@ -121,7 +122,13 @@ func (d *Dependencies) SubmitJoke(c *fiber.Ctx) error { }) } -func validateIfLinkExists(conn *pgxpool.Conn, ctx *context.Context, query squirrel.StatementBuilderType, link string) (bool, error) { +func validateIfLinkExists(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"). @@ -132,7 +139,7 @@ func validateIfLinkExists(conn *pgxpool.Conn, ctx *context.Context, query squirr } var validateLink string - err = conn.QueryRow(*ctx, sql, args...).Scan(&validateLink) + err = conn.QueryRow(context.Background(), sql, args...).Scan(&validateLink) if err != nil && err != pgx.ErrNoRows { return false, err } diff --git a/api/app/handler/submit/submit_get.go b/api/app/handler/submit/submit_get.go index 2b16163..8c3810c 100644 --- a/api/app/handler/submit/submit_get.go +++ b/api/app/handler/submit/submit_get.go @@ -83,7 +83,7 @@ func (d *Dependencies) GetSubmission(c *fiber.Ctx) error { sql = bob.ReplacePlaceholder(sqlQuery.String(), bob.Dollar) var submissions []Submission - results, err := d.DB.Query(*d.Context, sql, args...) + results, err := d.DB.Query(c.Context(), sql, args...) if err != nil { return err } diff --git a/api/app/middleware/auth.go b/api/app/middleware/auth.go index 96ddb09..1076ecb 100644 --- a/api/app/middleware/auth.go +++ b/api/app/middleware/auth.go @@ -1,20 +1,14 @@ package middleware import ( - "context" - "errors" - "time" + "jokes-bapak2-api/app/core/administrator" - "github.com/Masterminds/squirrel" phccrypto "github.com/aldy505/phc-crypto" "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" ) -var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) - -func RequireAuth(db *pgxpool.Pool, ctx *context.Context) fiber.Handler { +func RequireAuth(db *pgxpool.Pool) fiber.Handler { return func(c *fiber.Ctx) error { var auth Auth err := c.BodyParser(&auth) @@ -22,27 +16,17 @@ func RequireAuth(db *pgxpool.Pool, ctx *context.Context) fiber.Handler { return err } - // Check if key exists - sql, args, err := psql. - Select("token"). - From("administrators"). - Where(squirrel.Eq{"key": auth.Key}). - ToSql() + token, err := administrator.CheckKeyExists(db, c.Context(), auth.Key) if err != nil { return err } - var token string - err = db.QueryRow(*ctx, sql, args...).Scan(&token) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return c. - Status(fiber.StatusForbidden). - JSON(Error{ - Error: "Invalid key", - }) - } - return err + if token == "" { + return c. + Status(fiber.StatusForbidden). + JSON(Error{ + Error: "Invalid key", + }) } crypto, err := phccrypto.Use(phccrypto.Argon2, phccrypto.Config{}) @@ -56,33 +40,11 @@ func RequireAuth(db *pgxpool.Pool, ctx *context.Context) fiber.Handler { } if verify { - sql, args, err = psql. - Update("administrators"). - Set("last_used", time.Now().UTC().Format(time.RFC3339)). - ToSql() + id, err := administrator.GetUserID(db, c.Context(), auth.Key) if err != nil { return err } - _, err = db.Query(*ctx, 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(*ctx, sql, args...).Scan(&id) - if err != nil { - return err - } c.Locals("userID", id) return c.Next() } diff --git a/api/app/platform/database/create.go b/api/app/platform/database/create.go index c570f02..684b589 100644 --- a/api/app/platform/database/create.go +++ b/api/app/platform/database/create.go @@ -2,45 +2,39 @@ package database import ( "context" - "log" "github.com/aldy505/bob" "github.com/jackc/pgx/v4/pgxpool" ) // Setup the table connection, create table if not exists -func Setup(db *pgxpool.Pool, ctx *context.Context) error { - conn, err := db.Acquire(*ctx) - if err != nil { - log.Fatalln("30 - err here") - return err - } - defer conn.Release() - - err = setupAuthTable(conn, ctx) +func Setup(db *pgxpool.Pool) error { + conn, err := db.Acquire(context.Background()) if err != nil { return err } - conn2, err := db.Acquire(*ctx) - if err != nil { - log.Fatalln("32 - err here") - return err - } - defer conn2.Release() - - err = setupJokesTable(conn2, ctx) + err = setupAuthTable(conn) if err != nil { return err } - conn3, err := db.Acquire(*ctx) + conn, err = db.Acquire(context.Background()) if err != nil { return err } - defer conn3.Release() - err = setupSubmissionTable(conn3, ctx) + err = setupJokesTable(conn) + if err != nil { + return err + } + + conn, err = db.Acquire(context.Background()) + if err != nil { + return err + } + + err = setupSubmissionTable(conn) if err != nil { return err } @@ -48,10 +42,12 @@ func Setup(db *pgxpool.Pool, ctx *context.Context) error { return nil } -func setupAuthTable(conn *pgxpool.Conn, ctx *context.Context) error { +func setupAuthTable(conn *pgxpool.Conn) error { + defer conn.Release() + // Check if table exists var tableAuthExists bool - err := conn.QueryRow(*ctx, `SELECT EXISTS ( + err := conn.QueryRow(context.Background(), `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'administrators' @@ -72,7 +68,7 @@ func setupAuthTable(conn *pgxpool.Conn, ctx *context.Context) error { return err } - q, err := conn.Query(*ctx, sql) + q, err := conn.Query(context.Background(), sql) if err != nil { return err } @@ -81,10 +77,12 @@ func setupAuthTable(conn *pgxpool.Conn, ctx *context.Context) error { return nil } -func setupJokesTable(conn *pgxpool.Conn, ctx *context.Context) error { +func setupJokesTable(conn *pgxpool.Conn) error { + defer conn.Release() + // Check if table exists var tableJokesExists bool - err := conn.QueryRow(*ctx, `SELECT EXISTS ( + err := conn.QueryRow(context.Background(), `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'jokesbapak2' @@ -104,7 +102,7 @@ func setupJokesTable(conn *pgxpool.Conn, ctx *context.Context) error { return err } - q, err := conn.Query(*ctx, sql) + q, err := conn.Query(context.Background(), sql) if err != nil { return err } @@ -114,10 +112,12 @@ func setupJokesTable(conn *pgxpool.Conn, ctx *context.Context) error { return nil } -func setupSubmissionTable(conn *pgxpool.Conn, ctx *context.Context) error { +func setupSubmissionTable(conn *pgxpool.Conn) error { + defer conn.Release() + //Check if table exists var tableSubmissionExists bool - err := conn.QueryRow(*ctx, `SELECT EXISTS ( + err := conn.QueryRow(context.Background(), `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'submission' @@ -139,11 +139,12 @@ func setupSubmissionTable(conn *pgxpool.Conn, ctx *context.Context) error { return err } - q, err := conn.Query(*ctx, sql) + q, err := conn.Query(context.Background(), sql) if err != nil { return err } defer q.Close() } + return nil } diff --git a/api/app/platform/database/placeholder.sql b/api/app/platform/database/placeholder.sql index 07866bf..04e8b94 100644 --- a/api/app/platform/database/placeholder.sql +++ b/api/app/platform/database/placeholder.sql @@ -3,6 +3,8 @@ -- 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', ''); diff --git a/api/app/routes/dependencies.go b/api/app/routes/dependencies.go index 59eefdd..65f6bb5 100644 --- a/api/app/routes/dependencies.go +++ b/api/app/routes/dependencies.go @@ -1,8 +1,6 @@ package routes import ( - "context" - "github.com/Masterminds/squirrel" "github.com/allegro/bigcache/v3" "github.com/go-redis/redis/v8" @@ -12,12 +10,10 @@ import ( ) type Dependencies struct { - DB *pgxpool.Pool - Redis *redis.Client - Memory *bigcache.BigCache - HTTP *httpclient.Client - Query squirrel.StatementBuilderType - App *fiber.App - Context *context.Context - Cancel *context.CancelFunc + DB *pgxpool.Pool + Redis *redis.Client + Memory *bigcache.BigCache + HTTP *httpclient.Client + Query squirrel.StatementBuilderType + App *fiber.App } diff --git a/api/app/routes/health.go b/api/app/routes/health.go index 0cce80f..cf9e85c 100644 --- a/api/app/routes/health.go +++ b/api/app/routes/health.go @@ -10,9 +10,8 @@ import ( func (d *Dependencies) Health() { // Health check deps := health.Dependencies{ - DB: d.DB, - Redis: d.Redis, - Context: d.Context, + DB: d.DB, + Redis: d.Redis, } d.App.Get("/health", cache.New(cache.Config{Expiration: 30 * time.Minute}), deps.Health) diff --git a/api/app/routes/joke.go b/api/app/routes/joke.go index 8fa701b..c6c0766 100644 --- a/api/app/routes/joke.go +++ b/api/app/routes/joke.go @@ -10,12 +10,11 @@ import ( func (d *Dependencies) Joke() { deps := joke.Dependencies{ - DB: d.DB, - Redis: d.Redis, - Memory: d.Memory, - HTTP: d.HTTP, - Query: d.Query, - Context: d.Context, + DB: d.DB, + Redis: d.Redis, + Memory: d.Memory, + HTTP: d.HTTP, + Query: d.Query, } // Single route d.App.Get("/", deps.SingleJoke) @@ -34,11 +33,11 @@ func (d *Dependencies) Joke() { 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, d.Context), deps.AddNewJoke) + d.App.Put("/", middleware.RequireAuth(d.DB), deps.AddNewJoke) // Update a joke - d.App.Patch("/id/:id", middleware.RequireAuth(d.DB, d.Context), middleware.OnlyIntegerAsID(), deps.UpdateJoke) + 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, d.Context), middleware.OnlyIntegerAsID(), deps.DeleteJoke) + d.App.Delete("/id/:id", middleware.RequireAuth(d.DB), middleware.OnlyIntegerAsID(), deps.DeleteJoke) } diff --git a/api/app/routes/submit.go b/api/app/routes/submit.go index bdd13af..ce9822b 100644 --- a/api/app/routes/submit.go +++ b/api/app/routes/submit.go @@ -10,12 +10,11 @@ import ( func (d *Dependencies) Submit() { deps := submit.Dependencies{ - DB: d.DB, - Redis: d.Redis, - Memory: d.Memory, - HTTP: d.HTTP, - Query: d.Query, - Context: d.Context, + DB: d.DB, + Redis: d.Redis, + Memory: d.Memory, + HTTP: d.HTTP, + Query: d.Query, } // Get pending submitted joke diff --git a/api/go.mod b/api/go.mod index 7dae60f..1d66845 100644 --- a/api/go.mod +++ b/api/go.mod @@ -13,12 +13,13 @@ require ( github.com/go-redis/redis/v8 v8.11.0 github.com/gofiber/fiber/v2 v2.15.0 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/joho/godotenv v1.3.0 github.com/kr/text v0.2.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.0 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -33,6 +34,7 @@ require ( 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 diff --git a/api/go.sum b/api/go.sum index 04e5f52..0148694 100644 --- a/api/go.sum +++ b/api/go.sum @@ -215,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.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 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-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= diff --git a/api/main_test.go b/api/main_test.go deleted file mode 100644 index d4580c4..0000000 --- a/api/main_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package main_test - -import ( - "context" - "errors" - "flag" - "io/ioutil" - v1 "jokes-bapak2-api/app" - "jokes-bapak2-api/app/platform/database" - "log" - "net/http" - "os" - "strings" - "testing" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v4/pgxpool" - _ "github.com/joho/godotenv/autoload" - "github.com/stretchr/testify/assert" -) - -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 submissionData = []interface{}{1, "https://via.placeholder.com/300/01f/fff.png", "2021-08-03T18:20:38Z", "Test ", 0, 2, "https://via.placeholder.com/300/02f/fff.png", "2021-08-04T18:20:38Z", "Test ", 1} -var 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", ""} -var ctx context.Context = context.Background() - -func TestMain(m *testing.M) { - flag.Parse() - - log.Println("---- Preparing for integration test") - time.Sleep(time.Second * 5) - err := setup() - if err != nil { - log.Panicln(err) - } - time.Sleep(time.Second * 5) - log.Println("---- Preparation complete") - log.Print("\n") - - os.Exit(m.Run()) -} - -func setup() error { - poolConfig, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) - if err != nil { - return errors.New("Unable to create pool config: " + err.Error()) - } - - db, err := pgxpool.ConnectConfig(ctx, poolConfig) - if err != nil { - return errors.New("Unable to create connection: " + err.Error()) - } - - dj, err := db.Query(ctx, "DROP TABLE IF EXISTS \"jokesbapak2\"") - if err != nil { - return err - } - dj.Close() - - ds, err := db.Query(ctx, "DROP TABLE IF EXISTS \"submission\"") - if err != nil { - return err - } - ds.Close() - - da, err := db.Query(ctx, "DROP TABLE IF EXISTS \"administrators\"") - if err != nil { - return err - } - da.Close() - - err = database.Setup(db, &ctx) - if err != nil { - return err - } - - ia, err := db.Query(ctx, "INSERT INTO \"administrators\" (id, key, token, last_used) VALUES ($1, $2, $3, $4), ($5, $6, $7, $8);", administratorsData...) - if err != nil { - return err - } - ia.Close() - - ij, err := db.Query(ctx, "INSERT INTO \"jokesbapak2\" (id, link, creator) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);", jokesData...) - if err != nil { - return err - } - ij.Close() - - is, err := db.Query(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 { - return err - } - is.Close() - - db.Close() - - return nil -} - -var app *fiber.App = v1.New() - -func TestHealth(t *testing.T) { - req, _ := http.NewRequest("GET", "/health", nil) - res, err := app.Test(req, int(time.Minute*2)) - if err != nil { - t.Fatal(err) - } - - 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) - if err != nil { - t.Fatal(err) - } - defer res.Body.Close() - - assert.Nilf(t, err, "health") -} - -func TestTodayJoke(t *testing.T) { - req, _ := http.NewRequest("GET", "/today", nil) - res, err := app.Test(req, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "today joke") -} - -func TestSingleJoke(t *testing.T) { - req, _ := http.NewRequest("GET", "/", nil) - res, err := app.Test(req, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "single joke") -} - -func TestJokeByID_200(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/1", nil) - res, err := app.Test(req, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "joke by id") -} - -func TestJokeByID_404(t *testing.T) { - req, _ := http.NewRequest("GET", "/id/300", nil) - res, err := app.Test(req, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "joke by id") - assert.Equalf(t, "Requested ID was not found.", string(body), "joke by id") -} - -func TestTotalJokes(t *testing.T) { - req, _ := http.NewRequest("GET", "/total", nil) - res, err := app.Test(req, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "joke total") - assert.Equalf(t, "{\"message\":\"3\"}", string(body), "joke total") -} - -func TestAddNewJoke_201(t *testing.T) { - // TODO: Remove this line below, make this test works - t.SkipNow() - - reqBody := strings.NewReader("{\"link\":\"https://via.placeholder.com/300/04f/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, int(time.Minute*2)) - - 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) - defer res.Body.Close() - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"link\":\"https://via.placeholder.com/300/04f/ff0000.png\"}", string(body), "joke add") -} - -func TestAddNewJoke_NotValidImage(t *testing.T) { - // TODO: Remove this line below, make this test works - t.SkipNow() - - 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, int(time.Minute*2)) - - assert.Equalf(t, false, err != nil, "joke add") - assert.Equalf(t, 400, res.StatusCode, "joke add") - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - assert.Nilf(t, err, "joke add") - assert.Equalf(t, "{\"error\":\"URL provided is not a valid image\"}", string(body), "joke add") -} - -func TestGetSubmission_200(t *testing.T) { - req, _ := http.NewRequest("GET", "/submit", nil) - res, err := app.Test(req, int(time.Minute*2)) - - assert.Equalf(t, false, err != nil, "get submission") - assert.Equalf(t, 200, res.StatusCode, "get submission") - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - 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) { - req, _ := http.NewRequest("GET", "/submit?page=1&limit=5&approved=true", nil) - res, err := app.Test(req, int(time.Minute*2)) - - assert.Equalf(t, false, err != nil, "get submission") - assert.Equalf(t, 200, res.StatusCode, "get submission") - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - 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") -} - -func TestAddSubmission_200(t *testing.T) { - // TODO: Remove this line below, make this test works - t.Skip() - - reqBody := strings.NewReader(`{"link":"https://via.placeholder.com/400/02f/fff.png","author":"Test "}`) - req, _ := http.NewRequest("POST", "/submit", reqBody) - req.Header.Set("content-type", "application/json") - req.Header.Add("accept", "application/json") - res, err := app.Test(req, int(time.Minute*2)) - - assert.Equalf(t, false, err != nil, "post submission") - assert.Equalf(t, 201, res.StatusCode, "post submission") - assert.NotEqualf(t, 0, res.ContentLength, "post submission") - _, err = ioutil.ReadAll(res.Body) - defer res.Body.Close() - assert.Nilf(t, err, "post submission") -}