Limita una diretta attiva per account e valorizza la copertina Premium Full.

Blocca la seconda diretta sullo stesso utente, registra gli abusi in admin e presenta la copertina come grafica caricata dalla società, con demo Team MLTV.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 12:52:26 +02:00
co-authored by Cursor
parent 356aa28fad
commit d52434fb2e
62 changed files with 1509 additions and 109 deletions
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe MatchLiveTv::Demo do
it "usa Team MLTV come squadra demo ufficiale" do
expect(described_class.home_team).to eq("Team MLTV")
expect(described_class.category).to eq("U15 MASCHILE")
expect(described_class.venue).to eq("Pala MLTV")
end
it "localizza l'avversario senza nomi di società reali" do
I18n.with_locale(:it) do
expect(described_class.away_team).to eq("Squadra ospite")
expect(described_class.match_title).to eq("Team MLTV vs Squadra ospite")
end
I18n.with_locale(:en) do
expect(described_class.away_team).to eq("Team Guest")
expect(described_class.match_title).to eq("Team MLTV vs Team Guest")
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "Admin stream concurrency violations", type: :request do
let!(:admin) { AdminAccount.create!(username: "ops-abuse", password: "Password123") }
let!(:user) { User.create!(email: "abuse@test.it", name: "Abuser", password: "Password123", role: "coach") }
let!(:club) { Club.create!(name: "Abuse Club", sport: "volleyball") }
let!(:team) { club.teams.create!(name: "U16", sport: "volleyball") }
let!(:match_a) { team.matches.create!(opponent_name: "Rival A", sport: "volleyball") }
let!(:match_b) { team.matches.create!(opponent_name: "Rival B", sport: "volleyball") }
let!(:live) do
StreamSession.create!(match: match_a, user: user, platform: "matchlivetv", status: "live")
end
let!(:idle) do
StreamSession.create!(match: match_b, user: user, platform: "matchlivetv", status: "idle")
end
let!(:violation) do
StreamConcurrencyViolation.record!(attempted: idle, occupying: live, action: "start")
end
before do
post admin_login_path, params: { username: admin.username, password: "Password123" }
end
it "mostra l'elenco abusi" do
get admin_stream_concurrency_violations_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("abuse@test.it")
expect(response.body).to include("Abuse Club")
expect(response.body).to include("Rival A")
expect(response.body).to include("Rival B")
end
it "mostra il badge in dashboard" do
get admin_root_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("abuse@test.it")
end
it "mostra i tentativi nella scheda società" do
get admin_club_path(club)
expect(response).to have_http_status(:ok)
expect(response.body).to include("abuse@test.it")
expect(response.body).to include("Rival B")
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe "API start seconda diretta stesso account", type: :request do
let!(:user) { User.create!(email: "conc@test.it", name: "C", password: "Password123", role: "coach") }
let!(:club) { Club.create!(name: "Conc Club", sport: "volleyball") }
let!(:membership) { club.club_memberships.create!(user: user, role: "owner") }
let!(:team) { club.teams.create!(name: "Tigers", sport: "volleyball") }
let!(:match_a) { team.matches.create!(opponent_name: "A", sport: "volleyball") }
let!(:match_b) { team.matches.create!(opponent_name: "B", sport: "volleyball") }
let!(:live) do
StreamSession.create!(
match: match_a,
user: user,
platform: "matchlivetv",
status: "live",
started_at: 5.minutes.ago,
device_model: "Pixel 8",
client_os: "android"
)
end
let!(:idle) do
StreamSession.create!(
match: match_b,
user: user,
platform: "matchlivetv",
status: "idle",
device_model: "iPhone 15",
client_os: "ios"
)
end
def auth_headers
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
token = response.parsed_body["access_token"]
{ "Authorization" => "Bearer #{token}", "Content-Type" => "application/json" }
end
before do
plan = Plan.find_or_initialize_by(slug: "premium_full")
plan.name ||= "Premium Full"
plan.features = (plan.features || {}).merge(
"platforms" => %w[matchlivetv],
"concurrent_streams_limit" => 10
)
plan.save!
club.create_subscription!(plan: plan, status: "active") if club.subscription.blank?
allow(SessionChannel).to receive(:broadcast_message)
end
it "restituisce 403 user_concurrent_stream e salva l'evidenza" do
patch "/api/v1/sessions/#{idle.id}/start", headers: auth_headers
expect(response).to have_http_status(:forbidden)
body = response.parsed_body
expect(body["error_code"]).to eq("user_concurrent_stream")
expect(body["error"]).to be_present
expect(StreamConcurrencyViolation.where(attempted_session_id: idle.id, occupying_session_id: live.id)).to exist
end
end
@@ -16,7 +16,17 @@ RSpec.describe "Public features page", type: :request do
expect(response.body).to include("Chi filma, filma")
expect(response.body).to include("Accesso con link")
expect(response.body).to include("La diretta resta della tua società")
expect(response.body).to include("La partita finisce. Il ricordo resta.")
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
expect(response.body).to include("Copertina caricata per ogni partita")
expect(response.body).to include("Scopri Premium Full")
expect(response.body).to include("Team MLTV")
expect(response.body).to include("Squadra ospite")
expect(response.body).to include("Copertina personalizzata")
expect(response.body).not_to include("Lupi Santa Croce")
expect(response.body).not_to include("Rossi vs Neri")
expect(response.body).not_to include("Tigers Volley")
expect(response.body).not_to include("MAIN SPONSOR")
expect(response.body).not_to include("CON IL SUPPORTO DI")
expect(response.body).to include("Inizia gratis")
expect(response.body).to include("Confronta i piani")
expect(response.body).to include(public_signup_path)
@@ -10,6 +10,10 @@ RSpec.describe "Public home page", type: :request do
expect(response.body).to include(I18n.t("home.stores_label", locale: :it))
expect(response.body).to include(I18n.t("home.store_app_store_name", locale: :it))
expect(response.body).to include(I18n.t("home.store_play_name", locale: :it))
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
expect(response.body).to include("Team MLTV")
expect(response.body).not_to include("Lupi Santa Croce")
expect(response.body).to include("Scopri Premium Full")
expect(response.body.scan(MatchLiveTv.app_store_url).size).to eq(3)
expect(response.body.scan(MatchLiveTv.play_store_url).size).to eq(3)
end
@@ -26,6 +26,14 @@ RSpec.describe "Public pricing page", type: :request do
expect(response.body).to include("Account trasmettitori")
expect(response.body).to include("Niente password condivise")
expect(response.body).to include("plan-card--featured")
expect(response.body).to include("Copertina personalizzabile")
expect(response.body).to include("con sponsor")
expect(response.body).to include("Copertina personalizzata")
expect(response.body).to include("Dai visibilità ai tuoi sponsor")
expect(response.body).to include("Team MLTV")
expect(response.body).not_to include("Lupi Santa Croce")
expect(response.body).not_to include("MAIN SPONSOR")
expect(response.body).not_to include("CON IL SUPPORTO DI")
expect(response.body).to include("Soddisfatti o rimborsati")
expect(response.body).to include("30 giorni soddisfatti o rimborsati")
expect(response.body).to include("ti rimborsiamo il 100%")
+11 -11
View File
@@ -1,12 +1,12 @@
require "rails_helper"
RSpec.describe "Public team page", type: :request do
let(:club) { Club.create!(name: "ASD Eagles", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let(:club) { Club.create!(name: "Team MLTV", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let!(:team) do
club.teams.create!(
name: "Tigers Volley U17",
name: "Team MLTV U15",
sport_key: "pallavolo",
description: "Squadra giovanile under 17.",
description: "Squadra giovanile under 15.",
roster_public: true
)
end
@@ -15,9 +15,9 @@ RSpec.describe "Public team page", type: :request do
get public_team_page_path(team.slug)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Tigers Volley U17")
expect(response.body).to include("ASD Eagles")
expect(response.body).to include("Squadra giovanile under 17")
expect(response.body).to include("Team MLTV U15")
expect(response.body).to include("Team MLTV")
expect(response.body).to include("Squadra giovanile under 15")
end
it "restituisce 404 per slug inesistente" do
@@ -41,13 +41,13 @@ RSpec.describe "Public team page", type: :request do
end
RSpec.describe "Public team directory", type: :request do
let(:club) { Club.create!(name: "ASD Eagles", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let(:club) { Club.create!(name: "Team MLTV", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
let(:user) { User.create!(email: "dir2@test.com", name: "Coach", password: "Password123", role: "coach") }
let!(:team) { club.teams.create!(name: "Tigers Volley U17", sport_key: "pallavolo") }
let!(:team) { club.teams.create!(name: "Team MLTV U15", sport_key: "pallavolo") }
before do
club.club_memberships.create!(user: user, role: "owner")
match = team.matches.create!(opponent_name: "Rival", sport_key: "pallavolo", scheduled_at: 1.day.from_now)
match = team.matches.create!(opponent_name: MatchLiveTv::Demo::AWAY_TEAM, sport_key: "pallavolo", scheduled_at: 1.day.from_now)
StreamSession.create!(
match: match, user: user, status: "live", platform: "matchlivetv",
privacy_status: "public", publish_token: "tok-dir", started_at: Time.current
@@ -58,13 +58,13 @@ RSpec.describe "Public team directory", type: :request do
get public_team_pages_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("Tigers Volley U17")
expect(response.body).to include("Team MLTV U15")
expect(response.body).to include("In diretta")
end
it "filtra per sport via query string" do
get public_team_pages_path(sport: "basket")
expect(response.body).not_to include("Tigers Volley U17")
expect(response.body).not_to include("Team MLTV U15")
end
end
@@ -0,0 +1,99 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe Sessions::Start, "one live per user" do
let(:user) { User.create!(email: "tx@test.it", name: "Tx", password: "Password123", role: "coach") }
let(:club) { Club.create!(name: "Club A", sport: "volleyball") }
let(:team) { club.teams.create!(name: "Team A", sport: "volleyball") }
let(:match_a) { team.matches.create!(opponent_name: "Opp A", sport: "volleyball") }
let(:match_b) { team.matches.create!(opponent_name: "Opp B", sport: "volleyball") }
before do
club.club_memberships.create!(user: user, role: "owner")
plan = Plan.find_or_initialize_by(slug: "premium_full")
plan.name ||= "Premium Full"
plan.features = (plan.features || {}).merge(
"platforms" => %w[matchlivetv youtube],
"concurrent_streams_limit" => 10
)
plan.save!
club.create_subscription!(plan: plan, status: "active") if club.subscription.blank?
allow(SessionChannel).to receive(:broadcast_message)
end
def session_for(match, status:, device_model: "Pixel 8")
StreamSession.create!(
match: match,
user: user,
platform: "matchlivetv",
status: status,
device_manufacturer: "Google",
device_model: device_model,
client_os: "android"
)
end
it "avvia la prima diretta" do
session = session_for(match_a, status: "idle")
described_class.new(session).call
expect(session.reload.status).to eq("connecting")
expect(StreamConcurrencyViolation.count).to eq(0)
end
it "blocca la seconda diretta dello stesso account e registra l'abuso" do
live = session_for(match_a, status: "live", device_model: "Pixel 8")
live.update!(started_at: 10.minutes.ago)
second = session_for(match_b, status: "idle", device_model: "iPhone 15")
second.update!(client_os: "ios", device_manufacturer: "Apple")
expect {
described_class.new(second).call
}.to raise_error(Teams::EntitlementError) { |e|
expect(e.code).to eq("user_concurrent_stream")
}
expect(second.reload.status).to eq("idle")
violation = StreamConcurrencyViolation.last
expect(violation).to be_present
expect(violation.user_email).to eq(user.email)
expect(violation.occupying_session_id).to eq(live.id)
expect(violation.attempted_session_id).to eq(second.id)
expect(violation.devices_differ).to be(true)
expect(violation.attempt_action).to eq("start")
end
it "non blocca un secondo account sulla stessa società" do
other = User.create!(email: "tx2@test.it", name: "Tx2", password: "Password123", role: "coach")
club.club_memberships.create!(user: other, role: "owner")
session_for(match_a, status: "live")
other_session = StreamSession.create!(
match: match_b,
user: other,
platform: "matchlivetv",
status: "idle"
)
described_class.new(other_session).call
expect(other_session.reload.status).to eq("connecting")
end
it "considera occupante anche una sessione in pausa" do
session_for(match_a, status: "paused")
second = session_for(match_b, status: "idle")
expect {
described_class.new(second).call
}.to raise_error(Teams::EntitlementError) { |e|
expect(e.code).to eq("user_concurrent_stream")
}
end
it "non considera occupante una sessione idle" do
session_for(match_a, status: "idle")
second = session_for(match_b, status: "idle")
described_class.new(second).call
expect(second.reload.status).to eq("connecting")
end
end
@@ -4,13 +4,13 @@ RSpec.describe Teams::GenerateSlug do
let(:club) { Club.create!(name: "Test Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
it "genera uno slug dal nome" do
team = club.teams.build(name: "Crazy Volley U17B", sport_key: "pallavolo")
expect(described_class.call(team)).to eq("crazy-volley-u17b")
team = club.teams.build(name: "Team MLTV U15", sport_key: "pallavolo")
expect(described_class.call(team)).to eq("team-mltv-u15")
end
it "aggiunge un suffisso se lo slug esiste già" do
club.teams.create!(name: "Tigers", sport_key: "pallavolo")
team = club.teams.build(name: "Tigers", sport_key: "pallavolo")
expect(described_class.call(team)).to eq("tigers-2")
club.teams.create!(name: "Team MLTV", sport_key: "pallavolo")
team = club.teams.build(name: "Team MLTV", sport_key: "pallavolo")
expect(described_class.call(team)).to eq("team-mltv-2")
end
end