curzon_uk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +47 -0
- data/.github/workflows/release.yml +73 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +11 -0
- data/COMM-LICENSE +22 -0
- data/Gemfile +3 -0
- data/LICENSE +617 -0
- data/README.md +164 -0
- data/Rakefile +38 -0
- data/curzon_uk.gemspec +31 -0
- data/lib/curzon_uk/cinema.rb +109 -0
- data/lib/curzon_uk/internal/api_response.rb +35 -0
- data/lib/curzon_uk/internal/guest_token.rb +27 -0
- data/lib/curzon_uk/internal/parser/api/showtime.rb +62 -0
- data/lib/curzon_uk/internal/title_sanitizer.rb +32 -0
- data/lib/curzon_uk/performance.rb +82 -0
- data/lib/curzon_uk/version.rb +6 -0
- data/lib/curzon_uk.rb +30 -0
- data/test/fixtures/api/showtimes-SOH1.json +58 -0
- data/test/fixtures/api/sites.json +40 -0
- data/test/lib/curzon_uk/cinema_test.rb +84 -0
- data/test/lib/curzon_uk/internal/api_response_test.rb +52 -0
- data/test/lib/curzon_uk/internal/fixture_shape_test.rb +14 -0
- data/test/lib/curzon_uk/internal/guest_token_test.rb +91 -0
- data/test/lib/curzon_uk/internal/parser/api/showtime_test.rb +59 -0
- data/test/lib/curzon_uk/internal/title_sanitizer_test.rb +31 -0
- data/test/lib/curzon_uk/performance_test.rb +106 -0
- data/test/lib/curzon_uk/version_test.rb +7 -0
- data/test/live/api_shape_test.rb +19 -0
- data/test/live/integration_test.rb +27 -0
- data/test/live/live_helper.rb +11 -0
- data/test/package_test.rb +56 -0
- data/test/support/api_fixtures_helper.rb +17 -0
- data/test/support/api_shapes.rb +48 -0
- data/test/support/fake_api_response.rb +20 -0
- data/test/test_helper.rb +20 -0
- metadata +208 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sites": [
|
|
3
|
+
{
|
|
4
|
+
"id": "CAN2",
|
|
5
|
+
"name": { "text": "Canterbury" },
|
|
6
|
+
"contactDetails": {
|
|
7
|
+
"addressV2": {
|
|
8
|
+
"line1": { "text": "Unit 9," },
|
|
9
|
+
"line2": { "text": "The Example Riverside," },
|
|
10
|
+
"line3": { "text": "Canterbury, CT1 0ZZ" },
|
|
11
|
+
"line4": null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "SOH1",
|
|
17
|
+
"name": { "text": "Soho" },
|
|
18
|
+
"contactDetails": {
|
|
19
|
+
"addressV2": {
|
|
20
|
+
"line1": { "text": "99 Example Avenue," },
|
|
21
|
+
"line2": { "text": "London," },
|
|
22
|
+
"line3": { "text": "W1D 0ZZ" },
|
|
23
|
+
"line4": null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "VIC1",
|
|
29
|
+
"name": { "text": "Victoria" },
|
|
30
|
+
"contactDetails": {
|
|
31
|
+
"addressV2": {
|
|
32
|
+
"line1": { "text": "58 Example Street," },
|
|
33
|
+
"line2": { "text": "London," },
|
|
34
|
+
"line3": { "text": "SW1E 0ZZ " },
|
|
35
|
+
"line4": null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require_relative '../../test_helper'
|
|
2
|
+
|
|
3
|
+
class CurzonUk::CinemaTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@api_response = FakeApiResponse.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_new_upcases_id
|
|
9
|
+
assert_equal 'SOH1', CurzonUk::Cinema.new('soh1').id
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_all_returns_cinemas
|
|
13
|
+
with_fake_api do
|
|
14
|
+
cinemas = CurzonUk::Cinema.all
|
|
15
|
+
|
|
16
|
+
assert(cinemas.all? { |cinema| cinema.instance_of?(CurzonUk::Cinema) })
|
|
17
|
+
assert_equal %w(CAN2 SOH1 VIC1), cinemas.map(&:id)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_adr_strips_line_commas
|
|
22
|
+
expected = {
|
|
23
|
+
street_address: '99 Example Avenue',
|
|
24
|
+
extended_address: nil,
|
|
25
|
+
locality: 'London',
|
|
26
|
+
region: nil,
|
|
27
|
+
postal_code: 'W1D 0ZZ',
|
|
28
|
+
country_name: 'United Kingdom'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
with_fake_api do
|
|
32
|
+
assert_equal expected, CurzonUk::Cinema.new('SOH1').adr
|
|
33
|
+
assert_equal expected, CurzonUk::Cinema.new('SOH1').address
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_adr_with_town_and_postcode_on_one_line
|
|
38
|
+
with_fake_api do
|
|
39
|
+
adr = CurzonUk::Cinema.new('CAN2').adr
|
|
40
|
+
|
|
41
|
+
assert_equal 'Unit 9', adr[:street_address]
|
|
42
|
+
assert_equal 'The Example Riverside', adr[:extended_address]
|
|
43
|
+
assert_equal 'Canterbury', adr[:locality]
|
|
44
|
+
assert_equal 'CT1 0ZZ', adr[:postal_code]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_adr_trims_postcode
|
|
49
|
+
with_fake_api do
|
|
50
|
+
assert_equal 'SW1E 0ZZ', CurzonUk::Cinema.new('VIC1').postal_code
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_brand_and_full_name
|
|
55
|
+
with_fake_api do
|
|
56
|
+
assert_equal 'Curzon', CurzonUk::Cinema.new('SOH1').brand
|
|
57
|
+
assert_equal 'Curzon Soho', CurzonUk::Cinema.new('SOH1').full_name
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_name
|
|
62
|
+
with_fake_api do
|
|
63
|
+
assert_equal 'Soho', CurzonUk::Cinema.new('SOH1').name
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_slug
|
|
68
|
+
with_fake_api do
|
|
69
|
+
assert_equal 'curzon-soho', CurzonUk::Cinema.new('SOH1').slug
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_url
|
|
74
|
+
with_fake_api do
|
|
75
|
+
assert_equal 'https://www.curzon.com/venues/soho/', CurzonUk::Cinema.new('SOH1').url
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def with_fake_api(&block)
|
|
82
|
+
CurzonUk::Internal::ApiResponse.stub(:new, @api_response, &block)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class CurzonUk::Internal::ApiResponseTest < Minitest::Test
|
|
4
|
+
include ApiFixturesHelper
|
|
5
|
+
|
|
6
|
+
BASE_URL = 'https://digital-api.curzon.com/ocapi/v1'.freeze
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
CurzonUk.token = 'guest-jwt'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def teardown
|
|
13
|
+
CurzonUk.token = nil
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_sites_sends_token_without_region
|
|
18
|
+
stub_request(:get, "#{BASE_URL}/sites")
|
|
19
|
+
.with { |request| request.headers['Authorization'] == 'Bearer guest-jwt' && !request.headers.key?('Connect-Region-Code') }
|
|
20
|
+
.to_return(status: 200, body: sites_json)
|
|
21
|
+
|
|
22
|
+
assert_equal %w(CAN2 SOH1 VIC1), api.sites.map { |site| site['id'] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_showtimes
|
|
26
|
+
stub_request(:get, "#{BASE_URL}/showtimes/by-business-date/2026-10-24")
|
|
27
|
+
.with(query: { 'siteIds' => 'SOH1' })
|
|
28
|
+
.to_return(status: 200, body: showtimes_json('SOH1'))
|
|
29
|
+
|
|
30
|
+
showtimes = api.showtimes('SOH1', Date.new(2026, 10, 24))
|
|
31
|
+
|
|
32
|
+
assert_equal %w(businessDate relatedData showtimes), showtimes.keys.sort
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_missing_token_raises_before_request
|
|
36
|
+
CurzonUk.token = nil
|
|
37
|
+
|
|
38
|
+
assert_raises(CurzonUk::AuthenticationError) { api.sites }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_rejected_token_raises
|
|
42
|
+
stub_request(:get, "#{BASE_URL}/sites").to_return(status: 401)
|
|
43
|
+
|
|
44
|
+
assert_raises(CurzonUk::AuthenticationError) { api.sites }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def api
|
|
50
|
+
CurzonUk::Internal::ApiResponse.new
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class FixtureShapeTest < Minitest::Test
|
|
4
|
+
include ApiFixturesHelper
|
|
5
|
+
include ApiShapes
|
|
6
|
+
|
|
7
|
+
def test_sites_fixture_matches_shape
|
|
8
|
+
assert_sites_shape(JSON.parse(sites_json))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_showtimes_fixture_matches_shape
|
|
12
|
+
assert_showtimes_shape(JSON.parse(showtimes_json('SOH1')))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
class CurzonUk::Internal::GuestTokenTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@directory = Dir.mktmpdir('curzon-guest-token')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def teardown
|
|
11
|
+
FileUtils.remove_entry(@directory)
|
|
12
|
+
CurzonUk.token = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_fetch_returns_token_from_page_data
|
|
16
|
+
token = token_expiring_at(Time.now + 3600)
|
|
17
|
+
curl = fake_curl("#{page_with(token)}\n200")
|
|
18
|
+
|
|
19
|
+
assert_equal token, guest_token(curl).fetch
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_fetch_accepts_the_not_found_page
|
|
23
|
+
token = token_expiring_at(Time.now + 3600)
|
|
24
|
+
curl = fake_curl("#{page_with(token)}\n404")
|
|
25
|
+
|
|
26
|
+
assert_equal token, guest_token(curl).fetch
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_fetch_rejects_expired_token
|
|
30
|
+
curl = fake_curl("#{page_with(token_expiring_at(Time.now - 60))}\n200")
|
|
31
|
+
|
|
32
|
+
assert_raises(CurzonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_fetch_rejects_page_without_page_data
|
|
36
|
+
curl = fake_curl("<html></html>\n200")
|
|
37
|
+
|
|
38
|
+
assert_raises(CurzonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_fetch_rejects_redirect_to_queue
|
|
42
|
+
curl = fake_curl("\n302")
|
|
43
|
+
|
|
44
|
+
assert_raises(CurzonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_fetch_rejects_failed_command
|
|
48
|
+
curl = fake_curl('', exit_status: 7)
|
|
49
|
+
|
|
50
|
+
assert_raises(CurzonUk::AuthenticationError) { guest_token(curl).fetch }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_fetch_explains_missing_executable
|
|
54
|
+
error = assert_raises(CurzonUk::AuthenticationError) do
|
|
55
|
+
guest_token(File.join(@directory, 'missing')).fetch
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
assert_match(/curl-impersonate/, error.message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_acquire_token_sets_token
|
|
62
|
+
token = token_expiring_at(Time.now + 3600)
|
|
63
|
+
|
|
64
|
+
CurzonUk.acquire_token!(curl: fake_curl("#{page_with(token)}\n200"))
|
|
65
|
+
|
|
66
|
+
assert_equal token, CurzonUk.token
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def guest_token(curl)
|
|
72
|
+
CurzonUk::Internal::GuestToken.new(curl: curl)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def token_expiring_at(time)
|
|
76
|
+
payload = Base64.urlsafe_encode64(JSON.generate('exp' => time.to_i), padding: false)
|
|
77
|
+
"header.#{payload}.signature"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def page_with(token)
|
|
81
|
+
data = { 'api' => { 'apiUrl' => 'https://digital-api.curzon.com', 'authToken' => token } }
|
|
82
|
+
"<script>\n window.initialData = #{JSON.generate(data)};\n window.pageData = [];\n</script>"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fake_curl(output, exit_status: 0)
|
|
86
|
+
path = File.join(@directory, 'curl-impersonate')
|
|
87
|
+
File.write(path, "#!#{RbConfig.ruby}\nSTDOUT.write(#{output.dump})\nexit #{exit_status}\n")
|
|
88
|
+
File.chmod(0o755, path)
|
|
89
|
+
path
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative '../../../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class CurzonUk::Internal::Parser::Api::ShowtimeTest < Minitest::Test
|
|
4
|
+
FILMS = {
|
|
5
|
+
'HO1' => { 'title' => { 'text' => 'Sense and Sensibility (2026)' } },
|
|
6
|
+
'HO2' => { 'title' => { 'text' => 'UKJFF - Example Gala' } }
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
ATTRIBUTES = {
|
|
10
|
+
'1' => { 'shortName' => { 'text' => 'Captioned' }, 'name' => { 'text' => 'Open Captioned' } },
|
|
11
|
+
'2' => { 'shortName' => { 'text' => 'Baby Club' }, 'name' => { 'text' => 'Baby Club' } },
|
|
12
|
+
'3' => { 'shortName' => { 'text' => 'Q&A' }, 'name' => { 'text' => 'Screening followed by a Q&A' } },
|
|
13
|
+
'4' => { 'shortName' => { 'text' => 'Atmos' }, 'name' => { 'text' => 'Atmos' } }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def test_to_hash
|
|
17
|
+
expected = {
|
|
18
|
+
booking_url: 'https://www.curzon.com/ticketing/seats/?showtimeId=SOH1-1',
|
|
19
|
+
dimension: '2d',
|
|
20
|
+
festival: nil,
|
|
21
|
+
film_name: 'Sense and Sensibility',
|
|
22
|
+
film_year: 2026,
|
|
23
|
+
starting_at: Time.utc(2026, 10, 24, 17, 30),
|
|
24
|
+
variant: []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assert_equal expected, parse
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_3d_glasses
|
|
31
|
+
assert_equal '3d', parse(requires3d: true)[:dimension]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_maps_attribute_names_to_variants
|
|
35
|
+
assert_equal %w(subtitled baby q&a), parse(attribute_ids: %w(1 2 3 4))[:variant]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_festival
|
|
39
|
+
showtime = parse(film_id: 'HO2')
|
|
40
|
+
|
|
41
|
+
assert_equal Cinebase::Festival.new(name: 'UKJFF', year: 2026), showtime[:festival]
|
|
42
|
+
assert_equal 'Example Gala', showtime[:film_name]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse(attribute_ids: [], requires3d: false, film_id: 'HO1')
|
|
48
|
+
data = {
|
|
49
|
+
'id' => 'SOH1-1',
|
|
50
|
+
'filmId' => film_id,
|
|
51
|
+
'schedule' => { 'startsAt' => '2026-10-24T18:30:00+01:00' },
|
|
52
|
+
'attributeIds' => attribute_ids,
|
|
53
|
+
'requires3dGlasses' => requires3d
|
|
54
|
+
}
|
|
55
|
+
CurzonUk::Internal::Parser::Api::Showtime.new(
|
|
56
|
+
data, films: FILMS, attributes: ATTRIBUTES
|
|
57
|
+
).to_hash
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative '../../../test_helper'
|
|
2
|
+
|
|
3
|
+
class CurzonUk::Internal::TitleSanitizerTest < Minitest::Test
|
|
4
|
+
def test_removes_dochouse_strand
|
|
5
|
+
assert_sanitized 'Derek Vs Derek', 'DocHouse: Derek Vs Derek'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_removes_film_club_strand
|
|
9
|
+
assert_sanitized 'Black Beyond: Welcome II The Terrordome',
|
|
10
|
+
'Film Club: Black Beyond: Welcome II The Terrordome'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_removes_film_year
|
|
14
|
+
assert_sanitized 'Street Fighter', 'Street Fighter (2026)'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_keeps_titles_with_their_own_subtitles
|
|
18
|
+
[
|
|
19
|
+
'De Gaulle: Part 2- Liberte',
|
|
20
|
+
'Ghosts: The Possession of Button House',
|
|
21
|
+
'Hadestown: The Musical',
|
|
22
|
+
'Fallen Angels by Noël Coward'
|
|
23
|
+
].each { |title| assert_sanitized title, title }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def assert_sanitized(expected, title)
|
|
29
|
+
assert_equal expected, CurzonUk::Internal::TitleSanitizer.new(title).sanitized
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require_relative '../../test_helper'
|
|
2
|
+
|
|
3
|
+
class CurzonUk::PerformanceTest < Minitest::Test
|
|
4
|
+
DATE = Date.new(2026, 10, 24)
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
@api_response = FakeApiResponse.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_at_returns_performances
|
|
11
|
+
performances = performances_at('SOH1')
|
|
12
|
+
|
|
13
|
+
assert(performances.all? { |p| p.instance_of?(CurzonUk::Performance) })
|
|
14
|
+
assert_equal 5, performances.count
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_at_accepts_lower_case_id
|
|
18
|
+
assert_equal 5, performances_at('soh1').count
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_at_sets_cinema_details
|
|
22
|
+
performances = performances_at('SOH1')
|
|
23
|
+
|
|
24
|
+
assert_equal ['SOH1'], performances.map(&:cinema_id).uniq
|
|
25
|
+
assert_equal ['Soho'], performances.map(&:cinema_name).uniq
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_at_parses_first_performance
|
|
29
|
+
performance = performances_at('SOH1').first
|
|
30
|
+
|
|
31
|
+
assert_equal 'The Example Film', performance.film_name
|
|
32
|
+
assert_equal Time.utc(2026, 10, 24, 12, 0), performance.starting_at
|
|
33
|
+
assert_equal '2d', performance.dimension
|
|
34
|
+
assert_equal [], performance.variant
|
|
35
|
+
assert_equal 'https://www.curzon.com/ticketing/seats/?showtimeId=SOH1-1001', performance.booking_url
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_at_parses_3d_open_captions
|
|
39
|
+
performance = performances_at('SOH1')[1]
|
|
40
|
+
|
|
41
|
+
assert_equal '3d', performance.dimension
|
|
42
|
+
assert performance.subtitled?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_at_strips_strand_and_year
|
|
46
|
+
performance = performances_at('SOH1')[2]
|
|
47
|
+
|
|
48
|
+
assert_equal 'A Made Up Documentary', performance.film_name
|
|
49
|
+
assert_equal 2025, performance.film_year
|
|
50
|
+
assert_equal %w(q&a), performance.variant
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_at_strips_film_club
|
|
54
|
+
performance = performances_at('SOH1')[3]
|
|
55
|
+
|
|
56
|
+
assert_equal 'Pretend Classic', performance.film_name
|
|
57
|
+
assert_equal %w(baby), performance.variant
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_at_moves_festival_from_title
|
|
61
|
+
performance = performances_at('SOH1')[4]
|
|
62
|
+
|
|
63
|
+
assert_equal 'Imaginary Picture', performance.film_name
|
|
64
|
+
assert_equal Cinebase::Festival.new(name: 'UKJFF', year: 2026), performance.festival
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_at_keeps_after_midnight_on_next_utc_day
|
|
68
|
+
assert_equal Time.utc(2026, 10, 24, 23, 15), performances_at('SOH1').last.starting_at
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_at_requests_each_date_in_range
|
|
72
|
+
to = DATE + 2
|
|
73
|
+
|
|
74
|
+
with_fake_api { CurzonUk::Performance.at('SOH1', from: DATE, to: to) }
|
|
75
|
+
|
|
76
|
+
assert_equal (DATE..to).to_a, @api_response.requested_dates
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_at_rejects_range_ending_before_start
|
|
80
|
+
assert_raises(ArgumentError) { CurzonUk::Performance.at('SOH1', from: DATE, to: DATE - 1) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_new_sets_defaults
|
|
84
|
+
performance = CurzonUk::Performance.new(
|
|
85
|
+
film_name: 'Carol', cinema_id: 'SOH1', cinema_name: 'Soho',
|
|
86
|
+
starting_at: Time.utc(2013, 9, 12, 11, 0)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert_nil performance.booking_url
|
|
90
|
+
assert_equal '2d', performance.dimension
|
|
91
|
+
assert_nil performance.festival
|
|
92
|
+
assert_nil performance.film_year
|
|
93
|
+
refute performance.subtitled?
|
|
94
|
+
assert_equal [], performance.variant
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def performances_at(cinema_id)
|
|
100
|
+
with_fake_api { CurzonUk::Performance.at(cinema_id, from: DATE, to: DATE) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def with_fake_api(&block)
|
|
104
|
+
CurzonUk::Internal::ApiResponse.stub(:new, @api_response, &block)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative 'live_helper'
|
|
2
|
+
|
|
3
|
+
class LiveApiShapeTest < Minitest::Test
|
|
4
|
+
include ApiShapes
|
|
5
|
+
|
|
6
|
+
def test_sites_are_readable
|
|
7
|
+
assert_sites_shape('sites' => api.sites)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_showtimes_are_readable
|
|
11
|
+
assert_showtimes_shape(api.showtimes('SOH1', Date.today + 1))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def api
|
|
17
|
+
CurzonUk::Internal::ApiResponse.new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require_relative 'live_helper'
|
|
2
|
+
|
|
3
|
+
class LiveCinemaTest < Minitest::Test
|
|
4
|
+
def test_all_returns_cinemas
|
|
5
|
+
cinemas = CurzonUk::Cinema.all
|
|
6
|
+
|
|
7
|
+
assert(cinemas.all? { |cinema| cinema.instance_of?(CurzonUk::Cinema) })
|
|
8
|
+
assert_operator cinemas.size, :>, 10
|
|
9
|
+
assert_equal 'Soho', cinemas.detect { |cinema| cinema.id == 'SOH1' }.name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_every_cinema_has_a_postcode
|
|
13
|
+
CurzonUk::Cinema.all.each do |cinema|
|
|
14
|
+
refute_empty cinema.postal_code, "#{cinema.id} #{cinema.name}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class LivePerformanceTest < Minitest::Test
|
|
20
|
+
def test_at_returns_performances
|
|
21
|
+
tomorrow = Date.today + 1
|
|
22
|
+
performances = CurzonUk::Performance.at('SOH1', from: tomorrow, to: tomorrow)
|
|
23
|
+
|
|
24
|
+
assert(performances.all? { |p| p.instance_of?(CurzonUk::Performance) })
|
|
25
|
+
assert_operator performances.count, :>, 3
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'minitest/reporters'
|
|
3
|
+
|
|
4
|
+
Minitest::Reporters.use! [
|
|
5
|
+
Minitest::Reporters::DefaultReporter.new(color: true)
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
require File.expand_path('../../../lib/curzon_uk.rb', __FILE__)
|
|
9
|
+
require_relative '../support/api_shapes'
|
|
10
|
+
|
|
11
|
+
CurzonUk.token = ENV.fetch('CURZON_TOKEN') { CurzonUk.acquire_token! }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Run separately after building: CURZON_PACKAGE=pkg/...gem bundle exec ruby test/package_test.rb
|
|
2
|
+
require "minitest/autorun"
|
|
3
|
+
require "rubygems/installer"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "base64"
|
|
8
|
+
require "json"
|
|
9
|
+
|
|
10
|
+
class PlatformPackageTest < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
@directory = Dir.mktmpdir("curzon-package-test")
|
|
13
|
+
@spec = Gem::Installer.at(
|
|
14
|
+
ENV.fetch("CURZON_PACKAGE"), install_dir: @directory, ignore_dependencies: true
|
|
15
|
+
).install
|
|
16
|
+
@vendor = File.join(@spec.full_gem_path, "vendor", "curl-impersonate")
|
|
17
|
+
@binary = File.join(@vendor, "bin", "curl-impersonate")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def teardown
|
|
21
|
+
FileUtils.remove_entry(@directory)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_installs_runnable_executable_with_notices_and_provenance
|
|
25
|
+
assert File.executable?(@binary)
|
|
26
|
+
output, status = Open3.capture2(@binary, "--version")
|
|
27
|
+
assert status.success?
|
|
28
|
+
assert_includes output, "curl"
|
|
29
|
+
assert File.file?(File.join(@vendor, "licenses", "LICENSE_CURL"))
|
|
30
|
+
assert_includes File.read(File.join(@vendor, "SOURCE")), "2.2.3"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_acquires_token_with_gem_local_executable_and_no_curl_on_path
|
|
34
|
+
payload = Base64.urlsafe_encode64(JSON.generate("exp" => Time.now.to_i + 3600), padding: false)
|
|
35
|
+
token = "header.#{payload}.signature"
|
|
36
|
+
data = { "api" => { "authToken" => token } }
|
|
37
|
+
page = "<script>\nwindow.initialData = #{JSON.generate(data)};\n</script>\n200"
|
|
38
|
+
# Substitute only the network response, not the installed package's path resolution.
|
|
39
|
+
File.write(@binary, "#!#{RbConfig.ruby}\nSTDOUT.write(#{page.dump})\n")
|
|
40
|
+
output, error, status = Open3.capture3(
|
|
41
|
+
{ "PATH" => @directory, "RUBYOPT" => nil, "BUNDLER_SETUP" => nil,
|
|
42
|
+
"GEM_PATH" => Gem.path.join(File::PATH_SEPARATOR) }, RbConfig.ruby,
|
|
43
|
+
"-I#{File.join(@spec.full_gem_path, 'lib')}", *cinebase_load_path, "-rcurzon_uk",
|
|
44
|
+
"-e", "print CurzonUk.acquire_token!"
|
|
45
|
+
)
|
|
46
|
+
assert status.success?, error
|
|
47
|
+
assert_equal token, output
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# The package is installed without dependencies, so load the bundle's cinebase.
|
|
53
|
+
def cinebase_load_path
|
|
54
|
+
Gem.loaded_specs.fetch("cinebase").full_require_paths.map { |path| "-I#{path}" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Read hand-written fixtures from disk. They follow the shape in
|
|
2
|
+
# api_shapes.rb; they are never recorded API output.
|
|
3
|
+
module ApiFixturesHelper
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def sites_json
|
|
7
|
+
read_fixture('api/sites')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def showtimes_json(cinema_id)
|
|
11
|
+
read_fixture("api/showtimes-#{cinema_id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def read_fixture(filepath)
|
|
15
|
+
File.read(File.expand_path("../../fixtures/#{filepath}.json", __FILE__))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
# The parts of Vista responses the gem reads. Checked against the hand-written
|
|
4
|
+
# fixtures and, in the live suite, against real responses held only in memory.
|
|
5
|
+
module ApiShapes
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def assert_sites_shape(data)
|
|
9
|
+
sites = data.fetch('sites')
|
|
10
|
+
refute_empty sites
|
|
11
|
+
sites.each do |site|
|
|
12
|
+
assert_match(/\A[A-Z0-9]+\z/, site.fetch('id'))
|
|
13
|
+
assert_kind_of String, site.dig('name', 'text')
|
|
14
|
+
address = site.fetch('contactDetails').fetch('addressV2')
|
|
15
|
+
%w(line1 line2 line3 line4).each do |key|
|
|
16
|
+
line = address.fetch(key)
|
|
17
|
+
assert_kind_of String, line.fetch('text'), "site #{site['id']} #{key}" unless line.nil?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def assert_showtimes_shape(data)
|
|
23
|
+
related = data.fetch('relatedData')
|
|
24
|
+
films = related.fetch('films').to_h { |film| [film.fetch('id'), film] }
|
|
25
|
+
attributes = related.fetch('attributes').to_h do |attribute|
|
|
26
|
+
[attribute.fetch('id'), attribute]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
films.each_value { |film| assert_kind_of String, film.dig('title', 'text') }
|
|
30
|
+
attributes.each_value do |attribute|
|
|
31
|
+
assert_kind_of String, attribute.dig('shortName', 'text')
|
|
32
|
+
assert_kind_of String, attribute.dig('name', 'text')
|
|
33
|
+
end
|
|
34
|
+
related.fetch('sites').each { |site| assert_kind_of String, site.dig('name', 'text') }
|
|
35
|
+
|
|
36
|
+
showtimes = data.fetch('showtimes')
|
|
37
|
+
refute_empty showtimes
|
|
38
|
+
showtimes.each do |showtime|
|
|
39
|
+
assert_kind_of String, showtime.fetch('id')
|
|
40
|
+
assert_includes films.keys, showtime.fetch('filmId')
|
|
41
|
+
assert_kind_of Time, Time.iso8601(showtime.fetch('schedule').fetch('startsAt'))
|
|
42
|
+
assert_includes [true, false], showtime.fetch('requires3dGlasses')
|
|
43
|
+
showtime.fetch('attributeIds').each do |id|
|
|
44
|
+
assert_includes attributes.keys, id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|