add registration

This commit is contained in:
2026-09-13 01:42:22 +07:00
parent 0723644d3f
commit c12f81a7c4
20 changed files with 517 additions and 33 deletions
+7 -5
View File
@@ -1,9 +1,11 @@
package repos
type Team struct {
ID int
Name string
Status string
Password string
GameId int
CaptainEmail string
ParticipantsCount int32
ID int
Name string
Status string
Password string
GameId int
}
@@ -0,0 +1,114 @@
package teams_repo
import (
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestRegistrationPostgres(t *testing.T) {
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set TEST_DATABASE_URL to run PostgreSQL integration check")
}
ctx := context.Background()
admin, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer admin.Close()
schema := fmt.Sprintf("registration_test_%d", time.Now().UnixNano())
quoted := pgx.Identifier{schema}.Sanitize()
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil {
t.Fatal(err)
}
defer func() {
if _, err := admin.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil {
t.Error(err)
}
}()
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
t.Fatal(err)
}
config.ConnConfig.RuntimeParams["search_path"] = schema
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
t.Fatal(err)
}
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,
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 {
t.Fatal(err)
}
migration, err := os.ReadFile("../../../migrations/20260913130000_add_team_registration.sql")
if err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, strings.Split(string(migration), "-- +goose Down")[0]); err != nil {
t.Fatal(err)
}
repo := NewTeamsRepo(pool)
type result struct {
id int
err error
}
results := make(chan result, 8)
for range 8 {
go func() {
id, err := repo.RegisterTeam(ctx, 1, "delivery", "Team", "captain@example.org", 5, "abcdefgh")
results <- result{id, err}
}()
}
var id int
for range 8 {
got := <-results
if got.err != nil {
t.Fatal(got.err)
}
if id != 0 && got.id != id {
t.Fatal("concurrent delivery created a duplicate")
}
id = got.id
}
team, err := repo.GetTeamByID(ctx, id)
if err != nil {
t.Fatal(err)
}
if team.Name != "Team" || team.CaptainEmail != "captain@example.org" || team.ParticipantsCount != 5 || team.Password != "abcdefgh" {
t.Fatalf("incorrect team: %+v", team)
}
var noUser bool
if err := pool.QueryRow(ctx, "SELECT creator_id IS NULL FROM teams WHERE id=$1", id).Scan(&noUser); err != nil || !noUser {
t.Fatalf("captain has a user: %v", err)
}
teams, err := repo.GetTeamsByGameID(ctx, 1)
if err != nil || len(teams) != 1 || teams[0].CaptainEmail != team.CaptainEmail {
t.Fatalf("teams=%v err=%v", teams, err)
}
if _, err := pool.Exec(ctx, "UPDATE games SET status='run' WHERE id=1"); err != nil {
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)
}
if _, err := repo.RegisterTeam(ctx, 1, "new", "Team", "captain@example.org", 5, "abcdefgh"); !errors.Is(err, ErrRegistrationClosed) {
t.Fatalf("registration after start: %v", err)
}
if _, err := repo.RegisterTeam(ctx, 999, "new", "Team", "captain@example.org", 5, "abcdefgh"); !errors.Is(err, ErrRegistrationClosed) {
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)
}
}
+33
View File
@@ -30,6 +30,8 @@ func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error
`SELECT
id,
name,
captain_email,
participants_count,
status,
password,
game_id
@@ -40,6 +42,8 @@ func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error
err := row.Scan(
&team.ID,
&team.Name,
&team.CaptainEmail,
&team.ParticipantsCount,
&team.Status,
&team.Password,
&team.GameId,
@@ -63,6 +67,8 @@ func (r *TeamsRepo) GetTeamsByGameID(
`SELECT
id,
name,
captain_email,
participants_count,
status,
password
FROM teams
@@ -81,6 +87,8 @@ func (r *TeamsRepo) GetTeamsByGameID(
err := rows.Scan(
&team.ID,
&team.Name,
&team.CaptainEmail,
&team.ParticipantsCount,
&team.Status,
&team.Password,
)
@@ -155,3 +163,28 @@ func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamId int) error {
}
return nil
}
var ErrRegistrationClosed = errors.New("Игра не найдена или регистрация уже закрыта")
// RegisterTeam сохраняет одну команду на доставку, включая одновременные повторы.
func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string) (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)
if err == nil {
return id, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return 0, err
}
err = r.pool.QueryRow(ctx, `
INSERT INTO teams (game_id, registration_delivery_id, name, captain_email, participants_count, password)
SELECT id, $2, $3, $4, $5, $6 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).Scan(&id)
if errors.Is(err, pgx.ErrNoRows) {
return 0, ErrRegistrationClosed
}
return id, err
}