diff --git a/backend/internal/notifications/fcm_test.go b/backend/internal/notifications/fcm_test.go new file mode 100644 index 0000000..7622f05 --- /dev/null +++ b/backend/internal/notifications/fcm_test.go @@ -0,0 +1,114 @@ +package notifications + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestRegisterAndUnregisterToken(t *testing.T) { + svc := NewFCMService("") + ctx := context.Background() + userID := uuid.Must(uuid.NewV7()) + + if err := svc.RegisterToken(ctx, userID, ""); err == nil { + t.Fatal("expected error for empty token") + } + + if err := svc.RegisterToken(ctx, userID, "token-1"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := svc.RegisterToken(ctx, userID, "token-1"); err != nil { + t.Fatalf("unexpected error on duplicate token: %v", err) + } + + if len(svc.tokens[userID]) != 1 { + t.Fatalf("expected 1 token, got %d", len(svc.tokens[userID])) + } + + if err := svc.UnregisterToken(ctx, userID, "token-1"); err != nil { + t.Fatalf("unexpected error unregistering: %v", err) + } + + if len(svc.tokens[userID]) != 0 { + t.Fatalf("expected no tokens after unregister, got %d", len(svc.tokens[userID])) + } +} + +func TestSendPushSkipsWhenNoTokens(t *testing.T) { + svc := NewFCMService("") + ctx := context.Background() + + if err := svc.SendPush(ctx, uuid.Must(uuid.NewV7()), "title", "body", nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSendToFCMWithServerKey(t *testing.T) { + svc := NewFCMService("server-key") + ctx := context.Background() + + var capturedAuth string + svc.httpClient = &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + capturedAuth = req.Header.Get("Authorization") + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString("ok"))}, nil + }), + } + + err := svc.sendToFCM(ctx, FCMMessage{ + To: "token", + Notification: &FCMNotification{ + Title: "Hello", + Body: "World", + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if capturedAuth != "key=server-key" { + t.Fatalf("expected auth header to include server key, got %q", capturedAuth) + } +} + +func TestSendToFCMRejectsNonOK(t *testing.T) { + svc := NewFCMService("server-key") + ctx := context.Background() + + svc.httpClient = &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusBadRequest, Body: io.NopCloser(bytes.NewBufferString("bad"))}, nil + }), + } + + if err := svc.sendToFCM(ctx, FCMMessage{To: "token"}); err == nil { + t.Fatal("expected error for non-OK response") + } +} + +func TestNotifyOrderStatusChangedUsesDefaultEmoji(t *testing.T) { + svc := NewFCMService("") + ctx := context.Background() + buyerID := uuid.Must(uuid.NewV7()) + + _ = svc.RegisterToken(ctx, buyerID, "token-1") + + order := &domain.Order{ID: uuid.Must(uuid.NewV7()), Status: domain.OrderStatus("Em análise")} + buyer := &domain.User{ID: buyerID} + + if err := svc.NotifyOrderStatusChanged(ctx, order, buyer); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/backend/internal/notifications/service_test.go b/backend/internal/notifications/service_test.go new file mode 100644 index 0000000..bf21ae2 --- /dev/null +++ b/backend/internal/notifications/service_test.go @@ -0,0 +1,40 @@ +package notifications + +import ( + "bytes" + "context" + "log" + "strings" + "testing" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +func TestLoggerNotificationService(t *testing.T) { + buffer := &bytes.Buffer{} + original := log.Writer() + log.SetOutput(buffer) + defer log.SetOutput(original) + + svc := NewLoggerNotificationService() + ctx := context.Background() + order := &domain.Order{ID: uuid.Must(uuid.NewV7()), TotalCents: 12345, Status: domain.OrderStatusPaid} + buyer := &domain.User{Email: "buyer@example.com", Name: "Buyer"} + seller := &domain.User{Email: "seller@example.com", Name: "Seller"} + + if err := svc.NotifyOrderCreated(ctx, order, buyer, seller); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := svc.NotifyOrderStatusChanged(ctx, order, buyer); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + output := buffer.String() + if !strings.Contains(output, "Novo Pedido") { + t.Fatalf("expected output to include order created log, got %q", output) + } + if !strings.Contains(output, "Atualização do Pedido") { + t.Fatalf("expected output to include status change log, got %q", output) + } +} diff --git a/backend/internal/usecase/credit_line_test.go b/backend/internal/usecase/credit_line_test.go new file mode 100644 index 0000000..8c93a4a --- /dev/null +++ b/backend/internal/usecase/credit_line_test.go @@ -0,0 +1,110 @@ +package usecase + +import ( + "context" + "testing" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +func TestCheckCreditLineErrors(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + _, err := svc.CheckCreditLine(ctx, uuid.Must(uuid.NewV7()), 100) + if err == nil { + t.Fatal("expected error for missing company") + } + + company := domain.Company{ID: uuid.Must(uuid.NewV7())} + svc.repo.(*MockRepository).companies = append(svc.repo.(*MockRepository).companies, company) + + _, err = svc.CheckCreditLine(ctx, company.ID, 100) + if err == nil { + t.Fatal("expected error when credit line not enabled") + } +} + +func TestCheckCreditLineAvailable(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + company := domain.Company{ + ID: uuid.Must(uuid.NewV7()), + CreditLimitCents: 10000, + CreditUsedCents: 2500, + } + repo.companies = append(repo.companies, company) + + ok, err := svc.CheckCreditLine(ctx, company.ID, 5000) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Fatal("expected credit line to be available") + } +} + +func TestUseAndReleaseCreditLine(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + company := domain.Company{ + ID: uuid.Must(uuid.NewV7()), + CreditLimitCents: 9000, + CreditUsedCents: 1000, + } + repo.companies = append(repo.companies, company) + + if err := svc.UseCreditLine(ctx, company.ID, 3000); err != nil { + t.Fatalf("unexpected error using credit line: %v", err) + } + + updated, _ := repo.GetCompany(ctx, company.ID) + if updated.CreditUsedCents != 4000 { + t.Fatalf("expected credit used to be 4000, got %d", updated.CreditUsedCents) + } + + if err := svc.ReleaseCreditLine(ctx, company.ID, 5000); err != nil { + t.Fatalf("unexpected error releasing credit: %v", err) + } + + updated, _ = repo.GetCompany(ctx, company.ID) + if updated.CreditUsedCents != 0 { + t.Fatalf("expected credit used to floor at 0, got %d", updated.CreditUsedCents) + } +} + +func TestUseCreditLineInsufficient(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + company := domain.Company{ + ID: uuid.Must(uuid.NewV7()), + CreditLimitCents: 5000, + CreditUsedCents: 4500, + } + repo.companies = append(repo.companies, company) + + if err := svc.UseCreditLine(ctx, company.ID, 1000); err != ErrInsufficientCredit { + t.Fatalf("expected ErrInsufficientCredit, got %v", err) + } +} + +func TestSetCreditLimit(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + company := domain.Company{ID: uuid.Must(uuid.NewV7())} + repo.companies = append(repo.companies, company) + + if err := svc.SetCreditLimit(ctx, company.ID, 12000); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + updated, _ := repo.GetCompany(ctx, company.ID) + if updated.CreditLimitCents != 12000 { + t.Fatalf("expected credit limit to be 12000, got %d", updated.CreditLimitCents) + } +} diff --git a/backend/internal/usecase/payment_config_test.go b/backend/internal/usecase/payment_config_test.go new file mode 100644 index 0000000..8bf147f --- /dev/null +++ b/backend/internal/usecase/payment_config_test.go @@ -0,0 +1,53 @@ +package usecase + +import ( + "context" + "testing" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +func TestUpsertAndGetPaymentGatewayConfig(t *testing.T) { + svc, _ := newTestService() + ctx := context.Background() + + config := &domain.PaymentGatewayConfig{ + Provider: "stripe", + Active: true, + Credentials: "{\"secret\":\"secret\"}", + Environment: "sandbox", + Commission: 2.9, + } + + if err := svc.UpsertPaymentGatewayConfig(ctx, config); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + stored, err := svc.GetPaymentGatewayConfig(ctx, "stripe") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if stored == nil || stored.Provider != "stripe" { + t.Fatal("expected stored config to be returned") + } +} + +func TestOnboardSellerStoresAccount(t *testing.T) { + svc, repo := newTestService() + ctx := context.Background() + + sellerID := uuid.Must(uuid.NewV7()) + link, err := svc.OnboardSeller(ctx, sellerID, "stripe") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if link == "" { + t.Fatal("expected onboarding link") + } + + stored, _ := repo.GetSellerPaymentAccount(ctx, sellerID) + if stored == nil || stored.Status != "pending" { + t.Fatal("expected seller payment account to be stored as pending") + } +} diff --git a/backend/internal/usecase/usecase_test.go b/backend/internal/usecase/usecase_test.go index 9aaf340..4bf7678 100644 --- a/backend/internal/usecase/usecase_test.go +++ b/backend/internal/usecase/usecase_test.go @@ -19,6 +19,8 @@ type MockRepository struct { reviews []domain.Review shipping []domain.ShippingMethod shippingSettings map[uuid.UUID]domain.ShippingSettings + paymentConfigs map[string]domain.PaymentGatewayConfig + sellerAccounts map[uuid.UUID]domain.SellerPaymentAccount } func NewMockRepository() *MockRepository { @@ -31,6 +33,8 @@ func NewMockRepository() *MockRepository { reviews: make([]domain.Review, 0), shipping: make([]domain.ShippingMethod, 0), shippingSettings: make(map[uuid.UUID]domain.ShippingSettings), + paymentConfigs: make(map[string]domain.PaymentGatewayConfig), + sellerAccounts: make(map[uuid.UUID]domain.SellerPaymentAccount), } } @@ -355,18 +359,32 @@ func (m *MockRepository) ListWithdrawals(ctx context.Context, companyID uuid.UUI // Payment Config methods func (m *MockRepository) GetPaymentGatewayConfig(ctx context.Context, provider string) (*domain.PaymentGatewayConfig, error) { + if config, ok := m.paymentConfigs[provider]; ok { + copied := config + return &copied, nil + } return nil, nil } func (m *MockRepository) UpsertPaymentGatewayConfig(ctx context.Context, config *domain.PaymentGatewayConfig) error { + if config != nil { + m.paymentConfigs[config.Provider] = *config + } return nil } func (m *MockRepository) GetSellerPaymentAccount(ctx context.Context, sellerID uuid.UUID) (*domain.SellerPaymentAccount, error) { + if account, ok := m.sellerAccounts[sellerID]; ok { + copied := account + return &copied, nil + } return nil, nil } func (m *MockRepository) UpsertSellerPaymentAccount(ctx context.Context, account *domain.SellerPaymentAccount) error { + if account != nil { + m.sellerAccounts[account.SellerID] = *account + } return nil }