booking_repo_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package repository
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "time"
  7. "github.com/jackc/pgx/v5/pgconn"
  8. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
  9. )
  10. func TestBookingRepo_Create_CalculatesTotalPrice(t *testing.T) {
  11. if testPool == nil {
  12. t.Skip("DATABASE_URL_TEST not set")
  13. }
  14. cleanTables(t, "bookings", "places", "users")
  15. user := insertTestUser(t)
  16. rate := 1000
  17. place := insertTestPlace(t, user.ID, &rate)
  18. repo := NewBookingRepo(testPool)
  19. start := time.Now().Truncate(time.Hour).Add(2 * time.Hour)
  20. end := start.Add(3 * time.Hour)
  21. booking := &models.Booking{
  22. PlaceID: place.ID,
  23. UserID: user.ID,
  24. StartTime: start,
  25. EndTime: end,
  26. }
  27. if err := repo.Create(context.Background(), booking); err != nil {
  28. t.Fatalf("Create: %v", err)
  29. }
  30. if booking.ID == "" {
  31. t.Fatal("expected non-empty booking ID")
  32. }
  33. if booking.TotalPrice == nil {
  34. t.Fatal("expected TotalPrice to be calculated")
  35. }
  36. expected := 1000 * 3 // 1000 RUB/h * 3h
  37. if *booking.TotalPrice != expected {
  38. t.Errorf("TotalPrice = %d, want %d", *booking.TotalPrice, expected)
  39. }
  40. if booking.Currency != "RUB" {
  41. t.Errorf("Currency = %s, want RUB", booking.Currency)
  42. }
  43. if booking.Status != "pending" {
  44. t.Errorf("Status = %s, want pending", booking.Status)
  45. }
  46. }
  47. func TestBookingRepo_Create_NoPriceForNonStudio(t *testing.T) {
  48. if testPool == nil {
  49. t.Skip("DATABASE_URL_TEST not set")
  50. }
  51. cleanTables(t, "bookings", "places", "users")
  52. user := insertTestUser(t)
  53. place := insertTestPlace(t, user.ID, nil)
  54. place.Type = "place"
  55. repo := NewBookingRepo(testPool)
  56. start := time.Now().Truncate(time.Hour).Add(2 * time.Hour)
  57. end := start.Add(3 * time.Hour)
  58. booking := &models.Booking{
  59. PlaceID: place.ID,
  60. UserID: user.ID,
  61. StartTime: start,
  62. EndTime: end,
  63. }
  64. if err := repo.Create(context.Background(), booking); err != nil {
  65. t.Fatalf("Create: %v", err)
  66. }
  67. if booking.TotalPrice != nil {
  68. t.Errorf("TotalPrice = %d, want nil (place without hourly_rate)", *booking.TotalPrice)
  69. }
  70. }
  71. func TestBookingRepo_Create_PlaceNotFound(t *testing.T) {
  72. if testPool == nil {
  73. t.Skip("DATABASE_URL_TEST not set")
  74. }
  75. cleanTables(t, "bookings")
  76. repo := NewBookingRepo(testPool)
  77. booking := &models.Booking{
  78. PlaceID: "00000000-0000-0000-0000-000000000000",
  79. UserID: "00000000-0000-0000-0000-000000000000",
  80. StartTime: time.Now(),
  81. EndTime: time.Now().Add(1 * time.Hour),
  82. }
  83. err := repo.Create(context.Background(), booking)
  84. if !errors.Is(err, ErrPlaceNotFound) {
  85. t.Errorf("expected ErrPlaceNotFound, got %v", err)
  86. }
  87. }
  88. func TestBookingRepo_Create_OverlapRejected(t *testing.T) {
  89. if testPool == nil {
  90. t.Skip("DATABASE_URL_TEST not set")
  91. }
  92. cleanTables(t, "bookings", "places", "users")
  93. user := insertTestUser(t)
  94. rate := 500
  95. place := insertTestPlace(t, user.ID, &rate)
  96. repo := NewBookingRepo(testPool)
  97. start := time.Now().Truncate(time.Hour).Add(5 * time.Hour)
  98. end := start.Add(2 * time.Hour)
  99. // First booking succeeds
  100. b1 := &models.Booking{
  101. PlaceID: place.ID,
  102. UserID: user.ID,
  103. StartTime: start,
  104. EndTime: end,
  105. }
  106. if err := repo.Create(context.Background(), b1); err != nil {
  107. t.Fatalf("first booking: %v", err)
  108. }
  109. // Overlapping booking fails with 23P01 (exclusion constraint)
  110. b2 := &models.Booking{
  111. PlaceID: place.ID,
  112. UserID: user.ID,
  113. StartTime: start.Add(30 * time.Minute),
  114. EndTime: end.Add(30 * time.Minute),
  115. }
  116. err := repo.Create(context.Background(), b2)
  117. if err == nil {
  118. t.Fatal("expected error for overlapping booking, got nil")
  119. }
  120. var pgErr *pgconn.PgError
  121. if !errors.As(err, &pgErr) || pgErr.Code != "23P01" {
  122. t.Errorf("expected 23P01 exclusion violation, got: %v", err)
  123. }
  124. }
  125. func TestBookingRepo_Create_SameSlotDifferentPlace(t *testing.T) {
  126. if testPool == nil {
  127. t.Skip("DATABASE_URL_TEST not set")
  128. }
  129. cleanTables(t, "bookings", "places", "users")
  130. user := insertTestUser(t)
  131. rate := 500
  132. p1 := insertTestPlace(t, user.ID, &rate)
  133. p2 := insertTestPlace(t, user.ID, &rate)
  134. repo := NewBookingRepo(testPool)
  135. start := time.Now().Truncate(time.Hour).Add(10 * time.Hour)
  136. end := start.Add(2 * time.Hour)
  137. b1 := &models.Booking{PlaceID: p1.ID, UserID: user.ID, StartTime: start, EndTime: end}
  138. if err := repo.Create(context.Background(), b1); err != nil {
  139. t.Fatalf("first booking: %v", err)
  140. }
  141. b2 := &models.Booking{PlaceID: p2.ID, UserID: user.ID, StartTime: start, EndTime: end}
  142. if err := repo.Create(context.Background(), b2); err != nil {
  143. t.Fatalf("same slot different place: %v", err)
  144. }
  145. }