feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
package gapi
import (
"context"
"fmt"
"strings"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"google.golang.org/grpc/metadata"
)
const (
authHeaderKey = "authorization"
authTypeBearer = "bearer"
)
func (s *Server) authorizeUser(ctx context.Context, accessibleRoles []string) (*auth.Payload, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("missing metadata")
}
values := md.Get(authHeaderKey)
if len(values) == 0 {
return nil, fmt.Errorf("missing authorization header")
}
authHeader := values[0]
fields := strings.Fields(authHeader)
if len(fields) < 2 {
return nil, fmt.Errorf("invalid authorization header format")
}
authType := strings.ToLower(fields[0])
if authType != authTypeBearer {
return nil, fmt.Errorf("unsupported authorization type: %s", authType)
}
accessToken := fields[1]
payload, err := s.tokenMaker.VerifyToken(accessToken)
if err != nil {
return nil, fmt.Errorf("invalid token: %v", err)
}
if !role.HasPermission(payload.Role, accessibleRoles) {
return nil, fmt.Errorf("permission denied")
}
return payload, nil
}
+38
View File
@@ -0,0 +1,38 @@
package gapi
import (
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/timestamppb"
)
func convertUser(user db.User) *pb.User {
return &pb.User{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
CreatedAt: timestamppb.New(user.CreatedAt),
PasswordChangedAt: timestamppb.New(user.PasswordChangedAt),
}
}
func convertLoginUserResponse(
user db.User,
sessionID uuid.UUID,
accessToken string,
refreshToken string,
accessExpiredAt time.Time,
refreshExpiredAt time.Time,
) *pb.LoginUserResponse {
return &pb.LoginUserResponse{
User: convertUser(user),
SessionId: sessionID.String(),
AccessToken: accessToken,
RefreshToken: refreshToken,
AccessTokenExpiresAt: timestamppb.New(accessExpiredAt),
RefreshTokenExpiresAt: timestamppb.New(refreshExpiredAt),
}
}
+24
View File
@@ -0,0 +1,24 @@
package gapi
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func fieldViolation(field string, err error) *errdetails.BadRequest_FieldViolation {
return &errdetails.BadRequest_FieldViolation{
Field: field,
Description: err.Error(),
}
}
func invalidArgumentError(violations []*errdetails.BadRequest_FieldViolation) error {
badRequest := &errdetails.BadRequest{FieldViolations: violations}
stInvalid := status.New(codes.InvalidArgument, "invalid user request")
stDetails, err := stInvalid.WithDetails(badRequest)
if err != nil {
return stInvalid.Err()
}
return stDetails.Err()
}
+79
View File
@@ -0,0 +1,79 @@
package gapi
import (
"context"
"net/http"
"time"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func GrpcLogger(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
startTime := time.Now()
result, err := handler(ctx, req)
duration := time.Since(startTime)
statusCode := codes.Unknown
if st, ok := status.FromError(err); ok {
statusCode = st.Code()
}
logger := log.Info()
if err != nil {
logger = log.Error().Err(err)
}
logger.Str("protocol", "grpc").
Str("method", info.FullMethod).
Int("status", int(statusCode)).
Str("status_text", statusCode.String()).
Dur("duration", duration).
Msg("Recieved a gRPC request")
return result, err
}
type ResponseRecorder struct {
http.ResponseWriter
StatusCode int
Body []byte
}
func (rr *ResponseRecorder) WriteHeader(statusCode int) {
rr.StatusCode = statusCode
rr.ResponseWriter.WriteHeader(statusCode)
}
func (rr *ResponseRecorder) Write(b []byte) (int, error) {
rr.Body = b
return rr.ResponseWriter.Write(b)
}
func HTTPLogger(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
rr := &ResponseRecorder{
ResponseWriter: w,
StatusCode: http.StatusOK,
}
handler.ServeHTTP(rr, r)
duration := time.Since(startTime)
logger := log.Info()
if rr.StatusCode != http.StatusOK {
logger = log.Error().Bytes("body", rr.Body)
}
logger.Str("protocol", "http").
Str("method", r.Method).
Str("path", r.RequestURI).
Int("status", rr.StatusCode).
Str("status_text", http.StatusText(rr.StatusCode)).
Dur("duration", duration).
Msg("Recieved an HTTP request")
})
}
+46
View File
@@ -0,0 +1,46 @@
package gapi
import (
"context"
"fmt"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
)
func newTestServer(t *testing.T, store db.Store, td worker.TaskDistributor, userService service.UserService, authService service.AuthService) *Server {
config := config.Config{
AuthTokenExp: time.Minute,
AllowedOrigins: []string{
"*",
},
}
tokenMaker, err := auth.NewPasetoMaker(config)
require.NoError(t, err)
server, err := NewServer(config, store, td, tokenMaker, userService, authService)
require.NoError(t, err)
return server
}
func newContextWithBearerToken(t *testing.T, tokenMaker auth.Maker, username string, role string, duration time.Duration) context.Context {
authToken, _, err := tokenMaker.CreateToken(username, role, duration)
require.NoError(t, err)
bearerToken := fmt.Sprintf("%s %s", authTypeBearer, authToken)
md := metadata.MD{
authHeaderKey: []string{
bearerToken,
},
}
return metadata.NewIncomingContext(context.Background(), md)
}
+42
View File
@@ -0,0 +1,42 @@
package gapi
import (
"context"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
)
const (
grpcGatewayUserAgentHeader = "grpcgateway-user-agent"
userAgentHeader = "user-agent"
xForwardedForHeader = "x-forwarded-for"
)
type Metadata struct {
UserAgent string
ClientIP string
}
func (s *Server) extractMetadata(ctx context.Context) *Metadata {
mtdt := Metadata{}
md, ok := metadata.FromIncomingContext(ctx)
if ok {
if userAgent := md.Get(grpcGatewayUserAgentHeader); len(userAgent) > 0 {
mtdt.UserAgent = userAgent[0]
}
if userAgent := md.Get(userAgentHeader); len(userAgent) > 0 {
mtdt.UserAgent = userAgent[0]
}
if clientIPs := md.Get(xForwardedForHeader); len(clientIPs) > 0 {
mtdt.ClientIP = clientIPs[0]
}
}
if p, ok := peer.FromContext(ctx); ok {
mtdt.ClientIP = p.Addr.String()
}
return &mtdt
}
+58
View File
@@ -0,0 +1,58 @@
package gapi
import (
"context"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
violations := validateCreateUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
result, err := s.userService.CreateUser(ctx, service.RegisterUserRequest{
Username: req.GetUsername(),
Password: req.GetPassword(),
FullName: req.GetFullName(),
Email: req.GetEmail(),
})
if err != nil {
// Map service errors to gRPC status codes
if db.ErrorCode(err) == db.UniqueViolation {
return nil, status.Errorf(codes.AlreadyExists, "username already exists")
}
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
}
resp := &pb.CreateUserResponse{
User: convertUser(result.User),
}
return resp, nil
}
func validateCreateUserRequest(req *pb.CreateUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if err := val.ValidateFullName(req.GetFullName()); err != nil {
violations = append(violations, fieldViolation("full_name", err))
}
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
if err := val.ValidateEmail(req.GetEmail()); err != nil {
violations = append(violations, fieldViolation("email", err))
}
return violations
}
@@ -0,0 +1,182 @@
package gapi
import (
"context"
"database/sql"
"testing"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/internal/service"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func randomUser(t *testing.T) (user db.User, password string) {
password = testutil.RandomString(6)
hashedPassword, err := security.HashPassword(password)
if err != nil {
t.Fatal(err)
}
user = db.User{
Username: testutil.RandomOwner(),
HashedPassword: hashedPassword,
Role: role.MemberRole,
FullName: testutil.RandomOwner(),
Email: testutil.RandomEmail(),
}
return
}
func TestCreateUserRPC(t *testing.T) {
user, password := randomUser(t)
testCases := []struct {
name string
req *pb.CreateUserRequest
buildStubs func(userService *mocksvc.MockUserService)
checkResponse func(t *testing.T, res *pb.CreateUserResponse, err error)
}{
{
name: "OK",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
arg := service.RegisterUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
}
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(&service.RegisterUserResponse{
User: user,
}, nil)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, user.Username, res.User.Username)
require.Equal(t, user.FullName, res.User.FullName)
require.Equal(t, user.Email, res.User.Email)
require.NotEmpty(t, res.User.CreatedAt)
},
},
{
name: "InternalError",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, sql.ErrConnDone)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Internal)
},
},
{
name: "DuplicateUsername",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, db.ErrUniqueViolation)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.AlreadyExists)
},
},
{
name: "InvalidEmail",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: "invalid-email",
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
{
name: "PasswordTooShort",
req: &pb.CreateUserRequest{
Username: user.Username,
Password: "short",
FullName: user.FullName,
Email: user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
userCtrl := gomock.NewController(t)
defer userCtrl.Finish()
userService := mocksvc.NewMockUserService(userCtrl)
tc.buildStubs(userService)
// Start test server and send request
s := newTestServer(t, nil, nil, userService, nil)
res, err := s.CreateUser(context.Background(), tc.req)
tc.checkResponse(t, res, err)
})
}
}
+64
View File
@@ -0,0 +1,64 @@
package gapi
import (
"context"
"errors"
"strings"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) (*pb.LoginUserResponse, error) {
violations := validateLoginUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
mtdt := s.extractMetadata(ctx)
result, err := s.authService.Login(ctx, service.LoginCredentials{
Username: req.GetUsername(),
Password: req.GetPassword(),
UserAgent: mtdt.UserAgent,
ClientIP: mtdt.ClientIP,
})
if err != nil {
errMsg := err.Error()
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "user not found") {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if strings.Contains(errMsg, "incorrect password") {
return nil, status.Errorf(codes.Unauthenticated, "incorrect password")
}
return nil, status.Errorf(codes.Internal, "failed to login: %v", err)
}
resp := convertLoginUserResponse(
result.User,
result.SessionID,
result.AccessToken,
result.RefreshToken,
result.AccessTokenExpiresAt,
result.RefreshTokenExpiresAt,
)
return resp, nil
}
// TODO: Change to throw "username or password is incorrect" instead of specifying which one is wrong
func validateLoginUserRequest(req *pb.LoginUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
return violations
}
+88
View File
@@ -0,0 +1,88 @@
package gapi
import (
"context"
"errors"
"strings"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
authPayload, err := s.authorizeUser(ctx, []string{role.AdminRole, role.MemberRole})
if err != nil {
return nil, unauthenticatedError(err)
}
if authPayload.Role != role.AdminRole && authPayload.Username != req.GetUsername() {
return nil, status.Errorf(codes.PermissionDenied, "cannot update other user's information")
}
violations := validateUpdateUserRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
updateReq := service.UserUpdateRequest{
Username: req.GetUsername(),
}
// Set FullName if provided
if req.FullName != nil {
fullName := req.GetFullName()
updateReq.FullName = &fullName
}
// Set Email if provided
if req.Email != nil {
email := req.GetEmail()
updateReq.Email = &email
}
user, err := s.userService.UpdateUser(ctx, updateReq)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(err.Error(), "not found") {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
}
resp := &pb.UpdateUserResponse{
User: convertUser(*user),
}
return resp, nil
}
func validateUpdateUserRequest(req *pb.UpdateUserRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateUsername(req.GetUsername()); err != nil {
violations = append(violations, fieldViolation("username", err))
}
if req.FullName != nil {
if err := val.ValidateFullName(req.GetFullName()); err != nil {
violations = append(violations, fieldViolation("full_name", err))
}
}
if req.Password != nil {
if err := val.ValidatePassword(req.GetPassword()); err != nil {
violations = append(violations, fieldViolation("password", err))
}
}
if req.Email != nil {
if err := val.ValidateEmail(req.GetEmail()); err != nil {
violations = append(violations, fieldViolation("email", err))
}
}
return violations
}
func unauthenticatedError(err error) error {
return status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err)
}
@@ -0,0 +1,184 @@
package gapi
import (
"context"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestUpdateUserRPC(t *testing.T) {
user, _ := randomUser(t)
newName := testutil.RandomOwner()
newEmail := testutil.RandomEmail()
invalidEmail := "invalid-email-format"
testCases := []struct {
name string
req *pb.UpdateUserRequest
buildStubs func(userService *mocksvc.MockUserService)
buildContext func(t *testing.T, tokenMaker auth.Maker) context.Context
checkResponse func(t *testing.T, res *pb.UpdateUserResponse, err error)
}{
{
name: "OK",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
arg := service.UserUpdateRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
}
updatedUser := db.User{
Username: user.Username,
HashedPassword: user.HashedPassword,
FullName: newName,
Email: newEmail,
}
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(&updatedUser, nil)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.NoError(t, err)
require.NotNil(t, res)
updatedUser := res.GetUser()
require.Equal(t, user.Username, updatedUser.Username)
require.Equal(t, newName, updatedUser.FullName)
require.Equal(t, newEmail, updatedUser.Email)
},
}, {
name: "UserNotFound",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(1).
Return(nil, db.ErrRecordNotFound)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.NotFound)
},
}, {
name: "ExpiredToken",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, -time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Unauthenticated)
},
}, {
name: "NoAuthorizationHeader",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &newEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return context.Background()
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.Unauthenticated)
},
}, {
name: "InvalidEmail",
req: &pb.UpdateUserRequest{
Username: user.Username,
FullName: &newName,
Email: &invalidEmail,
},
buildStubs: func(userService *mocksvc.MockUserService) {
userService.EXPECT().
UpdateUser(gomock.Any(), gomock.Any()).
Times(0)
},
buildContext: func(t *testing.T, tokenMaker auth.Maker) context.Context {
return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute)
},
checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) {
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, st.Code(), codes.InvalidArgument)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
userCtrl := gomock.NewController(t)
defer userCtrl.Finish()
userService := mocksvc.NewMockUserService(userCtrl)
tc.buildStubs(userService)
// Start test server and send request
s := newTestServer(t, nil, nil, userService, nil)
ctx := tc.buildContext(t, s.tokenMaker)
// Call UpdateUser RPC
res, err := s.UpdateUser(ctx, tc.req)
tc.checkResponse(t, res, err)
})
}
}
@@ -0,0 +1,44 @@
package gapi
import (
"context"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/val"
"github.com/FernandoJVideira/simple_bank/pb"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *Server) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest) (*pb.VerifyEmailResponse, error) {
violations := validateVerifyEmailRequest(req)
if violations != nil {
return nil, invalidArgumentError(violations)
}
result, err := s.userService.VerifyEmail(ctx, service.EmailVerificationRequest{
EmailID: req.GetId(),
SecretCode: req.GetCode(),
})
if err != nil {
return nil, status.Error(codes.Internal, "failed to verify email")
}
resp := &pb.VerifyEmailResponse{
IsVerified: result.User.IsEmailVerified,
}
return resp, nil
}
func validateVerifyEmailRequest(req *pb.VerifyEmailRequest) (violations []*errdetails.BadRequest_FieldViolation) {
if err := val.ValidateEmailID(req.GetId()); err != nil {
violations = append(violations, fieldViolation("id", err))
}
if err := val.ValidateVerificationCode(req.GetCode()); err != nil {
violations = append(violations, fieldViolation("secret_code", err))
}
return violations
}
+43
View File
@@ -0,0 +1,43 @@
// Package gapi implements the gRPC server for the Simple Bank application.
package gapi
import (
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/FernandoJVideira/simple_bank/pb"
)
// Server serves gRPC requests for our banking service.
type Server struct {
pb.UnimplementedSimpleBankServer // Embedding for forward compatibility
config config.Config
store db.Store
tokenMaker auth.Maker
taskDistributor worker.TaskDistributor
userService service.UserService
authService service.AuthService
}
// NewServer creates a new gRPC server instance.
func NewServer(
config config.Config,
store db.Store,
taskDistributor worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
) (*Server, error) {
server := &Server{
config: config,
store: store,
tokenMaker: tokenMaker,
taskDistributor: taskDistributor,
userService: userService,
authService: authService,
}
return server, nil
}