mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 13:34:08 +01:00
Merge pull request #33 from ForFarmTeam/feature-test
Merge Feature Test to Main
This commit is contained in:
commit
af1715a71d
21
backend/cmd/generate_keys/main.go
Normal file
21
backend/cmd/generate_keys/main.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
key := make([]byte, 64)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
fmt.Println("Error generating key:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
secret := base64.StdEncoding.EncodeToString(key)
|
||||
fmt.Println("Generated JWT Secret (add to your .env as JWT_SECRET_KEY):")
|
||||
fmt.Println(secret)
|
||||
}
|
||||
@ -18,6 +18,7 @@ require (
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
google.golang.org/api v0.186.0
|
||||
)
|
||||
@ -30,6 +31,7 @@ require (
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.7 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-chi/httprate v0.15.0 // indirect
|
||||
@ -51,6 +53,7 @@ require (
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
@ -59,6 +62,7 @@ require (
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
|
||||
@ -160,6 +160,8 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
||||
239
backend/internal/api/auth_test.go
Normal file
239
backend/internal/api/auth_test.go
Normal file
@ -0,0 +1,239 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/forfarm/backend/internal/utilities"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockUserRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type EmailPasswordInput struct {
|
||||
Email string `json:"email" example:"Email address of the user"`
|
||||
Password string `json:"password" example:"Password of the user"`
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetByUUID(ctx context.Context, uuid string) (domain.User, error) {
|
||||
args := m.Called(ctx, uuid)
|
||||
return args.Get(0).(domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetByUsername(ctx context.Context, username string) (domain.User, error) {
|
||||
args := m.Called(ctx, username)
|
||||
return args.Get(0).(domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (domain.User, error) {
|
||||
args := m.Called(ctx, email)
|
||||
return args.Get(0).(domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) CreateOrUpdate(ctx context.Context, u *domain.User) error {
|
||||
args := m.Called(ctx, u)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Delete(ctx context.Context, id int64) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestRegisterHandler(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input RegisterInput
|
||||
mockSetup func(*MockUserRepository)
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "successful registration",
|
||||
input: RegisterInput{
|
||||
Body: EmailPasswordInput{
|
||||
Email: "test@example.com",
|
||||
Password: "ValidPass123!",
|
||||
},
|
||||
},
|
||||
mockSetup: func(m *MockUserRepository) {
|
||||
m.On("GetByEmail", mock.Anything, "test@example.com").Return(domain.User{}, domain.ErrNotFound)
|
||||
m.On("CreateOrUpdate", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
|
||||
{
|
||||
name: "existing email",
|
||||
input: RegisterInput{
|
||||
Body: struct {
|
||||
Email string `json:"email" example:"Email address of the user"`
|
||||
Password string `json:"password" example:"Password of the user"`
|
||||
}(struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
Email: "existing@example.com",
|
||||
Password: "ValidPass123!",
|
||||
}),
|
||||
},
|
||||
mockSetup: func(m *MockUserRepository) {
|
||||
m.On("GetByEmail", mock.Anything, "existing@example.com").Return(domain.User{
|
||||
Email: "existing@example.com",
|
||||
}, nil)
|
||||
},
|
||||
expectedError: huma.Error409Conflict("User with this email already exists"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRepo := &MockUserRepository{}
|
||||
if tt.mockSetup != nil {
|
||||
tt.mockSetup(mockRepo)
|
||||
}
|
||||
|
||||
api := &api{
|
||||
userRepo: mockRepo,
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
_, err := api.registerHandler(context.Background(), &tt.input)
|
||||
|
||||
if tt.expectedError == nil {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.expectedError.Error())
|
||||
}
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginHandler(t *testing.T) {
|
||||
correctPassword := "ValidPass123!"
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(correctPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate bcrypt hash: %v", err)
|
||||
}
|
||||
|
||||
userUUID := uuid.New().String()
|
||||
testUser := domain.User{
|
||||
UUID: userUUID,
|
||||
Email: "test@example.com",
|
||||
Password: string(hashedPassword),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelError,
|
||||
}))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input LoginInput
|
||||
mockSetup func(*MockUserRepository)
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "successful login",
|
||||
input: LoginInput{
|
||||
Body: EmailPasswordInput{
|
||||
Email: "test@example.com",
|
||||
Password: correctPassword,
|
||||
},
|
||||
},
|
||||
mockSetup: func(m *MockUserRepository) {
|
||||
m.On("GetByEmail", mock.Anything, "test@example.com").Return(testUser, nil)
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid credentials",
|
||||
input: LoginInput{
|
||||
Body: EmailPasswordInput{
|
||||
Email: "test@example.com",
|
||||
Password: "wrongpassword",
|
||||
},
|
||||
},
|
||||
mockSetup: func(m *MockUserRepository) {
|
||||
m.On("GetByEmail", mock.Anything, "test@example.com").Return(testUser, nil)
|
||||
},
|
||||
expectedError: huma.Error401Unauthorized("Invalid email or password"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRepo := &MockUserRepository{}
|
||||
if tt.mockSetup != nil {
|
||||
tt.mockSetup(mockRepo)
|
||||
}
|
||||
|
||||
api := &api{
|
||||
userRepo: mockRepo,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
_, err := api.loginHandler(context.Background(), &tt.input)
|
||||
|
||||
if tt.expectedError == nil {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.expectedError.Error())
|
||||
}
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginHandler_TokenGeneration(t *testing.T) {
|
||||
userUUID := uuid.New().String()
|
||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("ValidPass123!"), bcrypt.DefaultCost)
|
||||
testUser := domain.User{
|
||||
UUID: userUUID,
|
||||
Email: "test@example.com",
|
||||
Password: string(hashedPassword),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
mockRepo := &MockUserRepository{}
|
||||
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(testUser, nil)
|
||||
|
||||
api := &api{
|
||||
userRepo: mockRepo,
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
input := &LoginInput{
|
||||
Body: EmailPasswordInput{
|
||||
Email: "test@example.com",
|
||||
Password: "ValidPass123!",
|
||||
},
|
||||
}
|
||||
|
||||
output, err := api.loginHandler(context.Background(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, output.Body.Token)
|
||||
|
||||
err = utilities.VerifyJwtToken(output.Body.Token)
|
||||
assert.NoError(t, err)
|
||||
|
||||
extractedUUID, err := utilities.ExtractUUIDFromToken(output.Body.Token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, userUUID, extractedUUID)
|
||||
}
|
||||
237
backend/internal/repository/postgres_inventory_test.go
Normal file
237
backend/internal/repository/postgres_inventory_test.go
Normal file
@ -0,0 +1,237 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
"github.com/forfarm/backend/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockConnection is a mock implementation of the repository.Connection interface.
|
||||
type MockConnection struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockConnection) Exec(ctx context.Context, query string, args ...interface{}) (pgconn.CommandTag, error) {
|
||||
callArgs := []interface{}{ctx, query}
|
||||
callArgs = append(callArgs, args...)
|
||||
ret := m.Called(callArgs...)
|
||||
return ret.Get(0).(pgconn.CommandTag), ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockConnection) Query(ctx context.Context, query string, args ...interface{}) (pgx.Rows, error) {
|
||||
callArgs := []interface{}{ctx, query}
|
||||
callArgs = append(callArgs, args...)
|
||||
ret := m.Called(callArgs...)
|
||||
rows, _ := ret.Get(0).(pgx.Rows)
|
||||
return rows, ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockConnection) QueryRow(ctx context.Context, query string, args ...interface{}) pgx.Row {
|
||||
callArgs := []interface{}{ctx, query}
|
||||
callArgs = append(callArgs, args...)
|
||||
ret := m.Called(callArgs...)
|
||||
return ret.Get(0).(pgx.Row)
|
||||
}
|
||||
|
||||
func (m *MockConnection) BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) {
|
||||
ret := m.Called(ctx, txOptions)
|
||||
return ret.Get(0).(pgx.Tx), ret.Error(1)
|
||||
}
|
||||
|
||||
// MockRows is a mock implementation of pgx.Rows
|
||||
type MockRows struct {
|
||||
mock.Mock
|
||||
currentIndex int
|
||||
data []map[string]interface{}
|
||||
columns []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *MockRows) Next() bool {
|
||||
m.Called()
|
||||
if m.err != nil {
|
||||
return false
|
||||
}
|
||||
m.currentIndex++
|
||||
return m.currentIndex <= len(m.data)
|
||||
}
|
||||
|
||||
func (m *MockRows) Scan(dest ...interface{}) error {
|
||||
args := m.Called(dest...)
|
||||
if args.Error(0) != nil {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
if m.currentIndex > len(m.data) || m.currentIndex == 0 {
|
||||
return errors.New("scan called out of bounds")
|
||||
}
|
||||
|
||||
currentRow := m.data[m.currentIndex-1]
|
||||
for i, d := range dest {
|
||||
colName := strconv.Itoa(i)
|
||||
if len(m.columns) > i {
|
||||
colName = m.columns[i]
|
||||
}
|
||||
if val, ok := currentRow[colName]; ok {
|
||||
switch ptr := d.(type) {
|
||||
case *string:
|
||||
*ptr = val.(string)
|
||||
case *int:
|
||||
*ptr = val.(int)
|
||||
case *float64:
|
||||
*ptr = val.(float64)
|
||||
case *time.Time:
|
||||
*ptr = val.(time.Time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRows) Close() {
|
||||
m.Called()
|
||||
}
|
||||
|
||||
func (m *MockRows) Err() error {
|
||||
args := m.Called()
|
||||
if args.Error(0) != nil {
|
||||
return args.Error(0)
|
||||
}
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *MockRows) CommandTag() pgconn.CommandTag {
|
||||
return pgconn.CommandTag{}
|
||||
}
|
||||
|
||||
func (m *MockRows) Conn() *pgx.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRows) FieldDescriptions() []pgconn.FieldDescription {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRows) RawValues() [][]byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRows) Values() ([]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MockEventPublisher is a mock implementation of the domain.EventPublisher interface.
|
||||
type MockEventPublisher struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockEventPublisher) Publish(ctx context.Context, event domain.Event) error {
|
||||
args := m.Called(ctx, event)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// TestGetByID tests the GetByID function of the inventory repository.
|
||||
func TestGetByID(t *testing.T) {
|
||||
mockConn := new(MockConnection)
|
||||
mockRows := new(MockRows)
|
||||
inventoryRepo := repository.NewPostgresInventory(mockConn, nil, nil)
|
||||
|
||||
testID := uuid.New().String()
|
||||
testUserID := uuid.New().String()
|
||||
|
||||
columns := []string{"id", "user_id", "name", "category_id", "quantity", "unit_id", "date_added", "status_id", "created_at", "updated_at", "category_name", "status_name", "unit_name"}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// Test: Successful retrieval of an inventory item by ID.
|
||||
mockRows.On("Next").Return(true).Once()
|
||||
mockRows.On("Scan", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
mockRows.On("Close").Return().Once()
|
||||
mockRows.On("Err").Return(nil).Once()
|
||||
mockRows.On("CommandTag").Return(pgconn.CommandTag{}).Once()
|
||||
mockRows.On("FieldDescriptions").Return([]pgconn.FieldDescription{}).Once()
|
||||
mockRows.On("RawValues").Return([][]byte{}).Once()
|
||||
mockRows.On("Values").Return([]interface{}{}, nil).Once()
|
||||
|
||||
mockConn.On("Query", mock.Anything, mock.AnythingOfType("string"), testID, testUserID).Return(mockRows, nil).Once()
|
||||
|
||||
mockRows.data = []map[string]interface{}{
|
||||
{
|
||||
"id": testID,
|
||||
"user_id": testUserID,
|
||||
"name": "Test Item",
|
||||
"category_id": 1,
|
||||
"quantity": 10.5,
|
||||
"unit_id": 1,
|
||||
"date_added": time.Now(),
|
||||
"status_id": 1,
|
||||
"created_at": time.Now(),
|
||||
"updated_at": time.Now(),
|
||||
"category_name": "Category Name",
|
||||
"status_name": "Status Name",
|
||||
"unit_name": "Unit Name",
|
||||
},
|
||||
}
|
||||
mockRows.columns = columns
|
||||
|
||||
item, err := inventoryRepo.GetByID(context.Background(), testID, testUserID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testID, item.ID)
|
||||
mockConn.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
// Test: Item not found scenario.
|
||||
mockRows.On("Next").Return(false).Once()
|
||||
mockRows.On("Close").Return().Once()
|
||||
mockRows.On("Err").Return(nil).Once()
|
||||
mockRows.On("CommandTag").Return(pgconn.CommandTag{}).Once()
|
||||
mockRows.On("FieldDescriptions").Return([]pgconn.FieldDescription{}).Once()
|
||||
mockRows.On("RawValues").Return([][]byte{}).Once()
|
||||
mockRows.On("Values").Return([]interface{}{}, nil).Once()
|
||||
|
||||
mockConn.On("Query", mock.Anything, mock.AnythingOfType("string"), testID, testUserID).Return(mockRows, nil).Once()
|
||||
|
||||
_, err := inventoryRepo.GetByID(context.Background(), testID, testUserID)
|
||||
assert.ErrorIs(t, err, domain.ErrNotFound)
|
||||
mockConn.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("query error", func(t *testing.T) {
|
||||
// Test: Database query returns an error.
|
||||
mockConn.On("Query", mock.Anything, mock.AnythingOfType("string"), testID, testUserID).Return(nil, errors.New("database error")).Once()
|
||||
|
||||
_, err := inventoryRepo.GetByID(context.Background(), testID, testUserID)
|
||||
assert.Error(t, err)
|
||||
mockConn.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("scan error", func(t *testing.T) {
|
||||
// Test: Error during row scanning.
|
||||
mockRows.On("Next").Return(true).Once()
|
||||
mockRows.On("Scan", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("scan error")).Once()
|
||||
mockRows.On("Close").Return().Once()
|
||||
mockRows.On("Err").Return(nil).Once()
|
||||
mockRows.On("CommandTag").Return(pgconn.CommandTag{}).Once()
|
||||
mockRows.On("FieldDescriptions").Return([]pgconn.FieldDescription{}).Once()
|
||||
mockRows.On("RawValues").Return([][]byte{}).Once()
|
||||
mockRows.On("Values").Return([]interface{}{}, nil).Once()
|
||||
|
||||
mockConn.On("Query", mock.Anything, mock.AnythingOfType("string"), testID, testUserID).Return(mockRows, nil).Once()
|
||||
|
||||
_, err := inventoryRepo.GetByID(context.Background(), testID, testUserID)
|
||||
assert.Error(t, err)
|
||||
mockConn.AssertExpectations(t)
|
||||
})
|
||||
|
||||
}
|
||||
44
backend/internal/utilities/jwt_test.go
Normal file
44
backend/internal/utilities/jwt_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package utilities
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJWTTokenCreationAndVerification(t *testing.T) {
|
||||
testUUID := "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
token, err := CreateJwtToken(testUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
err = VerifyJwtToken(token)
|
||||
assert.NoError(t, err)
|
||||
|
||||
uuid, err := ExtractUUIDFromToken(token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testUUID, uuid)
|
||||
}
|
||||
|
||||
func TestExpiredJWTToken(t *testing.T) {
|
||||
|
||||
oldKey := defaultSecretKey
|
||||
defaultSecretKey = []byte("test-secret-key-1234567890-1234567890")
|
||||
defer func() { defaultSecretKey = oldKey }()
|
||||
|
||||
testUUID := "123e4567-e89b-12d3-a456-426614174000"
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"uuid": testUUID,
|
||||
"exp": time.Now().Add(-time.Hour).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString(defaultSecretKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = VerifyJwtToken(tokenString)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "token is expired")
|
||||
}
|
||||
97
backend/loadtest/main.go
Normal file
97
backend/loadtest/main.go
Normal file
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
mu sync.Mutex
|
||||
successes int
|
||||
failures int
|
||||
times []time.Duration
|
||||
}
|
||||
|
||||
func (m *Metrics) AddResult(success bool, duration time.Duration) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if success {
|
||||
m.successes++
|
||||
} else {
|
||||
m.failures++
|
||||
}
|
||||
m.times = append(m.times, duration)
|
||||
}
|
||||
|
||||
func (m *Metrics) PrintSummary(total int) {
|
||||
var totalTime time.Duration
|
||||
var min, max time.Duration
|
||||
|
||||
if len(m.times) > 0 {
|
||||
min = m.times[0]
|
||||
max = m.times[0]
|
||||
}
|
||||
|
||||
for _, t := range m.times {
|
||||
totalTime += t
|
||||
if t < min {
|
||||
min = t
|
||||
}
|
||||
if t > max {
|
||||
max = t
|
||||
}
|
||||
}
|
||||
|
||||
avg := time.Duration(0)
|
||||
if len(m.times) > 0 {
|
||||
avg = totalTime / time.Duration(len(m.times))
|
||||
}
|
||||
|
||||
fmt.Println("---------- Load Test Summary ----------")
|
||||
fmt.Printf("Total Requests: %d\n", total)
|
||||
fmt.Printf("Success: %d | Fail: %d\n", m.successes, m.failures)
|
||||
fmt.Printf("Min Time: %v | Max Time: %v | Avg Time: %v\n", min, max, avg)
|
||||
fmt.Printf("Success rate: %.2f%%\n", float64(m.successes)/float64(total)*100)
|
||||
fmt.Println("---------------------------------------")
|
||||
}
|
||||
|
||||
func hitEndpoint(wg *sync.WaitGroup, url string, metrics *Metrics) {
|
||||
defer wg.Done()
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
// req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
client := &http.Client{}
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
metrics.AddResult(false, duration)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
metrics.AddResult(success, duration)
|
||||
}
|
||||
|
||||
func main() {
|
||||
baseURL := "http://localhost:8000/plant"
|
||||
concurrentUsers := 200
|
||||
|
||||
var wg sync.WaitGroup
|
||||
metrics := &Metrics{}
|
||||
|
||||
for i := 0; i < concurrentUsers; i++ {
|
||||
wg.Add(1)
|
||||
go hitEndpoint(&wg, baseURL, metrics)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
metrics.PrintSummary(concurrentUsers)
|
||||
}
|
||||
@ -133,25 +133,25 @@ export default function BlogPage() {
|
||||
<div className="space-y-8">
|
||||
{/* Table of contents */}
|
||||
<div className="sticky top-24">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Table of Contents</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<nav className="space-y-2">
|
||||
{blog.tableOfContents?.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className={`text-left w-full px-2 py-1 text-sm rounded-md hover:bg-muted transition-colors ${
|
||||
item.level > 1 ? "ml-4" : ""
|
||||
}`}>
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/*<Card>*/}
|
||||
{/* <CardHeader>*/}
|
||||
{/* <CardTitle>Table of Contents</CardTitle>*/}
|
||||
{/* </CardHeader>*/}
|
||||
{/* <CardContent>*/}
|
||||
{/* <nav className="space-y-2">*/}
|
||||
{/* {blog.tableOfContents?.map((item) => (*/}
|
||||
{/* <button*/}
|
||||
{/* key={item.id}*/}
|
||||
{/* onClick={() => scrollToSection(item.id)}*/}
|
||||
{/* className={`text-left w-full px-2 py-1 text-sm rounded-md hover:bg-muted transition-colors ${*/}
|
||||
{/* item.level > 1 ? "ml-4" : ""*/}
|
||||
{/* }`}>*/}
|
||||
{/* {item.title}*/}
|
||||
{/* </button>*/}
|
||||
{/* ))}*/}
|
||||
{/* </nav>*/}
|
||||
{/* </CardContent>*/}
|
||||
{/*</Card>*/}
|
||||
|
||||
{/* Related articles */}
|
||||
{blog.relatedArticles && (
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
devIndicators: {
|
||||
buildActivity: false,
|
||||
},
|
||||
@ -9,7 +8,11 @@ const nextConfig: NextConfig = {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
hostname: "static.wixstatic.com",
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "static.wixstatic.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"framer-motion": "^12.4.10",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "15.1.0",
|
||||
"next": "15.2.4",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user