generated from VLADIMIR/template
up
This commit is contained in:
+20
-17
@@ -1,4 +1,4 @@
|
||||
package game_service
|
||||
package participation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"evening_detective_server/internal/modules/email_sender"
|
||||
"evening_detective_server/internal/repos/teams_repo"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -88,7 +89,7 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
exec(strings.Split(string(data), "-- +goose Down")[0])
|
||||
}
|
||||
repo := teams_repo.NewTeamsRepo(pool)
|
||||
service := &GameService{teamsRepo: repo, appBaseURL: "https://example.org"}
|
||||
service := NewParticipationService(repo, "https://example.org")
|
||||
state := func(id int, want string) {
|
||||
t.Helper()
|
||||
team, err := repo.GetTeamByID(ctx, id)
|
||||
@@ -98,9 +99,10 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
}
|
||||
state(1, "pending")
|
||||
state(2, "pending") // Existing teams are not changed by the migration.
|
||||
exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5;
|
||||
// Model registration output explicitly: direct SQL inserts do not auto-confirm teams.
|
||||
exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5 WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours','boundary@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours'+INTERVAL '1 microsecond','late@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email,participation_status,participation_source,participation_answered_at) SELECT 1,start_at-INTERVAL '24 hours'+INTERVAL '1 microsecond','late@example.org','confirmed','automatic',NOW() FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,captain_email) VALUES(2,'future@example.org');
|
||||
INSERT INTO teams(game_id,created_at) SELECT 1,start_at-INTERVAL '25 hours' FROM games WHERE id=1;`)
|
||||
state(3, "pending")
|
||||
@@ -156,7 +158,7 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
}
|
||||
sender.err = nil
|
||||
// Expired lease is recovered after a worker restart.
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id=3`)
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id IN (1,3)`)
|
||||
if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -171,19 +173,19 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal("email retry exceeded the eight-attempt limit")
|
||||
}
|
||||
token := participationToken("secret", 1)
|
||||
if _, err := repo.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("invalid token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("unknown token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, token); err != nil {
|
||||
if _, err := service.GetParticipation(ctx, token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state(1, "pending") // Viewing a link never answers.
|
||||
answers := make(chan error, 2)
|
||||
for _, answer := range []string{"confirmed", "cancelled"} {
|
||||
go func() { _, err := repo.AnswerParticipation(ctx, token, answer); answers <- err }()
|
||||
go func() { _, err := service.AnswerParticipation(ctx, token, answer); answers <- err }()
|
||||
}
|
||||
success, conflict := 0, 0
|
||||
for range 2 {
|
||||
@@ -199,16 +201,16 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
if success != 1 || conflict != 1 {
|
||||
t.Fatalf("concurrent answers: success=%d conflict=%d", success, conflict)
|
||||
}
|
||||
p, err := repo.GetParticipation(ctx, token)
|
||||
p, err := service.GetParticipation(ctx, token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.AnswerParticipation(ctx, token, p.Status); err != nil {
|
||||
if _, err := service.AnswerParticipation(ctx, token, p.Status); err != nil {
|
||||
t.Fatalf("same answer not idempotent: %v", err)
|
||||
}
|
||||
// Cancellation excludes teams from shared active listings and password-based gameplay.
|
||||
token3 := participationToken("secret", 3)
|
||||
if _, err := repo.AnswerParticipation(ctx, token3, "cancelled"); err != nil {
|
||||
if _, err := service.AnswerParticipation(ctx, token3, "cancelled"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active, err := repo.GetActiveTeamsByGameID(ctx, 1)
|
||||
@@ -220,13 +222,14 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal("cancelled team in active list")
|
||||
}
|
||||
}
|
||||
if _, _, _, err := service.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
gameService := game_service.NewGameService(nil, repo, nil, nil, "", nil, nil, nil, nil, nil, nil, nil, "")
|
||||
if _, _, _, err := gameService.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got story: %v", err)
|
||||
}
|
||||
if err := service.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
if err := gameService.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team added action: %v", err)
|
||||
}
|
||||
if _, err := service.GetTeamAnswers(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
if _, err := gameService.GetTeamAnswers(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got answers: %v", err)
|
||||
}
|
||||
token6 := participationToken("secret", 6)
|
||||
@@ -234,11 +237,11 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec(`UPDATE games SET status='run' WHERE id=1`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
t.Fatalf("running game accepted answer: %v", err)
|
||||
}
|
||||
exec(`UPDATE games SET status='draft',start_at=(NOW() AT TIME ZONE 'UTC')-INTERVAL '1 second' WHERE id=1`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
t.Fatalf("past scheduled start accepted answer: %v", err)
|
||||
}
|
||||
state(6, "pending")
|
||||
+14
-5
@@ -1,4 +1,4 @@
|
||||
package game_service
|
||||
package participation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -19,10 +19,19 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *GameService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
type ParticipationService struct {
|
||||
teamsRepo *teams_repo.TeamsRepo
|
||||
appBaseURL string
|
||||
}
|
||||
|
||||
func NewParticipationService(teamsRepo *teams_repo.TeamsRepo, appBaseURL string) *ParticipationService {
|
||||
return &ParticipationService{teamsRepo: teamsRepo, appBaseURL: appBaseURL}
|
||||
}
|
||||
|
||||
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.GetParticipation(ctx, token)
|
||||
}
|
||||
func (s *GameService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||
}
|
||||
|
||||
@@ -46,7 +55,7 @@ func participationEmail(p *teams_repo.Participation, baseURL, token string, loca
|
||||
return email_sender.Message{To: p.Email, Subject: "Подтвердите участие в «Вечернем детективе»", Body: body, HTML: markup}
|
||||
}
|
||||
|
||||
func (s *GameService) RunParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret, timezone string) {
|
||||
func (s *ParticipationService) RunParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret, timezone string) {
|
||||
base, err := url.Parse(s.appBaseURL)
|
||||
if err != nil || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.User != nil || base.RawQuery != "" || base.Fragment != "" || secret == "" {
|
||||
log.Print("participation email worker disabled: configure APP_BASE_URL and PARTICIPATION_SECRET")
|
||||
@@ -74,7 +83,7 @@ func (s *GameService) RunParticipationEmails(ctx context.Context, sender email_s
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameService) sendParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret string, location *time.Location) error {
|
||||
func (s *ParticipationService) sendParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret string, location *time.Location) error {
|
||||
for ctx.Err() == nil {
|
||||
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
p, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
||||
Reference in New Issue
Block a user