From cab35252e2b95f144f91e27a224074e3d8add341 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:40:12 -0500 Subject: [PATCH 01/13] comment out old tests --- backend/internal/tests/jwt_middleware_test.go | 262 +++++++++--------- backend/internal/tests/jwt_test.go | 250 ++++++++--------- backend/internal/tests/req_validator_test.go | 214 +++++++------- 3 files changed, 364 insertions(+), 362 deletions(-) diff --git a/backend/internal/tests/jwt_middleware_test.go b/backend/internal/tests/jwt_middleware_test.go index adbfb853..d94223e3 100644 --- a/backend/internal/tests/jwt_middleware_test.go +++ b/backend/internal/tests/jwt_middleware_test.go @@ -1,132 +1,134 @@ -package middleware +package tests -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" +// import ( +// "context" +// "fmt" +// "net/http" +// "net/http/httptest" +// "os" +// "testing" +// +// "KonferCA/SPUR/db" +// "KonferCA/SPUR/internal/jwt" +// "github.com/google/uuid" +// "github.com/jackc/pgx/v5/pgxpool" +// "github.com/labstack/echo/v4" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestProtectAPIForAccessToken(t *testing.T) { +// // setup test environment +// os.Setenv("JWT_SECRET", "secret") +// +// // Connect to test database +// ctx := context.Background() +// dbURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", +// "postgres", +// "postgres", +// "localhost", +// "5432", +// "postgres", +// "disable", +// ) +// +// dbPool, err := pgxpool.New(ctx, dbURL) +// if err != nil { +// t.Fatalf("failed to connect to database: %v", err) +// } +// defer dbPool.Close() +// +// // Clean up any existing test user +// _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") +// if err != nil { +// t.Fatalf("failed to clean up test user: %v", err) +// } +// +// // Create a test user directly in the database +// userID := uuid.New().String() +// _, err = dbPool.Exec(ctx, ` +// INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) +// VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) +// `, userID, "test@example.com", "hashedpassword", "Test", "User") +// if err != nil { +// t.Fatalf("failed to create test user: %v", err) +// } +// +// // Create Echo instance with the DB connection +// e := echo.New() +// queries := db.New(dbPool) +// middleware := ProtectAPI(jwt.ACCESS_TOKEN_TYPE, queries) +// e.Use(middleware) +// +// e.GET("/protected", func(c echo.Context) error { +// return c.String(http.StatusOK, "protected resource") +// }) +// +// // Get test user data from the database +// user, err := queries.GetUserByEmail(ctx, "test@example.com") +// if err != nil { +// t.Fatalf("failed to get test user: %v", err) +// } +// +// // Get the user's salt +// var salt []byte +// err = dbPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE id = $1", user.ID).Scan(&salt) +// if err != nil { +// t.Fatalf("failed to get user salt: %v", err) +// } +// +// // generate valid tokens using the actual salt +// accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, salt) +// assert.Nil(t, err) +// +// tests := []struct { +// name string +// expectedCode int +// token string +// }{ +// { +// name: "Accept access token", +// expectedCode: http.StatusOK, +// token: accessToken, +// }, +// { +// name: "Reject refresh token", +// expectedCode: http.StatusUnauthorized, +// token: refreshToken, +// }, +// { +// name: "Reject invalid token format", +// expectedCode: http.StatusUnauthorized, +// token: "invalid-token", +// }, +// { +// name: "Reject empty token", +// expectedCode: http.StatusUnauthorized, +// token: "", +// }, +// { +// name: "Reject token with invalid signature", +// expectedCode: http.StatusUnauthorized, +// token: accessToken + "tampered", +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// req := httptest.NewRequest(http.MethodGet, "/protected", nil) +// rec := httptest.NewRecorder() +// if test.token != "" { +// req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", test.token)) +// } +// e.ServeHTTP(rec, req) +// assert.Equal(t, test.expectedCode, rec.Code) +// }) +// } +// +// // Clean up test user after test +// _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") +// if err != nil { +// t.Fatalf("failed to clean up test user: %v", err) +// } +// } +// - "KonferCA/SPUR/db" - "KonferCA/SPUR/internal/jwt" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -func TestProtectAPIForAccessToken(t *testing.T) { - // setup test environment - os.Setenv("JWT_SECRET", "secret") - - // Connect to test database - ctx := context.Background() - dbURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", - "postgres", - "postgres", - "localhost", - "5432", - "postgres", - "disable", - ) - - dbPool, err := pgxpool.New(ctx, dbURL) - if err != nil { - t.Fatalf("failed to connect to database: %v", err) - } - defer dbPool.Close() - - // Clean up any existing test user - _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up test user: %v", err) - } - - // Create a test user directly in the database - userID := uuid.New().String() - _, err = dbPool.Exec(ctx, ` - INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) - VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) - `, userID, "test@example.com", "hashedpassword", "Test", "User") - if err != nil { - t.Fatalf("failed to create test user: %v", err) - } - - // Create Echo instance with the DB connection - e := echo.New() - queries := db.New(dbPool) - middleware := ProtectAPI(jwt.ACCESS_TOKEN_TYPE, queries) - e.Use(middleware) - - e.GET("/protected", func(c echo.Context) error { - return c.String(http.StatusOK, "protected resource") - }) - - // Get test user data from the database - user, err := queries.GetUserByEmail(ctx, "test@example.com") - if err != nil { - t.Fatalf("failed to get test user: %v", err) - } - - // Get the user's salt - var salt []byte - err = dbPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE id = $1", user.ID).Scan(&salt) - if err != nil { - t.Fatalf("failed to get user salt: %v", err) - } - - // generate valid tokens using the actual salt - accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, salt) - assert.Nil(t, err) - - tests := []struct { - name string - expectedCode int - token string - }{ - { - name: "Accept access token", - expectedCode: http.StatusOK, - token: accessToken, - }, - { - name: "Reject refresh token", - expectedCode: http.StatusUnauthorized, - token: refreshToken, - }, - { - name: "Reject invalid token format", - expectedCode: http.StatusUnauthorized, - token: "invalid-token", - }, - { - name: "Reject empty token", - expectedCode: http.StatusUnauthorized, - token: "", - }, - { - name: "Reject token with invalid signature", - expectedCode: http.StatusUnauthorized, - token: accessToken + "tampered", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/protected", nil) - rec := httptest.NewRecorder() - if test.token != "" { - req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", test.token)) - } - e.ServeHTTP(rec, req) - assert.Equal(t, test.expectedCode, rec.Code) - }) - } - - // Clean up test user after test - _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up test user: %v", err) - } -} \ No newline at end of file diff --git a/backend/internal/tests/jwt_test.go b/backend/internal/tests/jwt_test.go index 5b5a2630..475e609a 100644 --- a/backend/internal/tests/jwt_test.go +++ b/backend/internal/tests/jwt_test.go @@ -1,125 +1,125 @@ -package jwt - -import ( - "os" - "testing" - "time" - - "KonferCA/SPUR/db" - - "github.com/stretchr/testify/assert" -) - -func TestJWT(t *testing.T) { - // setup env - os.Setenv("JWT_SECRET", "secret") - os.Setenv("JWT_SECRET_VERIFY_EMAIL", "test-secret") - - userID := "some-user-id" - role := db.UserRole("user") - salt := []byte("test-salt") - - t.Run("token salt invalidation", func(t *testing.T) { - // Generate initial salt - initialSalt := []byte("initial-salt") - - // Generate tokens with initial salt - accessToken, refreshToken, err := GenerateWithSalt(userID, role, initialSalt) - assert.Nil(t, err) - assert.NotEmpty(t, accessToken) - assert.NotEmpty(t, refreshToken) - - // Verify tokens work with initial salt - claims, err := VerifyTokenWithSalt(accessToken, initialSalt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) - - // Change salt (simulating token invalidation) - newSalt := []byte("new-salt") - - // Old tokens should fail verification with new salt - _, err = VerifyTokenWithSalt(accessToken, newSalt) - assert.NotNil(t, err, "Token should be invalid with new salt") - - // Generate new tokens with new salt - newAccessToken, newRefreshToken, err := GenerateWithSalt(userID, role, newSalt) - assert.Nil(t, err) - assert.NotEmpty(t, newAccessToken) - assert.NotEmpty(t, newRefreshToken) - - // New tokens should work with new salt - claims, err = VerifyTokenWithSalt(newAccessToken, newSalt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - }) - - t.Run("two-step verification", func(t *testing.T) { - salt := []byte("test-salt") - - // Generate a token - accessToken, _, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - - // Step 1: Parse claims without verification - unverifiedClaims, err := ParseUnverifiedClaims(accessToken) - assert.Nil(t, err) - assert.Equal(t, userID, unverifiedClaims.UserID) - - // Step 2: Verify with salt - verifiedClaims, err := VerifyTokenWithSalt(accessToken, salt) - assert.Nil(t, err) - assert.Equal(t, userID, verifiedClaims.UserID) - - // Try to verify with wrong salt - wrongSalt := []byte("wrong-salt") - _, err = VerifyTokenWithSalt(accessToken, wrongSalt) - assert.NotNil(t, err, "Token should be invalid with wrong salt") - }) - - t.Run("generate access token", func(t *testing.T) { - accessToken, _, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - assert.NotEmpty(t, accessToken) - claims, err := VerifyTokenWithSalt(accessToken, salt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) - }) - - t.Run("generate refresh token", func(t *testing.T) { - _, refreshToken, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - assert.NotEmpty(t, refreshToken) - claims, err := VerifyTokenWithSalt(refreshToken, salt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, REFRESH_TOKEN_TYPE) - }) - - t.Run("verify email token", func(t *testing.T) { - email := "test@mail.com" - id := "some-id" - exp := time.Now().Add(time.Second * 5) - token, err := GenerateVerifyEmailToken(email, id, exp) - assert.Nil(t, err) - claims, err := VerifyEmailToken(token) - assert.Nil(t, err) - assert.Equal(t, claims.Email, email) - assert.Equal(t, claims.ID, id) - assert.Equal(t, claims.ExpiresAt.Unix(), exp.Unix()) - }) - - t.Run("deny expired verify email token", func(t *testing.T) { - email := "test@mail.com" - id := "some-id" - exp := time.Now().Add(-1 * 5 * time.Second) - token, err := GenerateVerifyEmailToken(email, id, exp) - assert.Nil(t, err) - _, err = VerifyEmailToken(token) - assert.NotNil(t, err) - }) -} +package tests + +// import ( +// "os" +// "testing" +// "time" +// +// "KonferCA/SPUR/db" +// +// "github.com/stretchr/testify/assert" +// ) +// +// func TestJWT(t *testing.T) { +// // setup env +// os.Setenv("JWT_SECRET", "secret") +// os.Setenv("JWT_SECRET_VERIFY_EMAIL", "test-secret") +// +// userID := "some-user-id" +// role := db.UserRole("user") +// salt := []byte("test-salt") +// +// t.Run("token salt invalidation", func(t *testing.T) { +// // Generate initial salt +// initialSalt := []byte("initial-salt") +// +// // Generate tokens with initial salt +// accessToken, refreshToken, err := GenerateWithSalt(userID, role, initialSalt) +// assert.Nil(t, err) +// assert.NotEmpty(t, accessToken) +// assert.NotEmpty(t, refreshToken) +// +// // Verify tokens work with initial salt +// claims, err := VerifyTokenWithSalt(accessToken, initialSalt) +// assert.Nil(t, err) +// assert.Equal(t, claims.UserID, userID) +// assert.Equal(t, claims.Role, role) +// assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) +// +// // Change salt (simulating token invalidation) +// newSalt := []byte("new-salt") +// +// // Old tokens should fail verification with new salt +// _, err = VerifyTokenWithSalt(accessToken, newSalt) +// assert.NotNil(t, err, "Token should be invalid with new salt") +// +// // Generate new tokens with new salt +// newAccessToken, newRefreshToken, err := GenerateWithSalt(userID, role, newSalt) +// assert.Nil(t, err) +// assert.NotEmpty(t, newAccessToken) +// assert.NotEmpty(t, newRefreshToken) +// +// // New tokens should work with new salt +// claims, err = VerifyTokenWithSalt(newAccessToken, newSalt) +// assert.Nil(t, err) +// assert.Equal(t, claims.UserID, userID) +// }) +// +// t.Run("two-step verification", func(t *testing.T) { +// salt := []byte("test-salt") +// +// // Generate a token +// accessToken, _, err := GenerateWithSalt(userID, role, salt) +// assert.Nil(t, err) +// +// // Step 1: Parse claims without verification +// unverifiedClaims, err := ParseUnverifiedClaims(accessToken) +// assert.Nil(t, err) +// assert.Equal(t, userID, unverifiedClaims.UserID) +// +// // Step 2: Verify with salt +// verifiedClaims, err := VerifyTokenWithSalt(accessToken, salt) +// assert.Nil(t, err) +// assert.Equal(t, userID, verifiedClaims.UserID) +// +// // Try to verify with wrong salt +// wrongSalt := []byte("wrong-salt") +// _, err = VerifyTokenWithSalt(accessToken, wrongSalt) +// assert.NotNil(t, err, "Token should be invalid with wrong salt") +// }) +// +// t.Run("generate access token", func(t *testing.T) { +// accessToken, _, err := GenerateWithSalt(userID, role, salt) +// assert.Nil(t, err) +// assert.NotEmpty(t, accessToken) +// claims, err := VerifyTokenWithSalt(accessToken, salt) +// assert.Nil(t, err) +// assert.Equal(t, claims.UserID, userID) +// assert.Equal(t, claims.Role, role) +// assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) +// }) +// +// t.Run("generate refresh token", func(t *testing.T) { +// _, refreshToken, err := GenerateWithSalt(userID, role, salt) +// assert.Nil(t, err) +// assert.NotEmpty(t, refreshToken) +// claims, err := VerifyTokenWithSalt(refreshToken, salt) +// assert.Nil(t, err) +// assert.Equal(t, claims.UserID, userID) +// assert.Equal(t, claims.Role, role) +// assert.Equal(t, claims.TokenType, REFRESH_TOKEN_TYPE) +// }) +// +// t.Run("verify email token", func(t *testing.T) { +// email := "test@mail.com" +// id := "some-id" +// exp := time.Now().Add(time.Second * 5) +// token, err := GenerateVerifyEmailToken(email, id, exp) +// assert.Nil(t, err) +// claims, err := VerifyEmailToken(token) +// assert.Nil(t, err) +// assert.Equal(t, claims.Email, email) +// assert.Equal(t, claims.ID, id) +// assert.Equal(t, claims.ExpiresAt.Unix(), exp.Unix()) +// }) +// +// t.Run("deny expired verify email token", func(t *testing.T) { +// email := "test@mail.com" +// id := "some-id" +// exp := time.Now().Add(-1 * 5 * time.Second) +// token, err := GenerateVerifyEmailToken(email, id, exp) +// assert.Nil(t, err) +// _, err = VerifyEmailToken(token) +// assert.NotNil(t, err) +// }) +// } diff --git a/backend/internal/tests/req_validator_test.go b/backend/internal/tests/req_validator_test.go index b78d685e..9c7a3287 100644 --- a/backend/internal/tests/req_validator_test.go +++ b/backend/internal/tests/req_validator_test.go @@ -1,108 +1,108 @@ -package middleware +package tests -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -func TestRequestBodyValidator(t *testing.T) { - type testStruct struct { - TestField bool `json:"test_field" validate:"required"` - } - - e := echo.New() - e.Validator = NewRequestBodyValidator() - e.POST("/", func(c echo.Context) error { - // check that the request body is the correct interface - i, ok := c.Get(REQUEST_BODY_KEY).(*testStruct) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError) - } - - // echo back - return c.JSON(http.StatusOK, i) - }, ValidateRequestBody(reflect.TypeOf(testStruct{}))) - - tests := []struct { - name string - payload interface{} - expectedCode int - }{ - { - name: "Valid request body", - payload: testStruct{ - TestField: true, - }, - expectedCode: http.StatusOK, - }, - { - name: "Invalid request body - validation error", - payload: testStruct{ - // will fail required validation - TestField: false, - }, - // expecting 500 since the middleware its expected to return - // the original ValidationErrors from validator pkg - expectedCode: http.StatusInternalServerError, - }, - { - name: "Empty request body", - payload: nil, - // expecting 500 since the middleware its expected to return - // the original ValidationErrors from validator pkg - expectedCode: http.StatusInternalServerError, - }, - { - name: "Invalid JSON format", - payload: `{ - "test_field": invalid - }`, - expectedCode: http.StatusBadRequest, - }, - { - name: "Wrong type in JSON", - payload: map[string]interface{}{ - "test_field": "not a boolean", - }, - expectedCode: http.StatusBadRequest, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var req *http.Request - - if tc.payload != nil { - var payload []byte - var err error - - // handle string payloads (for invalid JSON tests) - if strPayload, ok := tc.payload.(string); ok { - payload = []byte(strPayload) - } else { - payload, err = json.Marshal(tc.payload) - assert.NoError(t, err) - } - - req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - } else { - req = httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - } - - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - - t.Log(rec.Body.String()) - assert.Equal(t, tc.expectedCode, rec.Code) - }) - } -} +// import ( +// "bytes" +// "encoding/json" +// "net/http" +// "net/http/httptest" +// "reflect" +// "testing" +// +// "github.com/labstack/echo/v4" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestRequestBodyValidator(t *testing.T) { +// type testStruct struct { +// TestField bool `json:"test_field" validate:"required"` +// } +// +// e := echo.New() +// e.Validator = NewRequestBodyValidator() +// e.POST("/", func(c echo.Context) error { +// // check that the request body is the correct interface +// i, ok := c.Get(REQUEST_BODY_KEY).(*testStruct) +// if !ok { +// return echo.NewHTTPError(http.StatusInternalServerError) +// } +// +// // echo back +// return c.JSON(http.StatusOK, i) +// }, ValidateRequestBody(reflect.TypeOf(testStruct{}))) +// +// tests := []struct { +// name string +// payload interface{} +// expectedCode int +// }{ +// { +// name: "Valid request body", +// payload: testStruct{ +// TestField: true, +// }, +// expectedCode: http.StatusOK, +// }, +// { +// name: "Invalid request body - validation error", +// payload: testStruct{ +// // will fail required validation +// TestField: false, +// }, +// // expecting 500 since the middleware its expected to return +// // the original ValidationErrors from validator pkg +// expectedCode: http.StatusInternalServerError, +// }, +// { +// name: "Empty request body", +// payload: nil, +// // expecting 500 since the middleware its expected to return +// // the original ValidationErrors from validator pkg +// expectedCode: http.StatusInternalServerError, +// }, +// { +// name: "Invalid JSON format", +// payload: `{ +// "test_field": invalid +// }`, +// expectedCode: http.StatusBadRequest, +// }, +// { +// name: "Wrong type in JSON", +// payload: map[string]interface{}{ +// "test_field": "not a boolean", +// }, +// expectedCode: http.StatusBadRequest, +// }, +// } +// +// for _, tc := range tests { +// t.Run(tc.name, func(t *testing.T) { +// var req *http.Request +// +// if tc.payload != nil { +// var payload []byte +// var err error +// +// // handle string payloads (for invalid JSON tests) +// if strPayload, ok := tc.payload.(string); ok { +// payload = []byte(strPayload) +// } else { +// payload, err = json.Marshal(tc.payload) +// assert.NoError(t, err) +// } +// +// req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload)) +// req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) +// } else { +// req = httptest.NewRequest(http.MethodPost, "/", nil) +// req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) +// } +// +// rec := httptest.NewRecorder() +// e.ServeHTTP(rec, req) +// +// t.Log(rec.Body.String()) +// assert.Equal(t, tc.expectedCode, rec.Code) +// }) +// } +// } From 2d6bf25c425a6f5284ac83d562292515491cfafd Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:28:59 -0500 Subject: [PATCH 02/13] add missing package names --- backend/internal/v1/setup.go | 11 +++++++++++ backend/internal/v1/v1_auth/auth.go | 1 + backend/internal/v1/v1_auth/routes.go | 1 + backend/internal/v1/v1_auth/types.go | 1 + backend/internal/v1/v1_common/constants.go | 1 + backend/internal/v1/v1_common/errors.go | 1 + backend/internal/v1/v1_common/helpers.go | 1 + backend/internal/v1/v1_common/response.go | 1 + backend/internal/v1/v1_common/types.go | 1 + backend/internal/v1/v1_companies/ companies.go | 0 backend/internal/v1/v1_companies/companies.go | 1 + backend/internal/v1/v1_companies/routes.go | 1 + backend/internal/v1/v1_companies/types.go | 1 + backend/internal/v1/v1_health/healthcheck.go | 1 + backend/internal/v1/v1_health/routes.go | 1 + backend/internal/v1/v1_health/types.go | 1 + backend/internal/v1/v1_projects/answers.go | 1 + backend/internal/v1/v1_projects/comments.go | 1 + backend/internal/v1/v1_projects/documents.go | 1 + backend/internal/v1/v1_projects/projects.go | 1 + backend/internal/v1/v1_projects/questions.go | 1 + backend/internal/v1/v1_projects/routes.go | 1 + backend/internal/v1/v1_projects/types.go | 1 + backend/internal/v1/v1_teams/members.go | 1 + backend/internal/v1/v1_teams/routes.go | 1 + backend/internal/v1/v1_teams/types.go | 1 + backend/internal/v1/v1_transactions/routes.go | 1 + backend/internal/v1/v1_transactions/transactions.go | 1 + backend/internal/v1/v1_transactions/types.go | 1 + backend/internal/v1/v1_users/routes.go | 1 + backend/internal/v1/v1_users/types.go | 1 + backend/internal/v1/v1_users/users.go | 1 + 32 files changed, 41 insertions(+) delete mode 100644 backend/internal/v1/v1_companies/ companies.go create mode 100644 backend/internal/v1/v1_companies/companies.go diff --git a/backend/internal/v1/setup.go b/backend/internal/v1/setup.go index e69de29b..3aa023a1 100644 --- a/backend/internal/v1/setup.go +++ b/backend/internal/v1/setup.go @@ -0,0 +1,11 @@ +package v1 + +import ( + "KonferCA/SPUR/internal/interfaces" + + "github.com/labstack/echo/v4" +) + +func SetupRoutes(e *echo.Group, s interfaces.CoreServer) { + +} diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go index e69de29b..b8c4eb4e 100644 --- a/backend/internal/v1/v1_auth/auth.go +++ b/backend/internal/v1/v1_auth/auth.go @@ -0,0 +1 @@ +package v1auth diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go index e69de29b..b8c4eb4e 100644 --- a/backend/internal/v1/v1_auth/routes.go +++ b/backend/internal/v1/v1_auth/routes.go @@ -0,0 +1 @@ +package v1auth diff --git a/backend/internal/v1/v1_auth/types.go b/backend/internal/v1/v1_auth/types.go index e69de29b..b8c4eb4e 100644 --- a/backend/internal/v1/v1_auth/types.go +++ b/backend/internal/v1/v1_auth/types.go @@ -0,0 +1 @@ +package v1auth diff --git a/backend/internal/v1/v1_common/constants.go b/backend/internal/v1/v1_common/constants.go index e69de29b..bbb539c4 100644 --- a/backend/internal/v1/v1_common/constants.go +++ b/backend/internal/v1/v1_common/constants.go @@ -0,0 +1 @@ +package v1common diff --git a/backend/internal/v1/v1_common/errors.go b/backend/internal/v1/v1_common/errors.go index e69de29b..bbb539c4 100644 --- a/backend/internal/v1/v1_common/errors.go +++ b/backend/internal/v1/v1_common/errors.go @@ -0,0 +1 @@ +package v1common diff --git a/backend/internal/v1/v1_common/helpers.go b/backend/internal/v1/v1_common/helpers.go index e69de29b..bbb539c4 100644 --- a/backend/internal/v1/v1_common/helpers.go +++ b/backend/internal/v1/v1_common/helpers.go @@ -0,0 +1 @@ +package v1common diff --git a/backend/internal/v1/v1_common/response.go b/backend/internal/v1/v1_common/response.go index e69de29b..bbb539c4 100644 --- a/backend/internal/v1/v1_common/response.go +++ b/backend/internal/v1/v1_common/response.go @@ -0,0 +1 @@ +package v1common diff --git a/backend/internal/v1/v1_common/types.go b/backend/internal/v1/v1_common/types.go index e69de29b..bbb539c4 100644 --- a/backend/internal/v1/v1_common/types.go +++ b/backend/internal/v1/v1_common/types.go @@ -0,0 +1 @@ +package v1common diff --git a/backend/internal/v1/v1_companies/ companies.go b/backend/internal/v1/v1_companies/ companies.go deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/internal/v1/v1_companies/companies.go b/backend/internal/v1/v1_companies/companies.go new file mode 100644 index 00000000..bcb1b657 --- /dev/null +++ b/backend/internal/v1/v1_companies/companies.go @@ -0,0 +1 @@ +package v1companies diff --git a/backend/internal/v1/v1_companies/routes.go b/backend/internal/v1/v1_companies/routes.go index e69de29b..bcb1b657 100644 --- a/backend/internal/v1/v1_companies/routes.go +++ b/backend/internal/v1/v1_companies/routes.go @@ -0,0 +1 @@ +package v1companies diff --git a/backend/internal/v1/v1_companies/types.go b/backend/internal/v1/v1_companies/types.go index e69de29b..bcb1b657 100644 --- a/backend/internal/v1/v1_companies/types.go +++ b/backend/internal/v1/v1_companies/types.go @@ -0,0 +1 @@ +package v1companies diff --git a/backend/internal/v1/v1_health/healthcheck.go b/backend/internal/v1/v1_health/healthcheck.go index e69de29b..78071a1e 100644 --- a/backend/internal/v1/v1_health/healthcheck.go +++ b/backend/internal/v1/v1_health/healthcheck.go @@ -0,0 +1 @@ +package v1healthcheck diff --git a/backend/internal/v1/v1_health/routes.go b/backend/internal/v1/v1_health/routes.go index e69de29b..8b137891 100644 --- a/backend/internal/v1/v1_health/routes.go +++ b/backend/internal/v1/v1_health/routes.go @@ -0,0 +1 @@ + diff --git a/backend/internal/v1/v1_health/types.go b/backend/internal/v1/v1_health/types.go index e69de29b..78071a1e 100644 --- a/backend/internal/v1/v1_health/types.go +++ b/backend/internal/v1/v1_health/types.go @@ -0,0 +1 @@ +package v1healthcheck diff --git a/backend/internal/v1/v1_projects/answers.go b/backend/internal/v1/v1_projects/answers.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/answers.go +++ b/backend/internal/v1/v1_projects/answers.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/comments.go b/backend/internal/v1/v1_projects/comments.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/comments.go +++ b/backend/internal/v1/v1_projects/comments.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/documents.go b/backend/internal/v1/v1_projects/documents.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/documents.go +++ b/backend/internal/v1/v1_projects/documents.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/projects.go b/backend/internal/v1/v1_projects/projects.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/projects.go +++ b/backend/internal/v1/v1_projects/projects.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/questions.go b/backend/internal/v1/v1_projects/questions.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/questions.go +++ b/backend/internal/v1/v1_projects/questions.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/routes.go b/backend/internal/v1/v1_projects/routes.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/routes.go +++ b/backend/internal/v1/v1_projects/routes.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_projects/types.go b/backend/internal/v1/v1_projects/types.go index e69de29b..542e564d 100644 --- a/backend/internal/v1/v1_projects/types.go +++ b/backend/internal/v1/v1_projects/types.go @@ -0,0 +1 @@ +package v1projects diff --git a/backend/internal/v1/v1_teams/members.go b/backend/internal/v1/v1_teams/members.go index e69de29b..32e2eb01 100644 --- a/backend/internal/v1/v1_teams/members.go +++ b/backend/internal/v1/v1_teams/members.go @@ -0,0 +1 @@ +package v1teams diff --git a/backend/internal/v1/v1_teams/routes.go b/backend/internal/v1/v1_teams/routes.go index e69de29b..32e2eb01 100644 --- a/backend/internal/v1/v1_teams/routes.go +++ b/backend/internal/v1/v1_teams/routes.go @@ -0,0 +1 @@ +package v1teams diff --git a/backend/internal/v1/v1_teams/types.go b/backend/internal/v1/v1_teams/types.go index e69de29b..32e2eb01 100644 --- a/backend/internal/v1/v1_teams/types.go +++ b/backend/internal/v1/v1_teams/types.go @@ -0,0 +1 @@ +package v1teams diff --git a/backend/internal/v1/v1_transactions/routes.go b/backend/internal/v1/v1_transactions/routes.go index e69de29b..270dd9f9 100644 --- a/backend/internal/v1/v1_transactions/routes.go +++ b/backend/internal/v1/v1_transactions/routes.go @@ -0,0 +1 @@ +package v1transactions diff --git a/backend/internal/v1/v1_transactions/transactions.go b/backend/internal/v1/v1_transactions/transactions.go index e69de29b..270dd9f9 100644 --- a/backend/internal/v1/v1_transactions/transactions.go +++ b/backend/internal/v1/v1_transactions/transactions.go @@ -0,0 +1 @@ +package v1transactions diff --git a/backend/internal/v1/v1_transactions/types.go b/backend/internal/v1/v1_transactions/types.go index e69de29b..270dd9f9 100644 --- a/backend/internal/v1/v1_transactions/types.go +++ b/backend/internal/v1/v1_transactions/types.go @@ -0,0 +1 @@ +package v1transactions diff --git a/backend/internal/v1/v1_users/routes.go b/backend/internal/v1/v1_users/routes.go index e69de29b..b08d9c7d 100644 --- a/backend/internal/v1/v1_users/routes.go +++ b/backend/internal/v1/v1_users/routes.go @@ -0,0 +1 @@ +package v1users diff --git a/backend/internal/v1/v1_users/types.go b/backend/internal/v1/v1_users/types.go index e69de29b..b08d9c7d 100644 --- a/backend/internal/v1/v1_users/types.go +++ b/backend/internal/v1/v1_users/types.go @@ -0,0 +1 @@ +package v1users diff --git a/backend/internal/v1/v1_users/users.go b/backend/internal/v1/v1_users/users.go index e69de29b..b08d9c7d 100644 --- a/backend/internal/v1/v1_users/users.go +++ b/backend/internal/v1/v1_users/users.go @@ -0,0 +1 @@ +package v1users From f3c47c57d99b180cc8b155b16fd3643b4be581dd Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:49:56 -0500 Subject: [PATCH 03/13] add version to common pkg --- backend/common/version.go | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backend/common/version.go diff --git a/backend/common/version.go b/backend/common/version.go new file mode 100644 index 00000000..d9a1abe0 --- /dev/null +++ b/backend/common/version.go @@ -0,0 +1,3 @@ +package common + +var VERSION string From 6ce906bc7efebb7c30ae597536e3501f6802c618 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:50:19 -0500 Subject: [PATCH 04/13] complete CoreServer implementation to Server struct --- backend/internal/server/index.go | 48 ++++++++++++++++++++++++++----- backend/internal/server/routes.go | 4 ++- backend/internal/server/types.go | 35 ++++++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/backend/internal/server/index.go b/backend/internal/server/index.go index 3deeedab..d1f326f5 100644 --- a/backend/internal/server/index.go +++ b/backend/internal/server/index.go @@ -2,6 +2,7 @@ package server import ( "KonferCA/SPUR/db" + "KonferCA/SPUR/storage" "fmt" "os" @@ -9,11 +10,6 @@ import ( "github.com/labstack/echo/v4" ) -type Server struct { - DBPool *pgxpool.Pool - Echo *echo.Echo -} - /* Initializes a new Server instance and creates a connection pool to the database. The database connection string can be controlled by setting the following env variables: @@ -35,11 +31,17 @@ func New() (*Server, error) { return nil, err } + store, err := storage.NewStorage() + if err != nil { + return nil, err + } + e := echo.New() s := Server{ - DBPool: pool, - Echo: e, + DBPool: pool, + Echo: e, + Storage: store, } s.setupMiddlewares() @@ -48,6 +50,38 @@ func New() (*Server, error) { return &s, nil } +/* +Implement the CoreServer interface GetDB method that simply +returns the current db pool. +*/ +func (s *Server) GetDB() *pgxpool.Pool { + return s.DBPool +} + +/* +Implement the CoreServer interface GetQueries method that simply +returns a new instance of the db.Queries struct. +*/ +func (s *Server) GetQueries() *db.Queries { + return db.New(s.DBPool) +} + +/* +Implement the CoreServer interface GetStorage method that simply +returns an storage instance. +*/ +func (s *Server) GetStorage() *storage.Storage { + return s.Storage +} + +/* +Implement the CoreServer interface GetEcho method that simply +returns the root echo instance. +*/ +func (s *Server) GetEcho() *echo.Echo { + return s.Echo +} + /* Start the server and binds it to the given port. */ diff --git a/backend/internal/server/routes.go b/backend/internal/server/routes.go index dd51d0ed..3bf11449 100644 --- a/backend/internal/server/routes.go +++ b/backend/internal/server/routes.go @@ -1,8 +1,10 @@ package server +import v1 "KonferCA/SPUR/internal/v1" + /* Setup all the API routes of any version that will be available in this server. */ func (s *Server) setupRoutes() { - + v1.SetupRoutes(s) } diff --git a/backend/internal/server/types.go b/backend/internal/server/types.go index abb4e431..1b0f1c5b 100644 --- a/backend/internal/server/types.go +++ b/backend/internal/server/types.go @@ -1 +1,36 @@ package server + +import ( + "KonferCA/SPUR/storage" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" +) + +type Server struct { + DBPool *pgxpool.Pool + Echo *echo.Echo + Storage *storage.Storage +} + +type DatabaseInfo struct { + Connected bool `json:"connected"` + LatencyMs float64 `json:"latency_ms"` + PostgresVersion string `json:"postgres_version,omitempty"` + Error string `json:"error,omitempty"` +} + +type SystemInfo struct { + Version string `json:"version"` + GoVersion string `json:"go_version"` + NumGoRoutine int `json:"num_goroutines"` + MemoryUsage float64 `json:"memory_usage"` +} + +type HealthReport struct { + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Database DatabaseInfo `json:"database"` + System SystemInfo `json:"system"` +} From fc960a610eef03b22fbd690906a0699bac8497b4 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:51:09 -0500 Subject: [PATCH 05/13] setup healthcheck route --- backend/internal/v1/setup.go | 9 ++- backend/internal/v1/v1_health/healthcheck.go | 84 +++++++++++++++++++- backend/internal/v1/v1_health/routes.go | 14 ++++ backend/internal/v1/v1_health/types.go | 44 +++++++++- 4 files changed, 145 insertions(+), 6 deletions(-) diff --git a/backend/internal/v1/setup.go b/backend/internal/v1/setup.go index 3aa023a1..15f3b674 100644 --- a/backend/internal/v1/setup.go +++ b/backend/internal/v1/setup.go @@ -2,10 +2,11 @@ package v1 import ( "KonferCA/SPUR/internal/interfaces" - - "github.com/labstack/echo/v4" + "KonferCA/SPUR/internal/v1/v1_health" ) -func SetupRoutes(e *echo.Group, s interfaces.CoreServer) { - +func SetupRoutes(s interfaces.CoreServer) { + e := s.GetEcho() + g := e.Group("/api/v1") + v1health.SetupHealthcheckRoutes(g, s) } diff --git a/backend/internal/v1/v1_health/healthcheck.go b/backend/internal/v1/v1_health/healthcheck.go index 78071a1e..9e0ed50e 100644 --- a/backend/internal/v1/v1_health/healthcheck.go +++ b/backend/internal/v1/v1_health/healthcheck.go @@ -1 +1,83 @@ -package v1healthcheck +package v1health + +import ( + "KonferCA/SPUR/common" + "KonferCA/SPUR/internal/interfaces" + "context" + "net/http" + "runtime" + "time" + + "github.com/labstack/echo/v4" +) + +/* +V1 healthchekc endpoint handler. +*/ +func (h *Handler) handleHealthCheck(c echo.Context) error { + report := HealthReport{ + Timestamp: time.Now(), + System: getSystemInfo(), + } + + dbInfo := checkDatabase(h.server) + report.Database = dbInfo + + if dbInfo.Connected { + report.Status = "healthy" + } else { + report.Status = "unhealthy" + return c.JSON(http.StatusServiceUnavailable, report) + } + + return c.JSON(http.StatusOK, report) +} + +/* +getSystemInfo is a helper function that gathers basic information about +the backend which includes backend version, go binary version, number of active go routines and memory usage. +*/ +func getSystemInfo() SystemInfo { + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + + return SystemInfo{ + Version: common.VERSION, + GoVersion: runtime.Version(), + NumGoRoutine: runtime.NumGoroutine(), + MemoryUsage: float64(mem.Alloc) / 1024 / 1024, + } +} + +/* +checkDatabase is a helper function that gathers healthcheck information +on the database including connection status, latency, version, and errors. +*/ +func checkDatabase(s interfaces.CoreServer) DatabaseInfo { + info := DatabaseInfo{ + Connected: false, + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + start := time.Now() + + var version string + err := s.GetDB().QueryRow(ctx, "SELECT version()").Scan(&version) + + latency := time.Since(start) + info.LatencyMs = float64(latency.Microseconds()) / 1000.0 + + if err != nil { + info.Connected = false + info.Error = err.Error() + + return info + } + + info.Connected = true + info.PostgresVersion = version + + return info +} diff --git a/backend/internal/v1/v1_health/routes.go b/backend/internal/v1/v1_health/routes.go index 8b137891..99a44f05 100644 --- a/backend/internal/v1/v1_health/routes.go +++ b/backend/internal/v1/v1_health/routes.go @@ -1 +1,15 @@ +package v1health +import ( + "KonferCA/SPUR/internal/interfaces" + + "github.com/labstack/echo/v4" +) + +/* +Sets up all the healthcheck routes for V1. +*/ +func SetupHealthcheckRoutes(e *echo.Group, s interfaces.CoreServer) { + h := Handler{server: s} + e.GET("/health", h.handleHealthCheck) +} diff --git a/backend/internal/v1/v1_health/types.go b/backend/internal/v1/v1_health/types.go index 78071a1e..99322206 100644 --- a/backend/internal/v1/v1_health/types.go +++ b/backend/internal/v1/v1_health/types.go @@ -1 +1,43 @@ -package v1healthcheck +package v1health + +import ( + "KonferCA/SPUR/internal/interfaces" + "time" +) + +/* +Main Handler struct for V1 healthcheck routes. +*/ +type Handler struct { + server interfaces.CoreServer +} + +/* +DatabaseInfo represents a basic report of a database healthcheck. +*/ +type DatabaseInfo struct { + Connected bool `json:"connected"` + LatencyMs float64 `json:"latency_ms"` + PostgresVersion string `json:"postgres_version,omitempty"` + Error string `json:"error,omitempty"` +} + +/* +SystemInfo represents a basic system report for healthcheck. +*/ +type SystemInfo struct { + Version string `json:"version"` + GoVersion string `json:"go_version"` + NumGoRoutine int `json:"num_goroutines"` + MemoryUsage float64 `json:"memory_usage"` +} + +/* +HealthReport represents a bundle of different reports for healthcheck.g +*/ +type HealthReport struct { + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Database DatabaseInfo `json:"database"` + System SystemInfo `json:"system"` +} From 312bf79a4c34f3176454928693d7ba243dd01272 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:07 -0500 Subject: [PATCH 06/13] add query to get db version --- backend/.sqlc/queries/version.sql | 2 + backend/db/models.go | 235 ++++++++++++++++++++++++++++++ backend/db/version.sql.go | 21 +++ 3 files changed, 258 insertions(+) create mode 100644 backend/.sqlc/queries/version.sql create mode 100644 backend/db/models.go create mode 100644 backend/db/version.sql.go diff --git a/backend/.sqlc/queries/version.sql b/backend/.sqlc/queries/version.sql new file mode 100644 index 00000000..4d656405 --- /dev/null +++ b/backend/.sqlc/queries/version.sql @@ -0,0 +1,2 @@ +-- name: GetDBVersion :one +SELECT version(); diff --git a/backend/db/models.go b/backend/db/models.go new file mode 100644 index 00000000..da655c17 --- /dev/null +++ b/backend/db/models.go @@ -0,0 +1,235 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package db + +import ( + "database/sql/driver" + "fmt" + + "github.com/jackc/pgx/v5/pgtype" +) + +type ProjectStatus string + +const ( + ProjectStatusDraft ProjectStatus = "draft" + ProjectStatusPending ProjectStatus = "pending" + ProjectStatusVerified ProjectStatus = "verified" + ProjectStatusDeclined ProjectStatus = "declined" +) + +func (e *ProjectStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = ProjectStatus(s) + case string: + *e = ProjectStatus(s) + default: + return fmt.Errorf("unsupported scan type for ProjectStatus: %T", src) + } + return nil +} + +type NullProjectStatus struct { + ProjectStatus ProjectStatus + Valid bool // Valid is true if ProjectStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullProjectStatus) Scan(value interface{}) error { + if value == nil { + ns.ProjectStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.ProjectStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullProjectStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.ProjectStatus), nil +} + +func (e ProjectStatus) Valid() bool { + switch e { + case ProjectStatusDraft, + ProjectStatusPending, + ProjectStatusVerified, + ProjectStatusDeclined: + return true + } + return false +} + +func AllProjectStatusValues() []ProjectStatus { + return []ProjectStatus{ + ProjectStatusDraft, + ProjectStatusPending, + ProjectStatusVerified, + ProjectStatusDeclined, + } +} + +type UserRole string + +const ( + UserRoleAdmin UserRole = "admin" + UserRoleStartupOwner UserRole = "startup_owner" + UserRoleInvestor UserRole = "investor" +) + +func (e *UserRole) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UserRole(s) + case string: + *e = UserRole(s) + default: + return fmt.Errorf("unsupported scan type for UserRole: %T", src) + } + return nil +} + +type NullUserRole struct { + UserRole UserRole + Valid bool // Valid is true if UserRole is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullUserRole) Scan(value interface{}) error { + if value == nil { + ns.UserRole, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.UserRole.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullUserRole) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.UserRole), nil +} + +func (e UserRole) Valid() bool { + switch e { + case UserRoleAdmin, + UserRoleStartupOwner, + UserRoleInvestor: + return true + } + return false +} + +func AllUserRoleValues() []UserRole { + return []UserRole{ + UserRoleAdmin, + UserRoleStartupOwner, + UserRoleInvestor, + } +} + +type Company struct { + ID string + OwnerID string + Name string + WalletAddress *string + LinkedinUrl string + CreatedAt int64 + UpdatedAt int64 +} + +type Project struct { + ID string + CompanyID string + Title string + Description *string + Status ProjectStatus + CreatedAt int64 + UpdatedAt int64 +} + +type ProjectAnswer struct { + ID string + ProjectID string + QuestionID string + Answer string + CreatedAt int64 + UpdatedAt int64 +} + +type ProjectComment struct { + ID string + ProjectID string + TargetID string + Comment string + CommenterID string + CreatedAt int64 + UpdatedAt int64 +} + +type ProjectDocument struct { + ID string + ProjectID string + Name string + Url string + Section string + CreatedAt int64 + UpdatedAt int64 +} + +type ProjectQuestion struct { + ID string + Question string + Section string + CreatedAt int64 + UpdatedAt int64 +} + +type TeamMember struct { + ID string + CompanyID string + FirstName string + LastName string + Title string + Bio string + LinkedinUrl string + IsAccountOwner bool + CreatedAt int64 + UpdatedAt int64 +} + +type Transaction struct { + ID string + ProjectID string + CompanyID string + TxHash string + FromAddress string + ToAddress string + ValueAmount pgtype.Numeric +} + +type User struct { + ID string + Email string + Password string + Role UserRole + EmailVerified bool + CreatedAt int64 + UpdatedAt int64 + TokenSalt []byte +} + +type VerifyEmailToken struct { + ID string + UserID string + CreatedAt int64 + ExpiresAt int64 +} diff --git a/backend/db/version.sql.go b/backend/db/version.sql.go new file mode 100644 index 00000000..7fd094b2 --- /dev/null +++ b/backend/db/version.sql.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: version.sql + +package db + +import ( + "context" +) + +const getDBVersion = `-- name: GetDBVersion :one +SELECT version() +` + +func (q *Queries) GetDBVersion(ctx context.Context) (string, error) { + row := q.db.QueryRow(ctx, getDBVersion) + var version string + err := row.Scan(&version) + return version, err +} From 8b19da8d72a483c7ce9997bc4fe3facbced870da Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:25 -0500 Subject: [PATCH 07/13] update check db health check function --- backend/internal/v1/v1_health/healthcheck.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/internal/v1/v1_health/healthcheck.go b/backend/internal/v1/v1_health/healthcheck.go index 9e0ed50e..bedb38b6 100644 --- a/backend/internal/v1/v1_health/healthcheck.go +++ b/backend/internal/v1/v1_health/healthcheck.go @@ -2,7 +2,7 @@ package v1health import ( "KonferCA/SPUR/common" - "KonferCA/SPUR/internal/interfaces" + "KonferCA/SPUR/db" "context" "net/http" "runtime" @@ -20,7 +20,7 @@ func (h *Handler) handleHealthCheck(c echo.Context) error { System: getSystemInfo(), } - dbInfo := checkDatabase(h.server) + dbInfo := checkDatabase(h.server.GetQueries()) report.Database = dbInfo if dbInfo.Connected { @@ -53,7 +53,7 @@ func getSystemInfo() SystemInfo { checkDatabase is a helper function that gathers healthcheck information on the database including connection status, latency, version, and errors. */ -func checkDatabase(s interfaces.CoreServer) DatabaseInfo { +func checkDatabase(queries *db.Queries) DatabaseInfo { info := DatabaseInfo{ Connected: false, } @@ -63,8 +63,7 @@ func checkDatabase(s interfaces.CoreServer) DatabaseInfo { start := time.Now() - var version string - err := s.GetDB().QueryRow(ctx, "SELECT version()").Scan(&version) + version, err := queries.GetDBVersion(ctx) latency := time.Since(start) info.LatencyMs = float64(latency.Microseconds()) / 1000.0 From f58c7e44f7e409a0d9e5f34340798728017ad43c Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:36 -0500 Subject: [PATCH 08/13] skip s3 client in test env --- backend/storage/storage.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/storage/storage.go b/backend/storage/storage.go index 60872c89..69199b1e 100644 --- a/backend/storage/storage.go +++ b/backend/storage/storage.go @@ -1,6 +1,7 @@ package storage import ( + "KonferCA/SPUR/common" "bytes" "context" "fmt" @@ -28,7 +29,12 @@ func NewStorage() (*Storage, error) { return nil, fmt.Errorf("unable to load SDK config: %v", err) } - client := s3.NewFromConfig(cfg) + var client *s3.Client + if os.Getenv("APP_ENV") != common.TEST_ENV { + client = s3.NewFromConfig(cfg) + } else { + client = nil + } return &Storage{ s3Client: client, @@ -45,7 +51,7 @@ func (s *Storage) ValidateFileURL(url string) bool { // GetPresignedURL generates a presigned URL for uploading a file func (s *Storage) GetPresignedURL(ctx context.Context, key string) (string, error) { presignClient := s3.NewPresignClient(s.s3Client) - + putObjectInput := &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), @@ -84,4 +90,5 @@ func (s *Storage) DeleteFile(ctx context.Context, key string) error { } return nil -} \ No newline at end of file +} + From 1670e68f47e7d76cf36506ff73a6b4b2465175be Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:42 -0500 Subject: [PATCH 09/13] add healthcheck internal test --- .../internal/v1/v1_health/healthcheck_test.go | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 backend/internal/v1/v1_health/healthcheck_test.go diff --git a/backend/internal/v1/v1_health/healthcheck_test.go b/backend/internal/v1/v1_health/healthcheck_test.go new file mode 100644 index 00000000..81b3d7b4 --- /dev/null +++ b/backend/internal/v1/v1_health/healthcheck_test.go @@ -0,0 +1,44 @@ +package v1health + +import ( + "KonferCA/SPUR/common" + "KonferCA/SPUR/db" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetSystemInfo(t *testing.T) { + // this variable is set using build flags, but in test environment, that is not available + common.VERSION = "test" + + info := getSystemInfo() + + assert.NotEmpty(t, info.Version) + assert.NotEmpty(t, info.GoVersion) + assert.Greater(t, info.NumGoRoutine, 0) + assert.GreaterOrEqual(t, info.MemoryUsage, 0.0) +} + +func TestCheckDatabase(t *testing.T) { + pool, err := db.NewPool( + fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + "localhost", + "5432", + "postgres", + "postgres", + "postgres", + "disable", + ), + ) + assert.Nil(t, err) + + info := checkDatabase(db.New(pool)) + assert.True(t, info.Connected) + // latency must be higher than 0 if connection is actually proper + assert.Greater(t, info.LatencyMs, 0.0) + assert.Contains(t, info.PostgresVersion, "PostgreSQL 16") + assert.Empty(t, info.Error) +} From 5a7f62dbd7a42577ebbd519c774b50fc8c913bae Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:53 -0500 Subject: [PATCH 10/13] add test setup helper for env --- backend/internal/tests/setup.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/internal/tests/setup.go diff --git a/backend/internal/tests/setup.go b/backend/internal/tests/setup.go new file mode 100644 index 00000000..397a2f67 --- /dev/null +++ b/backend/internal/tests/setup.go @@ -0,0 +1,23 @@ +package tests + +import ( + "KonferCA/SPUR/common" + "os" +) + +/* +Sets up all the any environment variables that the application needs to +properly boot up and run. +*/ +func setupEnv() { + os.Setenv("APP_ENV", common.TEST_ENV) + // this is not a real bucket and is just here to allow + // the stogare.NewStorage to proceed + os.Setenv("AWS_S3_BUCKET", "test-bucket") + os.Setenv("DB_HOST", "localhost") + os.Setenv("DB_PORT", "5432") + os.Setenv("DB_USER", "postgres") + os.Setenv("DB_PASSWORD", "postgres") + os.Setenv("DB_NAME", "postgres") + os.Setenv("DB_SSLMODE", "disable") +} From a49ea821635e07d02b690b1af92ae058eee2a8c6 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:00:01 -0500 Subject: [PATCH 11/13] add server blackbox test --- backend/internal/tests/server_test.go | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/internal/tests/server_test.go diff --git a/backend/internal/tests/server_test.go b/backend/internal/tests/server_test.go new file mode 100644 index 00000000..684fb2d2 --- /dev/null +++ b/backend/internal/tests/server_test.go @@ -0,0 +1,41 @@ +package tests + +import ( + "KonferCA/SPUR/internal/server" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +/* +This file contains blackbox testing for the server package which includes +all routes that are exposed by the server. The main objective is to test +that all routes are behaving how they should be from the perspective of +a client that doesn't know the inner implementation details. +*/ +func TestServer(t *testing.T) { + setupEnv() + + s, err := server.New() + assert.Nil(t, err) + + t.Run("Test API V1 Health Check Route", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil) + rec := httptest.NewRecorder() + s.GetEcho().ServeHTTP(rec, req) + assert.Equal(t, rec.Code, http.StatusOK) + resBytes, err := io.ReadAll(rec.Body) + assert.Nil(t, err) + var resBody map[string]any + err = json.Unmarshal(resBytes, &resBody) + assert.Nil(t, err) + assert.Equal(t, resBody["status"], "healthy") + assert.NotEmpty(t, resBody["timestamp"]) + assert.NotEmpty(t, resBody["database"]) + assert.NotEmpty(t, resBody["system"]) + }) +} From a8807b9e0eff9c051e329f5744f38202b186e357 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:15:46 -0500 Subject: [PATCH 12/13] rename v1health to v1_health --- backend/internal/v1/setup.go | 2 +- backend/internal/v1/v1_health/healthcheck.go | 4 ++-- backend/internal/v1/v1_health/healthcheck_test.go | 2 +- backend/internal/v1/v1_health/routes.go | 2 +- backend/internal/v1/v1_health/types.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/internal/v1/setup.go b/backend/internal/v1/setup.go index 15f3b674..4852a23b 100644 --- a/backend/internal/v1/setup.go +++ b/backend/internal/v1/setup.go @@ -8,5 +8,5 @@ import ( func SetupRoutes(s interfaces.CoreServer) { e := s.GetEcho() g := e.Group("/api/v1") - v1health.SetupHealthcheckRoutes(g, s) + v1_health.SetupHealthcheckRoutes(g, s) } diff --git a/backend/internal/v1/v1_health/healthcheck.go b/backend/internal/v1/v1_health/healthcheck.go index bedb38b6..6d5c6a92 100644 --- a/backend/internal/v1/v1_health/healthcheck.go +++ b/backend/internal/v1/v1_health/healthcheck.go @@ -1,4 +1,4 @@ -package v1health +package v1_health import ( "KonferCA/SPUR/common" @@ -12,7 +12,7 @@ import ( ) /* -V1 healthchekc endpoint handler. +V1 healthcheck endpoint handler. */ func (h *Handler) handleHealthCheck(c echo.Context) error { report := HealthReport{ diff --git a/backend/internal/v1/v1_health/healthcheck_test.go b/backend/internal/v1/v1_health/healthcheck_test.go index 81b3d7b4..208af9ef 100644 --- a/backend/internal/v1/v1_health/healthcheck_test.go +++ b/backend/internal/v1/v1_health/healthcheck_test.go @@ -1,4 +1,4 @@ -package v1health +package v1_health import ( "KonferCA/SPUR/common" diff --git a/backend/internal/v1/v1_health/routes.go b/backend/internal/v1/v1_health/routes.go index 99a44f05..31dfed1a 100644 --- a/backend/internal/v1/v1_health/routes.go +++ b/backend/internal/v1/v1_health/routes.go @@ -1,4 +1,4 @@ -package v1health +package v1_health import ( "KonferCA/SPUR/internal/interfaces" diff --git a/backend/internal/v1/v1_health/types.go b/backend/internal/v1/v1_health/types.go index 99322206..46d653af 100644 --- a/backend/internal/v1/v1_health/types.go +++ b/backend/internal/v1/v1_health/types.go @@ -1,4 +1,4 @@ -package v1health +package v1_health import ( "KonferCA/SPUR/internal/interfaces" From 8f33de7e59cacdeb9dabdf208e24940417d9025c Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:26:16 -0500 Subject: [PATCH 13/13] remove duplicate types --- backend/internal/server/types.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/backend/internal/server/types.go b/backend/internal/server/types.go index 1b0f1c5b..a3383a38 100644 --- a/backend/internal/server/types.go +++ b/backend/internal/server/types.go @@ -2,7 +2,6 @@ package server import ( "KonferCA/SPUR/storage" - "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" @@ -13,24 +12,3 @@ type Server struct { Echo *echo.Echo Storage *storage.Storage } - -type DatabaseInfo struct { - Connected bool `json:"connected"` - LatencyMs float64 `json:"latency_ms"` - PostgresVersion string `json:"postgres_version,omitempty"` - Error string `json:"error,omitempty"` -} - -type SystemInfo struct { - Version string `json:"version"` - GoVersion string `json:"go_version"` - NumGoRoutine int `json:"num_goroutines"` - MemoryUsage float64 `json:"memory_usage"` -} - -type HealthReport struct { - Status string `json:"status"` - Timestamp time.Time `json:"timestamp"` - Database DatabaseInfo `json:"database"` - System SystemInfo `json:"system"` -}