Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ jobs:
- name: Run go vet
run: go vet ./...

- name: Check formatting
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not properly formatted:"
gofmt -s -l .
exit 1
fi

- name: Run linting with our script
run: ./run_tests.sh lint

Expand Down
7 changes: 6 additions & 1 deletion app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/aws/aws-lambda-go/lambda"
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
"github.com/williamkoller/cloud-architecture-golang/internal/usr/handler"
"github.com/williamkoller/cloud-architecture-golang/internal/usr/repository"
usr_router "github.com/williamkoller/cloud-architecture-golang/internal/usr/router"
)

Expand All @@ -23,12 +25,15 @@ var (
func init() {
gin.SetMode(gin.ReleaseMode)
router = gin.Default()
api := router.Group("/api")

router.GET("/health", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})

usr_router.RegisterUserRoutes(router)
userRepo := repository.NewInMemoryUserRepository()
useHandler := handler.NewUserHandler(userRepo)
usr_router.RegisterUserRoutes(api, useHandler)

ginLambdaV2 = ginadapter.NewV2(router)
}
Expand Down
70 changes: 70 additions & 0 deletions internal/usr/domain/usr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package domain

import (
"fmt"
"strings"

"github.com/williamkoller/cloud-architecture-golang/internal/usr/domain/vo"
)

type UserType string

const (
UserTypeAdmin UserType = "Admin"
UserTypeUser UserType = "User"
)

type User struct {
Name string
Email vo.Email
Password vo.Password
Active bool
UserType UserType
}

func (u User) Validate() error {
var missing []string

if strings.TrimSpace(u.Name) == "" {
missing = append(missing, "Name")
}
if u.Email == "" {
missing = append(missing, "Email")
}
if u.Password == "" {
missing = append(missing, "Password")
}
if u.UserType == "" {
missing = append(missing, "UserType")
}

if len(missing) > 0 {
return fmt.Errorf("the following fields are required: %s", strings.Join(missing, ", "))
}
return nil
}

func NewUser(name, emailRaw, passRaw string, active bool, userType UserType) (User, error) {
email, err := vo.NewEmail(emailRaw)
if err != nil {
return User{}, err
}

pass, err := vo.NewPassword(passRaw)
if err != nil {
return User{}, err
}
u := User{
Name: name,
Email: email,
Password: pass,
Active: active,
UserType: userType,
}

if err := u.Validate(); err != nil {
return User{}, err
}

return u, nil
}
97 changes: 97 additions & 0 deletions internal/usr/domain/usr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package domain

import (
"strings"
"testing"

"github.com/williamkoller/cloud-architecture-golang/internal/usr/domain/vo"
)

func TestUserValidate_MissingFields_OrderAndMessage(t *testing.T) {
u := User{
Name: " ", // TrimSpace → vazio
Email: vo.Email(""), // vazio
Password: vo.Password(""), // vazio
Active: false, // não é obrigatório
UserType: UserType(""), // vazio
}

err := u.Validate()
if err == nil {
t.Fatalf("expected error for missing fields, got nil")
}

got := err.Error()
want := "the following fields are required: Name, Email, Password, UserType"
if got != want {
t.Fatalf("error message mismatch:\n got: %q\nwant: %q", got, want)
}
}

func TestUserValidate_Success(t *testing.T) {
u := User{
Name: "Ana",
Email: vo.Email("ana@example.com"),
Password: vo.Password("$2a$10$fakehashjustforvalidate"), // só precisa ser não-vazio
Active: true,
UserType: UserTypeUser,
}

if err := u.Validate(); err != nil {
t.Fatalf("Validate() unexpected error: %v", err)
}
}

func TestNewUser_Success(t *testing.T) {
u, err := NewUser("Ana", "ana@example.com", "secret123", true, UserTypeAdmin)
if err != nil {
t.Fatalf("NewUser unexpected error: %v", err)
}

if u.Name != "Ana" {
t.Fatalf("Name: got %q, want %q", u.Name, "Ana")
}
if string(u.Email) != "ana@example.com" {
t.Fatalf("Email: got %q, want %q", string(u.Email), "ana@example.com")
}

// >>> Correções aqui: não comparar com texto puro <<<
// 1) Não deve ser igual ao raw
if string(u.Password) == "secret123" {
t.Fatalf("Password must be hashed, but equals raw password")
}
// 2) Deve ter cara de bcrypt
if !strings.HasPrefix(string(u.Password), "$2") {
t.Fatalf("Password hash does not look like bcrypt: %q", string(u.Password))
}
// 3) Compare deve validar a senha crua
if ok := u.Password.Compare("secret123"); !ok {
t.Fatalf("Password.Compare should return true for the correct raw password")
}

if u.Active != true {
t.Fatalf("Active: got %v, want %v", u.Active, true)
}
if u.UserType != UserTypeAdmin {
t.Fatalf("UserType: got %v, want %v", u.UserType, UserTypeAdmin)
}
}

func TestNewUser_InvalidEmail_ReturnsError(t *testing.T) {
// Email malformado para forçar erro em vo.NewEmail
if _, err := NewUser("Ana", "invalid-email", "secret123", true, UserTypeUser); err == nil {
t.Fatalf("expected error for invalid email, got nil")
}
}

func TestNewUser_EmptyPassword_ReturnsError(t *testing.T) {
if _, err := NewUser("Ana", "ana@example.com", "", true, UserTypeUser); err == nil {
t.Fatalf("expected error for empty password, got nil")
}
}

func TestNewUser_NameOnlySpaces_ReturnsError(t *testing.T) {
if _, err := NewUser(strings.Repeat(" ", 3), "ana@example.com", "secret123", true, UserTypeUser); err == nil {
t.Fatalf("expected error for name with only spaces, got nil")
}
}
39 changes: 39 additions & 0 deletions internal/usr/domain/vo/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package vo

import (
"errors"
"fmt"
"net/mail"
"strings"
)

type Email string

func NewEmail(value string) (Email, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "", errors.New("invalid email: empty")
}

addr, err := mail.ParseAddress(trimmed)
if err != nil {
return "", fmt.Errorf("invalid email: %w", err)
}

addrSpec := trimmed
if i := strings.Index(trimmed, "<"); i != -1 {
if j := strings.Index(trimmed[i:], ">"); j != -1 {
addrSpec = strings.TrimSpace(trimmed[i+1 : i+j])
}
}

if strings.ContainsAny(addrSpec, " \t\r\n") {
return "", errors.New("invalid email: whitespace inside address")
}

return Email(addr.Address), nil
}

func (e Email) String() string {
return string(e)
}
63 changes: 63 additions & 0 deletions internal/usr/domain/vo/email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package vo

import (
"testing"
)

func TestNewEmail_ValidAddresses(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "simple address",
input: "ana@example.com",
want: "ana@example.com",
},
{
name: "with display name",
input: "Ana Silva <ana@example.com>",
want: "ana@example.com",
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, err := NewEmail(tc.input)
if err != nil {
t.Fatalf("NewEmail(%q) unexpected error: %v", tc.input, err)
}
if string(got) != tc.want {
t.Fatalf("email value: got %q, want %q", string(got), tc.want)
}
})
}
}

func TestNewEmail_InvalidAddresses(t *testing.T) {
invalids := []string{
"",
"plainaddress",
"ana@",
"@example.com",
"ana@example,com",
"ana@ example.com",
"ana example@example",
}

for _, in := range invalids {
_, err := NewEmail(in)
if err == nil {
t.Fatalf("NewEmail(%q) expected error, got nil", in)
}
}
}

func TestEmail_String(t *testing.T) {
e := Email("user@example.com")
if got := e.String(); got != "user@example.com" {
t.Fatalf("String(): got %q, want %q", got, "user@example.com")
}
}
31 changes: 31 additions & 0 deletions internal/usr/domain/vo/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package vo

import (
"errors"

"golang.org/x/crypto/bcrypt"
)

type Password string

func NewPassword(raw string) (Password, error) {
if len(raw) < 6 {
return "", errors.New("password must be at least 6 characters long")
}

hash, err := bcrypt.GenerateFromPassword([]byte(raw), bcrypt.DefaultCost)
if err != nil {
return "", err
}

return Password(hash), nil
}

func (p Password) Compare(raw string) bool {
err := bcrypt.CompareHashAndPassword([]byte(p), []byte(raw))
return err == nil
}

func (p Password) String() string {
return string(p)
}
Loading
Loading