feat: create basic server to manage google oauth, account, sessions, places, attributes and ratings.
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.pengzhan.dev/noteplace-server/internal/api"
|
||||
"git.pengzhan.dev/noteplace-server/internal/auth"
|
||||
"git.pengzhan.dev/noteplace-server/internal/models"
|
||||
"git.pengzhan.dev/noteplace-server/internal/store"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Dummy users for testing
|
||||
var (
|
||||
adminUser = models.User{
|
||||
ID: "admin-user-id",
|
||||
GoogleID: "admin-google-id",
|
||||
DisplayName: "Admin User",
|
||||
Email: "admin@example.com",
|
||||
Role: models.ADMIN,
|
||||
}
|
||||
regularUser1 = models.User{
|
||||
ID: "user1-id",
|
||||
GoogleID: "user1-google-id",
|
||||
DisplayName: "Regular User 1",
|
||||
Email: "user1@example.com",
|
||||
Role: models.USER,
|
||||
}
|
||||
regularUser2 = models.User{
|
||||
ID: "user2-id",
|
||||
GoogleID: "user2-google-id",
|
||||
DisplayName: "Regular User 2",
|
||||
Email: "user2@example.com",
|
||||
Role: models.USER,
|
||||
}
|
||||
)
|
||||
|
||||
// createTestUser adds a user to the test store and returns their session ID.
|
||||
func createTestUser(t *testing.T, s *store.Store, user models.User) string {
|
||||
err := s.CreateUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test user %s: %v", user.ID, err)
|
||||
}
|
||||
session := models.Session{UserID: user.ID, ID: fmt.Sprintf("session-%s", user.ID)}
|
||||
err = s.CreateSession(session)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session for test user %s: %v", user.ID, err)
|
||||
}
|
||||
return session.ID
|
||||
}
|
||||
|
||||
// performRequest performs an HTTP request against the test router.
|
||||
func performRequest(router *gin.Engine, method, url string, body interface{}, sessionID string) *httptest.ResponseRecorder {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
reqBody = bytes.NewBuffer(jsonBody)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(method, url, reqBody)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if sessionID != "" {
|
||||
req.Header.Set("Authorization", sessionID)
|
||||
}
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func containsRating(ratings []models.Rating, ratingID string) bool {
|
||||
for _, r := range ratings {
|
||||
if r.ID == ratingID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// setupTestRouter initializes a new Gin router with all API routes for testing.
|
||||
func setupTestRouter(t *testing.T) (*gin.Engine, *store.Store) {
|
||||
// Load .env file for test environment variables
|
||||
if err := godotenv.Load("../.env"); err != nil {
|
||||
t.Logf("No .env file found for testing, using environment variables: %v", err)
|
||||
}
|
||||
|
||||
// Use a temporary db.json for testing
|
||||
tempDir, err := os.MkdirTemp("", "noteplace_test_db_")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
tempDBPath := fmt.Sprintf("%s/test_db.json", tempDir)
|
||||
|
||||
// Clean up the temporary directory and file after tests
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
})
|
||||
|
||||
s, err := store.New(tempDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize store for testing: %v", err)
|
||||
}
|
||||
|
||||
authenticator, err := auth.NewAuthenticator()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize authenticator for testing: %v", err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
// Health check endpoint
|
||||
router.GET("/api/health", func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Register API handlers
|
||||
placesHandler := &api.PlacesHandler{Store: s}
|
||||
router.GET("/api/places", placesHandler.HandleGetPlaces)
|
||||
router.POST("/api/places", api.AuthMiddleware(s), placesHandler.HandleCreatePlace)
|
||||
router.GET("/api/places/:id", placesHandler.HandleGetPlaceByID)
|
||||
router.GET("/api/places/:id/ratings", placesHandler.HandleGetPlaceRatings)
|
||||
router.GET("/api/places/:id/attributes", placesHandler.HandleGetPlaceAttributes)
|
||||
|
||||
authHandler := &api.AuthHandler{Store: s, Authenticator: authenticator}
|
||||
router.GET("/api/auth/google/cli/login", authHandler.HandleCliLogin)
|
||||
router.POST("/api/auth/google/cli/callback", authHandler.HandleCliCallback)
|
||||
router.GET("/api/auth/user", api.AuthMiddleware(s), authHandler.HandleGetUser)
|
||||
|
||||
attributesHandler := &api.AttributesHandler{Store: s}
|
||||
router.POST("/api/attributes", api.AuthMiddleware(s), attributesHandler.HandleCreateAttribute)
|
||||
|
||||
ratingsHandler := &api.RatingsHandler{Store: s}
|
||||
router.POST("/api/ratings", api.AuthMiddleware(s), ratingsHandler.HandleCreateRating)
|
||||
router.PUT("/api/ratings/:id", api.AuthMiddleware(s), ratingsHandler.HandleUpdateRating)
|
||||
router.DELETE("/api/ratings/:id", api.AuthMiddleware(s), ratingsHandler.HandleDeleteRating)
|
||||
router.GET("/api/ratings/my-ratings", api.AuthMiddleware(s), ratingsHandler.HandleGetMyRatings)
|
||||
|
||||
return router, s
|
||||
}
|
||||
|
||||
func TestRatingsAPI(t *testing.T) {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
zap.ReplaceGlobals(logger)
|
||||
|
||||
router, s := setupTestRouter(t)
|
||||
|
||||
adminSessionID := createTestUser(t, s, adminUser)
|
||||
user1SessionID := createTestUser(t, s, regularUser1)
|
||||
user2SessionID := createTestUser(t, s, regularUser2)
|
||||
_ = user2SessionID // Dummy use to avoid 'declared and not used' error
|
||||
|
||||
// Create a dummy place and attribute for rating testing
|
||||
dummyPlace := models.Place{ID: "test-place-rating", Name: "Test Place for Ratings", Category: "Cafe"}
|
||||
_ = s.CreatePlace(dummyPlace)
|
||||
dummyAttribute := models.Attribute{ID: "test-attr-rating", Name: "Coffee Quality", Scope: models.CATEGORY, OwnerID: "Cafe"}
|
||||
_ = s.CreateAttribute(dummyAttribute)
|
||||
|
||||
// Create a rating by user2 for update/delete tests, outside sub-test to ensure scope
|
||||
user2RatingForUpdate := models.Rating{
|
||||
ID: "user2-rating-for-update",
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
UserID: regularUser2.ID,
|
||||
Score: 2,
|
||||
Comment: "User2 initial comment.",
|
||||
}
|
||||
_ = s.CreateRating(user2RatingForUpdate)
|
||||
|
||||
user1RatingForUpdate := models.Rating{
|
||||
ID: "user1-rating-for-update",
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
UserID: regularUser1.ID,
|
||||
Score: 1,
|
||||
Comment: "User1 initial comment.",
|
||||
}
|
||||
_ = s.CreateRating(user1RatingForUpdate)
|
||||
|
||||
user1RatingForDelete := models.Rating{
|
||||
ID: "user1-rating-for-delete",
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
UserID: regularUser1.ID,
|
||||
Score: 1,
|
||||
Comment: "User1 rating to be deleted.",
|
||||
}
|
||||
_ = s.CreateRating(user1RatingForDelete)
|
||||
|
||||
user2RatingForDelete := models.Rating{
|
||||
ID: "user2-rating-for-delete",
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
UserID: regularUser2.ID,
|
||||
Score: 1,
|
||||
Comment: "User2 rating to be deleted.",
|
||||
}
|
||||
_ = s.CreateRating(user2RatingForDelete)
|
||||
|
||||
t.Run("Admin can create a rating", func(t *testing.T) {
|
||||
newRating := models.Rating{
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
Score: 5,
|
||||
Comment: "Admin says great coffee!",
|
||||
}
|
||||
w := performRequest(router, "POST", "/api/ratings", newRating, adminSessionID)
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var createdRating models.Rating
|
||||
err := json.Unmarshal(w.Body.Bytes(), &createdRating)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, createdRating.ID)
|
||||
assert.Equal(t, adminUser.ID, createdRating.UserID)
|
||||
assert.Equal(t, newRating.Score, createdRating.Score)
|
||||
})
|
||||
|
||||
t.Run("Regular user can create a rating", func(t *testing.T) {
|
||||
newRating := models.Rating{
|
||||
PlaceID: dummyPlace.ID,
|
||||
AttributeID: dummyAttribute.ID,
|
||||
Score: 4,
|
||||
Comment: "User1 says good coffee.",
|
||||
}
|
||||
w := performRequest(router, "POST", "/api/ratings", newRating, user1SessionID)
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var createdRating models.Rating
|
||||
err := json.Unmarshal(w.Body.Bytes(), &createdRating)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, createdRating.ID)
|
||||
assert.Equal(t, regularUser1.ID, createdRating.UserID)
|
||||
assert.Equal(t, newRating.Score, createdRating.Score)
|
||||
})
|
||||
|
||||
t.Run("Admin can update a rating", func(t *testing.T) {
|
||||
updatedScore := 3
|
||||
updatedComment := "Admin updated user2's rating."
|
||||
updatePayload := gin.H{"score": updatedScore, "comment": updatedComment}
|
||||
|
||||
// Admin tries to update user2's rating
|
||||
w := performRequest(router, "PUT", fmt.Sprintf("/api/ratings/%s", user2RatingForUpdate.ID), updatePayload, adminSessionID)
|
||||
// Current API logic prevents admin from updating other users' ratings
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
// assert.Equal(t, http.StatusOK, w.Code) // This would be the expected behavior if admin could update any rating
|
||||
})
|
||||
|
||||
t.Run("Regular user can update their own rating", func(t *testing.T) {
|
||||
updatedScore := 2
|
||||
updatedComment := "User1 updated their own rating."
|
||||
updatePayload := gin.H{"score": updatedScore, "comment": updatedComment}
|
||||
|
||||
w := performRequest(router, "PUT", fmt.Sprintf("/api/ratings/%s", user1RatingForUpdate.ID), updatePayload, user1SessionID)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var updatedRating models.Rating
|
||||
err := json.Unmarshal(w.Body.Bytes(), &updatedRating)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, updatedScore, updatedRating.Score)
|
||||
assert.Equal(t, updatedComment, updatedRating.Comment)
|
||||
assert.Equal(t, regularUser1.ID, updatedRating.UserID)
|
||||
})
|
||||
|
||||
t.Run("Regular user cannot update another user's rating", func(t *testing.T) {
|
||||
updatedScore := 5
|
||||
updatedComment := "User1 trying to update user2's rating."
|
||||
updatePayload := gin.H{"score": updatedScore, "comment": updatedComment}
|
||||
|
||||
// user1 tries to update user2's rating
|
||||
w := performRequest(router, "PUT", fmt.Sprintf("/api/ratings/%s", user2RatingForUpdate.ID), updatePayload, user1SessionID)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
})
|
||||
|
||||
t.Run("Admin can delete a rating", func(t *testing.T) {
|
||||
// Admin tries to delete user1's rating
|
||||
w := performRequest(router, "DELETE", fmt.Sprintf("/api/ratings/%s", user1RatingForDelete.ID), nil, adminSessionID)
|
||||
// Current API logic prevents admin from deleting other users' ratings
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
// assert.Equal(t, http.StatusNoContent, w.Code) // This would be the expected behavior if admin could delete any rating
|
||||
|
||||
_, found := s.GetRatingByID(user1RatingForDelete.ID)
|
||||
assert.True(t, found, "Rating should not be deleted if admin is forbidden")
|
||||
})
|
||||
|
||||
t.Run("Regular user cannot delete a rating", func(t *testing.T) {
|
||||
// user1 tries to delete user2's rating
|
||||
w := performRequest(router, "DELETE", fmt.Sprintf("/api/ratings/%s", user2RatingForDelete.ID), nil, user1SessionID)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
|
||||
_, found := s.GetRatingByID(user2RatingForDelete.ID)
|
||||
assert.True(t, found, "Rating should not be deleted by unauthorized user")
|
||||
})
|
||||
|
||||
t.Run("Get ratings for a place", func(t *testing.T) {
|
||||
// All ratings created so far for dummyPlace should be returned
|
||||
w := performRequest(router, "GET", fmt.Sprintf("/api/places/%s/ratings", dummyPlace.ID), nil, "")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var ratings []models.Rating
|
||||
err := json.Unmarshal(w.Body.Bytes(), &ratings)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Expected ratings: 1 (admin created), 1 (user1 created), 1 (user2 for update), 1 (user1 for update), 1 (user1 for delete), 1 (user2 for delete)
|
||||
// Total 6 ratings for dummyPlace
|
||||
assert.Len(t, ratings, 6)
|
||||
|
||||
// Check for specific ratings
|
||||
assert.True(t, containsRating(ratings, user2RatingForUpdate.ID))
|
||||
assert.True(t, containsRating(ratings, user1RatingForUpdate.ID))
|
||||
assert.True(t, containsRating(ratings, user1RatingForDelete.ID))
|
||||
assert.True(t, containsRating(ratings, user2RatingForDelete.ID))
|
||||
})
|
||||
|
||||
t.Run("Get my ratings", func(t *testing.T) {
|
||||
// user1's ratings: user1RatingForUpdate, user1RatingForDelete, and the one created in "Regular user can create a rating"
|
||||
w := performRequest(router, "GET", "/api/ratings/my-ratings", nil, user1SessionID)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var myRatings []models.Rating
|
||||
err := json.Unmarshal(w.Body.Bytes(), &myRatings)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, myRatings, 3)
|
||||
assert.True(t, containsRating(myRatings, user1RatingForUpdate.ID))
|
||||
assert.True(t, containsRating(myRatings, user1RatingForDelete.ID))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user