Compare commits

..

6 Commits

Author SHA1 Message Date
dfe00f8359 add mock 2023-09-12 17:08:17 +07:00
dcacc9288d fix user endpoint 2023-09-12 17:07:57 +07:00
d1ba0251c0 update user model 2023-09-12 17:07:03 +07:00
4f962bb104 add data seed and updae users table 2023-09-12 12:52:01 +07:00
96dfa4ae29 add commets drop table 2023-09-11 14:24:11 +07:00
ed608f170a add migration for comments 2023-09-11 12:20:10 +07:00
27 changed files with 696 additions and 53 deletions

View File

@ -2,12 +2,17 @@ include dev.env
migrateup:
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose up
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}_test?sslmode=disable" -verbose up
migratedown:
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -verbose down $N
migrate -path db/migrations -database "${DB_TYPE}://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}_test?sslmode=disable" -verbose down $N
seed:
./import_csv.sh
mock-generate:
mockgen -package mockdb -destination db/mock/store.go git.nochill.in/nochill/naice_pos/db/sqlc Store
mockgen -package mockdb -destination db/mock/store.go git.nochill.in/nochill/hiling_go/db/sqlc Store
sqlc:
sqlc generate && make mock-generate

View File

@ -2,14 +2,14 @@ package api
import "github.com/gin-gonic/gin"
func errorResponse(err error, msg string) gin.H {
func ErrorResponse(err error, msg string) gin.H {
return gin.H{
"error": err.Error(),
"message": msg,
}
}
func validResponse(data interface{}, msg string) gin.H {
func ValidResponse(data interface{}, msg string) gin.H {
return gin.H{
"message": msg,
"data": data,

View File

@ -13,7 +13,7 @@ type Server struct {
config util.Config
store db.Store
tokenMaker token.Maker
router *gin.Engine
Router *gin.Engine
}
func NewServer(config util.Config, store db.Store) (*Server, error) {
@ -35,11 +35,11 @@ func NewServer(config util.Config, store db.Store) (*Server, error) {
func (server *Server) getRoutes() {
router := gin.Default()
router.POST("/user/create", server.createUser)
router.POST("/user/signup", server.createUser)
server.router = router
server.Router = router
}
func (server *Server) Start(address string) error {
return server.router.Run(address)
return server.Router.Run(address)
}

32
api/test/main_test.go Normal file
View File

@ -0,0 +1,32 @@
package api_test
import (
"os"
"testing"
"time"
api "git.nochill.in/nochill/hiling_go/api"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
"github.com/stretchr/testify/require"
)
func newTestServer(t *testing.T, store db.Store) *api.Server {
config := util.Config{
TokenSymmetricKey: util.RandomString(32),
TokenDuration: time.Minute,
}
server, err := api.NewServer(config, store)
require.NoError(t, err)
return server
}
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Exit(m.Run())
}

60
api/test/user_test.go Normal file
View File

@ -0,0 +1,60 @@
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
mockdb "git.nochill.in/nochill/hiling_go/db/mock"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestSignupAPI(t *testing.T) {
user, pass := createUser(t)
t.Run("OK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(user, nil)
server := newTestServer(t, store)
recorder := httptest.NewRecorder()
data, err := json.Marshal(gin.H{
"username": user.Username,
"password": pass,
})
require.NoError(t, err)
url := "/user/signup"
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err)
server.Router.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
})
}
func createUser(t *testing.T) (user db.User, password string) {
passw := util.RandomString(10)
hashedPassword, err := util.HashPassword(passw)
require.NoError(t, err)
user = db.User{
Username: util.RandomString(10),
Password: hashedPassword,
}
return
}

View File

@ -1,27 +1,106 @@
package api
import (
"database/sql"
"errors"
"fmt"
"net/http"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/lib/pq"
)
type CreateUserRequest struct {
ClientIpV4 string `json:"client_ip" binding:"required"`
type createUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=7"`
}
type createUserResponse struct {
ID int32 `json:"id"`
Username string `json:"username"`
AvatarPicture string `json:"avatar_picture"` // avatar_url
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
BanReason string `json:"ban_reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
IsAdmin sql.NullBool `json:"is_admin"`
IsCritics sql.NullBool `json:"is_critics"`
IsVerified sql.NullBool `json:"is_verified"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type ApiError struct {
Field string
Msg string
}
func msgForTag(field string, param string, tag string) string {
switch tag {
case "min":
return fmt.Sprintf("%s character min %s", field, param)
default:
return fmt.Sprintf("%s %s", field, tag)
}
}
func (server *Server) createUser(ctx *gin.Context) {
var req CreateUserRequest
var req createUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
err := server.store.CreateUser(ctx, req.ClientIpV4)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make([]ApiError, len(ve))
for i, fe := range ve {
out[i] = ApiError{fe.Field(), msgForTag(fe.Field(), fe.Param(), fe.Tag())}
}
ctx.JSON(http.StatusBadRequest, gin.H{"errors": out})
}
return
}
}
hashedPassword, err := util.HashPassword(req.Password)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return
}
ctx.Writer.WriteHeader(http.StatusOK)
arg := db.CreateUserParams{
Username: req.Username,
Password: hashedPassword,
}
user, err := server.store.CreateUser(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusConflict, ErrorResponse(err, "Something went wrong while try to save"))
}
}
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return
}
res := createUserResponse{
ID: user.ID,
Username: user.Username,
AvatarPicture: user.AvatarPicture.String,
BannedAt: sql.NullTime{Valid: user.BannedAt.Valid, Time: user.BannedAt.Time},
BannedUntil: sql.NullTime{Valid: user.BannedUntil.Valid, Time: user.BannedUntil.Time},
BanReason: user.BanReason.String,
IsPermaban: user.IsPermaban,
IsAdmin: user.IsAdmin,
IsCritics: user.IsCritics,
IsVerified: user.IsVerified,
CreatedAt: sql.NullTime{Valid: true, Time: user.CreatedAt.Time},
UpdatedAt: sql.NullTime{Valid: true, Time: user.UpdatedAt.Time},
}
ctx.JSON(http.StatusOK, res)
}

View File

@ -0,0 +1,10 @@
id#address#name#submitted_by#thumbnail#regency_id#google_maps_link
1#Jalan Raya Beside the bridge Ubud#Murnis Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu
2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu
3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc
4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu
5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu
6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu
7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY
8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu
9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu
1 id#address#name#submitted_by#thumbnail#regency_id#google_maps_link
2 1#Jalan Raya Beside the bridge Ubud#Murni’s Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu
3 2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu
4 3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc
5 4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu
6 5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu
7 6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu
8 7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY
9 8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu
10 9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu

2
db/csv_seeder/user.csv Normal file
View File

@ -0,0 +1,2 @@
id,username,password
1,user123,password
1 id username password
2 1 user123 password

View File

View File

@ -1,4 +1,7 @@
DROP TABLE IF EXISTS location_images;
DROP TABLE IF EXISTS comments;
DROP TABLE IF EXISTS client_ips;
DROP TABLE IF EXISTS user_reports;
DROP TABLE IF EXISTS reviews;
DROP TABLE IF EXISTS locations;
DROP TABLE IF EXISTS regencies;
@ -6,3 +9,7 @@ DROP TABLE IF EXISTS provinces;
DROP TABLE IF EXISTS regions;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS users;
DROP TYPE IF EXISTS user_reports_type;
DROP TYPE IF EXISTS comment_type;

View File

@ -1,20 +1,54 @@
CREATE TABLE users(
CREATE TABLE users(
"id" serial primary key not null,
"email" varchar unique,
"username" varchar unique,
"password" varchar,
"username" varchar unique not null,
"password" varchar not null,
"avatar_picture" varchar,
"google_sign_in_payload" varchar,
"banned_at" timestamp,
"banned_until" timestamp,
"ban_reason" varchar,
"is_permaban" boolean,
"is_admin" boolean,
"is_critics" boolean,
"is_verified" boolean,
"ipv4_address" varchar(15) not null,
"social_media" jsonb,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
);
CREATE TABLE client_ips(
"id" serial primary key not null,
"ipv4" varchar(15) not null,
"ipv6" varchar(40),
"banned_at" timestamp,
"banned_until" timestamp,
"reason" varchar,
"is_permaban" boolean,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TYPE user_reports_type as ENUM(
'comments',
'reviews',
'locations',
'users',
'stories'
);
CREATE TABLE user_reports(
"id" serial primary key not null,
"message" text not null,
"date" timestamp not null,
"report_target" integer not null,
"report_type" user_reports_type not null,
"submitted_by" integer references "users"("id") not null,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TABLE regions(
"id" serial primary key not null,
@ -41,12 +75,12 @@ CREATE TABLE regencies(
CREATE TABLE locations(
"id" serial primary key not null,
"address" varchar,
"name" varchar,
"address" varchar not null,
"name" varchar not null,
"google_maps_link" varchar,
"submitted_by" integer references "users"("id") not null,
"total_visited" integer,
"thumbnail" varchar,
"thumbnail" varchar not null,
"regency_id" smallint references "regencies"("id") not null,
"is_deleted" boolean,
"created_at" timestamp default(now()),
@ -80,5 +114,21 @@ CREATE TABLE reviews (
"location_id" integer references "locations"("id") not null,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TYPE comment_type AS ENUM(
'stories',
'news',
'reviews'
);
CREATE TABLE comments(
"id" serial primary key not null,
"submitted_by" integer not null,
"comment_on" integer not null,
"comment_type" comment_type not null,
"reply_to" integer,
"is_hide" boolean,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);

80
db/mock/store.go Normal file
View File

@ -0,0 +1,80 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: git.nochill.in/nochill/hiling_go/db/sqlc (interfaces: Store)
// Package mockdb is a generated GoMock package.
package mockdb
import (
context "context"
reflect "reflect"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
gomock "go.uber.org/mock/gomock"
)
// MockStore is a mock of Store interface.
type MockStore struct {
ctrl *gomock.Controller
recorder *MockStoreMockRecorder
}
// MockStoreMockRecorder is the mock recorder for MockStore.
type MockStoreMockRecorder struct {
mock *MockStore
}
// NewMockStore creates a new mock instance.
func NewMockStore(ctrl *gomock.Controller) *MockStore {
mock := &MockStore{ctrl: ctrl}
mock.recorder = &MockStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
return m.recorder
}
// CreateUser mocks base method.
func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1)
}
// UpdatePassword mocks base method.
func (m *MockStore) UpdatePassword(arg0 context.Context, arg1 db.UpdatePasswordParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePassword indicates an expected call of UpdatePassword.
func (mr *MockStoreMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockStore)(nil).UpdatePassword), arg0, arg1)
}
// UpdateUser mocks base method.
func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockStoreMockRecorder) UpdateUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0, arg1)
}

View File

@ -1,6 +1,22 @@
-- name: CreateUser :exec
-- name: CreateUser :one
INSERT INTO users (
ipv4_address
) VALUES (
$1
);
username,
password
) VALUES ($1, $2)
RETURNING *;
-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE(sqlc.narg(email), email),
username = COALESCE(sqlc.narg(username), username),
avatar_picture = COALESCE(sqlc.narg(avatar_picture), avatar_picture)
WHERE
id = sqlc.arg(id)
RETURNING *;
-- name: UpdatePassword :exec
UPDATE users
SET password = $1
WHERE id = $2;

View File

@ -6,10 +6,124 @@ package db
import (
"database/sql"
"database/sql/driver"
"fmt"
"time"
"github.com/sqlc-dev/pqtype"
)
type CommentType string
const (
CommentTypeStories CommentType = "stories"
CommentTypeNews CommentType = "news"
CommentTypeReviews CommentType = "reviews"
)
func (e *CommentType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CommentType(s)
case string:
*e = CommentType(s)
default:
return fmt.Errorf("unsupported scan type for CommentType: %T", src)
}
return nil
}
type NullCommentType struct {
CommentType CommentType `json:"comment_type"`
Valid bool `json:"valid"` // Valid is true if CommentType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCommentType) Scan(value interface{}) error {
if value == nil {
ns.CommentType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CommentType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCommentType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CommentType), nil
}
type UserReportsType string
const (
UserReportsTypeComments UserReportsType = "comments"
UserReportsTypeReviews UserReportsType = "reviews"
UserReportsTypeLocations UserReportsType = "locations"
UserReportsTypeUsers UserReportsType = "users"
UserReportsTypeStories UserReportsType = "stories"
)
func (e *UserReportsType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = UserReportsType(s)
case string:
*e = UserReportsType(s)
default:
return fmt.Errorf("unsupported scan type for UserReportsType: %T", src)
}
return nil
}
type NullUserReportsType struct {
UserReportsType UserReportsType `json:"user_reports_type"`
Valid bool `json:"valid"` // Valid is true if UserReportsType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullUserReportsType) Scan(value interface{}) error {
if value == nil {
ns.UserReportsType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.UserReportsType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullUserReportsType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.UserReportsType), nil
}
type ClientIp struct {
ID int32 `json:"id"`
Ipv4 string `json:"ipv4"`
Ipv6 sql.NullString `json:"ipv6"`
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
Reason sql.NullString `json:"reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type Comment struct {
ID int32 `json:"id"`
SubmittedBy int32 `json:"submitted_by"`
CommentOn int32 `json:"comment_on"`
CommentType CommentType `json:"comment_type"`
ReplyTo sql.NullInt32 `json:"reply_to"`
IsHide sql.NullBool `json:"is_hide"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type Location struct {
ID int32 `json:"id"`
Address sql.NullString `json:"address"`
@ -79,17 +193,29 @@ type Tag struct {
type User struct {
ID int32 `json:"id"`
Email sql.NullString `json:"email"`
Username sql.NullString `json:"username"`
Password sql.NullString `json:"password"`
Username string `json:"username"`
Password string `json:"password"`
AvatarPicture sql.NullString `json:"avatar_picture"`
GoogleSignInPayload sql.NullString `json:"google_sign_in_payload"`
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
BanReason sql.NullString `json:"ban_reason"`
IsPermaban sql.NullBool `json:"is_permaban"`
IsAdmin sql.NullBool `json:"is_admin"`
IsCritics sql.NullBool `json:"is_critics"`
IsVerified sql.NullBool `json:"is_verified"`
Ipv4Address string `json:"ipv4_address"`
SocialMedia pqtype.NullRawMessage `json:"social_media"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type UserReport struct {
ID int32 `json:"id"`
Message string `json:"message"`
Date time.Time `json:"date"`
ReportTarget int32 `json:"report_target"`
ReportType UserReportsType `json:"report_type"`
SubmittedBy int32 `json:"submitted_by"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}

View File

@ -9,7 +9,9 @@ import (
)
type Querier interface {
CreateUser(ctx context.Context, ipv4Address string) error
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
}
var _ Querier = (*Queries)(nil)

32
db/sqlc/test/main_test.go Normal file
View File

@ -0,0 +1,32 @@
package db_test
import (
"database/sql"
"log"
"os"
"testing"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
_ "github.com/lib/pq"
)
var testQueries *db.Queries
var testDB *sql.DB
func TestMain(m *testing.M) {
var err error
config, err := util.LoadConfig("../../..")
if err != nil {
log.Fatal("cannot load config: ", err)
}
testDB, err = sql.Open(config.DBDriver, config.DBSourceTest)
if err != nil {
log.Fatal("cannot connect db: ", err)
}
testQueries = db.New(testDB)
os.Exit(m.Run())
}

View File

@ -0,0 +1,23 @@
package db_test
import (
"context"
"testing"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/stretchr/testify/require"
)
func TestCreateUser(t *testing.T) {
arg := db.CreateUserParams{
Username: util.RandomString(10),
Password: util.RandomString(10),
}
user, err := testQueries.CreateUser(context.Background(), arg)
require.NoError(t, err)
require.Equal(t, user.Username, arg.Username)
require.Equal(t, user.Password, arg.Password)
}

View File

@ -7,17 +7,105 @@ package db
import (
"context"
"database/sql"
)
const createUser = `-- name: CreateUser :exec
const createUser = `-- name: CreateUser :one
INSERT INTO users (
ipv4_address
) VALUES (
$1
)
username,
password
) VALUES ($1, $2)
RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, social_media, created_at, updated_at
`
func (q *Queries) CreateUser(ctx context.Context, ipv4Address string) error {
_, err := q.db.ExecContext(ctx, createUser, ipv4Address)
type CreateUserParams struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.Password)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Password,
&i.AvatarPicture,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updatePassword = `-- name: UpdatePassword :exec
UPDATE users
SET password = $1
WHERE id = $2
`
type UpdatePasswordParams struct {
Password string `json:"password"`
ID int32 `json:"id"`
}
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
_, err := q.db.ExecContext(ctx, updatePassword, arg.Password, arg.ID)
return err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE($1, email),
username = COALESCE($2, username),
avatar_picture = COALESCE($3, avatar_picture)
WHERE
id = $4
RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, social_media, created_at, updated_at
`
type UpdateUserParams struct {
Email sql.NullString `json:"email"`
Username sql.NullString `json:"username"`
AvatarPicture sql.NullString `json:"avatar_picture"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUser,
arg.Email,
arg.Username,
arg.AvatarPicture,
arg.ID,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Password,
&i.AvatarPicture,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

1
go.mod
View File

@ -45,6 +45,7 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/mock v0.2.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect

2
go.sum
View File

@ -241,6 +241,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU=
go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=

16
import_csv.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
cp ./db/csv_seeder/* /tmp/
sudo -u postgres psql \
-c '\copy users(id,username,password) FROM '"'/tmp/user.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-d hiling_dev &&
sudo -u postgres psql \
-c '\copy users(id,username,password) FROM '"'/tmp/user.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-d hiling_dev_test

11
notes
View File

@ -33,3 +33,14 @@ https://en.wikipedia.org/wiki/Provinces_of_Indonesia
##########################################################################################
######################### CUSTOM GIN VALIDATION ERR MESSAGE ##############################
make custom err message, and get all the type, the err message sucks now
https://github.com/gin-gonic/gin/issues/430 (using middleware)
tbh i'd raher use like a wrapper instead of middleware but we'll see
##########################################################################################

View File

@ -9,6 +9,7 @@ import (
type Config struct {
DBDriver string `mapstructure:"DB_TYPE"`
DBSource string `mapstructure:"DB_SOURCE"`
DBSourceTest string `mapstructure:"DB_SOURCE_TEST"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"`

View File

@ -9,7 +9,7 @@ import (
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("Failed to hash password: %w", err)
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}