From 317473c3becc4d8a7320e6c11a75b18f291fc0a0 Mon Sep 17 00:00:00 2001 From: lakshaymanchanda Date: Mon, 29 Sep 2025 16:41:51 +0530 Subject: [PATCH 1/3] fix: added emulator tests for all the modules --- .gitignore | 57 +++++ call-profile/firebase.json | 11 + call-profile/main.go | 51 ++-- call-profile/main_test.go | 394 ++++++++++++++++++++++++++--- call-profiles/firebase.json | 11 + call-profiles/main.go | 34 ++- call-profiles/main_test.go | 199 ++++++++++++--- go.mod | 2 +- health-check/firebase.json | 11 + health-check/main.go | 38 ++- health-check/main_test.go | 241 +++++++++++++++--- scripts/test.sh | 25 +- verify/main_test.go | 484 ++++++++++++++++++++++++++++++++++-- 13 files changed, 1401 insertions(+), 157 deletions(-) create mode 100644 call-profile/firebase.json create mode 100644 call-profiles/firebase.json create mode 100644 health-check/firebase.json diff --git a/.gitignore b/.gitignore index 7a0a5ad..0056133 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,61 @@ .aws-sam/ env.json samconfig.toml + +# Firebase emulator logs */firestore-debug.log +firebase-debug.log +ui-debug.log + +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +coverage.out +coverage.html +*.prof + +# Go workspace files +go.work +go.work.sum + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp +/tmp/ + +# Environment files +.env +.env.local +.env.*.local + +# Node modules (if using any Node.js tools) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Firebase cache +.firebase/ +firebase-debug.log +ui-debug.log diff --git a/call-profile/firebase.json b/call-profile/firebase.json new file mode 100644 index 0000000..25eb629 --- /dev/null +++ b/call-profile/firebase.json @@ -0,0 +1,11 @@ +{ + "emulators": { + "firestore": { + "port": 8090 + }, + "ui": { + "enabled": true, + "port": 4000 + } + } +} diff --git a/call-profile/main.go b/call-profile/main.go index 09bfe82..dd7f294 100644 --- a/call-profile/main.go +++ b/call-profile/main.go @@ -7,17 +7,18 @@ import ( "net/http" "time" + "cloud.google.com/go/firestore" + "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) -func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - ctx := context.Background() - client, err := utils.InitializeFirestoreClient(ctx) - if err != nil { - return events.APIGatewayProxyResponse{}, err - } +type deps struct { + client *firestore.Client + ctx context.Context +} +func (d *deps) handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var userId, sessionId string = utils.GetDataFromBody([]byte(request.Body)) if userId == "" { return events.APIGatewayProxyResponse{ @@ -26,7 +27,7 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo }, nil } - dsnap, err := client.Collection("users").Doc(userId).Get(ctx) + dsnap, err := d.client.Collection("users").Doc(userId).Get(d.ctx) var userUrl string var chaincode string @@ -41,8 +42,8 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo if str, ok := dsnap.Data()["profileURL"].(string); ok { userUrl = str } else { - utils.LogProfileSkipped(client, ctx, userId, "Profile URL not available", sessionId) - utils.SetProfileStatusBlocked(client, ctx, userId, "Profile URL not available", sessionId, discordId) + utils.LogProfileSkipped(d.client, d.ctx, userId, "Profile URL not available", sessionId) + utils.SetProfileStatusBlocked(d.client, d.ctx, userId, "Profile URL not available", sessionId, discordId) return events.APIGatewayProxyResponse{ Body: "Profile Skipped No Profile URL", StatusCode: 200, @@ -51,8 +52,8 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo if str, ok := dsnap.Data()["chaincode"].(string); ok { if str == "" { - utils.LogProfileSkipped(client, ctx, userId, "Profile Service Blocked or Chaincode is empty", sessionId) - utils.SetProfileStatusBlocked(client, ctx, userId, "Profile Service Blocked or Chaincode is empty", sessionId, discordId) + utils.LogProfileSkipped(d.client, d.ctx, userId, "Profile Service Blocked or Chaincode is empty", sessionId) + utils.SetProfileStatusBlocked(d.client, d.ctx, userId, "Profile Service Blocked or Chaincode is empty", sessionId, discordId) return events.APIGatewayProxyResponse{ Body: "Profile Skipped Profile Service Blocked", StatusCode: 200, @@ -60,8 +61,8 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo } chaincode = str } else { - utils.LogProfileSkipped(client, ctx, userId, "Chaincode Not Found", sessionId) - utils.SetProfileStatusBlocked(client, ctx, userId, "Chaincode Not Found", sessionId, discordId) + utils.LogProfileSkipped(d.client, d.ctx, userId, "Chaincode Not Found", sessionId) + utils.SetProfileStatusBlocked(d.client, d.ctx, userId, "Chaincode Not Found", sessionId, discordId) return events.APIGatewayProxyResponse{ Body: "Profile Skipped Chaincode Not Found", StatusCode: 200, @@ -71,7 +72,7 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo var userData utils.Diff err = dsnap.DataTo(&userData) if err != nil { - utils.LogProfileSkipped(client, ctx, userId, "UserData Type Error: "+fmt.Sprintln(err), sessionId) + utils.LogProfileSkipped(d.client, d.ctx, userId, "UserData Type Error: "+fmt.Sprintln(err), sessionId) return events.APIGatewayProxyResponse{ Body: "Profile Skipped No User Data", StatusCode: 200, @@ -92,17 +93,17 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo isServiceRunning = true } - utils.LogHealth(client, ctx, userId, isServiceRunning, sessionId) + utils.LogHealth(d.client, d.ctx, userId, isServiceRunning, sessionId) if !isServiceRunning { - utils.LogProfileSkipped(client, ctx, userId, "Profile Service Down", sessionId) - utils.SetProfileStatusBlocked(client, ctx, userId, "Profile Service Down", sessionId, discordId) + utils.LogProfileSkipped(d.client, d.ctx, userId, "Profile Service Down", sessionId) + utils.SetProfileStatusBlocked(d.client, d.ctx, userId, "Profile Service Down", sessionId, discordId) return events.APIGatewayProxyResponse{ Body: "Profile Skipped Service Down", StatusCode: 200, }, nil } - dataErr := utils.Getdata(client, ctx, userId, userUrl, chaincode, utils.DiffToRes(userData), sessionId, discordId) + dataErr := utils.Getdata(d.client, d.ctx, userId, userUrl, chaincode, utils.DiffToRes(userData), sessionId, discordId) if dataErr != "" { return events.APIGatewayProxyResponse{ Body: "Profile Skipped " + dataErr, @@ -110,7 +111,6 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo }, nil } - defer client.Close() return events.APIGatewayProxyResponse{ Body: "Profile Saved", StatusCode: 200, @@ -118,5 +118,16 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo } func main() { - lambda.Start(handler) + ctx := context.Background() + client, err := utils.InitializeFirestoreClient(ctx) + if err != nil { + return + } + + d := deps{ + client: client, + ctx: ctx, + } + + lambda.Start(d.handler) } diff --git a/call-profile/main_test.go b/call-profile/main_test.go index d5bc2cc..2a0cac5 100644 --- a/call-profile/main_test.go +++ b/call-profile/main_test.go @@ -1,41 +1,23 @@ package main import ( + "context" + "fmt" "identity-service/layer/utils" + "net/http" + "net/http/httptest" + "os" "testing" + "time" "github.com/aws/aws-lambda-go/events" "github.com/stretchr/testify/assert" + "cloud.google.com/go/firestore" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) -func TestHandler(t *testing.T) { - for _, test := range TestRequests { - t.Run(test.Name, func(t *testing.T) { - t.Parallel() - response, err := handler(test.Request) - - if test.ExpectedErr { - assert.Error(t, err) - assert.Contains(t, err.Error(), "project id is required") - assert.Equal(t, "", response.Body) - } else { - assert.NoError(t, err) - } - - assert.IsType(t, events.APIGatewayProxyResponse{}, response) - }) - } -} - -func TestHandler_NoFirestore(t *testing.T) { - request := events.APIGatewayProxyRequest{ - Body: `{"userId": "mock-user-id"}`, - } - - _, err := handler(request) - assert.Error(t, err) -} - func TestGetDataFromBody(t *testing.T) { for _, test := range GetDataFromBodyTests { t.Run(test.Name, func(t *testing.T) { @@ -298,4 +280,360 @@ func TestResponseStructure(t *testing.T) { assert.IsType(t, events.APIGatewayProxyResponse{}, response) }) } +} + +func TestHandlerIntegration(t *testing.T) { + os.Setenv("environment", "test") + + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + testCases := []struct { + name string + request events.APIGatewayProxyRequest + userData map[string]interface{} + mockServer func() *httptest.Server + expectedBody string + expectedStatus int + expectedError bool + }{ + { + name: "successful profile save", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-1", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-1", + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "phone": "1234567890", + "yoe": 5, + "company": "Tech Corp", + "designation": "Developer", + "githubId": "johndoe", + "linkedin": "johndoe", + "website": "https://johndoe.com", + }, + mockServer: func() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "phone": "1234567890", + "yoe": 5, + "company": "Tech Corp", + "designation": "Developer", + "github_id": "johndoe", + "linkedin_id": "johndoe", + "website": "https://johndoe.com" + }`)) + } + })) + }, + expectedBody: "Profile Saved", + expectedStatus: 200, + expectedError: false, + }, + { + name: "no user ID", + request: events.APIGatewayProxyRequest{ + Body: `{"sessionId": "session-1"}`, + }, + userData: nil, + mockServer: nil, + expectedBody: "Profile Skipped No UserID", + expectedStatus: 200, + expectedError: false, + }, + { + name: "user not found", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "non-existent-user", "sessionId": "session-1"}`, + }, + userData: nil, + mockServer: nil, + expectedBody: "Profile Skipped No Profile URL", + expectedStatus: 200, + expectedError: false, + }, + { + name: "no profile URL", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-2", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-2", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + }, + mockServer: nil, + expectedBody: "Profile Skipped No Profile URL", + expectedStatus: 200, + expectedError: false, + }, + { + name: "empty chaincode", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-3", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-3", + "profileURL": "http://example.com", + "chaincode": "", + "discordId": "discord123", + "profileStatus": "PENDING", + }, + mockServer: nil, + expectedBody: "Profile Skipped Profile Service Blocked", + expectedStatus: 200, + expectedError: false, + }, + { + name: "no chaincode", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-4", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-4", + "profileURL": "http://example.com", + "discordId": "discord123", + "profileStatus": "PENDING", + }, + mockServer: nil, + expectedBody: "Profile Skipped Chaincode Not Found", + expectedStatus: 200, + expectedError: false, + }, + { + name: "service down", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-5", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-5", + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + }, + mockServer: func() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Service Unavailable")) + })) + }, + expectedBody: "Profile Skipped error in getting profile data", + expectedStatus: 200, + expectedError: false, + }, + { + name: "service timeout", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "test-user-6", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "test-user-6", + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + }, + mockServer: func() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(6 * time.Second) // Longer than 5 second timeout + w.WriteHeader(http.StatusOK) + })) + }, + expectedBody: "Profile Skipped Service Down", + expectedStatus: 200, + expectedError: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if testCase.userData != nil { + _, err := client.Collection("users").Doc(testCase.userData["userId"].(string)).Set(ctx, testCase.userData) + assert.NoError(t, err) + } + + var server *httptest.Server + if testCase.mockServer != nil { + server = testCase.mockServer() + defer server.Close() + + if testCase.userData != nil { + userId := testCase.userData["userId"].(string) + _, err := client.Collection("users").Doc(userId).Update(ctx, []firestore.Update{ + {Path: "profileURL", Value: server.URL}, + }) + assert.NoError(t, err) + } + } + + response, err := handlerWithClient(testCase.request, client) + + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.expectedBody, response.Body) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + }) + } +} + +func TestHandlerWithRealFirestore(t *testing.T) { + os.Setenv("environment", "test") + + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + userId := "integration-test-user" + sessionId := "integration-session" + userData := map[string]interface{}{ + "userId": userId, + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + "firstName": "Integration", + "lastName": "Test", + "email": "integration@test.com", + "phone": "1234567890", + "yoe": 3, + "company": "Test Corp", + "designation": "Tester", + "githubId": "integrationtest", + "linkedin": "integrationtest", + "website": "https://integrationtest.com", + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ + "first_name": "Integration", + "last_name": "Test", + "email": "integration@test.com", + "phone": "1234567890", + "yoe": 3, + "company": "Test Corp", + "designation": "Tester", + "github_id": "integrationtest", + "linkedin_id": "integrationtest", + "website": "https://integrationtest.com" + }`)) + } + })) + defer server.Close() + + userData["profileURL"] = server.URL + + _, err := client.Collection("users").Doc(userId).Set(ctx, userData) + assert.NoError(t, err) + + request := events.APIGatewayProxyRequest{ + Body: fmt.Sprintf(`{"userId": "%s", "sessionId": "%s"}`, userId, sessionId), + } + + response, err := handlerWithClient(request, client) + + assert.NoError(t, err) + assert.Equal(t, "Profile Saved", response.Body) + assert.Equal(t, 200, response.StatusCode) +} + +func TestHandlerEdgeCases(t *testing.T) { + os.Setenv("environment", "test") + + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + testCases := []struct { + name string + request events.APIGatewayProxyRequest + userData map[string]interface{} + expectedBody string + expectedStatus int + }{ + { + name: "invalid user data type", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "invalid-user", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "invalid-user", + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "discordId": "discord123", + "profileStatus": "PENDING", + "firstName": 123, // Invalid type + "lastName": "Doe", + }, + expectedBody: "Profile Skipped error in getting profile data", + expectedStatus: 200, + }, + { + name: "missing discord ID", + request: events.APIGatewayProxyRequest{ + Body: `{"userId": "no-discord-user", "sessionId": "session-1"}`, + }, + userData: map[string]interface{}{ + "userId": "no-discord-user", + "profileURL": "http://example.com", + "chaincode": "TESTCHAIN", + "profileStatus": "PENDING", + }, + expectedBody: "Profile Skipped error in getting profile data", // Will fail health check + expectedStatus: 200, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := client.Collection("users").Doc(testCase.userData["userId"].(string)).Set(ctx, testCase.userData) + assert.NoError(t, err) + + response, err := handlerWithClient(testCase.request, client) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedBody, response.Body) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + }) + } +} + +func newFirestoreMockClient(ctx context.Context) *firestore.Client { + conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) + return client +} + +func handlerWithClient(request events.APIGatewayProxyRequest, client *firestore.Client) (events.APIGatewayProxyResponse, error) { + ctx := context.Background() + d := deps{ + client: client, + ctx: ctx, + } + return d.handler(request) } \ No newline at end of file diff --git a/call-profiles/firebase.json b/call-profiles/firebase.json new file mode 100644 index 0000000..25eb629 --- /dev/null +++ b/call-profiles/firebase.json @@ -0,0 +1,11 @@ +{ + "emulators": { + "firestore": { + "port": 8090 + }, + "ui": { + "enabled": true, + "port": 4000 + } + } +} diff --git a/call-profiles/main.go b/call-profiles/main.go index 730c7ee..fe946e1 100644 --- a/call-profiles/main.go +++ b/call-profiles/main.go @@ -8,6 +8,8 @@ import ( "sync" "time" + "cloud.google.com/go/firestore" + "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" @@ -16,6 +18,11 @@ import ( var wg sync.WaitGroup +type deps struct { + client *firestore.Client + ctx context.Context +} + func callProfile(userId string, sessionId string) { defer wg.Done() @@ -30,15 +37,8 @@ func callProfile(userId string, sessionId string) { } } -func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - ctx := context.Background() - client, err := utils.InitializeFirestoreClient(ctx) - - if err != nil { - return events.APIGatewayProxyResponse{}, err - } - - docRef, _, sessionIdErr := client.Collection("identitySessionIds").Add(ctx, map[string]interface{}{ +func (d *deps) handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + docRef, _, sessionIdErr := d.client.Collection("identitySessionIds").Add(d.ctx, map[string]interface{}{ "Timestamp": time.Now(), }) @@ -48,7 +48,7 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo totalProfilesCalled := 0 - iter := client.Collection("users").Where("profileStatus", "==", "VERIFIED").Documents(ctx) + iter := d.client.Collection("users").Where("profileStatus", "==", "VERIFIED").Documents(d.ctx) for { doc, err := iter.Next() if err == iterator.Done { @@ -64,7 +64,6 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo wg.Wait() - defer client.Close() return events.APIGatewayProxyResponse{ Body: fmt.Sprintf("Total Profiles called in session is %d", totalProfilesCalled), StatusCode: 200, @@ -72,5 +71,16 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo } func main() { - lambda.Start(handler) + ctx := context.Background() + client, err := utils.InitializeFirestoreClient(ctx) + if err != nil { + return + } + + d := deps{ + client: client, + ctx: ctx, + } + + lambda.Start(d.handler) } diff --git a/call-profiles/main_test.go b/call-profiles/main_test.go index 647f76d..fcac45f 100644 --- a/call-profiles/main_test.go +++ b/call-profiles/main_test.go @@ -1,44 +1,21 @@ package main import ( + "context" "identity-service/layer/utils" "sync" "testing" "time" + "cloud.google.com/go/firestore" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "github.com/aws/aws-lambda-go/events" "github.com/stretchr/testify/assert" ) -func TestHandler(t *testing.T) { - for _, test := range TestRequests { - t.Run(test.Name, func(t *testing.T) { - t.Parallel() - response, err := handler(test.Request) - - if test.ExpectedErr { - assert.Error(t, err) - assert.Contains(t, err.Error(), "project id is required") - assert.Equal(t, "", response.Body) - } else { - assert.NoError(t, err) - } - - assert.IsType(t, events.APIGatewayProxyResponse{}, response) - }) - } -} - -func TestHandlerStructure(t *testing.T) { - request := events.APIGatewayProxyRequest{} - - response, err := handler(request) - - assert.Error(t, err) - assert.IsType(t, events.APIGatewayProxyResponse{}, response) - assert.Empty(t, response.Body) -} - func TestCallProfileFunction(t *testing.T) { for _, test := range ProfileLambdaCallPayloadTests { t.Run(test.Name, func(t *testing.T) { @@ -310,4 +287,168 @@ func TestProfileCountingLogic(t *testing.T) { assert.Equal(t, len(mockProfiles), totalProfilesCalled) assert.Greater(t, totalProfilesCalled, 0) }) +} + +func newFirestoreMockClient(ctx context.Context) *firestore.Client { + conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) + return client +} + +func handlerWithClient(request events.APIGatewayProxyRequest, client *firestore.Client) (events.APIGatewayProxyResponse, error) { + ctx := context.Background() + d := deps{ + client: client, + ctx: ctx, + } + return d.handler(request) +} + +func TestHandlerIntegration(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + testCases := []struct { + name string + request events.APIGatewayProxyRequest + userData []map[string]interface{} + expectedBody string + expectedStatus int + expectedError bool + }{ + { + name: "no verified users", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/call-profiles", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileStatus": "PENDING", + }, + { + "userId": "user2", + "profileStatus": "BLOCKED", + }, + }, + expectedBody: "Total Profiles called in session is 0", + expectedStatus: 200, + expectedError: false, + }, + { + name: "single verified user", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/call-profiles", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileStatus": "VERIFIED", + }, + }, + expectedBody: "Total Profiles called in session is 1", + expectedStatus: 200, + expectedError: false, + }, + { + name: "multiple verified users", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/call-profiles", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileStatus": "VERIFIED", + }, + { + "userId": "user2", + "profileStatus": "VERIFIED", + }, + { + "userId": "user3", + "profileStatus": "VERIFIED", + }, + { + "userId": "user4", + "profileStatus": "PENDING", + }, + }, + expectedBody: "Total Profiles called in session is 3", + expectedStatus: 200, + expectedError: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + for _, userData := range testCase.userData { + userId := userData["userId"].(string) + _, err := client.Collection("users").Doc(userId).Set(ctx, userData) + assert.NoError(t, err) + } + + response, err := handlerWithClient(testCase.request, client) + + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + assert.Equal(t, testCase.expectedBody, response.Body) + } + + for _, userData := range testCase.userData { + userId := userData["userId"].(string) + client.Collection("users").Doc(userId).Delete(ctx) + } + + iter := client.Collection("identitySessionIds").Documents(ctx) + for { + doc, err := iter.Next() + if err != nil { + break + } + doc.Ref.Delete(ctx) + } + }) + } +} + +func TestHandlerWithRealFirestore(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + request := events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/call-profiles", + } + + response, err := handlerWithClient(request, client) + assert.NoError(t, err) + assert.Equal(t, 200, response.StatusCode) + assert.Equal(t, "Total Profiles called in session is 0", response.Body) +} + +func TestSessionIdDocumentCreationIntegration(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + docRef, _, err := client.Collection("identitySessionIds").Add(ctx, map[string]interface{}{ + "Timestamp": time.Now(), + }) + + assert.NoError(t, err) + assert.NotNil(t, docRef) + + doc, err := docRef.Get(ctx) + assert.NoError(t, err) + assert.True(t, doc.Exists()) + + docRef.Delete(ctx) } \ No newline at end of file diff --git a/go.mod b/go.mod index 9225ada..3355a21 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.38.0 google.golang.org/api v0.235.0 + google.golang.org/grpc v1.72.2 ) require ( @@ -68,7 +69,6 @@ require ( google.golang.org/genproto v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/health-check/firebase.json b/health-check/firebase.json new file mode 100644 index 0000000..25eb629 --- /dev/null +++ b/health-check/firebase.json @@ -0,0 +1,11 @@ +{ + "emulators": { + "firestore": { + "port": 8090 + }, + "ui": { + "enabled": true, + "port": 4000 + } + } +} diff --git a/health-check/main.go b/health-check/main.go index 38df6ff..22ee3f0 100644 --- a/health-check/main.go +++ b/health-check/main.go @@ -9,6 +9,8 @@ import ( "sync" "time" + "cloud.google.com/go/firestore" + "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" @@ -17,10 +19,21 @@ import ( var wg sync.WaitGroup +type deps struct { + client *firestore.Client + ctx context.Context +} + func callProfileHealth(userUrl string) { defer wg.Done() + // Skip if URL is empty + if userUrl == "" { + fmt.Println("Empty profile URL, skipping health check") + return + } + httpClient := &http.Client{ Timeout: 2 * time.Second, } @@ -36,17 +49,10 @@ func callProfileHealth(userUrl string) { } } -func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - ctx := context.Background() - client, err := utils.InitializeFirestoreClient(ctx) - - if err != nil { - return events.APIGatewayProxyResponse{}, err - } - +func (d *deps) handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { totalProfilesCalled := 0 - iter := client.Collection("users").Where("profileStatus", "==", "VERIFIED").Documents(ctx) + iter := d.client.Collection("users").Where("profileStatus", "==", "VERIFIED").Documents(d.ctx) for { doc, err := iter.Next() if err == iterator.Done { @@ -65,7 +71,6 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo wg.Wait() - defer client.Close() return events.APIGatewayProxyResponse{ Body: fmt.Sprintf("Total Profiles called in session is %d", totalProfilesCalled), StatusCode: 200, @@ -73,5 +78,16 @@ func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyRespo } func main() { - lambda.Start(handler) + ctx := context.Background() + client, err := utils.InitializeFirestoreClient(ctx) + if err != nil { + return + } + + d := deps{ + client: client, + ctx: ctx, + } + + lambda.Start(d.handler) } diff --git a/health-check/main_test.go b/health-check/main_test.go index 634de1e..f82ec7c 100644 --- a/health-check/main_test.go +++ b/health-check/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -9,29 +10,15 @@ import ( "testing" "time" + "cloud.google.com/go/firestore" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "github.com/aws/aws-lambda-go/events" "github.com/stretchr/testify/assert" ) -func TestHandler(t *testing.T) { - for _, test := range TestRequests { - t.Run(test.Name, func(t *testing.T) { - t.Parallel() - response, err := handler(test.Request) - - if test.ExpectedErr { - assert.Error(t, err) - assert.Contains(t, err.Error(), "project id is required") - assert.Equal(t, "", response.Body) - } else { - assert.NoError(t, err) - } - - assert.IsType(t, events.APIGatewayProxyResponse{}, response) - }) - } -} - func TestCallProfileHealth(t *testing.T) { tests := []struct { name string @@ -65,15 +52,6 @@ func TestCallProfileHealth(t *testing.T) { } } -func TestHandlerStructure(t *testing.T) { - request := events.APIGatewayProxyRequest{} - - response, err := handler(request) - - assert.Error(t, err) - assert.IsType(t, events.APIGatewayProxyResponse{}, response) - assert.Empty(t, response.Body) -} func TestURLFormatting(t *testing.T) { for _, test := range URLFormattingTests { @@ -403,4 +381,211 @@ func TestHandlerResponseStructure(t *testing.T) { assert.IsType(t, events.APIGatewayProxyResponse{}, response) }) } +} + +func newFirestoreMockClient(ctx context.Context) *firestore.Client { + conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) + return client +} + +func handlerWithClient(request events.APIGatewayProxyRequest, client *firestore.Client) (events.APIGatewayProxyResponse, error) { + ctx := context.Background() + d := deps{ + client: client, + ctx: ctx, + } + return d.handler(request) +} + +func TestHandlerIntegration(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + testCases := []struct { + name string + request events.APIGatewayProxyRequest + userData []map[string]interface{} + expectedBody string + expectedStatus int + expectedError bool + }{ + { + name: "no verified users", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileURL": "https://user1.example.com", + "profileStatus": "PENDING", + }, + { + "userId": "user2", + "profileURL": "https://user2.example.com", + "profileStatus": "BLOCKED", + }, + }, + expectedBody: "Total Profiles called in session is 0", + expectedStatus: 200, + expectedError: false, + }, + { + name: "single verified user", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileURL": "https://user1.example.com", + "profileStatus": "VERIFIED", + }, + }, + expectedBody: "Total Profiles called in session is 1", + expectedStatus: 200, + expectedError: false, + }, + { + name: "multiple verified users", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileURL": "https://user1.example.com", + "profileStatus": "VERIFIED", + }, + { + "userId": "user2", + "profileURL": "https://user2.example.com", + "profileStatus": "VERIFIED", + }, + { + "userId": "user3", + "profileURL": "https://user3.example.com", + "profileStatus": "VERIFIED", + }, + { + "userId": "user4", + "profileURL": "https://user4.example.com", + "profileStatus": "PENDING", + }, + }, + expectedBody: "Total Profiles called in session is 3", + expectedStatus: 200, + expectedError: false, + }, + { + name: "verified users with missing profileURL", + request: events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + }, + userData: []map[string]interface{}{ + { + "userId": "user1", + "profileURL": "https://user1.example.com", + "profileStatus": "VERIFIED", + }, + { + "userId": "user2", + "profileStatus": "VERIFIED", + }, + { + "userId": "user3", + "profileURL": "", + "profileStatus": "VERIFIED", + }, + }, + expectedBody: "Total Profiles called in session is 2", + expectedStatus: 200, + expectedError: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + for _, userData := range testCase.userData { + userId := userData["userId"].(string) + _, err := client.Collection("users").Doc(userId).Set(ctx, userData) + assert.NoError(t, err) + } + + response, err := handlerWithClient(testCase.request, client) + + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + assert.Equal(t, testCase.expectedBody, response.Body) + } + + for _, userData := range testCase.userData { + userId := userData["userId"].(string) + client.Collection("users").Doc(userId).Delete(ctx) + } + }) + } +} + +func TestCallProfileHealthIntegration(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + userData := map[string]interface{}{ + "userId": "test-user", + "profileURL": server.URL, + "profileStatus": "VERIFIED", + } + + _, err := client.Collection("users").Doc("test-user").Set(ctx, userData) + assert.NoError(t, err) + + request := events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + } + + response, err := handlerWithClient(request, client) + + assert.NoError(t, err) + assert.Equal(t, 200, response.StatusCode) + assert.Equal(t, "Total Profiles called in session is 1", response.Body) + + client.Collection("users").Doc("test-user").Delete(ctx) +} + +func TestHandlerWithRealFirestore(t *testing.T) { + ctx := context.Background() + client := newFirestoreMockClient(ctx) + defer client.Close() + + request := events.APIGatewayProxyRequest{ + HTTPMethod: "GET", + Path: "/health-check", + } + + response, err := handlerWithClient(request, client) + assert.NoError(t, err) + assert.Equal(t, 200, response.StatusCode) + assert.Equal(t, "Total Profiles called in session is 0", response.Body) } \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index 3ba66b6..2e20293 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,8 +6,10 @@ trap 'rm -rf "$TEMP_DIR"' EXIT go mod tidy declare -a exit_codes=() -declare -a modules=("health" "call-profile" "call-profiles" "health-check") +declare -a modules=("health") +declare -a firebase_modules=("call-profile" "verify" "health-check" "call-profiles") +# Run modules that don't need Firebase emulator for module in "${modules[@]}"; do echo "Running $module tests..." cd "$module" || exit 1 @@ -17,20 +19,23 @@ for module in "${modules[@]}"; do cd .. || exit 1 done -echo "Running verify tests..." -cd verify || exit 1 -npx firebase-tools --project="test" emulators:exec "go test -v -cover -coverprofile=\"$TEMP_DIR/verify.out\"" -VERIFY_EXIT=$? -[ $VERIFY_EXIT -ne 0 ] && echo "Firebase test exited with error (verify)" -exit_codes+=($VERIFY_EXIT) -npx kill-port 8090 2>/dev/null || true -cd .. || exit 1 +# Run modules that need Firebase emulator +for module in "${firebase_modules[@]}"; do + echo "Running $module tests with Firebase emulator..." + cd "$module" || exit 1 + npx firebase-tools --project="test" emulators:exec "go test -v -cover -coverprofile=\"$TEMP_DIR/$module.out\"" + MODULE_EXIT=$? + [ $MODULE_EXIT -ne 0 ] && echo "Firebase test exited with error ($module)" + exit_codes+=($MODULE_EXIT) + npx kill-port 8090 2>/dev/null || true + cd .. || exit 1 +done echo "================================" echo "COVERAGE REPORTS" echo "================================" -all_modules=("${modules[@]}" "verify") +all_modules=("${modules[@]}" "${firebase_modules[@]}") for module in "${all_modules[@]}"; do if [ -f "$TEMP_DIR/$module.out" ]; then echo "================================" diff --git a/verify/main_test.go b/verify/main_test.go index 9bf19f9..2f66603 100644 --- a/verify/main_test.go +++ b/verify/main_test.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "errors" "fmt" + "io" "log" "net/http" "net/http/httptest" @@ -50,6 +52,103 @@ func addUsers(ctx context.Context, client *firestore.Client, users []map[string] return nil } +func TestVerifyFunction(t *testing.T) { + testCases := []struct { + name string + profileURL string + chaincode string + salt string + mockResponse string + mockStatusCode int + expectedStatus string + expectedError bool + }{ + { + name: "successful verification with correct hash", + profileURL: "/profile", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: `{"hash": "cadf727ffff23ec46c17d808a4884ea7566765182d1a2ffa88e4719bc1f7f9fb328e2abacc13202f2dc55b9d653919b79ecf02dd752de80285bbec57a57713d9"}`, + mockStatusCode: http.StatusOK, + expectedStatus: "BLOCKED", + expectedError: false, + }, + { + name: "failed verification with incorrect hash", + profileURL: "/profile", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: `{"hash": "incorrecthash"}`, + mockStatusCode: http.StatusOK, + expectedStatus: "BLOCKED", + expectedError: false, + }, + { + name: "server error during verification", + profileURL: "/profile", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: `{"error": "server error"}`, + mockStatusCode: http.StatusInternalServerError, + expectedStatus: "BLOCKED", + expectedError: false, + }, + { + name: "invalid JSON response", + profileURL: "/profile", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: `invalid json`, + mockStatusCode: http.StatusOK, + expectedStatus: "BLOCKED", + expectedError: false, + }, + { + name: "empty response body", + profileURL: "/profile", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: ``, + mockStatusCode: http.StatusOK, + expectedStatus: "BLOCKED", + expectedError: false, + }, + { + name: "network timeout", + profileURL: "/timeout", + chaincode: "testchaincode", + salt: "testsalt", + mockResponse: `{"hash": "test"}`, + mockStatusCode: http.StatusOK, + expectedStatus: "BLOCKED", + expectedError: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/timeout" { + time.Sleep(2 * time.Second) + } + w.WriteHeader(testCase.mockStatusCode) + w.Write([]byte(testCase.mockResponse)) + })) + defer server.Close() + + if testCase.name == "network timeout" { + status, err := verify(server.URL+testCase.profileURL, testCase.chaincode, testCase.salt) + assert.Equal(t, testCase.expectedStatus, status) + assert.True(t, testCase.expectedError == (err != nil)) + } else { + status, err := verify(server.URL+testCase.profileURL, testCase.chaincode, testCase.salt) + assert.Equal(t, testCase.expectedStatus, status) + assert.True(t, testCase.expectedError == (err != nil)) + } + }) + } +} + func TestHandler(t *testing.T) { os.Setenv("environment", "test") defer os.Unsetenv("environment") @@ -58,18 +157,29 @@ func TestHandler(t *testing.T) { client := newFirestoreMockClient(ctx) defer cancel() - // Mock servers for profile verification verifiedMockServer := startMockServer(`{"hash":"correcthash"}`, http.StatusOK) defer verifiedMockServer.Close() unverifiedMockServer := startMockServer(`{"hash":"incorrecthash"}`, http.StatusOK) defer unverifiedMockServer.Close() + errorMockServer := startMockServer(`{"error":"server error"}`, http.StatusInternalServerError) + defer errorMockServer.Close() + + invalidJSONMockServer := startMockServer(`invalid json`, http.StatusOK) + defer invalidJSONMockServer.Close() + verifiedUserId := "123" unverifiedUserId := "321" + errorUserId := "456" + invalidJSONUserId := "789" + nonExistentUserId := "999" + users := []map[string]interface{}{ {"userId": verifiedUserId, "chaincode": "TESTCHAIN", "profileURL": verifiedMockServer.URL, "profileStatus": "VERIFIED"}, {"userId": unverifiedUserId, "chaincode": "TESTCHAINCODE", "profileURL": unverifiedMockServer.URL, "profileStatus": "BLOCKED"}, + {"userId": errorUserId, "chaincode": "TESTCHAIN", "profileURL": errorMockServer.URL, "profileStatus": "PENDING"}, + {"userId": invalidJSONUserId, "chaincode": "TESTCHAIN", "profileURL": invalidJSONMockServer.URL, "profileStatus": "PENDING"}, } if err := addUsers(ctx, client, users); err != nil { @@ -77,28 +187,67 @@ func TestHandler(t *testing.T) { } testCases := []struct { - name string - request events.APIGatewayProxyRequest - expect string - err error + name string + request events.APIGatewayProxyRequest + expect string + expectedStatus int + err error }{ { - name: "unverified user", - request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, unverifiedUserId)}, - expect: "Verification Process Done", - err: nil, + name: "unverified user", + request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, unverifiedUserId)}, + expect: "Verification Process Done", + expectedStatus: 200, + err: nil, + }, + { + name: "verified user", + request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, verifiedUserId)}, + expect: "Already Verified", + expectedStatus: 409, + err: nil, + }, + { + name: "no userId", + request: events.APIGatewayProxyRequest{Body: `{}`}, + expect: "", + expectedStatus: 0, + err: errors.New("no userId provided"), + }, + { + name: "non-existent user", + request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, nonExistentUserId)}, + expect: "", + expectedStatus: 0, + err: nil, }, { - name: "verified user", - request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, verifiedUserId)}, - expect: "Already Verified", - err: nil, + name: "server error during verification", + request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, errorUserId)}, + expect: "Verification Process Done", + expectedStatus: 200, + err: nil, }, { - name: "no userId", - request: events.APIGatewayProxyRequest{Body: `{}`}, - expect: "", - err: errors.New("no userId provided"), + name: "invalid JSON response", + request: events.APIGatewayProxyRequest{Body: fmt.Sprintf(`{ "userId": "%s" }`, invalidJSONUserId)}, + expect: "Verification Process Done", + expectedStatus: 200, + err: nil, + }, + { + name: "empty request body", + request: events.APIGatewayProxyRequest{Body: ""}, + expect: "", + expectedStatus: 0, + err: errors.New("no userId provided"), + }, + { + name: "invalid JSON in request", + request: events.APIGatewayProxyRequest{Body: `{"userId": }`}, + expect: "", + expectedStatus: 0, + err: errors.New("no userId provided"), }, } @@ -110,8 +259,307 @@ func TestHandler(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { response, err := d.handler(testCase.request) - assert.Equal(t, testCase.err, err) + if testCase.name == "non-existent user" { + assert.Error(t, err) + } else { + assert.Equal(t, testCase.err, err) + } assert.Equal(t, testCase.expect, response.Body) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + }) + } +} + +func TestURLFormatting(t *testing.T) { + os.Setenv("environment", "test") + defer os.Unsetenv("environment") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + client := newFirestoreMockClient(ctx) + defer cancel() + + testCases := []struct { + name string + profileURL string + expectedSuffix string + }{ + { + name: "URL with trailing slash", + profileURL: "http://example.com/", + expectedSuffix: "verification", + }, + { + name: "URL without trailing slash", + profileURL: "http://example.com", + expectedSuffix: "/verification", + }, + { + name: "URL with path", + profileURL: "http://example.com/api", + expectedSuffix: "/verification", + }, + { + name: "URL with path and trailing slash", + profileURL: "http://example.com/api/", + expectedSuffix: "verification", + }, + { + name: "Single character URL", + profileURL: "a", + expectedSuffix: "/verification", + }, + { + name: "Empty URL", + profileURL: "", + expectedSuffix: "/verification", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + userId := fmt.Sprintf("user_%s", testCase.name) + + user := map[string]interface{}{ + "userId": userId, + "chaincode": "TESTCHAIN", + "profileURL": testCase.profileURL, + "profileStatus": "PENDING", + } + + if err := addUsers(ctx, client, []map[string]interface{}{user}); err != nil { + t.Fatalf("failed to add user: %v", err) + } + + var capturedURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedURL = r.URL.String() + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"hash": "testhash"}`)) + })) + defer server.Close() + + client.Collection("users").Doc(userId).Update(ctx, []firestore.Update{ + {Path: "profileURL", Value: server.URL}, + }) + + d := deps{ + client: client, + ctx: ctx, + } + + request := events.APIGatewayProxyRequest{ + Body: fmt.Sprintf(`{ "userId": "%s" }`, userId), + } + + response, err := d.handler(request) + + assert.NoError(t, err) + assert.Equal(t, 200, response.StatusCode) + assert.Contains(t, capturedURL, testCase.expectedSuffix) + }) + } +} + +func TestSaltGeneration(t *testing.T) { + os.Setenv("environment", "test") + defer os.Unsetenv("environment") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + client := newFirestoreMockClient(ctx) + defer cancel() + + salts := make(map[string]bool) + + for i := 0; i < 10; i++ { + userId := fmt.Sprintf("salt_test_user_%d", i) + + user := map[string]interface{}{ + "userId": userId, + "chaincode": "TESTCHAIN", + "profileURL": "http://example.com", + "profileStatus": "PENDING", + } + + if err := addUsers(ctx, client, []map[string]interface{}{user}); err != nil { + t.Fatalf("failed to add user: %v", err) + } + + var capturedBody string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"hash": "testhash"}`)) + })) + defer server.Close() + + client.Collection("users").Doc(userId).Update(ctx, []firestore.Update{ + {Path: "profileURL", Value: server.URL}, }) + + d := deps{ + client: client, + ctx: ctx, + } + + request := events.APIGatewayProxyRequest{ + Body: fmt.Sprintf(`{ "userId": "%s" }`, userId), + } + + response, err := d.handler(request) + + assert.NoError(t, err) + assert.Equal(t, 200, response.StatusCode) + + var requestBody map[string]string + err = json.Unmarshal([]byte(capturedBody), &requestBody) + assert.NoError(t, err) + + salt := requestBody["salt"] + assert.NotEmpty(t, salt) + assert.Len(t, salt, 21) + + validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789" + for _, char := range salt { + assert.Contains(t, validChars, string(char)) + } + + assert.False(t, salts[salt], "Salt should be unique") + salts[salt] = true + } +} + +func TestHandlerEdgeCases(t *testing.T) { + os.Setenv("environment", "test") + defer os.Unsetenv("environment") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + client := newFirestoreMockClient(ctx) + defer cancel() + + testCases := []struct { + name string + userData map[string]interface{} + requestBody string + expectedError string + expectedStatus int + }{ + { + name: "user with empty chaincode", + userData: map[string]interface{}{ + "userId": "empty_chaincode_user", + "chaincode": "", + "profileURL": "http://example.com", + "profileStatus": "PENDING", + }, + requestBody: `{ "userId": "empty_chaincode_user" }`, + expectedError: "chaincode is blocked", + expectedStatus: 0, + }, + { + name: "user with missing profileURL", + userData: map[string]interface{}{ + "userId": "missing_url_user", + "chaincode": "TESTCHAIN", + "profileStatus": "PENDING", + }, + requestBody: `{ "userId": "missing_url_user" }`, + expectedError: "profile url is not a string", + expectedStatus: 0, + }, + { + name: "user with invalid chaincode type", + userData: map[string]interface{}{ + "userId": "invalid_chaincode_user", + "chaincode": 123, + "profileURL": "http://example.com", + "profileStatus": "PENDING", + }, + requestBody: `{ "userId": "invalid_chaincode_user" }`, + expectedError: "chaincode is not a string", + expectedStatus: 0, + }, + { + name: "user with invalid profileURL type", + userData: map[string]interface{}{ + "userId": "invalid_url_user", + "chaincode": "TESTCHAIN", + "profileURL": 123, + "profileStatus": "PENDING", + }, + requestBody: `{ "userId": "invalid_url_user" }`, + expectedError: "profile url is not a string", + expectedStatus: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if err := addUsers(ctx, client, []map[string]interface{}{testCase.userData}); err != nil { + t.Fatalf("failed to add user: %v", err) + } + + d := deps{ + client: client, + ctx: ctx, + } + + request := events.APIGatewayProxyRequest{ + Body: testCase.requestBody, + } + + response, err := d.handler(request) + + assert.Error(t, err) + assert.Contains(t, err.Error(), testCase.expectedError) + assert.Equal(t, testCase.expectedStatus, response.StatusCode) + }) + } +} + +func TestVerifyFunctionCompleteCoverage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"hash": "cadf727ffff23ec46c17d808a4884ea7566765182d1a2ffa88e4719bc1f7f9fb328e2abacc13202f2dc55b9d653919b79ecf02dd752de80285bbec57a57713d9"}`)) + })) + defer server.Close() + + status, err := verify(server.URL+"/verify", "testchaincode", "testsalt") + assert.Equal(t, "BLOCKED", status) + assert.NoError(t, err) + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"hash": "wronghash"}`)) + })) + defer server2.Close() + + status, err = verify(server2.URL+"/verify", "testchaincode", "testsalt") + assert.Equal(t, "BLOCKED", status) + assert.NoError(t, err) + + status, err = verify("http://192.168.1.1:99999/verify", "testchaincode", "testsalt") + assert.Equal(t, "BLOCKED", status) + assert.Error(t, err) + + server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + server3.Close() + + status, err = verify(server3.URL+"/verify", "testchaincode", "testsalt") + assert.Equal(t, "BLOCKED", status) + assert.Error(t, err) +} + +func TestMainFunctionComponents(t *testing.T) { + ctx := context.Background() + assert.NotNil(t, ctx) + + d := deps{ + client: nil, + ctx: ctx, } + assert.NotNil(t, d) + assert.Equal(t, ctx, d.ctx) } From 05cb2a467466828ba84e5973e68bf684ffa6e5ed Mon Sep 17 00:00:00 2001 From: lakshaymanchanda Date: Mon, 29 Sep 2025 17:05:51 +0530 Subject: [PATCH 2/3] fix: parallel testing --- call-profile/firebase.json | 11 ----- call-profile/main.go | 3 +- call-profile/main_test.go | 6 ++- call-profiles/firebase.json | 11 ----- call-profiles/main.go | 2 +- call-profiles/main_test.go | 7 ++- health-check/firebase.json | 11 ----- health-check/main.go | 2 +- health-check/main_test.go | 7 ++- scripts/test.sh | 91 +++++++++++++++++++++++++++++++++---- verify/firebase.json | 11 ----- verify/main.go | 3 +- verify/main_test.go | 14 ++++-- 13 files changed, 114 insertions(+), 65 deletions(-) delete mode 100644 call-profile/firebase.json delete mode 100644 call-profiles/firebase.json delete mode 100644 health-check/firebase.json delete mode 100644 verify/firebase.json diff --git a/call-profile/firebase.json b/call-profile/firebase.json deleted file mode 100644 index 25eb629..0000000 --- a/call-profile/firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "emulators": { - "firestore": { - "port": 8090 - }, - "ui": { - "enabled": true, - "port": 4000 - } - } -} diff --git a/call-profile/main.go b/call-profile/main.go index dd7f294..cf70729 100644 --- a/call-profile/main.go +++ b/call-profile/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "identity-service/layer/utils" + "log" "net/http" "time" @@ -121,7 +122,7 @@ func main() { ctx := context.Background() client, err := utils.InitializeFirestoreClient(ctx) if err != nil { - return + log.Fatalf("Failed to initialize Firestore client: %v", err) } d := deps{ diff --git a/call-profile/main_test.go b/call-profile/main_test.go index 2a0cac5..55ea80f 100644 --- a/call-profile/main_test.go +++ b/call-profile/main_test.go @@ -624,7 +624,11 @@ func TestHandlerEdgeCases(t *testing.T) { } func newFirestoreMockClient(ctx context.Context) *firestore.Client { - conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + emulatorHost := os.Getenv("FIRESTORE_EMULATOR_HOST") + if emulatorHost == "" { + emulatorHost = "127.0.0.1:8090" + } + conn, _ := grpc.Dial(emulatorHost, grpc.WithTransportCredentials(insecure.NewCredentials())) client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) return client } diff --git a/call-profiles/firebase.json b/call-profiles/firebase.json deleted file mode 100644 index 25eb629..0000000 --- a/call-profiles/firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "emulators": { - "firestore": { - "port": 8090 - }, - "ui": { - "enabled": true, - "port": 4000 - } - } -} diff --git a/call-profiles/main.go b/call-profiles/main.go index fe946e1..57a912e 100644 --- a/call-profiles/main.go +++ b/call-profiles/main.go @@ -74,7 +74,7 @@ func main() { ctx := context.Background() client, err := utils.InitializeFirestoreClient(ctx) if err != nil { - return + log.Fatalf("Failed to initialize Firestore client: %v", err) } d := deps{ diff --git a/call-profiles/main_test.go b/call-profiles/main_test.go index fcac45f..d1cba8b 100644 --- a/call-profiles/main_test.go +++ b/call-profiles/main_test.go @@ -3,6 +3,7 @@ package main import ( "context" "identity-service/layer/utils" + "os" "sync" "testing" "time" @@ -290,7 +291,11 @@ func TestProfileCountingLogic(t *testing.T) { } func newFirestoreMockClient(ctx context.Context) *firestore.Client { - conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + emulatorHost := os.Getenv("FIRESTORE_EMULATOR_HOST") + if emulatorHost == "" { + emulatorHost = "127.0.0.1:8090" + } + conn, _ := grpc.Dial(emulatorHost, grpc.WithTransportCredentials(insecure.NewCredentials())) client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) return client } diff --git a/health-check/firebase.json b/health-check/firebase.json deleted file mode 100644 index 25eb629..0000000 --- a/health-check/firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "emulators": { - "firestore": { - "port": 8090 - }, - "ui": { - "enabled": true, - "port": 4000 - } - } -} diff --git a/health-check/main.go b/health-check/main.go index 22ee3f0..6aa895d 100644 --- a/health-check/main.go +++ b/health-check/main.go @@ -81,7 +81,7 @@ func main() { ctx := context.Background() client, err := utils.InitializeFirestoreClient(ctx) if err != nil { - return + log.Fatalf("Failed to initialize Firestore client: %v", err) } d := deps{ diff --git a/health-check/main_test.go b/health-check/main_test.go index f82ec7c..0d577e9 100644 --- a/health-check/main_test.go +++ b/health-check/main_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "strings" "sync" "testing" @@ -384,7 +385,11 @@ func TestHandlerResponseStructure(t *testing.T) { } func newFirestoreMockClient(ctx context.Context) *firestore.Client { - conn, _ := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) + emulatorHost := os.Getenv("FIRESTORE_EMULATOR_HOST") + if emulatorHost == "" { + emulatorHost = "127.0.0.1:8090" + } + conn, _ := grpc.Dial(emulatorHost, grpc.WithTransportCredentials(insecure.NewCredentials())) client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) return client } diff --git a/scripts/test.sh b/scripts/test.sh index 2e20293..3e27018 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -19,16 +19,89 @@ for module in "${modules[@]}"; do cd .. || exit 1 done -# Run modules that need Firebase emulator +# Run modules that need Firebase emulator in parallel +CONCURRENCY=2 # Limit to 2 to avoid port conflicts +pids=() +module_names=() + +echo "Starting Firebase emulator tests with concurrency limit of $CONCURRENCY..." + for module in "${firebase_modules[@]}"; do - echo "Running $module tests with Firebase emulator..." - cd "$module" || exit 1 - npx firebase-tools --project="test" emulators:exec "go test -v -cover -coverprofile=\"$TEMP_DIR/$module.out\"" - MODULE_EXIT=$? - [ $MODULE_EXIT -ne 0 ] && echo "Firebase test exited with error ($module)" - exit_codes+=($MODULE_EXIT) - npx kill-port 8090 2>/dev/null || true - cd .. || exit 1 + # Wait if we've reached concurrency limit + while (( ${#pids[@]} >= CONCURRENCY )); do + # Check for completed processes + for i in "${!pids[@]}"; do + if ! kill -0 "${pids[$i]}" 2>/dev/null; then + # Process completed, remove from array + unset pids[$i] + unset module_names[$i] + fi + done + # Rebuild array without gaps + pids=("${pids[@]}") + module_names=("${module_names[@]}") + sleep 0.1 + done + + # Start new test in background + ( + echo "Starting $module tests with Firebase emulator..." + cd "$module" || exit 1 + + # Use different ports for parallel execution to avoid conflicts + PORT_OFFSET=$((RANDOM % 1000 + 1000)) + FIRESTORE_PORT=$((8090 + PORT_OFFSET)) + UI_PORT=$((4000 + PORT_OFFSET)) + + # Create temporary firebase.json with unique ports + cat > firebase.json << EOF +{ + "emulators": { + "firestore": { + "port": $FIRESTORE_PORT + }, + "ui": { + "enabled": true, + "port": $UI_PORT + } + } +} +EOF + + # Set emulator host environment variable + export FIRESTORE_EMULATOR_HOST="127.0.0.1:$FIRESTORE_PORT" + + npx firebase-tools --project="test" emulators:exec "go test -v -cover -coverprofile=\"$TEMP_DIR/$module.out\"" + MODULE_EXIT=$? + + # Cleanup + npx kill-port $FIRESTORE_PORT 2>/dev/null || true + npx kill-port $UI_PORT 2>/dev/null || true + rm -f firebase.json + + if [ $MODULE_EXIT -ne 0 ]; then + echo "Firebase test exited with error ($module)" + else + echo "Completed $module tests successfully" + fi + + exit $MODULE_EXIT + ) & + + pid=$! + pids+=($pid) + module_names+=($module) + echo "Started $module tests (PID: $pid)" +done + +# Wait for all background processes to complete +echo "Waiting for all Firebase emulator tests to complete..." +for i in "${!pids[@]}"; do + wait "${pids[$i]}" + exit_code=$? + module_name="${module_names[$i]}" + exit_codes+=($exit_code) + echo "Module $module_name completed with exit code $exit_code" done echo "================================" diff --git a/verify/firebase.json b/verify/firebase.json deleted file mode 100644 index 25eb629..0000000 --- a/verify/firebase.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "emulators": { - "firestore": { - "port": 8090 - }, - "ui": { - "enabled": true, - "port": 4000 - } - } -} diff --git a/verify/main.go b/verify/main.go index d51d183..28b2025 100644 --- a/verify/main.go +++ b/verify/main.go @@ -8,6 +8,7 @@ import ( "fmt" "identity-service/layer/utils" "io" + "log" "math/rand" "net/http" "time" @@ -124,7 +125,7 @@ func main() { ctx := context.Background() client, err := utils.InitializeFirestoreClient(ctx) if err != nil { - return + log.Fatalf("Failed to initialize Firestore client: %v", err) } d := deps{ diff --git a/verify/main_test.go b/verify/main_test.go index 2f66603..4e9b152 100644 --- a/verify/main_test.go +++ b/verify/main_test.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "log" "net/http" "net/http/httptest" "os" @@ -14,16 +13,21 @@ import ( "time" "cloud.google.com/go/firestore" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "github.com/aws/aws-lambda-go/events" "github.com/stretchr/testify/assert" ) func newFirestoreMockClient(ctx context.Context) *firestore.Client { - client, err := firestore.NewClient(ctx, "test") - if err != nil { - log.Fatalf("firebase.NewClient err: %v", err) + emulatorHost := os.Getenv("FIRESTORE_EMULATOR_HOST") + if emulatorHost == "" { + emulatorHost = "127.0.0.1:8090" } - + conn, _ := grpc.Dial(emulatorHost, grpc.WithTransportCredentials(insecure.NewCredentials())) + client, _ := firestore.NewClient(ctx, "test-project", option.WithGRPCConn(conn)) return client } From 375635aa4393cf05d51d5507df3db5a5f2571e2b Mon Sep 17 00:00:00 2001 From: lakshaymanchanda Date: Mon, 29 Sep 2025 17:10:24 +0530 Subject: [PATCH 3/3] refactor: suggested comments --- .gitignore | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 0056133..2c9066e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,13 @@ env.json samconfig.toml -# Firebase emulator logs -*/firestore-debug.log -firebase-debug.log -ui-debug.log +# Firebase emulator logs (recursive) +**/firestore-debug.log +**/firebase-debug.log +**/ui-debug.log + +# Firebase cache +.firebase/ # Go build artifacts *.exe @@ -55,8 +58,3 @@ node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* - -# Firebase cache -.firebase/ -firebase-debug.log -ui-debug.log