generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package teams_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTeamOperationsRejectInvalidIDs(t *testing.T) {
|
||||
for _, id := range []string{"", "12", "invalid", "00000000-0000-0000-0000-000000000000", "019A1234-5678-7ABC-8DEF-000000000001", "019a123456787abc8def000000000001"} {
|
||||
t.Run(id, func(t *testing.T) {
|
||||
repo := NewTeamsRepo(nil)
|
||||
ctx := context.Background()
|
||||
_, err := repo.GetTeamByID(ctx, id)
|
||||
for _, result := range []error{err, repo.UpdateTeam(ctx, id, "Team"), repo.DeleteTeam(ctx, id)} {
|
||||
if !errors.Is(result, ErrTeamNotFound) {
|
||||
t.Fatal(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const maxReviewReminderAttempts = 8
|
||||
|
||||
// Participation describes a team's participation state.
|
||||
type Participation struct {
|
||||
TeamID int
|
||||
TeamID string
|
||||
TeamName string
|
||||
GameName string
|
||||
StartAt time.Time
|
||||
@@ -39,7 +39,7 @@ type Participation struct {
|
||||
|
||||
// ReviewReminder describes a pending review reminder.
|
||||
type ReviewReminder struct {
|
||||
TeamID int
|
||||
TeamID string
|
||||
TeamName string
|
||||
GameID int
|
||||
GameName string
|
||||
@@ -130,7 +130,7 @@ func (r *TeamsRepo) ClaimParticipationEmail(ctx context.Context) (*Participation
|
||||
}
|
||||
|
||||
// SetParticipationToken stores a participation token.
|
||||
func (r *TeamsRepo) SetParticipationToken(ctx context.Context, id int, token string) error {
|
||||
func (r *TeamsRepo) SetParticipationToken(ctx context.Context, id string, token string) error {
|
||||
hash, err := tokenHash(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -140,7 +140,7 @@ func (r *TeamsRepo) SetParticipationToken(ctx context.Context, id int, token str
|
||||
}
|
||||
|
||||
// ConfirmParticipationByFirstAction confirms participation after the first action.
|
||||
func (r *TeamsRepo) ConfirmParticipationByFirstAction(ctx context.Context, teamID int) error {
|
||||
func (r *TeamsRepo) ConfirmParticipationByFirstAction(ctx context.Context, teamID string) error {
|
||||
_, err := r.pool.Exec(ctx, `UPDATE teams SET participation_status='confirmed',
|
||||
participation_source='first_action', participation_answered_at=NOW()
|
||||
WHERE id=$1 AND participation_status='pending'`, teamID)
|
||||
@@ -148,7 +148,7 @@ func (r *TeamsRepo) ConfirmParticipationByFirstAction(ctx context.Context, teamI
|
||||
}
|
||||
|
||||
// FinishParticipationEmail completes a participation email job.
|
||||
func (r *TeamsRepo) FinishParticipationEmail(ctx context.Context, id, attempt int, sendErr error) error {
|
||||
func (r *TeamsRepo) FinishParticipationEmail(ctx context.Context, id string, attempt int, sendErr error) error {
|
||||
message := ""
|
||||
if sendErr != nil {
|
||||
message = sendErr.Error()
|
||||
@@ -216,7 +216,7 @@ func (r *TeamsRepo) ClaimReviewReminder(ctx context.Context) (*ReviewReminder, e
|
||||
}
|
||||
|
||||
// FinishReviewReminder completes a review reminder job.
|
||||
func (r *TeamsRepo) FinishReviewReminder(ctx context.Context, id, attempt int, sendErr error) error {
|
||||
func (r *TeamsRepo) FinishReviewReminder(ctx context.Context, id string, attempt int, sendErr error) error {
|
||||
message := ""
|
||||
if sendErr != nil {
|
||||
message = sendErr.Error()
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestRegistrationPostgres(t *testing.T) {
|
||||
}
|
||||
defer pool.Close()
|
||||
_, err = pool.Exec(ctx, `CREATE TABLE games (id SERIAL PRIMARY KEY, status TEXT DEFAULT 'draft');
|
||||
CREATE TABLE teams (id SERIAL PRIMARY KEY, name VARCHAR(255), creator_id INTEGER,
|
||||
CREATE TABLE teams (id UUID PRIMARY KEY, name VARCHAR(255), creator_id INTEGER,
|
||||
game_id INTEGER REFERENCES games(id), password TEXT, status TEXT DEFAULT 'register', created_at TIMESTAMP DEFAULT NOW());
|
||||
INSERT INTO games DEFAULT VALUES; INSERT INTO games DEFAULT VALUES;`)
|
||||
if err != nil {
|
||||
@@ -67,7 +67,7 @@ func TestRegistrationPostgres(t *testing.T) {
|
||||
}
|
||||
repo := NewTeamsRepo(pool)
|
||||
type result struct {
|
||||
id int
|
||||
id string
|
||||
err error
|
||||
}
|
||||
results := make(chan result, 8)
|
||||
@@ -77,13 +77,13 @@ func TestRegistrationPostgres(t *testing.T) {
|
||||
results <- result{id, err}
|
||||
}()
|
||||
}
|
||||
var id int
|
||||
var id string
|
||||
for range 8 {
|
||||
got := <-results
|
||||
if got.err != nil {
|
||||
t.Fatal(got.err)
|
||||
}
|
||||
if id != 0 && got.id != id {
|
||||
if id != "" && got.id != id {
|
||||
t.Fatal("concurrent delivery created a duplicate")
|
||||
}
|
||||
id = got.id
|
||||
@@ -107,7 +107,7 @@ func TestRegistrationPostgres(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := repo.RegisterTeam(ctx, 1, "delivery", "Changed", "other@example.org", 9, "different"); err != nil || got != id {
|
||||
t.Fatalf("retry after start: %d %v", got, err)
|
||||
t.Fatalf("retry after start: %s %v", got, err)
|
||||
}
|
||||
if _, err := repo.RegisterTeam(ctx, 1, "new", "Team", "captain@example.org", 5, "abcdefgh"); !errors.Is(err, ErrRegistrationClosed) {
|
||||
t.Fatalf("registration after start: %v", err)
|
||||
@@ -116,7 +116,7 @@ func TestRegistrationPostgres(t *testing.T) {
|
||||
t.Fatalf("missing game: %v", err)
|
||||
}
|
||||
if otherID, err := repo.RegisterTeam(ctx, 2, "delivery", "Team", "captain@example.org", 5, "abcdefgh"); err != nil || otherID == id {
|
||||
t.Fatalf("other game: %d %v", otherID, err)
|
||||
t.Fatalf("other game: %s %v", otherID, err)
|
||||
}
|
||||
for _, email := range []string{"", "manual@example.org"} {
|
||||
manualID, err := repo.AddTeamWithParticipation(ctx, 1, 2, "Manual", "password", email, 0, false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"evening_detective_server/internal/repos"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -27,7 +28,10 @@ func NewTeamsRepo(pool *pgxpool.Pool) *TeamsRepo {
|
||||
}
|
||||
|
||||
// GetTeamByID returns a team by ID.
|
||||
func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error) {
|
||||
func (r *TeamsRepo) GetTeamByID(ctx context.Context, id string) (*repos.Team, error) {
|
||||
if !validTeamID(id) {
|
||||
return nil, ErrTeamNotFound
|
||||
}
|
||||
team := &repos.Team{}
|
||||
row := r.pool.QueryRow(
|
||||
ctx,
|
||||
@@ -36,7 +40,10 @@ func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error
|
||||
name,
|
||||
captain_email,
|
||||
participants_count,
|
||||
participation_status, participation_source, participation_email_error, participation_sent_at IS NOT NULL,
|
||||
participation_status,
|
||||
participation_source,
|
||||
participation_email_error,
|
||||
participation_sent_at IS NOT NULL,
|
||||
status,
|
||||
password,
|
||||
game_id
|
||||
@@ -76,7 +83,10 @@ func (r *TeamsRepo) GetTeamsByGameID(
|
||||
name,
|
||||
captain_email,
|
||||
participants_count,
|
||||
participation_status, participation_source, participation_email_error, participation_sent_at IS NOT NULL,
|
||||
participation_status,
|
||||
participation_source,
|
||||
participation_email_error,
|
||||
participation_sent_at IS NOT NULL,
|
||||
status,
|
||||
password
|
||||
FROM teams
|
||||
@@ -120,22 +130,33 @@ func (r *TeamsRepo) AddTeam(
|
||||
gameID int,
|
||||
name string,
|
||||
password string,
|
||||
) (int, error) {
|
||||
) (string, error) {
|
||||
return r.AddTeamWithParticipation(ctx, creatorID, gameID, name, password, "", 0, false)
|
||||
}
|
||||
|
||||
// AddTeamWithParticipation creates a team with participation data.
|
||||
func (r *TeamsRepo) AddTeamWithParticipation(ctx context.Context, creatorID, gameID int, name, password, captainEmail string, participantsCount int32, autoConfirm bool) (int, error) {
|
||||
id := 0
|
||||
func (r *TeamsRepo) AddTeamWithParticipation(ctx context.Context, creatorID, gameID int, name, password, captainEmail string, participantsCount int32, autoConfirm bool) (string, error) {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var teamID string
|
||||
status, source := "pending", ""
|
||||
if autoConfirm {
|
||||
status, source = "confirmed", "automatic"
|
||||
}
|
||||
err := r.pool.QueryRow(
|
||||
err = r.pool.QueryRow(
|
||||
ctx,
|
||||
`INSERT INTO teams (name, creator_id, game_id, password, participation_status, participation_source, participation_answered_at, captain_email, participants_count)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CASE WHEN $5 = 'confirmed' THEN NOW() ELSE NULL END, $7, $8)
|
||||
RETURNING id`,
|
||||
`INSERT INTO teams (
|
||||
id, name, creator_id, game_id, password, participation_status,
|
||||
participation_source, participation_answered_at, captain_email, participants_count
|
||||
)
|
||||
VALUES (
|
||||
$9, $1, $2, $3, $4, $5, $6,
|
||||
CASE WHEN $5 = 'confirmed' THEN NOW() ELSE NULL END, $7, $8
|
||||
)
|
||||
RETURNING
|
||||
id`,
|
||||
name,
|
||||
creatorID,
|
||||
gameID,
|
||||
@@ -144,16 +165,20 @@ func (r *TeamsRepo) AddTeamWithParticipation(ctx context.Context, creatorID, gam
|
||||
source,
|
||||
captainEmail,
|
||||
participantsCount,
|
||||
).Scan(&id)
|
||||
id.String(),
|
||||
).Scan(&teamID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
return teamID, nil
|
||||
}
|
||||
|
||||
// UpdateTeam changes a team's name.
|
||||
func (r *TeamsRepo) UpdateTeam(ctx context.Context, teamID int, name string) error {
|
||||
func (r *TeamsRepo) UpdateTeam(ctx context.Context, teamID string, name string) error {
|
||||
if !validTeamID(teamID) {
|
||||
return ErrTeamNotFound
|
||||
}
|
||||
tag, err := r.pool.Exec(
|
||||
ctx,
|
||||
`UPDATE teams
|
||||
@@ -173,7 +198,10 @@ func (r *TeamsRepo) UpdateTeam(ctx context.Context, teamID int, name string) err
|
||||
}
|
||||
|
||||
// DeleteTeam removes a team.
|
||||
func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamID int) error {
|
||||
func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamID string) error {
|
||||
if !validTeamID(teamID) {
|
||||
return ErrTeamNotFound
|
||||
}
|
||||
tag, err := r.pool.Exec(
|
||||
ctx,
|
||||
`DELETE FROM teams
|
||||
@@ -192,32 +220,73 @@ func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamID int) error {
|
||||
// ErrRegistrationClosed indicates that team registration is unavailable.
|
||||
var ErrRegistrationClosed = errors.New("игра не найдена или регистрация уже закрыта")
|
||||
|
||||
func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string) (int, error) {
|
||||
func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string) (string, error) {
|
||||
return r.RegisterTeamWithParticipation(ctx, gameID, deliveryID, name, email, participantsCount, password, false)
|
||||
}
|
||||
|
||||
// RegisterTeamWithParticipation registers a team and its participation data.
|
||||
func (r *TeamsRepo) RegisterTeamWithParticipation(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string, autoConfirm bool) (int, error) {
|
||||
var id int
|
||||
err := r.pool.QueryRow(ctx, `SELECT id FROM teams WHERE game_id = $1 AND registration_delivery_id = $2`, gameID, deliveryID).Scan(&id)
|
||||
func (r *TeamsRepo) RegisterTeamWithParticipation(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string, autoConfirm bool) (string, error) {
|
||||
var id string
|
||||
err := r.pool.QueryRow(
|
||||
ctx,
|
||||
`SELECT
|
||||
id
|
||||
FROM teams
|
||||
WHERE game_id = $1 AND registration_delivery_id = $2`,
|
||||
gameID,
|
||||
deliveryID,
|
||||
).Scan(&id)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
newID, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
status, source := "pending", ""
|
||||
if autoConfirm {
|
||||
status, source = "confirmed", "automatic"
|
||||
}
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
INSERT INTO teams (game_id, registration_delivery_id, name, captain_email, participants_count, password, participation_status, participation_source, participation_answered_at)
|
||||
SELECT id, $2, $3, $4, $5, $6, $7, $8, CASE WHEN $7 = 'confirmed' THEN NOW() ELSE NULL END FROM games WHERE id = $1 AND status = 'draft'
|
||||
ON CONFLICT (game_id, registration_delivery_id)
|
||||
DO UPDATE SET registration_delivery_id = EXCLUDED.registration_delivery_id
|
||||
RETURNING id`, gameID, deliveryID, name, email, participantsCount, password, status, source).Scan(&id)
|
||||
err = r.pool.QueryRow(
|
||||
ctx,
|
||||
`INSERT INTO teams (
|
||||
id, game_id, registration_delivery_id, name, captain_email,
|
||||
participants_count, password, participation_status,
|
||||
participation_source, participation_answered_at
|
||||
)
|
||||
SELECT
|
||||
$9,
|
||||
id,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
CASE WHEN $7 = 'confirmed' THEN NOW() ELSE NULL END
|
||||
FROM games
|
||||
WHERE id = $1 AND status = 'draft'
|
||||
ON CONFLICT (game_id, registration_delivery_id)
|
||||
DO UPDATE
|
||||
SET registration_delivery_id = EXCLUDED.registration_delivery_id
|
||||
RETURNING
|
||||
id`,
|
||||
gameID,
|
||||
deliveryID,
|
||||
name,
|
||||
email,
|
||||
participantsCount,
|
||||
password,
|
||||
status,
|
||||
source,
|
||||
newID.String(),
|
||||
).Scan(&id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, ErrRegistrationClosed
|
||||
return "", ErrRegistrationClosed
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
@@ -232,3 +301,8 @@ func (r *TeamsRepo) GetActiveTeamsByGameID(ctx context.Context, gameID int) ([]*
|
||||
}
|
||||
return active, err
|
||||
}
|
||||
|
||||
func validTeamID(id string) bool {
|
||||
parsed, err := uuid.Parse(id)
|
||||
return err == nil && parsed != uuid.Nil && parsed.String() == id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user