// Package mysqltest provides real room-service MySQL repositories for tests. package mysqltest import ( "context" "database/sql" "testing" "google.golang.org/protobuf/proto" roomeventsv1 "hyapp.local/api/proto/events/room/v1" "hyapp/internal/testutil/mysqlschema" "hyapp/pkg/appcode" "hyapp/services/room-service/internal/room/outbox" mysqlstorage "hyapp/services/room-service/internal/storage/mysql" ) // Repository wraps the production room MySQL repository with test-only read helpers. type Repository struct { *mysqlstorage.Repository t testing.TB schema *mysqlschema.Schema } // NewRepository creates an isolated room schema and opens the production repository. func NewRepository(t testing.TB) *Repository { t.Helper() schema := mysqlschema.New(t, mysqlschema.Config{ EnvVar: "ROOM_SERVICE_MYSQL_TEST_DSN", InitDBPath: initDBPath(t), DatabasePrefix: "hyapp_room_test", }) repository, err := mysqlstorage.Open(context.Background(), schema.DSN, 4, 4) if err != nil { t.Fatalf("open room mysql repository failed: %v", err) } wrapped := &Repository{Repository: repository, t: t, schema: schema} t.Cleanup(wrapped.Close) return wrapped } // Close shuts down the repository; mysqlschema owns schema cleanup. func (r *Repository) Close() { r.t.Helper() if r.Repository != nil { _ = r.Repository.Close() } } // SetRoomStatus simulates an external admin status update against room-service tables. func (r *Repository) SetRoomStatus(roomID string, status string) { r.t.Helper() ctx := context.Background() if _, err := r.schema.DB.ExecContext(ctx, `UPDATE rooms SET status = ? WHERE app_code = ? AND room_id = ?`, status, appcode.Default, roomID); err != nil { r.t.Fatalf("update room status failed: %v", err) } if _, err := r.schema.DB.ExecContext(ctx, `UPDATE room_list_entries SET status = ? WHERE app_code = ? AND room_id = ?`, status, appcode.Default, roomID); err != nil { r.t.Fatalf("update room list status failed: %v", err) } } // DeleteRoomListEntry removes the discovery projection while keeping rooms metadata. func (r *Repository) DeleteRoomListEntry(roomID string) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM room_list_entries WHERE app_code = ? AND room_id = ?`, appcode.Default, roomID); err != nil { r.t.Fatalf("delete room list entry failed: %v", err) } } // SetRoomSortScore rewrites discovery sort_score for deterministic list-order tests. func (r *Repository) SetRoomSortScore(roomID string, sortScore int64) { r.t.Helper() r.requireRoomListEntry(roomID) if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET sort_score = ? WHERE app_code = ? AND room_id = ?`, sortScore, appcode.Default, roomID); err != nil { r.t.Fatalf("update room sort score failed: %v", err) } } // SetRoomPresence rewrites discovery presence counters for deterministic hot bucket tests. func (r *Repository) SetRoomPresence(roomID string, onlineCount int32, occupiedSeatCount int32) { r.t.Helper() r.requireRoomListEntry(roomID) if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET online_count = ?, occupied_seat_count = ? WHERE app_code = ? AND room_id = ?`, onlineCount, occupiedSeatCount, appcode.Default, roomID); err != nil { r.t.Fatalf("update room presence failed: %v", err) } } func (r *Repository) requireRoomListEntry(roomID string) { r.t.Helper() var exists int if err := r.schema.DB.QueryRowContext(context.Background(), `SELECT 1 FROM room_list_entries WHERE app_code = ? AND room_id = ? LIMIT 1`, appcode.Default, roomID).Scan(&exists); err != nil { r.t.Fatalf("room list entry %s is missing: %v", roomID, err) } } // PinRoom inserts an active regional pin exactly as the admin backend writes it. func (r *Repository) PinRoom(roomID string, regionID int64, weight int64, expiresAtMS int64) { r.PinRoomWithType(roomID, "region", regionID, weight, expiresAtMS) } // PinRoomWithType inserts an active pin with the same scope fields used by admin writes. func (r *Repository) PinRoomWithType(roomID string, pinType string, regionID int64, weight int64, expiresAtMS int64) { r.t.Helper() nowMS := int64(1000) if _, err := r.schema.DB.ExecContext(context.Background(), ` INSERT INTO room_region_pins ( app_code, visible_region_id, pin_type, room_id, weight, status, pinned_at_ms, expires_at_ms, cancelled_at_ms, created_by_admin_id, cancelled_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, 0, 1, 0, ?, ?) ON DUPLICATE KEY UPDATE weight = VALUES(weight), status = VALUES(status), pin_type = VALUES(pin_type), pinned_at_ms = VALUES(pinned_at_ms), expires_at_ms = VALUES(expires_at_ms), cancelled_at_ms = 0, updated_at_ms = VALUES(updated_at_ms) `, appcode.Default, regionID, pinType, roomID, weight, nowMS, expiresAtMS, nowMS, nowMS); err != nil { r.t.Fatalf("pin room failed: %v", err) } } // SetRoomCreatedAt rewrites discovery created_at_ms for deterministic new-tab order tests. func (r *Repository) SetRoomCreatedAt(roomID string, createdAtMS int64) { r.t.Helper() if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET created_at_ms = ? WHERE app_code = ? AND room_id = ?`, createdAtMS, appcode.Default, roomID); err != nil { r.t.Fatalf("update room created_at_ms failed: %v", err) } } // OutboxRecord reads one outbox row, including delivered rows that normal scans skip. func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) { r.t.Helper() row := r.schema.DB.QueryRowContext(context.Background(), ` SELECT app_code, event_id, event_type, room_id, status, worker_id, lock_until_ms, envelope, created_at_ms, retry_count, next_retry_at_ms, COALESCE(last_error, '') FROM room_outbox WHERE app_code = ? AND event_id = ? `, appcode.Default, eventID) var envelopeBytes []byte var record outbox.Record var lockUntil sql.Null[int64] if err := row.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.NextRetryAtMS, &record.LastError); err != nil { if err == sql.ErrNoRows { return outbox.Record{}, false } r.t.Fatalf("query room outbox record failed: %v", err) } if lockUntil.Valid { record.LockUntilMS = lockUntil.V } var envelope roomeventsv1.EventEnvelope if err := proto.Unmarshal(envelopeBytes, &envelope); err != nil { r.t.Fatalf("decode room outbox envelope failed: %v", err) } record.Envelope = &envelope return record, true } func initDBPath(t testing.TB) string { t.Helper() return mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_room_service.sql", ) }