diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..4410f22 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,47 @@ +name: Build and Test + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + test: + name: Run Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Run unit tests for thread-service + working-directory: code/services/thread-service + run: go test ./test/... + + - name: Run unit tests for vote-service + working-directory: code/services/vote-service + run: go test ./test/... + + - name: Run unit tests for search-service + working-directory: code/services/search-service + run: go test ./test/... + + - name: Run unit tests for popular-service + working-directory: code/services/popular-service + run: go test ./test/... + + - name: Run unit tests for community-service + working-directory: code/services/community-service + run: go test ./test/... + + - name: Run unit tests for comment-service + working-directory: code/services/comment-service + run: go test ./test/... diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yml similarity index 100% rename from .github/workflows/main.yaml rename to .github/workflows/main.yml diff --git a/code/services/comment-service/go.mod b/code/services/comment-service/go.mod index b5d1d94..d30e932 100644 --- a/code/services/comment-service/go.mod +++ b/code/services/comment-service/go.mod @@ -6,17 +6,23 @@ toolchain go1.24.1 require ( gen v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.6 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/comment-service/go.sum b/code/services/comment-service/go.sum index 15c472b..18f075a 100644 --- a/code/services/comment-service/go.sum +++ b/code/services/comment-service/go.sum @@ -1,3 +1,6 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,6 +13,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= @@ -36,3 +49,8 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/comment-service/test/server_test.go b/code/services/comment-service/test/server_test.go new file mode 100644 index 0000000..ffa31ec --- /dev/null +++ b/code/services/comment-service/test/server_test.go @@ -0,0 +1,164 @@ +package test + +import ( + "context" + "testing" + + src "comment-service/src" + commentpb "gen/comment-service/pb" + dbpb "gen/db-service/pb" + models "gen/models/pb" + threadpb "gen/thread-service/pb" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +type MockDBClient struct { + dbpb.DBServiceClient + ListCommentsFunc func(ctx context.Context, req *dbpb.ListCommentsRequest, opts ...grpc.CallOption) (*dbpb.ListCommentsResponse, error) + CreateCommentFunc func(ctx context.Context, req *dbpb.CreateCommentRequest, opts ...grpc.CallOption) (*dbpb.CreateCommentResponse, error) + GetCommentFunc func(ctx context.Context, req *dbpb.GetCommentRequest, opts ...grpc.CallOption) (*models.Comment, error) + UpdateCommentFunc func(ctx context.Context, req *dbpb.UpdateCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteCommentFunc func(ctx context.Context, req *dbpb.DeleteCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockDBClient) ListComments(ctx context.Context, req *dbpb.ListCommentsRequest, opts ...grpc.CallOption) (*dbpb.ListCommentsResponse, error) { + return m.ListCommentsFunc(ctx, req, opts...) +} + +func (m *MockDBClient) CreateComment(ctx context.Context, req *dbpb.CreateCommentRequest, opts ...grpc.CallOption) (*dbpb.CreateCommentResponse, error) { + return m.CreateCommentFunc(ctx, req, opts...) +} + +func (m *MockDBClient) GetComment(ctx context.Context, req *dbpb.GetCommentRequest, opts ...grpc.CallOption) (*models.Comment, error) { + return m.GetCommentFunc(ctx, req, opts...) +} + +func (m *MockDBClient) UpdateComment(ctx context.Context, req *dbpb.UpdateCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateCommentFunc(ctx, req, opts...) +} + +func (m *MockDBClient) DeleteComment(ctx context.Context, req *dbpb.DeleteCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.DeleteCommentFunc(ctx, req, opts...) +} + +type MockThreadClient struct { + threadpb.ThreadServiceClient + UpdateThreadFunc func(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockThreadClient) UpdateThread(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateThreadFunc(ctx, req, opts...) +} + +func TestCreateComment_Validation(t *testing.T) { + tests := []struct { + name string + req *commentpb.CreateCommentRequest + wantErr error + }{ + { + name: "missing parent id", + req: &commentpb.CreateCommentRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Parent id is required"), + }, + { + name: "missing content", + req: &commentpb.CreateCommentRequest{ + ParentId: "123", + }, + wantErr: status.Error(codes.InvalidArgument, "Content is required"), + }, + { + name: "content too long", + req: &commentpb.CreateCommentRequest{ + ParentId: "123", + Content: string(make([]byte, 501)), + }, + wantErr: status.Error(codes.InvalidArgument, "Content exceeds maximum length of 500 characters"), + }, + { + name: "valid request", + req: &commentpb.CreateCommentRequest{ + ParentId: "123", + Content: "test comment", + ParentType: models.CommentParentType_THREAD, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.CommentServer{ + DBClient: &MockDBClient{ + CreateCommentFunc: func(ctx context.Context, req *dbpb.CreateCommentRequest, opts ...grpc.CallOption) (*dbpb.CreateCommentResponse, error) { + return &dbpb.CreateCommentResponse{ + Id: "123", + }, nil + }, + }, + ThreadClient: &MockThreadClient{ + UpdateThreadFunc: func(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, + }, + } + + _, err := server.CreateComment(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetComment_Validation(t *testing.T) { + tests := []struct { + name string + req *commentpb.GetCommentRequest + wantErr error + }{ + { + name: "missing id", + req: &commentpb.GetCommentRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Comment id is required"), + }, + { + name: "valid request", + req: &commentpb.GetCommentRequest{ + Id: "123", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.CommentServer{ + DBClient: &MockDBClient{ + GetCommentFunc: func(ctx context.Context, req *dbpb.GetCommentRequest, opts ...grpc.CallOption) (*models.Comment, error) { + return &models.Comment{ + Id: "123", + Content: "test comment", + }, nil + }, + }, + ThreadClient: &MockThreadClient{}, + } + + _, err := server.GetComment(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/code/services/community-service/go.mod b/code/services/community-service/go.mod index e441ff1..02a3b92 100644 --- a/code/services/community-service/go.mod +++ b/code/services/community-service/go.mod @@ -6,17 +6,21 @@ toolchain go1.24.1 require ( gen v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.6 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/community-service/go.sum b/code/services/community-service/go.sum index 15c472b..709e1e0 100644 --- a/code/services/community-service/go.sum +++ b/code/services/community-service/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,6 +12,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= @@ -36,3 +43,6 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/community-service/test/server_test.go b/code/services/community-service/test/server_test.go new file mode 100644 index 0000000..9819e04 --- /dev/null +++ b/code/services/community-service/test/server_test.go @@ -0,0 +1,162 @@ +package test + +import ( + "context" + "testing" + + src "community-service/src" + communitypb "gen/community-service/pb" + dbpb "gen/db-service/pb" + models "gen/models/pb" + threadpb "gen/thread-service/pb" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/grpc" +) + +type MockDBClient struct { + dbpb.DBServiceClient + ListCommunitiesFunc func(ctx context.Context, req *dbpb.ListCommunitiesRequest, opts ...grpc.CallOption) (*dbpb.ListCommunitiesResponse, error) + CreateCommunityFunc func(ctx context.Context, req *dbpb.CreateCommunityRequest, opts ...grpc.CallOption) (*dbpb.CreateCommunityResponse, error) + GetCommunityFunc func(ctx context.Context, req *dbpb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) + UpdateCommunityFunc func(ctx context.Context, req *dbpb.UpdateCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteCommunityFunc func(ctx context.Context, req *dbpb.DeleteCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockDBClient) ListCommunities(ctx context.Context, req *dbpb.ListCommunitiesRequest, opts ...grpc.CallOption) (*dbpb.ListCommunitiesResponse, error) { + return m.ListCommunitiesFunc(ctx, req, opts...) +} + +func (m *MockDBClient) CreateCommunity(ctx context.Context, req *dbpb.CreateCommunityRequest, opts ...grpc.CallOption) (*dbpb.CreateCommunityResponse, error) { + return m.CreateCommunityFunc(ctx, req, opts...) +} + +func (m *MockDBClient) GetCommunity(ctx context.Context, req *dbpb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) { + return m.GetCommunityFunc(ctx, req, opts...) +} + +func (m *MockDBClient) UpdateCommunity(ctx context.Context, req *dbpb.UpdateCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateCommunityFunc(ctx, req, opts...) +} + +func (m *MockDBClient) DeleteCommunity(ctx context.Context, req *dbpb.DeleteCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.DeleteCommunityFunc(ctx, req, opts...) +} + +type MockThreadClient struct { + threadpb.ThreadServiceClient + ListThreadsFunc func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) + DeleteThreadFunc func(ctx context.Context, req *threadpb.DeleteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockThreadClient) ListThreads(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return m.ListThreadsFunc(ctx, req, opts...) +} + +func (m *MockThreadClient) DeleteThread(ctx context.Context, req *threadpb.DeleteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.DeleteThreadFunc(ctx, req, opts...) +} + +func TestCreateCommunity_Validation(t *testing.T) { + tests := []struct { + name string + req *communitypb.CreateCommunityRequest + wantErr error + }{ + { + name: "missing name", + req: &communitypb.CreateCommunityRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Community name is required"), + }, + { + name: "name too short", + req: &communitypb.CreateCommunityRequest{ + Name: "ab", + }, + wantErr: status.Error(codes.InvalidArgument, "Name must be between 3 and 50 characters long"), + }, + { + name: "name too long", + req: &communitypb.CreateCommunityRequest{ + Name: "a", + }, + wantErr: status.Error(codes.InvalidArgument, "Name must be between 3 and 50 characters long"), + }, + { + name: "valid request", + req: &communitypb.CreateCommunityRequest{ + Name: "test-community", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.CommunityServer{ + DBClient: &MockDBClient{ + CreateCommunityFunc: func(ctx context.Context, req *dbpb.CreateCommunityRequest, opts ...grpc.CallOption) (*dbpb.CreateCommunityResponse, error) { + return &dbpb.CreateCommunityResponse{ + Id: "123", + }, nil + }, + }, + ThreadClient: &MockThreadClient{}, + } + + _, err := server.CreateCommunity(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetCommunity_Validation(t *testing.T) { + tests := []struct { + name string + req *communitypb.GetCommunityRequest + wantErr error + }{ + { + name: "missing id", + req: &communitypb.GetCommunityRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Community id is required"), + }, + { + name: "valid request", + req: &communitypb.GetCommunityRequest{ + Id: "123", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.CommunityServer{ + DBClient: &MockDBClient{ + GetCommunityFunc: func(ctx context.Context, req *dbpb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) { + return &models.Community{ + Id: "123", + Name: "test-community", + }, nil + }, + }, + ThreadClient: &MockThreadClient{}, + } + + _, err := server.GetCommunity(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/code/services/popular-service/go.mod b/code/services/popular-service/go.mod index df21999..6ddef57 100644 --- a/code/services/popular-service/go.mod +++ b/code/services/popular-service/go.mod @@ -6,17 +6,21 @@ toolchain go1.24.1 require ( gen v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/popular-service/go.sum b/code/services/popular-service/go.sum index 15c472b..709e1e0 100644 --- a/code/services/popular-service/go.sum +++ b/code/services/popular-service/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,6 +12,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= @@ -36,3 +43,6 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/popular-service/test/server_test.go b/code/services/popular-service/test/server_test.go new file mode 100644 index 0000000..2adbf95 --- /dev/null +++ b/code/services/popular-service/test/server_test.go @@ -0,0 +1,159 @@ +package test + +import ( + "context" + "testing" + + commentpb "gen/comment-service/pb" + models "gen/models/pb" + popularpb "gen/popular-service/pb" + threadpb "gen/thread-service/pb" + src "popular-service/src" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/grpc" +) + +type MockThreadClient struct { + threadpb.ThreadServiceClient + ListThreadsFunc func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) +} + +func (m *MockThreadClient) ListThreads(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return m.ListThreadsFunc(ctx, req, opts...) +} + +type MockCommentClient struct { + commentpb.CommentServiceClient + ListCommentsFunc func(ctx context.Context, req *commentpb.ListCommentsRequest, opts ...grpc.CallOption) (*commentpb.ListCommentsResponse, error) +} + +func (m *MockCommentClient) ListComments(ctx context.Context, req *commentpb.ListCommentsRequest, opts ...grpc.CallOption) (*commentpb.ListCommentsResponse, error) { + return m.ListCommentsFunc(ctx, req, opts...) +} + +func TestGetPopularThreads_Validation(t *testing.T) { + tests := []struct { + name string + req *popularpb.GetPopularThreadsRequest + wantErr error + }{ + { + name: "negative offset", + req: &popularpb.GetPopularThreadsRequest{ + Offset: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Offset must be a positive integer"), + }, + { + name: "zero limit", + req: &popularpb.GetPopularThreadsRequest{ + Limit: int32Ptr(0), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit must be a positive integer"), + }, + { + name: "negative limit", + req: &popularpb.GetPopularThreadsRequest{ + Limit: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit must be a positive integer"), + }, + { + name: "valid request", + req: &popularpb.GetPopularThreadsRequest{ + Offset: int32Ptr(0), + Limit: int32Ptr(10), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.PopularServer{ + ThreadClient: &MockThreadClient{ + ListThreadsFunc: func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return &threadpb.ListThreadsResponse{ + Threads: []*models.Thread{}, + }, nil + }, + }, + CommentClient: &MockCommentClient{}, + } + + _, err := server.GetPopularThreads(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetPopularComments_Validation(t *testing.T) { + tests := []struct { + name string + req *popularpb.GetPopularCommentsRequest + wantErr error + }{ + { + name: "negative offset", + req: &popularpb.GetPopularCommentsRequest{ + Offset: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Offset must be a positive integer"), + }, + { + name: "zero limit", + req: &popularpb.GetPopularCommentsRequest{ + Limit: int32Ptr(0), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit must be a positive integer"), + }, + { + name: "negative limit", + req: &popularpb.GetPopularCommentsRequest{ + Limit: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit must be a positive integer"), + }, + { + name: "valid request", + req: &popularpb.GetPopularCommentsRequest{ + Offset: int32Ptr(0), + Limit: int32Ptr(10), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.PopularServer{ + ThreadClient: &MockThreadClient{}, + CommentClient: &MockCommentClient{ + ListCommentsFunc: func(ctx context.Context, req *commentpb.ListCommentsRequest, opts ...grpc.CallOption) (*commentpb.ListCommentsResponse, error) { + return &commentpb.ListCommentsResponse{ + Comments: []*models.Comment{}, + }, nil + }, + }, + } + + _, err := server.GetPopularComments(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func int32Ptr(i int32) *int32 { + return &i +} diff --git a/code/services/search-service/go.mod b/code/services/search-service/go.mod index b799d82..fdde518 100644 --- a/code/services/search-service/go.mod +++ b/code/services/search-service/go.mod @@ -6,17 +6,21 @@ toolchain go1.24.1 require ( gen v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/search-service/go.sum b/code/services/search-service/go.sum index 15c472b..709e1e0 100644 --- a/code/services/search-service/go.sum +++ b/code/services/search-service/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,6 +12,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= @@ -36,3 +43,6 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/search-service/test/server_test.go b/code/services/search-service/test/server_test.go new file mode 100644 index 0000000..93f2d58 --- /dev/null +++ b/code/services/search-service/test/server_test.go @@ -0,0 +1,224 @@ +package test + +import ( + "context" + "testing" + + communitypb "gen/community-service/pb" + models "gen/models/pb" + searchpb "gen/search-service/pb" + threadpb "gen/thread-service/pb" + src "search-service/src" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/grpc" +) + +type MockCommunityClient struct { + communitypb.CommunityServiceClient + ListCommunitiesFunc func(ctx context.Context, req *communitypb.ListCommunitiesRequest, opts ...grpc.CallOption) (*communitypb.ListCommunitiesResponse, error) +} + +func (m *MockCommunityClient) ListCommunities(ctx context.Context, req *communitypb.ListCommunitiesRequest, opts ...grpc.CallOption) (*communitypb.ListCommunitiesResponse, error) { + return m.ListCommunitiesFunc(ctx, req, opts...) +} + +type MockThreadClient struct { + threadpb.ThreadServiceClient + ListThreadsFunc func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) +} + +func (m *MockThreadClient) ListThreads(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return m.ListThreadsFunc(ctx, req, opts...) +} + +func TestGlobalSearch_Validation(t *testing.T) { + tests := []struct { + name string + req *searchpb.SearchRequest + wantErr error + }{ + { + name: "empty query", + req: &searchpb.SearchRequest{Query: ""}, + wantErr: status.Error(codes.InvalidArgument, "Query is empty"), + }, + { + name: "negative offset", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Offset cannot be negative"), + }, + { + name: "negative limit", + req: &searchpb.SearchRequest{ + Query: "test", + Limit: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit cannot be negative"), + }, + { + name: "valid request", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(0), + Limit: int32Ptr(10), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.SearchServer{ + CommunityClient: &MockCommunityClient{ + ListCommunitiesFunc: func(ctx context.Context, req *communitypb.ListCommunitiesRequest, opts ...grpc.CallOption) (*communitypb.ListCommunitiesResponse, error) { + return &communitypb.ListCommunitiesResponse{ + Communities: []*models.Community{}, + }, nil + }, + }, + ThreadClient: &MockThreadClient{ + ListThreadsFunc: func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return &threadpb.ListThreadsResponse{ + Threads: []*models.Thread{}, + }, nil + }, + }, + } + + _, err := server.GlobalSearch(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCommunitySearch_Validation(t *testing.T) { + tests := []struct { + name string + req *searchpb.SearchRequest + wantErr error + }{ + { + name: "empty query", + req: &searchpb.SearchRequest{Query: ""}, + wantErr: status.Error(codes.InvalidArgument, "Query is empty"), + }, + { + name: "negative offset", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Offset cannot be negative"), + }, + { + name: "negative limit", + req: &searchpb.SearchRequest{ + Query: "test", + Limit: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit cannot be negative"), + }, + { + name: "valid request", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(0), + Limit: int32Ptr(10), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.SearchServer{ + CommunityClient: &MockCommunityClient{ + ListCommunitiesFunc: func(ctx context.Context, req *communitypb.ListCommunitiesRequest, opts ...grpc.CallOption) (*communitypb.ListCommunitiesResponse, error) { + return &communitypb.ListCommunitiesResponse{ + Communities: []*models.Community{}, + }, nil + }, + }, + } + + _, err := server.CommunitySearch(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestThreadSearch_Validation(t *testing.T) { + tests := []struct { + name string + req *searchpb.SearchRequest + wantErr error + }{ + { + name: "empty query", + req: &searchpb.SearchRequest{Query: ""}, + wantErr: status.Error(codes.InvalidArgument, "Query is empty"), + }, + { + name: "negative offset", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Offset cannot be negative"), + }, + { + name: "negative limit", + req: &searchpb.SearchRequest{ + Query: "test", + Limit: int32Ptr(-1), + }, + wantErr: status.Error(codes.InvalidArgument, "Limit cannot be negative"), + }, + { + name: "valid request", + req: &searchpb.SearchRequest{ + Query: "test", + Offset: int32Ptr(0), + Limit: int32Ptr(10), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.SearchServer{ + ThreadClient: &MockThreadClient{ + ListThreadsFunc: func(ctx context.Context, req *threadpb.ListThreadsRequest, opts ...grpc.CallOption) (*threadpb.ListThreadsResponse, error) { + return &threadpb.ListThreadsResponse{ + Threads: []*models.Thread{}, + }, nil + }, + }, + } + + _, err := server.ThreadSearch(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func int32Ptr(i int32) *int32 { return &i } diff --git a/code/services/thread-service/go.mod b/code/services/thread-service/go.mod index 5adaafc..a0aa6a9 100644 --- a/code/services/thread-service/go.mod +++ b/code/services/thread-service/go.mod @@ -11,12 +11,16 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/thread-service/go.sum b/code/services/thread-service/go.sum index 15c472b..0cca277 100644 --- a/code/services/thread-service/go.sum +++ b/code/services/thread-service/go.sum @@ -1,7 +1,20 @@ +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -10,8 +23,17 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= @@ -22,12 +44,19 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= @@ -36,3 +65,7 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/thread-service/test/server_test.go b/code/services/thread-service/test/server_test.go new file mode 100644 index 0000000..242bcd0 --- /dev/null +++ b/code/services/thread-service/test/server_test.go @@ -0,0 +1,250 @@ +package test + +import ( + "context" + "testing" + + communitypb "gen/community-service/pb" + dbpb "gen/db-service/pb" + models "gen/models/pb" + threadpb "gen/thread-service/pb" + src "thread-service/src" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/grpc" +) + +type MockDBClient struct { + dbpb.DBServiceClient + ListThreadsFunc func(ctx context.Context, req *dbpb.ListThreadsRequest, opts ...grpc.CallOption) (*dbpb.ListThreadsResponse, error) + CreateThreadFunc func(ctx context.Context, req *dbpb.CreateThreadRequest, opts ...grpc.CallOption) (*dbpb.CreateThreadResponse, error) + GetThreadFunc func(ctx context.Context, req *dbpb.GetThreadRequest, opts ...grpc.CallOption) (*models.Thread, error) + UpdateThreadFunc func(ctx context.Context, req *dbpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteThreadFunc func(ctx context.Context, req *dbpb.DeleteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockDBClient) ListThreads(ctx context.Context, req *dbpb.ListThreadsRequest, opts ...grpc.CallOption) (*dbpb.ListThreadsResponse, error) { + return m.ListThreadsFunc(ctx, req, opts...) +} + +func (m *MockDBClient) CreateThread(ctx context.Context, req *dbpb.CreateThreadRequest, opts ...grpc.CallOption) (*dbpb.CreateThreadResponse, error) { + return m.CreateThreadFunc(ctx, req, opts...) +} + +func (m *MockDBClient) GetThread(ctx context.Context, req *dbpb.GetThreadRequest, opts ...grpc.CallOption) (*models.Thread, error) { + return m.GetThreadFunc(ctx, req, opts...) +} + +func (m *MockDBClient) UpdateThread(ctx context.Context, req *dbpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateThreadFunc(ctx, req, opts...) +} + +func (m *MockDBClient) DeleteThread(ctx context.Context, req *dbpb.DeleteThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.DeleteThreadFunc(ctx, req, opts...) +} + +type MockCommunityClient struct { + communitypb.CommunityServiceClient + GetCommunityFunc func(ctx context.Context, req *communitypb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) + UpdateCommunityFunc func(ctx context.Context, req *communitypb.UpdateCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockCommunityClient) GetCommunity(ctx context.Context, req *communitypb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) { + return m.GetCommunityFunc(ctx, req, opts...) +} + +func (m *MockCommunityClient) UpdateCommunity(ctx context.Context, req *communitypb.UpdateCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateCommunityFunc(ctx, req, opts...) +} + +func TestCreateThread_Validation(t *testing.T) { + tests := []struct { + name string + req *threadpb.CreateThreadRequest + wantErr error + }{ + { + name: "missing community id", + req: &threadpb.CreateThreadRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Community id is required"), + }, + { + name: "missing title", + req: &threadpb.CreateThreadRequest{ + CommunityId: "123", + }, + wantErr: status.Error(codes.InvalidArgument, "Title is required"), + }, + { + name: "missing content", + req: &threadpb.CreateThreadRequest{ + CommunityId: "123", + Title: "test thread", + }, + wantErr: status.Error(codes.InvalidArgument, "Content is required"), + }, + { + name: "valid request", + req: &threadpb.CreateThreadRequest{ + CommunityId: "123", + Title: "test thread", + Content: "test content", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.ThreadServer{ + DBClient: &MockDBClient{ + CreateThreadFunc: func(ctx context.Context, req *dbpb.CreateThreadRequest, opts ...grpc.CallOption) (*dbpb.CreateThreadResponse, error) { + return &dbpb.CreateThreadResponse{Id: "123"}, nil + }, + }, + CommunityClient: &MockCommunityClient{ + GetCommunityFunc: func(ctx context.Context, req *communitypb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) { + return &models.Community{ + Id: "123", + Name: "test-community", + }, nil + }, + UpdateCommunityFunc: func(ctx context.Context, req *communitypb.UpdateCommunityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, + }, + } + _, err := server.CreateThread(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetThread_Validation(t *testing.T) { + tests := []struct { + name string + req *threadpb.GetThreadRequest + wantErr error + }{ + { + name: "missing id", + req: &threadpb.GetThreadRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Thread id is required"), + }, + { + name: "valid request", + req: &threadpb.GetThreadRequest{ + Id: "123", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.ThreadServer{ + DBClient: &MockDBClient{ + GetThreadFunc: func(ctx context.Context, req *dbpb.GetThreadRequest, opts ...grpc.CallOption) (*models.Thread, error) { + return &models.Thread{ + Id: "123", + CommunityId: "456", + Title: "test thread", + Content: "test content", + }, nil + }, + }, + } + + _, err := server.GetThread(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestListThreads_Validation(t *testing.T) { + tests := []struct { + name string + req *threadpb.ListThreadsRequest + wantErr error + }{ + { + name: "empty community id", + req: &threadpb.ListThreadsRequest{CommunityId: strPtr("")}, + wantErr: status.Error(codes.InvalidArgument, "Community id cannot be empty"), + }, + { + name: "empty title", + req: &threadpb.ListThreadsRequest{Title: strPtr("")}, + wantErr: status.Error(codes.InvalidArgument, "Title cannot be empty"), + }, + { + name: "negative_offset", + req: &threadpb.ListThreadsRequest{CommunityId: strPtr("123"), Offset: int32Ptr(-1), Limit: int32Ptr(10), SortBy: strPtr("new")}, + wantErr: status.Error(codes.InvalidArgument, "Offset must be a positive integer"), + }, + { + name: "zero_limit", + req: &threadpb.ListThreadsRequest{CommunityId: strPtr("123"), Offset: int32Ptr(0), Limit: int32Ptr(0), SortBy: strPtr("new")}, + wantErr: status.Error(codes.InvalidArgument, "Limit must be a positive integer"), + }, + { + name: "empty_sort", + req: &threadpb.ListThreadsRequest{CommunityId: strPtr("123"), Offset: int32Ptr(0), Limit: int32Ptr(10), SortBy: strPtr("")}, + wantErr: status.Error(codes.InvalidArgument, "Sort cannot be empty"), + }, + { + name: "valid request", + req: &threadpb.ListThreadsRequest{ + CommunityId: strPtr("123"), + Title: strPtr("test"), + Offset: int32Ptr(0), + Limit: int32Ptr(10), + SortBy: strPtr("created_at"), + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.ThreadServer{ + DBClient: &MockDBClient{ + ListThreadsFunc: func(ctx context.Context, req *dbpb.ListThreadsRequest, opts ...grpc.CallOption) (*dbpb.ListThreadsResponse, error) { + return &dbpb.ListThreadsResponse{ + Threads: []*models.Thread{}, + }, nil + }, + }, + CommunityClient: &MockCommunityClient{ + GetCommunityFunc: func(ctx context.Context, req *communitypb.GetCommunityRequest, opts ...grpc.CallOption) (*models.Community, error) { + return &models.Community{ + Id: "123", + Name: "test-community", + }, nil + }, + }, + } + + _, err := server.ListThreads(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func strPtr(s string) *string { return &s } +func int32Ptr(i int32) *int32 { return &i } diff --git a/code/services/vote-service/go.mod b/code/services/vote-service/go.mod index 85a42c0..5bdbb93 100644 --- a/code/services/vote-service/go.mod +++ b/code/services/vote-service/go.mod @@ -6,17 +6,23 @@ toolchain go1.24.1 require ( gen v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.6 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace gen => ../../gen diff --git a/code/services/vote-service/go.sum b/code/services/vote-service/go.sum index 15c472b..8b1570d 100644 --- a/code/services/vote-service/go.sum +++ b/code/services/vote-service/go.sum @@ -1,3 +1,6 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,6 +13,18 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= @@ -36,3 +51,8 @@ google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/code/services/vote-service/test/server_test.go b/code/services/vote-service/test/server_test.go new file mode 100644 index 0000000..807473f --- /dev/null +++ b/code/services/vote-service/test/server_test.go @@ -0,0 +1,121 @@ +package test + +import ( + "context" + "testing" + + commentpb "gen/comment-service/pb" + threadpb "gen/thread-service/pb" + votepb "gen/vote-service/pb" + src "vote-service/src" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/grpc" +) + +type MockThreadClient struct { + threadpb.ThreadServiceClient + UpdateThreadFunc func(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockThreadClient) UpdateThread(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateThreadFunc(ctx, req, opts...) +} + +type MockCommentClient struct { + commentpb.CommentServiceClient + UpdateCommentFunc func(ctx context.Context, req *commentpb.UpdateCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func (m *MockCommentClient) UpdateComment(ctx context.Context, req *commentpb.UpdateCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return m.UpdateCommentFunc(ctx, req, opts...) +} + +func TestUpvoteThread_Validation(t *testing.T) { + tests := []struct { + name string + req *votepb.VoteThreadRequest + wantErr error + }{ + { + name: "missing thread id", + req: &votepb.VoteThreadRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Thread id is required"), + }, + { + name: "valid request", + req: &votepb.VoteThreadRequest{ + ThreadId: "123", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.VoteServer{ + ThreadClient: &MockThreadClient{ + UpdateThreadFunc: func(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, + }, + CommentClient: &MockCommentClient{}, + } + + _, err := server.UpvoteThread(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUpvoteComment_Validation(t *testing.T) { + tests := []struct { + name string + req *votepb.VoteCommentRequest + wantErr error + }{ + { + name: "missing comment id", + req: &votepb.VoteCommentRequest{}, + wantErr: status.Error(codes.InvalidArgument, "Comment id is required"), + }, + { + name: "valid request", + req: &votepb.VoteCommentRequest{ + CommentId: "123", + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := &src.VoteServer{ + ThreadClient: &MockThreadClient{ + UpdateThreadFunc: func(ctx context.Context, req *threadpb.UpdateThreadRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, + }, + CommentClient: &MockCommentClient{ + UpdateCommentFunc: func(ctx context.Context, req *commentpb.UpdateCommentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, + }, + } + + _, err := server.UpvoteComment(context.Background(), tt.req) + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/docs/phases/phase8.md b/docs/phases/phase8.md index 74ba814..2ca6b8b 100644 --- a/docs/phases/phase8.md +++ b/docs/phases/phase8.md @@ -1,3 +1,3 @@ ## 🔍 Phase 8 - Automation with CI/CD -The CI/CD workflow can be found [here](../../.github/workflows/main.yaml). \ No newline at end of file +The CI/CD workflow can be found [here](../../.github/workflows). \ No newline at end of file