Updatigng rcurly testings
This commit is contained in:
parent
4eab4ce75e
commit
2dbf194314
|
|
@ -12,4 +12,7 @@ ruby/.rails5-gems
|
|||
web/.rails5-gems
|
||||
websocket-gateway/.rails5-gems
|
||||
.pg_data/
|
||||
|
||||
# playwright
|
||||
test-results
|
||||
web/playwright-report
|
||||
|
|
|
|||
|
|
@ -43,9 +43,13 @@ JamAdmin::Application.configure do
|
|||
|
||||
config.redis_host = "localhost:6379:1" # go to another db to not cross pollute into dev/production redis dbs
|
||||
|
||||
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
|
||||
config.recurly_private_api_key = '4631527f203b41848523125b3ae51341'
|
||||
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
|
||||
config.recurly_public_api_key = 'sc-s6G2OA80Rwyvsb1RmS3mAE'
|
||||
config.recurly_subdomain = 'jamkazam-test'
|
||||
# NOTE: old jamkazam-test credentials are stale and no longer authorize.
|
||||
# config.recurly_private_api_key = '4631527f203b41848523125b3ae51341'
|
||||
# config.recurly_public_api_key = 'sc-s6G2OA80Rwyvsb1RmS3mAE'
|
||||
# config.recurly_subdomain = 'jamkazam-test'
|
||||
#
|
||||
# Re-use development sandbox credentials for test for now.
|
||||
config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb'
|
||||
config.recurly_public_api_key = 'ewr1-HciusxMNfSSjz5WlupGk0C'
|
||||
config.recurly_subdomain = 'jamkazam-development'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# Recurly Subscription Testing Progress
|
||||
|
||||
## Goal
|
||||
Document current Recurly/subscription architecture and close test gaps for subscription lifecycle behavior during Rails 8 migration.
|
||||
|
||||
## Status
|
||||
- [x] Create task tracker
|
||||
- [x] Sweep code paths for Recurly/subscription behavior (`web` + `ruby`)
|
||||
- [x] Draft architecture and current-test documentation
|
||||
- [x] Draft gap-based test plan
|
||||
- [x] Add first request specs for API subscription/payment flow
|
||||
- [x] Add and run live Recurly internet integration tests (opt-out)
|
||||
- [ ] Add ruby specs for `RecurlyClient#sync_subscription` and hourly job paths
|
||||
- [ ] Add browser-level flow coverage for payment-first vs plan-first UX
|
||||
- [ ] Update docs with final test references
|
||||
|
||||
## Notes
|
||||
- Started: 2026-03-02
|
||||
- Existing subscription-focused specs are heavily stale; key suites are currently disabled with `xdescribe`:
|
||||
- `ruby/spec/jam_ruby/recurly_client_spec.rb`
|
||||
- `ruby/spec/jam_ruby/models/user_subscriptions_spec.rb`
|
||||
- Existing active webhook coverage is present in both `ruby` model spec and `web` request spec.
|
||||
- Added and validated new request coverage:
|
||||
- `web/spec/requests/api_recurly_subscription_flow_spec.rb`
|
||||
- Verified with `cd web && bundle exec rspec spec/requests/api_recurly_subscription_flow_spec.rb` (3 examples, 0 failures).
|
||||
- Added and validated live Recurly API coverage:
|
||||
- `ruby/spec/jam_ruby/integration/recurly_live_integration_spec.rb`
|
||||
- Verified with `cd ruby && bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb` (2 examples, 0 failures).
|
||||
- Verified opt-out path with `SKIP_LIVE_RECURLY=1` marks examples pending (no failures).
|
||||
- Credential check findings:
|
||||
- `test.rb` Recurly test credentials (`jamkazam-test`) are stale (`HTTP Basic: Access denied`).
|
||||
- Working combo found in repo: key `55f2...` with subdomain `jamkazam-development`.
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
# Subscriptions and Recurly Integration
|
||||
|
||||
## Scope
|
||||
This doc summarizes how subscription and Recurly behavior currently works across `web` and `ruby`, and what is currently tested.
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
### UI/API entry points (`web`)
|
||||
Frontend calls are in `web/app/assets/javascripts/jam_rest.js`:
|
||||
- `POST /api/recurly/update_payment` (`updatePayment`)
|
||||
- `POST /api/recurly/change_subscription` (`changeSubscription`)
|
||||
- `GET /api/recurly/get_subscription` (`getSubscription`)
|
||||
- `POST /api/recurly/create_subscription` (`createSubscription`, legacy path)
|
||||
- `POST /api/recurly/cancel_subscription` (`cancelSubscription`)
|
||||
|
||||
Primary subscription UI logic:
|
||||
- `web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee`
|
||||
- Plan changes call `rest.changeSubscription(...)`.
|
||||
- `web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee`
|
||||
- Payment method updates tokenize in Recurly.js and call `rest.updatePayment({recurly_token})`.
|
||||
|
||||
### API controller (`web/app/controllers/api_recurly_controller.rb`)
|
||||
Main endpoints and behavior:
|
||||
- `change_subscription_plan`
|
||||
- Sets `desired_plan_code` via `RecurlyClient#update_desired_subscription`.
|
||||
- If plan is blank, intends free tier/cancel behavior.
|
||||
- Returns effective and desired plan status payload.
|
||||
- `update_payment`
|
||||
- Uses token to ensure/update account billing.
|
||||
- Immediately calls `RecurlyClient#handle_create_subscription(current_user, current_user.desired_plan_code, account)`.
|
||||
- This is the key bridge that makes payment-first and plan-first converge.
|
||||
- `get_subscription`
|
||||
- Returns subscription state + account billing presence + plan metadata.
|
||||
- `create_subscription`
|
||||
- Legacy direct purchase path via `Sale.purchase_subscription`.
|
||||
- `cancel_subscription`
|
||||
- Cancels tracked subscription and returns subscription json.
|
||||
|
||||
Also in this controller, Recurly-backed commerce exists for non-plan purchases:
|
||||
- `place_order` -> `Sale.place_order` (JamTracks/gift cards).
|
||||
|
||||
### Recurly client and business logic (`ruby/lib/jam_ruby/recurly_client.rb`)
|
||||
Main responsibilities:
|
||||
- Account lifecycle: `create_account`, `get_account`, `update_account`, `update_billing_info`, `find_or_create_account`.
|
||||
- Subscription lifecycle:
|
||||
- `update_desired_subscription` (records user intent, cancels on free, or calls `handle_create_subscription` for paid plan).
|
||||
- `handle_create_subscription` (creates/reactivates subscription, sets `recurly_subscription_id`, effective plan behavior, trial handling, playtime reset).
|
||||
- `create_subscription` (reactivation path first; otherwise new subscription create).
|
||||
- `find_subscription` (repairs missing local `recurly_subscription_id`, removes expired references).
|
||||
- Ongoing sync:
|
||||
- `sync_subscription(user)` reconciles local plan with trial/admin-license/account/subscription/past_due state.
|
||||
- `sync_transactions` imports successful subscription purchases for affiliate distributions and advances `GenericState.recurly_transactions_last_sync_at`.
|
||||
|
||||
### Hourly background sync
|
||||
- `ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb` executes hourly and calls:
|
||||
- `User.hourly_check`
|
||||
- `ruby/lib/jam_ruby/models/user.rb`:
|
||||
- `hourly_check` -> `subscription_sync` + `subscription_transaction_sync`
|
||||
- `subscription_sync` selects eligible users and calls `RecurlyClient#sync_subscription`.
|
||||
- `subscription_transaction_sync` calls `RecurlyClient#sync_transactions` from last sync timestamp.
|
||||
|
||||
## Other Recurly Usage
|
||||
- `ruby/lib/jam_ruby/models/sale.rb`
|
||||
- Recurly invoice/adjustment flow for JamTrack/gift card purchasing (`place_order` and related methods).
|
||||
- `purchase_subscription` legacy subscription purchase flow.
|
||||
- `ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb`
|
||||
- Parses webhook XML and stores transaction records.
|
||||
- `web/app/controllers/api_recurly_web_hook_controller.rb`
|
||||
- Receives webhook, validates type via `RecurlyTransactionWebHook.is_transaction_web_hook?`, persists via `create_from_xml`.
|
||||
|
||||
## Current Tests (Recurly/Subscription Focus)
|
||||
|
||||
### `web/spec/requests/api_recurly_web_hook_controller_spec.rb` (active)
|
||||
- `no auth`: webhook endpoint requires basic auth (401 without it).
|
||||
- `succeeds`: valid successful-payment webhook returns 200.
|
||||
- `returns 422 on error`: invalid account/user mapping raises and returns 422.
|
||||
- `returns 200 for unknown hook event`: unknown xml root is ignored and still returns 200.
|
||||
|
||||
### `web/spec/controllers/api_recurly_spec.rb` (mostly disabled/commented)
|
||||
- Contains setup for account CRUD controller tests.
|
||||
- All actual examples are commented out; effectively no active `ApiRecurlyController` coverage here.
|
||||
|
||||
### `ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb` (active)
|
||||
- `deletes jam_track_right when refunded`: asserts webhook parse path for refund events and related sale links (current expectation checks right remains present).
|
||||
- `deletes jam_track_right when voided`: same for void (current expectation checks right remains present).
|
||||
- `successful payment/refund/failed payment/void/not a transaction web hook`: validates root-name recognition in `is_transaction_web_hook?`.
|
||||
- `create_from_xml` successful payment/refund/void: verifies persisted transaction fields map from XML correctly.
|
||||
|
||||
### `ruby/spec/jam_ruby/recurly_client_spec.rb` (`xdescribe`, disabled)
|
||||
Defined (but disabled) examples:
|
||||
- `can create account`
|
||||
- `can create account with errors`
|
||||
- `with account` group:
|
||||
- `can find account`
|
||||
- `can update account`
|
||||
- `can update billing`
|
||||
- `purchases jamtrack` (empty test body)
|
||||
- `can remove account`
|
||||
- `can refund subscription` (commented block)
|
||||
|
||||
### `ruby/spec/jam_ruby/models/user_subscriptions_spec.rb` (`xdescribe`, disabled)
|
||||
Defined (but disabled) examples:
|
||||
- User sync group:
|
||||
- `empty results`
|
||||
- `user not in trial`
|
||||
- `revert admin user down`
|
||||
- Subscription transaction sync (network + mocked):
|
||||
- `fetches transactions created after GenericState.recurly_transactions_last_sync_at`
|
||||
- `creates AffiliateDistribution records for successful recurring transactions`
|
||||
- `does not create AffiliateDistribution for same transaction previously been created`
|
||||
- `does not create AffiliateDistribution records when there is no affiliate partner`
|
||||
- `does not create AffiliateDistribution if out of affiliate window`
|
||||
- `assigns correct affiliate partner`
|
||||
- `updates affiliate referral fee`
|
||||
- `change affiliate rate and updates referral fee`
|
||||
- `sets subscription product_type`
|
||||
- `sets subscription product_code`
|
||||
- `does not error out if begin_time is nil`
|
||||
- `changes GenericState.recurly_transactions_last_sync_at`
|
||||
|
||||
## Coverage Gaps Right Now
|
||||
- No active request/controller coverage for `ApiRecurlyController` subscription endpoints:
|
||||
- `update_payment`, `change_subscription_plan`, `get_subscription`, `cancel_subscription`.
|
||||
- No active executable coverage for the critical hourly sync decision tree in `RecurlyClient#sync_subscription`.
|
||||
- No active browser test asserting payment-first and plan-first flows converge to charge/start-subscription behavior.
|
||||
- No active automated coverage for first-free-month gold behavior over time progression.
|
||||
|
||||
## Live Recurly Internet Tests
|
||||
- Added active live integration spec (opt-out via env var):
|
||||
- `ruby/spec/jam_ruby/integration/recurly_live_integration_spec.rb`
|
||||
- What it verifies against real Recurly API responses:
|
||||
- `RecurlyClient#find_or_create_account` and `#get_account` create/fetch a real account and parse billing/account fields.
|
||||
- `RecurlyClient#update_desired_subscription` creates a paid subscription (`jamsubgold`) and returns parsed subscription data.
|
||||
- `RecurlyClient#payment_history` and `#invoice_history` return parsed hash arrays from live response bodies.
|
||||
- How to run:
|
||||
- default (runs live): `cd ruby && bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb`
|
||||
- opt-out: `cd ruby && SKIP_LIVE_RECURLY=1 bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb`
|
||||
- Optional credential override:
|
||||
- `RECURLY_PRIVATE_API_KEY=... RECURLY_SUBDOMAIN=...`
|
||||
- Current credential status discovered during validation:
|
||||
- Keys configured in `web/config/environments/test.rb` (`jamkazam-test`) return `HTTP Basic: Access denied`.
|
||||
- Working sandbox/development combo in-repo for live tests: key `55f2...` with subdomain `jamkazam-development`.
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
require 'spec_helper'
|
||||
require 'jam_ruby/recurly_client'
|
||||
|
||||
# Live Recurly integration checks.
|
||||
# Default: enabled.
|
||||
# Opt-out: SKIP_LIVE_RECURLY=1 bundle exec rspec spec/jam_ruby/integration/recurly_live_integration_spec.rb
|
||||
# Uses sandbox/development Recurly credentials by default.
|
||||
describe 'Recurly live integration', live_recurly: true do
|
||||
let(:client) { RecurlyClient.new }
|
||||
|
||||
let(:recurly_private_api_key) do
|
||||
ENV['RECURLY_PRIVATE_API_KEY'] || '55f2fdfa4d014e64a94eaba1e93f39bb'
|
||||
end
|
||||
|
||||
let(:recurly_subdomain) do
|
||||
ENV['RECURLY_SUBDOMAIN'] || 'jamkazam-development'
|
||||
end
|
||||
|
||||
let(:billing_info) do
|
||||
{
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
address1: '100 Test Lane',
|
||||
address2: 'Suite 1',
|
||||
city: user.city,
|
||||
state: user.state,
|
||||
country: user.country,
|
||||
zip: '78701',
|
||||
number: '4111-1111-1111-1111',
|
||||
month: '12',
|
||||
year: (Time.now.year + 2).to_s,
|
||||
verification_value: '123'
|
||||
}
|
||||
end
|
||||
|
||||
let(:user) do
|
||||
FactoryBot.create(
|
||||
:user,
|
||||
email: "recurly-live-#{SecureRandom.hex(6)}@example.com",
|
||||
subscription_trial_ends_at: 1.day.ago,
|
||||
desired_plan_code: nil,
|
||||
subscription_plan_code: nil,
|
||||
recurly_code: nil,
|
||||
recurly_subscription_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
skip('Set SKIP_LIVE_RECURLY=0 (or unset it) to run live Recurly integration tests') if ENV['SKIP_LIVE_RECURLY'] == '1'
|
||||
|
||||
Recurly.api_key = recurly_private_api_key
|
||||
Recurly.subdomain = recurly_subdomain
|
||||
end
|
||||
|
||||
after do
|
||||
begin
|
||||
if user.recurly_code.present?
|
||||
account = Recurly::Account.find(user.recurly_code)
|
||||
account.destroy if account
|
||||
end
|
||||
rescue Recurly::API::ResourceNotFound
|
||||
# already gone
|
||||
rescue Recurly::Resource::NotFound
|
||||
# already gone
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates and fetches a Recurly account via RecurlyClient' do
|
||||
account = client.find_or_create_account(user, billing_info)
|
||||
|
||||
expect(account).not_to be_nil
|
||||
expect(account.account_code).to eq(user.id)
|
||||
expect(account.billing_info).not_to be_nil
|
||||
expect(account.billing_info.last_four).to eq('1111')
|
||||
|
||||
fetched = client.get_account(user)
|
||||
expect(fetched).not_to be_nil
|
||||
expect(fetched.account_code).to eq(user.id)
|
||||
end
|
||||
|
||||
it 'creates a paid subscription and returns parsed payment/invoice history hashes' do
|
||||
account = client.find_or_create_account(user, billing_info)
|
||||
expect(account).not_to be_nil
|
||||
|
||||
plan = Recurly::Plan.find('jamsubgold')
|
||||
expect(plan.plan_code).to eq('jamsubgold')
|
||||
|
||||
result, subscription, _account = client.update_desired_subscription(user, 'jamsubgold')
|
||||
|
||||
expect(result).to eq(true)
|
||||
expect(subscription).not_to be_nil
|
||||
expect(subscription.uuid).not_to be_nil
|
||||
|
||||
payments = client.payment_history(user, limit: 5)
|
||||
expect(payments).to be_a(Array)
|
||||
unless payments.empty?
|
||||
expect(payments.first).to include(:created_at, :amount_in_cents, :status, :action, :currency)
|
||||
end
|
||||
|
||||
invoices, invoice_account = client.invoice_history(user, limit: 5)
|
||||
expect(invoice_account).not_to be_nil
|
||||
expect(invoices).to be_a(Array)
|
||||
unless invoices.empty?
|
||||
expect(invoices.first).to include(:created_at, :subtotal_in_cents, :tax_in_cents, :total_in_cents, :state, :currency)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Subscription/Recurly Test Plan (Rails 8 Branch)
|
||||
|
||||
## Objective
|
||||
Close subscription lifecycle regressions by adding executable tests in `web` and `ruby` that do not depend on live Recurly network calls.
|
||||
|
||||
## What Exists
|
||||
- Webhook coverage exists (request + model) and is active.
|
||||
- Core subscription API and sync logic coverage exists historically, but major suites are disabled (`xdescribe`).
|
||||
- Active live Recurly integration coverage now exists in (opt-out via `SKIP_LIVE_RECURLY=1`):
|
||||
- `ruby/spec/jam_ruby/integration/recurly_live_integration_spec.rb`
|
||||
|
||||
## Priority 1: API Subscription Lifecycle (`web/spec/requests`)
|
||||
Add request specs for `ApiRecurlyController` with `RecurlyClient` stubbed.
|
||||
|
||||
### 1.1 Plan-first then payment
|
||||
- `POST /api/recurly/change_subscription` with paid plan and no billing info:
|
||||
- expect desired plan is set and response indicates payment method still needed.
|
||||
- `POST /api/recurly/update_payment` with token:
|
||||
- expect `handle_create_subscription` called with user’s desired plan.
|
||||
- expect success payload includes plan metadata and `has_billing_info = true`.
|
||||
|
||||
### 1.2 Payment-first then plan
|
||||
- `POST /api/recurly/update_payment` first (no desired plan yet):
|
||||
- expect no failure and billing info persisted.
|
||||
- Then `POST /api/recurly/change_subscription` to paid plan:
|
||||
- expect desired plan update call and success response.
|
||||
|
||||
### 1.3 Negative paths
|
||||
- `change_subscription` with unchanged plan:
|
||||
- expect 422 and `No change made to plan`.
|
||||
- `update_payment` when client raises `RecurlyClientError`:
|
||||
- expect 404 + serialized error payload.
|
||||
|
||||
## Priority 2: Sync Decision Tree (`ruby/spec/jam_ruby/models`)
|
||||
Add focused unit specs for `RecurlyClient#sync_subscription` with Recurly API mocked.
|
||||
|
||||
### 2.1 Unchanged/good-standing path
|
||||
- account exists, not past_due, active subscription, desired == effective.
|
||||
- expect sync code `good_standing_unchanged` and no plan mutation.
|
||||
|
||||
### 2.2 Canceled/expired path
|
||||
- no active subscription (or expired).
|
||||
- expect effective plan set to free (`nil`) and sync code `no_subscription_or_expired`.
|
||||
|
||||
### 2.3 Past-due path
|
||||
- account `has_past_due_invoice = true`.
|
||||
- expect effective plan dropped to free and sync code `is_past_due_changed`.
|
||||
|
||||
### 2.4 Trial and free-month behavior
|
||||
- user in trial with desired gold and active account.
|
||||
- verify trial code path + post-trial behavior once time advances (use `Timecop` or `travel_to` depending what is stable in this suite).
|
||||
|
||||
## Priority 3: Hourly Job Integration (`ruby/spec/jam_ruby/resque` or model-level)
|
||||
- Validate `JamRuby::HourlyJob.perform` triggers `User.hourly_check`.
|
||||
- Validate `User.subscription_sync` only selects intended users and calls client sync.
|
||||
- Validate `User.subscription_transaction_sync` advances `GenericState.recurly_transactions_last_sync_at` using mocked transaction stream.
|
||||
|
||||
## Priority 4: Browser Coverage (`web/spec/features`)
|
||||
Add high-value feature coverage for subscription management screen:
|
||||
- plan-first flow shows payment-needed state and routes to update payment.
|
||||
- payment-first flow then selecting plan results in active paid subscription state.
|
||||
|
||||
Use stubs/fakes around `RecurlyClient` in feature env to avoid external dependency.
|
||||
|
||||
## Execution Order
|
||||
1. Request specs for `ApiRecurlyController` (fast, high signal).
|
||||
2. `RecurlyClient#sync_subscription` specs (logic-heavy, deterministic).
|
||||
3. Hourly sync integration coverage.
|
||||
4. Browser feature tests for UX flow parity.
|
||||
|
||||
## Definition of Done
|
||||
- New tests are active (not `xdescribe`/commented out).
|
||||
- Both plan-first and payment-first lifecycles are covered.
|
||||
- Hourly sync scenarios for unchanged, canceled/expired, and past-due are covered.
|
||||
- First-free-month/gold behavior has explicit time-based assertions.
|
||||
- `docs/dev/subscriptions.md` updated with links to new spec files and scenarios.
|
||||
|
|
@ -385,10 +385,11 @@
|
|||
|
||||
$.each(feeds.entries, function(i, feed) {
|
||||
if(feed.type == 'music_session') {
|
||||
feed.has_mount = !!feed['has_mount?'];
|
||||
var options = {
|
||||
feed_item: feed,
|
||||
status_class: feed['is_over?'] ? 'ended' : 'inprogress',
|
||||
mount_class: feed['has_mount?'] ? 'has-mount' : 'no-mount'
|
||||
mount_class: feed.has_mount ? 'has-mount' : 'no-mount'
|
||||
}
|
||||
var $feedItem = $(context._.template($('#template-feed-music-session').html(), options, {variable: 'data'}));
|
||||
var $controls = $feedItem.find('.session-controls');
|
||||
|
|
@ -447,11 +448,12 @@
|
|||
}
|
||||
// pump some useful data about mixing into the feed item
|
||||
feed.mix_info = recordingUtils.createMixInfo({state: feed.mix_state})
|
||||
feed.has_mix = !!feed['has_mix?'];
|
||||
|
||||
var options = {
|
||||
feed_item: feed,
|
||||
candidate_claimed_recording: obtainCandidate(feed),
|
||||
mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix',
|
||||
mix_class: feed.has_mix ? 'has-mix' : 'no-mix',
|
||||
}
|
||||
|
||||
var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'}));
|
||||
|
|
@ -674,4 +676,4 @@
|
|||
|
||||
return this;
|
||||
}
|
||||
})(window, jQuery)
|
||||
})(window, jQuery)
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@
|
|||
|
||||
// also used by musicians page
|
||||
%script{type: 'text/template', id: 'template-account-session-latency'}
|
||||
%span.latency{class: "{{data.latency_style}}", 'data-user-id' => "{{data.id}}", 'data-audio-latency' => "{{data.audio_latency || ''}}", 'data-full-score' => "{{data.full_score || ''}}", 'data-internet-score' => "{{data.internet_score || ''}}"}
|
||||
%span.latency{class: "{{data.latency_style}}", 'data-user-id' => "{{data.id}}", 'data-audio-latency' => "{{data.audio_latency}}", 'data-full-score' => "{{data.full_score}}", 'data-internet-score' => "{{data.internet_score}}"}
|
||||
{{data.latency_text}}
|
||||
%span.latency-info
|
||||
{{data.latency_info}}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
|
||||
<script type="text/template" id="template-latency">
|
||||
<tr class="mb15">
|
||||
<td class="{{data.latency_style}} latency-value" data-user-id="{{data.id}}" data-audio-latency="{{data.audio_latency || ''}}" data-full-score="{{data.full_score || ''}}" data-internet-score="{{data.internet_score || ''}}">
|
||||
<td class="{{data.latency_style}} latency-value" data-user-id="{{data.id}}" data-audio-latency="{{data.audio_latency}}" data-full-score="{{data.full_score}}" data-internet-score="{{data.internet_score}}">
|
||||
{{data.latency_text}}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -227,4 +227,3 @@
|
|||
</script>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
/ session status
|
||||
%a.left.play-button{href:'#'}
|
||||
= image_tag 'content/icon_playbutton.png', width:20, height:20, class:'play-icon'
|
||||
= "{% if(data.feed_item['has_mount?']) { %}"
|
||||
= "{% if(data.feed_item.has_mount) { %}"
|
||||
= "{% if(data.feed_item.fan_access) { %}"
|
||||
%audio{preload: 'none'}
|
||||
%source{src: '{{data.feed_item.active_music_session.mount.url}}', type: '{{data.feed_item.active_music_session.mount.mime_type}}'}
|
||||
|
|
@ -84,4 +84,4 @@
|
|||
= '{% }) %}'
|
||||
|
||||
%br{:clear => "all"}/
|
||||
%br/
|
||||
%br/
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
/ play button
|
||||
%a.left.play-button{:href => "#"}
|
||||
= image_tag 'content/icon_playbutton.png', width:20, height:20, class:'play-icon'
|
||||
= "{% if(data.feed_item['has_mix?']) { %}"
|
||||
= "{% if(data.feed_item.has_mix) { %}"
|
||||
%audio{preload: 'none'}
|
||||
%source{src: '{{data.candidate_claimed_recording.mix.mp3_url}}', type:'audio/mpeg'}
|
||||
%source{src: '{{data.candidate_claimed_recording.mix.ogg_url}}', type:'audio/ogg'}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
BROWSER=chromium npx playwright test -c spec/playwright/playwright.config.js
|
||||
|
|
@ -108,11 +108,15 @@ SampleApp::Application.configure do
|
|||
config.ftue_network_test_duration = 1
|
||||
config.ftue_network_test_max_clients = 5
|
||||
|
||||
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
|
||||
config.recurly_private_api_key = '4631527f203b41848523125b3ae51341'
|
||||
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
|
||||
config.recurly_public_api_key = 'sc-s6G2OA80Rwyvsb1RmS3mAE'
|
||||
config.recurly_subdomain = 'jamkazam-test'
|
||||
# NOTE: old jamkazam-test credentials are stale and no longer authorize.
|
||||
# config.recurly_private_api_key = '4631527f203b41848523125b3ae51341'
|
||||
# config.recurly_public_api_key = 'sc-s6G2OA80Rwyvsb1RmS3mAE'
|
||||
# config.recurly_subdomain = 'jamkazam-test'
|
||||
#
|
||||
# Re-use development sandbox credentials for test for now.
|
||||
config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb'
|
||||
config.recurly_public_api_key = 'ewr1-HciusxMNfSSjz5WlupGk0C'
|
||||
config.recurly_subdomain = 'jamkazam-development'
|
||||
config.log_to = ['file']
|
||||
config.log_level = :info # Reduce verbosity for tests
|
||||
config.logger = Logger.new(Rails.root.join('log/test.log'))
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,58 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const APP_ORIGIN = process.env.FRONTEND_URL || 'http://www.jamkazam.test:3000';
|
||||
const APP_HOST = new URL(APP_ORIGIN).hostname;
|
||||
const REMEMBER_TOKEN = process.env.REMEMBER_TOKEN || 'xAkhA3BiUZTjTM7hovMP_g';
|
||||
|
||||
test.describe('Musicians Page', () => {
|
||||
test('loads without template syntax errors', async ({ page, context }) => {
|
||||
const jsErrors = [];
|
||||
|
||||
page.on('pageerror', (err) => {
|
||||
jsErrors.push(String(err));
|
||||
});
|
||||
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'remember_token',
|
||||
value: REMEMBER_TOKEN,
|
||||
domain: APP_HOST,
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
name: 'act_as_native_client',
|
||||
value: 'true',
|
||||
domain: APP_HOST,
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await context.addInitScript(() => {
|
||||
try {
|
||||
window.localStorage.setItem('jk.webClient.webrtc', '1');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/client#/musicians', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL(/\/client#\/musicians/i, { timeout: 20000 });
|
||||
|
||||
await expect(page.locator('#musician-search-filter-results')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
const syntaxErrors = jsErrors.filter((err) =>
|
||||
/expected expression, got '&'|Uncaught SyntaxError.*expected expression.*&/i.test(err)
|
||||
);
|
||||
|
||||
await test.info().attach('musicians-page-errors.json', {
|
||||
body: JSON.stringify({ jsErrors, syntaxErrors, url: page.url() }, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
|
||||
expect(syntaxErrors, `Musicians page syntax errors: ${JSON.stringify(syntaxErrors, null, 2)}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const APP_ORIGIN = process.env.FRONTEND_URL || 'http://www.jamkazam.test:3000';
|
||||
const APP_HOST = new URL(APP_ORIGIN).hostname;
|
||||
const REMEMBER_TOKEN = process.env.REMEMBER_TOKEN || 'xAkhA3BiUZTjTM7hovMP_g';
|
||||
const USER_ID = process.env.PROFILE_USER_ID || 'bf9b20e1-a799-44da-9bc2-7e009b801ef8';
|
||||
|
||||
test.describe('Profile History Tab', () => {
|
||||
test('renders history feed without template syntax errors', async ({ page, context }) => {
|
||||
const jsErrors = [];
|
||||
|
||||
page.on('pageerror', (err) => {
|
||||
jsErrors.push(String(err));
|
||||
});
|
||||
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'remember_token',
|
||||
value: REMEMBER_TOKEN,
|
||||
domain: APP_HOST,
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
name: 'act_as_native_client',
|
||||
value: 'true',
|
||||
domain: APP_HOST,
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await context.addInitScript(() => {
|
||||
try {
|
||||
window.localStorage.setItem('jk.webClient.webrtc', '1');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`/client#/profile/${USER_ID}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL(new RegExp(`/client#/profile/${USER_ID}`, 'i'), { timeout: 20000 });
|
||||
|
||||
const historyLink = page.locator('.profile-nav a', { hasText: /history/i }).first();
|
||||
await expect(historyLink).toBeVisible({ timeout: 20000 });
|
||||
await historyLink.click();
|
||||
|
||||
await expect(page.locator('#user-profile-feed-entry-list')).toBeVisible({ timeout: 20000 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const syntaxErrors = jsErrors.filter((err) =>
|
||||
/expected expression, got '&'|Uncaught SyntaxError.*expected expression.*&/i.test(err)
|
||||
);
|
||||
|
||||
await test.info().attach('profile-history-errors.json', {
|
||||
body: JSON.stringify({ jsErrors, syntaxErrors, url: page.url() }, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
|
||||
expect(syntaxErrors, `Profile history syntax errors: ${JSON.stringify(syntaxErrors, null, 2)}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
require 'spec_helper'
|
||||
require 'jam_ruby/recurly_client'
|
||||
|
||||
describe 'ApiRecurly subscription flow', type: :request do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:client) { instance_double(RecurlyClient) }
|
||||
let(:subscription_payload) { { id: 'sub-123', state: 'active' } }
|
||||
|
||||
def sign_in_as(target_user)
|
||||
post '/sessions', params: {
|
||||
'session[email]' => target_user.email,
|
||||
'session[password]' => target_user.password
|
||||
}
|
||||
expect(response.status).to eq(302)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(RecurlyClient).to receive(:new).and_return(client)
|
||||
sign_in_as(user)
|
||||
end
|
||||
|
||||
it 'supports plan-first then payment flow' do
|
||||
account_without_billing = double('recurly_account_without_billing', has_past_due_invoice: false)
|
||||
allow(account_without_billing).to receive(:[]).with(:billing_info).and_return(nil)
|
||||
|
||||
allow(client).to receive(:update_desired_subscription) do |current_user, plan_code|
|
||||
current_user.update_attribute(:desired_plan_code, plan_code)
|
||||
[true, nil, account_without_billing]
|
||||
end
|
||||
|
||||
post '/api/recurly/change_subscription',
|
||||
params: { plan_code: 'jamsubgold' }.to_json,
|
||||
headers: { 'CONTENT_TYPE' => 'application/json' }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(JSON.parse(response.body)['desired_plan_code']).to eq('jamsubgold')
|
||||
|
||||
account_with_billing = double('recurly_account_with_billing', has_past_due_invoice: false)
|
||||
allow(account_with_billing).to receive(:[]).with(:billing_info).and_return({ last_four: '1111' })
|
||||
|
||||
allow(client).to receive(:find_or_create_account).and_return(account_with_billing)
|
||||
allow(client).to receive(:update_billing_info_from_token)
|
||||
expect(client).to receive(:handle_create_subscription) do |current_user, plan_code, account|
|
||||
expect(current_user.id).to eq(user.id)
|
||||
expect(plan_code).to eq('jamsubgold')
|
||||
expect(account).to eq(account_with_billing)
|
||||
[true, subscription_payload, account_with_billing]
|
||||
end
|
||||
|
||||
post '/api/recurly/update_payment',
|
||||
params: { recurly_token: 'tok_123' }.to_json,
|
||||
headers: { 'CONTENT_TYPE' => 'application/json' }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['subscription']).to include('id' => 'sub-123', 'state' => 'active')
|
||||
expect(body['has_billing_info']).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns 422 when no subscription plan change is made' do
|
||||
account = double('recurly_account', has_past_due_invoice: false)
|
||||
allow(account).to receive(:[]).with(:billing_info).and_return({ last_four: '1111' })
|
||||
allow(client).to receive(:update_desired_subscription).and_return([false, nil, account])
|
||||
|
||||
post '/api/recurly/change_subscription',
|
||||
params: { plan_code: 'jamsubgold' }.to_json,
|
||||
headers: { 'CONTENT_TYPE' => 'application/json' }
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(JSON.parse(response.body)['message']).to eq('No change made to plan')
|
||||
end
|
||||
|
||||
it 'returns recurly error payload when update_payment fails' do
|
||||
allow(client).to receive(:find_or_create_account).and_raise(RecurlyClientError.new('token rejected'))
|
||||
|
||||
post '/api/recurly/update_payment',
|
||||
params: { recurly_token: 'bad_token' }.to_json,
|
||||
headers: { 'CONTENT_TYPE' => 'application/json' }
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
expect(JSON.parse(response.body)['errors']).to include('message' => 'token rejected')
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue