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.
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status

This commit is contained in:
qpismont 2025-04-17 20:24:05 +00:00
parent 3f7fe22392
commit 1f31d9c78e
22 changed files with 296 additions and 2 deletions

View file

@ -24,3 +24,9 @@ type AccountCreate struct {
Password string Password string
RoleId int RoleId int
} }
type AccountRegister struct {
Username string
Password string
RoleId int
}

View file

@ -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)
} }

View file

@ -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
}

View file

@ -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"
) )

View file

@ -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
}

View file

@ -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()

View 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)
}

View file

@ -0,0 +1,6 @@
package api
type GetVariableResponse struct {
Name string `json:"name"`
Value string `json:"value"`
}

View 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)
}

View 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)

View file

@ -0,0 +1,5 @@
package domain
type VariableRepository interface {
FetchOneByName(name string) (*Variable, error)
}

View file

@ -0,0 +1,7 @@
package domain
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
type VariableService interface {
GetVariable(name string) (*Variable, *core.HTTPError)
}

View 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"`
}

View file

@ -0,0 +1,5 @@
package repository
const (
SqlFetchOneByName = "SELECT * FROM variables WHERE name = $1"
)

View 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
}

View 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)
}

View 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
}

View 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)
}

View 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');

View 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

View 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
View file

@ -0,0 +1 @@
INSERT INTO variables (name, value) VALUES ('first_account_created', 'false');