Updatigng rcurly testings

This commit is contained in:
Seth Call 2026-03-02 21:03:43 -06:00
parent 4eab4ce75e
commit 2dbf194314
17 changed files with 595 additions and 104 deletions

3
.gitignore vendored
View File

@ -12,4 +12,7 @@ ruby/.rails5-gems
web/.rails5-gems
websocket-gateway/.rails5-gems
.pg_data/
# playwright
test-results
web/playwright-report

View File

@ -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

View File

@ -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`.

142
docs/dev/subscriptions.md Normal file
View File

@ -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`.

View File

@ -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

View File

@ -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 users 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.

View File

@ -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)

View File

@ -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}}

View File

@ -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>

View File

@ -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/

View File

@ -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'}

2
web/bin/test-playwright Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
BROWSER=chromium npx playwright test -c spec/playwright/playwright.config.js

View File

@ -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

View File

@ -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([]);
});
});

View File

@ -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([]);
});
});

View File

@ -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