package event import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "nixcn-cms/data" "nixcn-cms/internal/authtoken" "nixcn-cms/service/service_event" "nixcn-cms/testutil" ) func init() { gin.SetMode(gin.TestMode) } func issueToken(t *testing.T, userId uuid.UUID) string { t.Helper() tok := &authtoken.Token{Application: viper.GetString("server.application")} access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId) require.NoError(t, err) return access } func newEventRouter(t *testing.T) *gin.Engine { t.Helper() r := gin.New() ApiHandler(r.Group("/event")) return r } func postJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func getRequest(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func patchJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func deleteReq(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodDelete, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func getWithBearer(t *testing.T, r *gin.Engine, path, token string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func patchWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func deleteWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder { t.Helper() b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodDelete, path, bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } // ---- Create ---- func TestEventCreateHandler(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 40) token := issueToken(t, user.UserId) r := newEventRouter(t) w := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "official", Name: "Handler Test Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]any) require.True(t, ok) assert.NotEmpty(t, data["event_id"]) } func TestEventCreateHandlerMissingAuth(t *testing.T) { testutil.Setup(t) r := newEventRouter(t) w := postJSON(t, r, "/event/create", map[string]any{"type": "party"}) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestEventCreateHandlerInvalidJSON(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 40) token := issueToken(t, owner.UserId) r := newEventRouter(t) req := httptest.NewRequest(http.MethodPost, "/event/create", bytes.NewBufferString("{bad json")) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ---- Info ---- func TestEventInfoHandlerNotFound(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/info?event_id=00000000-0000-0000-0000-000000000001", token) assert.Equal(t, http.StatusNotFound, w.Code) } // ---- List ---- func TestEventListHandlerRequiresOffset(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/list", token) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestEventListHandlerWithOffset(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/list?offset=0&limit=10", token) assert.Equal(t, http.StatusOK, w.Code) } // ---- Join ---- func TestEventJoinHandlerInvalidEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) require.NoError(t, new(data.User).PatchByUserId(t.Context(), user.UserId, data.WithNickname("Joiner"))) token := issueToken(t, user.UserId) r := newEventRouter(t) w := postWithBearer(t, r, "/event/join", token, map[string]any{"event_id": "not-a-uuid"}) assert.Equal(t, http.StatusBadRequest, w.Code) } // ---- Delete ---- func TestEventDeleteHandlerNotFound(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 40) token := issueToken(t, user.UserId) r := newEventRouter(t) nonExistentId := uuid.New().String() w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{ "event_id": nonExistentId, }) assert.Equal(t, http.StatusNotFound, w.Code) } func TestEventDeleteHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 40) token := issueToken(t, owner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "party", Name: "Delete Me", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{"event_id": eventId}) assert.Equal(t, http.StatusOK, w.Code) } // ---- Info (success) ---- func TestEventInfoHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) ownerToken := issueToken(t, owner.UserId) userToken := issueToken(t, user.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{ Type: "party", Name: "Info Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) w := getWithBearer(t, r, "/event/info?event_id="+eventId, userToken) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) d, ok := resp["data"].(map[string]any) require.True(t, ok) assert.Equal(t, eventId, d["event_id"]) } // ---- Join (success) ---- func TestEventJoinHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10) require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner"))) ownerToken := issueToken(t, owner.UserId) joinerToken := issueToken(t, joiner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{ Type: "party", Name: "Join Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) w := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId}) assert.Equal(t, http.StatusOK, w.Code) } // ---- Update ---- func TestEventUpdateHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, owner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "party", Name: "Original Name", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) newName := "Updated Name" w := patchWithBearer(t, r, "/event/update", token, map[string]any{ "event_id": eventId, "name": newName, }) assert.Equal(t, http.StatusOK, w.Code) } func TestEventUpdateHandlerInvalidJSON(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) req := httptest.NewRequest(http.MethodPatch, "/event/update", bytes.NewBufferString("{bad")) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ---- Stats ---- func TestEventStatsHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, owner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "party", Name: "Stats Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) w := getWithBearer(t, r, "/event/stats?event_id="+eventId, token) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) d, ok := resp["data"].(map[string]any) require.True(t, ok) assert.NotNil(t, d["join_count"]) } func TestEventStatsHandlerMissingEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/stats?event_id=not-a-uuid", token) assert.Equal(t, http.StatusBadRequest, w.Code) } // ---- Checkin ---- func TestEventCheckinHandlerInvalidEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/checkin?event_id=not-a-uuid", token) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestEventCheckinHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10) require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner"))) ownerToken := issueToken(t, owner.UserId) joinerToken := issueToken(t, joiner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{ Type: "party", Name: "Checkin Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) joinW := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId}) require.Equal(t, http.StatusOK, joinW.Code) w := getWithBearer(t, r, "/event/checkin?event_id="+eventId, joinerToken) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) d, ok := resp["data"].(map[string]any) require.True(t, ok) assert.NotEmpty(t, d["checkin_code"]) } // ---- CheckinSubmit ---- func TestEventCheckinSubmitHandlerInvalidCode(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 20) token := issueToken(t, user.UserId) r := newEventRouter(t) w := postWithBearer(t, r, "/event/checkin/submit", token, service_event.CheckinSubmitData{ CheckinCode: "000000", }) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestEventCheckinSubmitHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10) require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner"))) ownerToken := issueToken(t, owner.UserId) joinerToken := issueToken(t, joiner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{ Type: "party", Name: "Checkin Submit Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) joinW := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId}) require.Equal(t, http.StatusOK, joinW.Code) checkinW := getWithBearer(t, r, "/event/checkin?event_id="+eventId, joinerToken) require.Equal(t, http.StatusOK, checkinW.Code) var checkinResp map[string]any require.NoError(t, json.Unmarshal(checkinW.Body.Bytes(), &checkinResp)) code := checkinResp["data"].(map[string]any)["checkin_code"].(string) w := postWithBearer(t, r, "/event/checkin/submit", ownerToken, service_event.CheckinSubmitData{ CheckinCode: code, }) assert.Equal(t, http.StatusOK, w.Code) } // ---- CheckinQuery ---- func TestEventCheckinQueryHandlerInvalidEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/checkin/query?event_id=not-a-uuid", token) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestEventCheckinQueryHandlerNotJoined(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/checkin/query?event_id="+uuid.New().String(), token) assert.Equal(t, http.StatusNotFound, w.Code) } // ---- Attendance ---- func TestEventAttendanceHandlerInvalidEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) w := getWithBearer(t, r, "/event/attendance?event_id=not-a-uuid", token) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestEventAttendanceHandlerSuccess(t *testing.T) { testutil.SetupWithAuth(t) owner := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, owner.UserId) r := newEventRouter(t) createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "party", Name: "Attendance Event", Subtitle: "sub", StartTime: time.Now().Add(24 * time.Hour), EndTime: time.Now().Add(48 * time.Hour), Quota: 100, Limit: 150, }) require.Equal(t, http.StatusOK, createW.Code) var createResp map[string]any require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp)) eventId := createResp["data"].(map[string]any)["event_id"].(string) w := getWithBearer(t, r, "/event/attendance?event_id="+eventId, token) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) d, ok := resp["data"].(map[string]any) require.True(t, ok) assert.NotNil(t, d["total"]) } // ---- Guide ---- func TestEventGuideHandlerInvalidEventId(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) // The guide handler returns 500 for invalid UUID (source behaviour) w := getWithBearer(t, r, "/event/guide?event_id=not-a-uuid", token) assert.NotEqual(t, http.StatusNotFound, w.Code, "route must be registered") } // ---- Permission enforcement ---- func TestEventCreateHandlerLowPermission(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 10) token := issueToken(t, user.UserId) r := newEventRouter(t) w := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{ Type: "party", Name: "Should Fail", Quota: 10, Limit: 20, }) assert.Equal(t, http.StatusForbidden, w.Code) } func TestEventDeleteHandlerLowPermission(t *testing.T) { testutil.SetupWithAuth(t) user := testutil.SeedUser(t, testutil.RandomEmail(), 30) token := issueToken(t, user.UserId) r := newEventRouter(t) w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{ "event_id": uuid.New().String(), }) assert.Equal(t, http.StatusForbidden, w.Code) }