Add account registration feature: introduce AccountRegister type, implement Register method in AccountService, and add corresponding tests. Also, create variable management API with CRUD operations and database migration for variables table.
This commit is contained in:
parent
3f7fe22392
commit
1f31d9c78e
22 changed files with 296 additions and 2 deletions
|
@ -24,3 +24,9 @@ type AccountCreate struct {
|
||||||
Password string
|
Password string
|
||||||
RoleId int
|
RoleId int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccountRegister struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
RoleId int
|
||||||
|
}
|
||||||
|
|
|
@ -4,5 +4,6 @@ import "gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
|
||||||
type AccountService interface {
|
type AccountService interface {
|
||||||
Login(login AccountLogin) (*AccountWithToken, *core.HTTPError)
|
Login(login AccountLogin) (*AccountWithToken, *core.HTTPError)
|
||||||
|
Register(register AccountRegister) (int, *core.HTTPError)
|
||||||
GetAccount(id int) (*Account, *core.HTTPError)
|
GetAccount(id int) (*Account, *core.HTTPError)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,3 +62,18 @@ func (r *Repository) FetchOneByUsername(username string) (*domain.Account, error
|
||||||
|
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FetchOneByRoleId(roleId int) (*domain.Account, error) {
|
||||||
|
var account domain.Account
|
||||||
|
|
||||||
|
err := r.db.Get(&account, SqlFetchOneByRoleId, roleId)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,4 +4,5 @@ const (
|
||||||
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
|
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
|
||||||
SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1"
|
SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1"
|
||||||
SqlFetchOneById = "SELECT * FROM accounts WHERE id = $1"
|
SqlFetchOneById = "SELECT * FROM accounts WHERE id = $1"
|
||||||
|
SqlFetchOneByRoleId = "SELECT * FROM accounts WHERE role_id = $1"
|
||||||
)
|
)
|
||||||
|
|
|
@ -60,3 +60,32 @@ func (s *Service) Login(login domain.AccountLogin) (*domain.AccountWithToken, *c
|
||||||
Token: token,
|
Token: token,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) Register(register domain.AccountRegister) (int, *core.HTTPError) {
|
||||||
|
accountExist, err := s.repository.FetchOneByUsername(register.Username)
|
||||||
|
if err != nil {
|
||||||
|
return 0, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if accountExist != nil {
|
||||||
|
return 0, domain.ErrAccountAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := core.HashPassword(register.Password)
|
||||||
|
if err != nil {
|
||||||
|
return 0, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
account := domain.Account{
|
||||||
|
Username: register.Username,
|
||||||
|
Password: hashedPassword,
|
||||||
|
RoleId: register.RoleId,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := s.repository.Insert(account)
|
||||||
|
if err != nil {
|
||||||
|
return 0, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ func (m *MockRepository) FetchOneById(id int) (*domain.Account, error) {
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args.Get(0).(*domain.Account), args.Error(1)
|
return args.Get(0).(*domain.Account), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ func (m *MockRepository) FetchOneByUsername(username string) (*domain.Account, e
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return args.Get(0).(*domain.Account), args.Error(1)
|
return args.Get(0).(*domain.Account), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +38,34 @@ func (m *MockRepository) Insert(account domain.Account) (int, error) {
|
||||||
return args.Int(0), args.Error(1)
|
return args.Int(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) FetchOneByRoleId(roleId int) (*domain.Account, error) {
|
||||||
|
args := m.Called(roleId)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.Get(0).(*domain.Account), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Register(t *testing.T) {
|
||||||
|
mockRepo := new(MockRepository)
|
||||||
|
|
||||||
|
mockRepo.On("FetchOneByUsername", "admin").Return(nil, nil)
|
||||||
|
mockRepo.On("Insert", mock.Anything).Return(1, nil)
|
||||||
|
|
||||||
|
service := NewService(mockRepo)
|
||||||
|
|
||||||
|
id, _ := service.Register(domain.AccountRegister{
|
||||||
|
Username: "admin",
|
||||||
|
Password: "admin",
|
||||||
|
RoleId: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, 1, id)
|
||||||
|
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestService_GetAccount(t *testing.T) {
|
func TestService_GetAccount(t *testing.T) {
|
||||||
db := test.SetupTestDB(t, "../../..")
|
db := test.SetupTestDB(t, "../../..")
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
40
internal/variables/api/controller.go
Normal file
40
internal/variables/api/controller.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
service domain.VariableService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(service domain.VariableService) Controller {
|
||||||
|
return Controller{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Controller) GetVariableByName(w *core.Response, r *http.Request) {
|
||||||
|
name := r.PathValue("name")
|
||||||
|
|
||||||
|
variable, httpErr := c.service.GetVariable(name)
|
||||||
|
if httpErr != nil {
|
||||||
|
w.WriteError(httpErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := GetVariableResponse{
|
||||||
|
Name: variable.Name,
|
||||||
|
Value: variable.Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(core.NewInternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Json(responseJson)
|
||||||
|
}
|
6
internal/variables/api/dto.go
Normal file
6
internal/variables/api/dto.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type GetVariableResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
16
internal/variables/api/routes.go
Normal file
16
internal/variables/api/routes.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/repository"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/service"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
|
||||||
|
repository := repository.NewRepository(db)
|
||||||
|
service := service.NewService(repository)
|
||||||
|
controller := NewController(service)
|
||||||
|
|
||||||
|
srv.HandleFunc("GET /variables/{name}", controller.GetVariableByName)
|
||||||
|
}
|
9
internal/variables/domain/errors.go
Normal file
9
internal/variables/domain/errors.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrVariableNotFound = core.NewHTTPError(http.StatusNotFound, "variable not found", nil)
|
5
internal/variables/domain/repository.go
Normal file
5
internal/variables/domain/repository.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type VariableRepository interface {
|
||||||
|
FetchOneByName(name string) (*Variable, error)
|
||||||
|
}
|
7
internal/variables/domain/service.go
Normal file
7
internal/variables/domain/service.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
|
||||||
|
type VariableService interface {
|
||||||
|
GetVariable(name string) (*Variable, *core.HTTPError)
|
||||||
|
}
|
11
internal/variables/domain/variable.go
Normal file
11
internal/variables/domain/variable.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Variable struct {
|
||||||
|
Id int `db:"id" json:"id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Value string `db:"value" json:"value"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
5
internal/variables/repository/const.go
Normal file
5
internal/variables/repository/const.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
const (
|
||||||
|
SqlFetchOneByName = "SELECT * FROM variables WHERE name = $1"
|
||||||
|
)
|
24
internal/variables/repository/variable.go
Normal file
24
internal/variables/repository/variable.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VariableRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(db *sqlx.DB) *VariableRepository {
|
||||||
|
return &VariableRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VariableRepository) FetchOneByName(name string) (*domain.Variable, error) {
|
||||||
|
var variable domain.Variable
|
||||||
|
err := r.db.Get(&variable, "SELECT * FROM variables WHERE name = $1", name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &variable, nil
|
||||||
|
}
|
20
internal/variables/repository/variable_test.go
Normal file
20
internal/variables/repository/variable_test.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVariableRepository_FetchOneByName(t *testing.T) {
|
||||||
|
db := test.SetupTestDB(t, "../../..")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repository := NewRepository(db)
|
||||||
|
|
||||||
|
variable, err := repository.FetchOneByName("first_account_created")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "first_account_created", variable.Name)
|
||||||
|
assert.Equal(t, "false", variable.Value)
|
||||||
|
}
|
27
internal/variables/service/variable.go
Normal file
27
internal/variables/service/variable.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VariableService struct {
|
||||||
|
repository domain.VariableRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repository domain.VariableRepository) *VariableService {
|
||||||
|
return &VariableService{repository: repository}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VariableService) GetVariable(name string) (*domain.Variable, *core.HTTPError) {
|
||||||
|
variable, err := s.repository.FetchOneByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if variable == nil {
|
||||||
|
return nil, domain.ErrVariableNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable, nil
|
||||||
|
}
|
24
internal/variables/service/variable_test.go
Normal file
24
internal/variables/service/variable_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/variables/repository"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVariableService_GetVariable(t *testing.T) {
|
||||||
|
db := test.SetupTestDB(t, "../../..")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repository := repository.NewRepository(db)
|
||||||
|
service := NewService(repository)
|
||||||
|
|
||||||
|
variable, _ := service.GetVariable("first_account_created")
|
||||||
|
|
||||||
|
assert.NotNil(t, variable)
|
||||||
|
assert.Equal(t, 1, variable.Id)
|
||||||
|
assert.Equal(t, "first_account_created", variable.Name)
|
||||||
|
assert.Equal(t, "false", variable.Value)
|
||||||
|
}
|
14
migrations/20250417194859_variables.up.sql
Normal file
14
migrations/20250417194859_variables.up.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE public.variables
|
||||||
|
(
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
name text,
|
||||||
|
value text,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS public.variables
|
||||||
|
OWNER to dev;
|
||||||
|
|
||||||
|
INSERT INTO public.variables (name, value) VALUES ('first_account_created', 'false');
|
5
scripts/create_migration.sh
Normal file
5
scripts/create_migration.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
timestamp=$(date +%Y%m%d%H%M%S)
|
||||||
|
migration_file="migrations/$(date +%Y%m%d%H%M%S)_$1.up.sql"
|
||||||
|
touch $migration_file
|
|
@ -1,11 +1,9 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
#Check if the migrate command is available
|
|
||||||
if ! command -v migrate &> /dev/null; then
|
if ! command -v migrate &> /dev/null; then
|
||||||
echo "migrate could not be found"
|
echo "migrate could not be found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
#Execute migrate up
|
|
||||||
migrate -path ./migrations -database "$DATABASE_URL" -verbose up
|
migrate -path ./migrations -database "$DATABASE_URL" -verbose up
|
||||||
|
|
1
test/fixtures/01-variables.sql
vendored
Normal file
1
test/fixtures/01-variables.sql
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
INSERT INTO variables (name, value) VALUES ('first_account_created', 'false');
|
Loading…
Reference in a new issue