Skip to content

Commit e69737c

Browse files
test: add concurrent, edge case, and error handling tests for trip endpoints
1 parent 9500b12 commit e69737c

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

store_trips_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"sync"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -192,3 +193,85 @@ func TestStore_StartTrip_AfterEndingPrevious(t *testing.T) {
192193
assert.NotEqual(t, trip1.ID, trip2.ID)
193194
assert.Equal(t, "active", trip2.Status)
194195
}
196+
197+
func TestStore_StartTrip_ConcurrentAttempts(t *testing.T) {
198+
store := newTestStore(t)
199+
userID := setupTripTestData(t, store)
200+
201+
const goroutines = 10
202+
var wg sync.WaitGroup
203+
wg.Add(goroutines)
204+
205+
successes := make(chan int64, goroutines)
206+
failures := make(chan error, goroutines)
207+
208+
for i := 0; i < goroutines; i++ {
209+
go func() {
210+
defer wg.Done()
211+
trip, err := store.StartTrip(context.Background(), userID, "bus-trip-1", "route-5", "")
212+
if err != nil {
213+
failures <- err
214+
return
215+
}
216+
successes <- trip.ID
217+
}()
218+
}
219+
220+
wg.Wait()
221+
close(successes)
222+
close(failures)
223+
224+
// Exactly one goroutine should succeed.
225+
var successCount int
226+
for range successes {
227+
successCount++
228+
}
229+
assert.Equal(t, 1, successCount, "exactly one concurrent StartTrip should succeed")
230+
231+
// The rest should fail with ErrActiveTripExists or ErrNotAssigned (serialization error).
232+
var failCount int
233+
for range failures {
234+
failCount++
235+
}
236+
assert.Equal(t, goroutines-1, failCount)
237+
238+
// Verify only one trip in DB.
239+
var count int
240+
err := store.pool.QueryRow(context.Background(), "SELECT COUNT(*) FROM trips WHERE user_id = $1 AND status = 'active'", userID).Scan(&count)
241+
require.NoError(t, err)
242+
assert.Equal(t, 1, count, "only one active trip should exist after concurrent attempts")
243+
}
244+
245+
func TestStore_StartTrip_EmptyOptionalFields(t *testing.T) {
246+
store := newTestStore(t)
247+
userID := setupTripTestData(t, store)
248+
ctx := context.Background()
249+
250+
// route_id and gtfs_trip_id are optional — empty strings should work.
251+
trip, err := store.StartTrip(ctx, userID, "bus-trip-1", "", "")
252+
require.NoError(t, err)
253+
assert.Equal(t, "", trip.RouteID)
254+
assert.Equal(t, "", trip.GtfsTripID)
255+
}
256+
257+
func TestStore_EndTrip_UpdatedAtChanges(t *testing.T) {
258+
store := newTestStore(t)
259+
userID := setupTripTestData(t, store)
260+
ctx := context.Background()
261+
262+
trip, err := store.StartTrip(ctx, userID, "bus-trip-1", "route-5", "")
263+
require.NoError(t, err)
264+
265+
// Get created_at for comparison.
266+
var createdAt, updatedAt interface{}
267+
err = store.pool.QueryRow(ctx, "SELECT created_at, updated_at FROM trips WHERE id = $1", trip.ID).Scan(&createdAt, &updatedAt)
268+
require.NoError(t, err)
269+
270+
err = store.EndTrip(ctx, trip.ID, userID)
271+
require.NoError(t, err)
272+
273+
var newUpdatedAt interface{}
274+
err = store.pool.QueryRow(ctx, "SELECT updated_at FROM trips WHERE id = $1", trip.ID).Scan(&newUpdatedAt)
275+
require.NoError(t, err)
276+
assert.NotEqual(t, updatedAt, newUpdatedAt, "updated_at should change after ending trip")
277+
}

trip_handlers_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,116 @@ func TestHandleEndTrip_MissingClaims(t *testing.T) {
199199

200200
assert.Equal(t, http.StatusInternalServerError, w.Code)
201201
}
202+
203+
func TestHandleStartTrip_TrailingJSON(t *testing.T) {
204+
store := &mockTripStarter{}
205+
handler := handleStartTrip(store)
206+
207+
body := []byte(`{"vehicle_id":"bus-1"}{"extra":true}`)
208+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
209+
req.Header.Set("Content-Type", "application/json")
210+
claims := jwt.MapClaims{"sub": "42"}
211+
ctx := context.WithValue(req.Context(), claimsKey, claims)
212+
req = req.WithContext(ctx)
213+
w := httptest.NewRecorder()
214+
handler(w, req)
215+
216+
assert.Equal(t, http.StatusBadRequest, w.Code)
217+
}
218+
219+
func TestHandleEndTrip_TrailingJSON(t *testing.T) {
220+
store := &mockTripEnder{}
221+
handler := handleEndTrip(store)
222+
223+
body := []byte(`{"trip_id":1}{"extra":true}`)
224+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
225+
req.Header.Set("Content-Type", "application/json")
226+
claims := jwt.MapClaims{"sub": "42"}
227+
ctx := context.WithValue(req.Context(), claimsKey, claims)
228+
req = req.WithContext(ctx)
229+
w := httptest.NewRecorder()
230+
handler(w, req)
231+
232+
assert.Equal(t, http.StatusBadRequest, w.Code)
233+
}
234+
235+
func TestHandleStartTrip_UnknownFields(t *testing.T) {
236+
store := &mockTripStarter{}
237+
handler := handleStartTrip(store)
238+
239+
body := []byte(`{"vehicle_id":"bus-1","unknown_field":"value"}`)
240+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
241+
req.Header.Set("Content-Type", "application/json")
242+
claims := jwt.MapClaims{"sub": "42"}
243+
ctx := context.WithValue(req.Context(), claimsKey, claims)
244+
req = req.WithContext(ctx)
245+
w := httptest.NewRecorder()
246+
handler(w, req)
247+
248+
assert.Equal(t, http.StatusBadRequest, w.Code)
249+
}
250+
251+
func TestHandleStartTrip_InvalidSubClaim(t *testing.T) {
252+
store := &mockTripStarter{}
253+
handler := handleStartTrip(store)
254+
255+
body, _ := json.Marshal(StartTripRequest{VehicleID: "bus-1"})
256+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
257+
req.Header.Set("Content-Type", "application/json")
258+
// sub is not a valid integer string
259+
claims := jwt.MapClaims{"sub": "not-a-number"}
260+
ctx := context.WithValue(req.Context(), claimsKey, claims)
261+
req = req.WithContext(ctx)
262+
w := httptest.NewRecorder()
263+
handler(w, req)
264+
265+
assert.Equal(t, http.StatusUnauthorized, w.Code)
266+
}
267+
268+
func TestHandleEndTrip_InvalidSubClaim(t *testing.T) {
269+
store := &mockTripEnder{}
270+
handler := handleEndTrip(store)
271+
272+
body, _ := json.Marshal(EndTripRequest{TripID: 1})
273+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
274+
req.Header.Set("Content-Type", "application/json")
275+
claims := jwt.MapClaims{"sub": "not-a-number"}
276+
ctx := context.WithValue(req.Context(), claimsKey, claims)
277+
req = req.WithContext(ctx)
278+
w := httptest.NewRecorder()
279+
handler(w, req)
280+
281+
assert.Equal(t, http.StatusUnauthorized, w.Code)
282+
}
283+
284+
func TestHandleStartTrip_EmptySub(t *testing.T) {
285+
store := &mockTripStarter{}
286+
handler := handleStartTrip(store)
287+
288+
body, _ := json.Marshal(StartTripRequest{VehicleID: "bus-1"})
289+
req := httptest.NewRequest("POST", "/", bytes.NewReader(body))
290+
req.Header.Set("Content-Type", "application/json")
291+
claims := jwt.MapClaims{"sub": ""}
292+
ctx := context.WithValue(req.Context(), claimsKey, claims)
293+
req = req.WithContext(ctx)
294+
w := httptest.NewRecorder()
295+
handler(w, req)
296+
297+
assert.Equal(t, http.StatusUnauthorized, w.Code)
298+
}
299+
300+
func TestHandleStartTrip_InternalServerError(t *testing.T) {
301+
store := &mockTripStarter{err: assert.AnError}
302+
handler := handleStartTrip(store)
303+
w := tripRequest(t, handler, "42", StartTripRequest{VehicleID: "bus-1"})
304+
305+
assert.Equal(t, http.StatusInternalServerError, w.Code)
306+
}
307+
308+
func TestHandleEndTrip_InternalServerError(t *testing.T) {
309+
store := &mockTripEnder{err: assert.AnError}
310+
handler := handleEndTrip(store)
311+
w := tripRequest(t, handler, "42", EndTripRequest{TripID: 1})
312+
313+
assert.Equal(t, http.StatusInternalServerError, w.Code)
314+
}

0 commit comments

Comments
 (0)