cineworld_uk 4.1.0-x86_64-darwin

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.agents/setup +51 -0
  3. data/.github/workflows/test.yml +43 -0
  4. data/.gitignore +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +225 -0
  7. data/COMM-LICENSE +22 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +617 -0
  10. data/README.md +225 -0
  11. data/Rakefile +59 -0
  12. data/cineworld_uk.gemspec +30 -0
  13. data/lib/cineworld_uk/cinema.rb +168 -0
  14. data/lib/cineworld_uk/internal/api_response.rb +116 -0
  15. data/lib/cineworld_uk/internal/guest_token.rb +73 -0
  16. data/lib/cineworld_uk/internal/parser/api/cinema_address.rb +81 -0
  17. data/lib/cineworld_uk/internal/parser/api/film.rb +48 -0
  18. data/lib/cineworld_uk/internal/parser/api/performance.rb +41 -0
  19. data/lib/cineworld_uk/internal/title_sanitizer.rb +68 -0
  20. data/lib/cineworld_uk/performance.rb +104 -0
  21. data/lib/cineworld_uk/version.rb +6 -0
  22. data/lib/cineworld_uk.rb +31 -0
  23. data/tasks/package.rake +84 -0
  24. data/test/fixtures/api/cinema-list.json +1 -0
  25. data/test/fixtures/api/dates-14.json +1 -0
  26. data/test/fixtures/api/performances-tomorrow-14.json +1 -0
  27. data/test/fixtures/api/vista-showtimes-brighton.json +1 -0
  28. data/test/lib/cineworld_uk/cinema_test.rb +249 -0
  29. data/test/lib/cineworld_uk/internal/api_response_test.rb +207 -0
  30. data/test/lib/cineworld_uk/internal/parser/api/cinema_address_test.rb +94 -0
  31. data/test/lib/cineworld_uk/internal/parser/api/film_test.rb +160 -0
  32. data/test/lib/cineworld_uk/internal/parser/api/performance_test.rb +62 -0
  33. data/test/lib/cineworld_uk/internal/title_sanitizer_test.rb +153 -0
  34. data/test/lib/cineworld_uk/performance_test.rb +183 -0
  35. data/test/lib/cineworld_uk/version_test.rb +7 -0
  36. data/test/live/integration_test.rb +51 -0
  37. data/test/package_test.rb +49 -0
  38. data/test/support/fixture_reader.rb +32 -0
  39. data/test/test_helper.rb +12 -0
  40. data/vendor/curl-impersonate/SOURCE +5 -0
  41. data/vendor/curl-impersonate/bin/curl-impersonate +0 -0
  42. data/vendor/curl-impersonate/licenses/LICENSE +21 -0
  43. data/vendor/curl-impersonate/licenses/LICENSE_BORINGSSL +238 -0
  44. data/vendor/curl-impersonate/licenses/LICENSE_BROTLI +19 -0
  45. data/vendor/curl-impersonate/licenses/LICENSE_CARES +24 -0
  46. data/vendor/curl-impersonate/licenses/LICENSE_CURL +22 -0
  47. data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP2 +23 -0
  48. data/vendor/curl-impersonate/licenses/LICENSE_NGHTTP3 +22 -0
  49. data/vendor/curl-impersonate/licenses/LICENSE_NGTCP2 +22 -0
  50. data/vendor/curl-impersonate/licenses/LICENSE_ZLIB +22 -0
  51. data/vendor/curl-impersonate/licenses/LICENSE_ZSTD +30 -0
  52. metadata +233 -0
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "tmpdir"
5
+
6
+ # This block groups independent Minitest cases, not application logic.
7
+ describe "Vista retrieval" do # rubocop:disable Metrics/BlockLength
8
+ before do
9
+ CineworldUk.token = "test-guest-token"
10
+ [CineworldUk::Cinema, CineworldUk::Performance].each do |klass|
11
+ klass.instance_variables.each do |name|
12
+ klass.remove_instance_variable(name)
13
+ end
14
+ end
15
+ fixture = "../../../fixtures/api/vista-showtimes-brighton.json"
16
+ @capture = JSON.parse(File.read(File.expand_path(fixture, __dir__)))
17
+ @base = "https://digital-api.cineworld.co.uk/ocapi/v1/"
18
+ sites = @capture.fetch("relatedData").fetch("sites")
19
+ stub_request(:get, @base + "sites").with(
20
+ headers: {
21
+ "Authorization" => "Bearer test-guest-token",
22
+ "Connect-Region-Code" => "GB"
23
+ }
24
+ ).to_return(body: JSON.generate("sites" => sites))
25
+ end
26
+
27
+ after do
28
+ CineworldUk.token = nil
29
+ [CineworldUk::Cinema, CineworldUk::Performance].each do |klass|
30
+ klass.instance_variables.each do |name|
31
+ klass.remove_instance_variable(name)
32
+ end
33
+ end
34
+ end
35
+
36
+ def stub_day(day, body)
37
+ stub_request(:get, @base + "showtimes/by-business-date/#{day}").with(
38
+ query: { "siteIds" => "014" },
39
+ headers: { "Authorization" => "Bearer test-guest-token",
40
+ "Connect-Region-Code" => "GB" }
41
+ ).to_return(body: JSON.generate(body))
42
+ end
43
+
44
+ it "retrieves cinemas and advertised session times from Vista" do
45
+ stub_day("2026-10-14", @capture)
46
+ cinema = CineworldUk::Cinema.all.first
47
+ _(cinema.id).must_equal 14
48
+ _(cinema.name).must_equal "Brighton"
49
+ _(cinema.postal_code).must_equal "BN2 5UF"
50
+ _(cinema.street_address).must_equal "Brighton Marina Village"
51
+ assert_requested :get, @base + "sites", times: 1
52
+ performances = CineworldUk::Performance.at(
53
+ 14, from: Date.new(2026, 10, 14), to: Date.new(2026, 10, 14)
54
+ )
55
+ _(performances.size).must_equal 17
56
+ first = performances.first
57
+ _(first.film_name).must_equal "Other Mommy"
58
+ _(first.starting_at).must_equal Time.utc(2026, 10, 14, 14, 40)
59
+ _(first.booking_url).must_equal(
60
+ "https://web.cineworld.co.uk/order/showtimes/014-165507/seats"
61
+ )
62
+ _(first.variant).must_include "audio_described"
63
+ _(first.dimension).must_equal "2d"
64
+ assert_requested :get, @base + "sites", times: 1
65
+ assert_requested :get, @base + "showtimes/by-business-date/2026-10-14",
66
+ query: { "siteIds" => "014" }, times: 1
67
+ end
68
+
69
+ it "keeps format session-specific and includes the last requested day" do
70
+ @capture["showtimes"] =
71
+ [@capture["showtimes"].first,
72
+ Marshal.load(Marshal.dump(@capture["showtimes"].first))]
73
+ @capture["showtimes"][1]["id"] = "014-second"
74
+ @capture["showtimes"][1]["requires3dGlasses"] = true
75
+ @capture["showtimes"][1]["schedule"]["startsAt"] =
76
+ "2026-10-25T16:40:00+00:00"
77
+ stub_day("2026-10-24",
78
+ "showtimes" => [],
79
+ "relatedData" => { "films" => [], "attributes" => [],
80
+ "sites" => [] })
81
+ stub_day("2026-10-25", @capture)
82
+ results = CineworldUk::Performance.at("014", from: Date.new(2026, 10, 24),
83
+ to: Date.new(2026, 10, 25))
84
+ _(results.map(&:dimension)).must_equal %w[2d 3d]
85
+ _(results.last.starting_at).must_equal Time.utc(2026, 10, 25, 16, 40)
86
+ _(results.first.cinema_name).must_equal "Brighton"
87
+ assert_not_requested :get, @base + "sites"
88
+ end
89
+
90
+ it "rejects missing and rejected tokens without exposing credentials" do
91
+ CineworldUk.token = nil
92
+ assert_raises(CineworldUk::AuthenticationError) { CineworldUk::Cinema.all }
93
+ CineworldUk.token = "expired-secret"
94
+ stub_request(:get, @base + "sites").to_return(status: 401,
95
+ body: "expired-secret")
96
+ error = assert_raises(CineworldUk::AuthenticationError) do
97
+ CineworldUk::Cinema.all
98
+ end
99
+ refute_includes error.message, "expired-secret"
100
+ CineworldUk.token = "replacement"
101
+ stub_request(:get, @base + "sites").with(
102
+ headers: { "Authorization" => "Bearer replacement" }
103
+ ).to_return(body: '{"sites":[]}')
104
+ _(CineworldUk::Cinema.all).must_equal []
105
+ end
106
+
107
+ it "defaults to fourteen days and rejects reversed ranges" do
108
+ day = Date.new(2026, 10, 14)
109
+ (day..day + 13).each do |date|
110
+ stub_day(date.iso8601,
111
+ "showtimes" => [],
112
+ "relatedData" => { "films" => [], "attributes" => [],
113
+ "sites" => [] })
114
+ end
115
+ _(CineworldUk::Performance.at(14, from: day)).must_equal []
116
+ assert_requested :get, %r{showtimes/by-business-date/}, times: 14
117
+ assert_raises(ArgumentError) do
118
+ CineworldUk::Performance.at(14, from: day, to: day - 1)
119
+ end
120
+ assert_not_requested :get, @base + "sites"
121
+ end
122
+
123
+ it "does not follow redirects or hide forbidden responses" do
124
+ [302, 403].each do |status|
125
+ stub_request(:get, @base + "sites").to_return(
126
+ status: status, headers: { "Location" => "https://example.com/" }
127
+ )
128
+ error = assert_raises(RuntimeError) { CineworldUk::Cinema.all }
129
+ assert_includes error.message, status.to_s
130
+ end
131
+ end
132
+ end
133
+
134
+ describe "Guest token acquisition" do
135
+ before do
136
+ @directory = Dir.mktmpdir("cineworld token ")
137
+ @curl = File.join(@directory, "curl-impersonate")
138
+ CineworldUk.token = "existing-token"
139
+ end
140
+
141
+ after do
142
+ FileUtils.remove_entry(@directory)
143
+ CineworldUk.token = nil
144
+ end
145
+
146
+ def serve_token_page(html, status: 200, exit_code: 0)
147
+ output = "#{html}\n#{status}"
148
+ File.write(@curl, "#!#{RbConfig.ruby}\nSTDOUT.write(#{output.dump})\nexit #{exit_code}\n")
149
+ File.chmod(0o700, @curl)
150
+ end
151
+
152
+ def token_page(expires)
153
+ payload = Base64.urlsafe_encode64(JSON.generate("exp" => expires), padding: false)
154
+ token = "header.#{payload}.signature"
155
+ data = { "props" => { "pageProps" => { "environment" => { "gasToken" => token } } } }
156
+ ["<script type='application/json' id='__NEXT_DATA__'>#{JSON.generate(data)}</script>", token]
157
+ end
158
+
159
+ it "sets and returns the guest JWT from either a 200 or 404 page" do
160
+ html, token = token_page(Time.now.to_i + 3600)
161
+ [200, 404].each do |status|
162
+ serve_token_page(html, status: status)
163
+ _(CineworldUk.acquire_token!(curl: @curl)).must_equal token
164
+ _(CineworldUk.token).must_equal token
165
+ end
166
+ end
167
+
168
+ it "rejects expired tokens including the expiry boundary without replacing the current token" do
169
+ now = Time.at(1_800_000_000)
170
+ Time.stub(:now, now) do
171
+ [-1, 0, 1].each do |offset|
172
+ html, token = token_page(now.to_i + offset)
173
+ serve_token_page(html)
174
+ if offset.positive?
175
+ _(CineworldUk.acquire_token!(curl: @curl)).must_equal token
176
+ else
177
+ assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
178
+ _(CineworldUk.token).must_equal "existing-token"
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ it "reports blocked, redirected, missing and malformed page data without exposing the response" do
185
+ html, token = token_page(Time.now.to_i + 3600)
186
+ invalid_payload = Base64.urlsafe_encode64("broken-secret", padding: false)
187
+ [[html, 403], [html, 302], ["Cloudflare block", 200],
188
+ ["<script id='__NEXT_DATA__'>{</script>", 200],
189
+ ["<script id='__NEXT_DATA__'>{}</script>", 200],
190
+ [html.sub(token, "broken-secret"), 200],
191
+ [html.sub(token, "header.#{invalid_payload}.signature"), 200]].each do |body, status|
192
+ serve_token_page(body, status: status)
193
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
194
+ refute_includes error.full_message, token
195
+ refute_includes error.full_message, "broken-secret"
196
+ _(CineworldUk.token).must_equal "existing-token"
197
+ end
198
+ end
199
+
200
+ it "reports a missing executable or curl failure as an authentication error" do
201
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
202
+ assert_includes error.message, "Install curl-impersonate"
203
+ serve_token_page("", status: 0, exit_code: 28)
204
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
205
+ assert_includes error.message, "curl exit 28"
206
+ end
207
+ end
@@ -0,0 +1,94 @@
1
+ require_relative '../../../../../test_helper'
2
+ require_relative '../../../../../support/fixture_reader'
3
+
4
+ describe CineworldUk::Internal::Parser::Api::CinemaAddress do
5
+ include Support::FixtureReader
6
+
7
+ let(:described_class) { CineworldUk::Internal::Parser::Api::CinemaAddress }
8
+ let(:api_response) { Minitest::Mock.new }
9
+
10
+ before { WebMock.disable_net_connect! }
11
+ after { WebMock.allow_net_connect! }
12
+
13
+ describe '#to_hash' do
14
+ subject { described_class.new(id).to_hash }
15
+
16
+ before do
17
+ api_response.expect(:cinema_list, cinema_list_json)
18
+ end
19
+
20
+ describe 'passed simple (Brighton)' do
21
+ let(:id) { 14 }
22
+
23
+ it 'returns address hash' do
24
+ CineworldUk::Internal::ApiResponse.stub :new, api_response do
25
+ _(subject).must_equal(street_address: 'Brighton Marina Village',
26
+ extended_address: nil,
27
+ locality: 'Brighton',
28
+ region: nil,
29
+ postal_code: 'BN2 5UF',
30
+ country: 'United Kingdom')
31
+ end
32
+ end
33
+ end
34
+
35
+ describe 'passed three line (NEC)' do
36
+ let(:id) { 90 }
37
+
38
+ it 'returns address hash' do
39
+ CineworldUk::Internal::ApiResponse.stub :new, api_response do
40
+ _(subject).must_equal(street_address: 'Resorts World',
41
+ extended_address: 'Pendigo Way',
42
+ locality: 'Birmingham',
43
+ region: nil,
44
+ postal_code: 'B40 1PU',
45
+ country: 'United Kingdom')
46
+ end
47
+ end
48
+ end
49
+
50
+ describe 'passed three line (Edinburgh)' do
51
+ let(:id) { 37 }
52
+
53
+ it 'returns address hash' do
54
+ CineworldUk::Internal::ApiResponse.stub :new, api_response do
55
+ _(subject).must_equal(street_address: 'Fountain Park',
56
+ extended_address: '130/3 Dundee Street',
57
+ locality: 'Edinburgh',
58
+ region: nil,
59
+ postal_code: 'EH11 1AF',
60
+ country: 'United Kingdom')
61
+ end
62
+ end
63
+ end
64
+
65
+ describe 'passed two line (Leicester Square)' do
66
+ let(:id) { 103 }
67
+
68
+ it 'returns address hash' do
69
+ CineworldUk::Internal::ApiResponse.stub :new, api_response do
70
+ _(subject).must_equal(street_address: '5-6 Leicester Square',
71
+ extended_address: nil,
72
+ locality: 'London',
73
+ region: 'London',
74
+ postal_code: 'WC2H 7NA',
75
+ country: 'United Kingdom')
76
+ end
77
+ end
78
+ end
79
+
80
+ # describe 'passed non-existant api' do
81
+ # let(:id) { 0 }
82
+ #
83
+ # it 'returns hash of nils' do
84
+ # _(subject).must_be_instance_of(Hash)
85
+ # _(subject).must_equal(street_address: nil,
86
+ # extended_address: nil,
87
+ # locality: "not an address",
88
+ # region: nil,
89
+ # postal_code: "not an address",
90
+ # country: "United Kingdom")
91
+ # end
92
+ # end
93
+ end
94
+ end
@@ -0,0 +1,160 @@
1
+ require_relative '../../../../../test_helper'
2
+ require_relative '../../../../../support/fixture_reader'
3
+
4
+ describe CineworldUk::Internal::Parser::Api::Film do
5
+ include Support::FixtureReader
6
+
7
+ let(:described_class) { CineworldUk::Internal::Parser::Api::Film }
8
+ let(:data) { random_film }
9
+
10
+ before { WebMock.disable_net_connect! }
11
+ after { WebMock.allow_net_connect! }
12
+
13
+ describe '#id' do
14
+ subject { described_class.new(data).id }
15
+
16
+ it 'is a string' do
17
+ _(subject).must_be_instance_of(String)
18
+ end
19
+
20
+ it 'is the film id from the data' do
21
+ _(subject).must_equal(data['id'])
22
+ end
23
+ end
24
+
25
+ describe '#dimension' do
26
+ subject { described_class.new(data).dimension }
27
+
28
+ it 'is a string' do
29
+ _(subject).must_be_instance_of(String)
30
+ end
31
+
32
+ it 'is 2d or 3d' do
33
+ _(subject).must_match(/[23]d/)
34
+ end
35
+
36
+ describe 'attributeIds includes "2d"' do
37
+ let(:data) { random_film.merge('attributeIds' => ['2d']) }
38
+
39
+ it 'is 2d' do
40
+ _(subject).must_equal('2d')
41
+ end
42
+ end
43
+
44
+ describe 'attributeIds includes "3d"' do
45
+ let(:data) { random_film.merge('attributeIds' => ['3d']) }
46
+
47
+ it 'is 3d' do
48
+ _(subject).must_equal('3d')
49
+ end
50
+ end
51
+
52
+ describe 'attributeIds with imax but not 3d' do
53
+ let(:data) { random_film.merge('attributeIds' => ['imax', '2d']) }
54
+
55
+ it 'is 2d' do
56
+ _(subject).must_equal('2d')
57
+ end
58
+ end
59
+
60
+ describe 'attributeIds with imax and 3d' do
61
+ let(:data) { random_film.merge('attributeIds' => ['imax', '3d']) }
62
+
63
+ it 'is 3d' do
64
+ _(subject).must_equal('3d')
65
+ end
66
+ end
67
+ end
68
+
69
+ describe '#name' do
70
+ subject { described_class.new(data).name }
71
+
72
+ it 'is a string' do
73
+ _(subject).must_be_instance_of(String)
74
+ end
75
+
76
+ describe 'film name' do
77
+ let(:data) do
78
+ random_film.merge('name' => 'Test Film Name')
79
+ end
80
+
81
+ it 'is the film name' do
82
+ _(subject).must_equal('Test Film Name')
83
+ end
84
+ end
85
+
86
+ describe 'film name is unsanitized' do
87
+ let(:data) { random_film.merge('name' => 'ROH: Something') }
88
+
89
+ it 'is sanitized' do
90
+ _(subject).must_equal('Royal Opera House: Something')
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '#variant' do
96
+ subject { described_class.new(data).variant }
97
+
98
+ it 'is a sorted array of strings' do
99
+ _(subject).must_be_instance_of(Array)
100
+ subject.each { |element| _(element).must_be_instance_of(String) }
101
+ _(subject.sort).must_equal(subject)
102
+ end
103
+
104
+ describe 'attributeIds includes "junior"' do
105
+ let(:data) do
106
+ random_film.merge('attributeIds' => ['junior'])
107
+ end
108
+
109
+ it 'includes "kids"' do
110
+ _(subject).must_include('kids')
111
+ end
112
+ end
113
+
114
+ describe 'attributeIds includes "imax"' do
115
+ let(:data) do
116
+ random_film.merge('attributeIds' => ['imax', '3d'])
117
+ end
118
+
119
+ it 'includes "imax"' do
120
+ _(subject).must_include('imax')
121
+ end
122
+ end
123
+
124
+ describe 'attributeIds includes "unlimited-screening"' do
125
+ let(:data) do
126
+ random_film.merge('attributeIds' => ['unlimited-screening'])
127
+ end
128
+
129
+ it 'includes "members"' do
130
+ _(subject).must_include('members')
131
+ end
132
+ end
133
+
134
+ describe 'attributeIds includes "qa"' do
135
+ let(:data) do
136
+ random_film.merge('attributeIds' => ['qa'])
137
+ end
138
+
139
+ it 'includes "q&a"' do
140
+ _(subject).must_include('q&a')
141
+ end
142
+ end
143
+
144
+ describe 'combination' do
145
+ let(:data) do
146
+ random_film.merge('attributeIds' => ['imax', 'unlimited-screening'])
147
+ end
148
+
149
+ it 'includes "members"' do
150
+ _(subject).must_include('imax', 'members')
151
+ end
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def random_film
158
+ JSON.parse(performances_tomorrow_json(14))['body']['films'].sample
159
+ end
160
+ end
@@ -0,0 +1,62 @@
1
+ require_relative '../../../../../test_helper'
2
+ require_relative '../../../../../support/fixture_reader'
3
+
4
+ describe CineworldUk::Internal::Parser::Api::Performance do
5
+ include Support::FixtureReader
6
+
7
+ let(:described_class) { CineworldUk::Internal::Parser::Api::Performance }
8
+ let(:data) { random_performance }
9
+
10
+ before { WebMock.disable_net_connect! }
11
+ after { WebMock.allow_net_connect! }
12
+
13
+ describe '#booking_url' do
14
+ subject { described_class.new(data).booking_url }
15
+
16
+ it 'should be a url on the cineworld website' do
17
+ _(subject).must_match(%r{cineworld.co.uk})
18
+ end
19
+ end
20
+
21
+ describe '#film_id' do
22
+ subject { described_class.new(data).film_id }
23
+
24
+ it 'should be a string' do
25
+ _(subject).must_be_instance_of(String)
26
+ end
27
+ end
28
+
29
+ describe '#starting_at' do
30
+ subject { described_class.new(data).starting_at }
31
+
32
+ it 'should be a time' do
33
+ _(subject).must_be_instance_of(Time)
34
+ end
35
+ end
36
+
37
+ describe '#variant' do
38
+ subject { described_class.new(data).variant }
39
+
40
+ describe 'has "audio-described" in attributeIds' do
41
+ let(:data) { random_performance.merge('attributeIds' => ['audio-described']) }
42
+
43
+ it 'includes "audio_described"' do
44
+ _(subject).must_include('audio_described')
45
+ end
46
+ end
47
+
48
+ describe 'has "subbed" in attributeIds' do
49
+ let(:data) { random_performance.merge('attributeIds' => ['subbed']) }
50
+
51
+ it 'includes "subtitled"' do
52
+ _(subject).must_include('subtitled')
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def random_performance
60
+ JSON.parse(performances_tomorrow_json(14))['body']['events'].sample
61
+ end
62
+ end
@@ -0,0 +1,153 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ describe CineworldUk::Internal::TitleSanitizer do
4
+ let(:described_class) { CineworldUk::Internal::TitleSanitizer }
5
+
6
+ describe '#sanitized' do
7
+ subject { described_class.new(title).sanitized }
8
+
9
+ describe 'with 2d in title' do
10
+ let(:title) { 'Iron Man 3 2D' }
11
+
12
+ it 'removes dimension' do
13
+ _(subject).must_equal('Iron Man 3')
14
+ end
15
+ end
16
+
17
+ describe 'with 3d in title' do
18
+ let(:title) { 'Iron Man 3 3d' }
19
+
20
+ it 'removes dimension' do
21
+ _(subject).must_equal('Iron Man 3')
22
+ end
23
+ end
24
+
25
+ describe 'in hindi' do
26
+ let(:title) { 'Bang Bang! (Hindi)' }
27
+
28
+ it 'removes language' do
29
+ _(subject).must_equal('Bang Bang!')
30
+ end
31
+ end
32
+
33
+ describe 'Unlimited Screening' do
34
+ let(:title) { 'Nightcrawler - Unlimited Screening' }
35
+
36
+ it 'removes prefix' do
37
+ _(subject).must_equal('Nightcrawler')
38
+ end
39
+ end
40
+
41
+ describe 'Austism screening' do
42
+ let(:title) { 'Autism Friendly Screening: Dolphin Tale 2' }
43
+
44
+ it 'removes prefix' do
45
+ _(subject).must_equal('Dolphin Tale 2')
46
+ end
47
+ end
48
+
49
+ describe 'Bolshoi screening' do
50
+ let(:title) { 'Bolshoi Ballet: The Legend of Love' }
51
+
52
+ it 'removes prefix' do
53
+ _(subject).must_equal('Bolshoi Ballet: The Legend of Love')
54
+ end
55
+ end
56
+
57
+ describe 'English National Opera screening' do
58
+ let(:title) { 'ENO: La Traviata ' }
59
+
60
+ it 'removes prefix' do
61
+ _(subject).must_equal('English National Opera: La Traviata')
62
+ end
63
+ end
64
+
65
+ describe 'with IMAX' do
66
+ let(:title) { '(IMAX) Fury' }
67
+
68
+ it 'removes prefix' do
69
+ _(subject).must_equal('Fury')
70
+ end
71
+ end
72
+
73
+ describe 'Met Opera screening' do
74
+ let(:title) { 'MET Opera - Le Nozze Di Figaro' }
75
+
76
+ it 'removes prefix' do
77
+ _(subject).must_equal('Met Opera: Le Nozze Di Figaro')
78
+ end
79
+ end
80
+
81
+ describe 'NT Live screening' do
82
+ let(:title) { 'NT Live Encore: Frankenstein (starring Benedict Cumberbatch)' }
83
+
84
+ it 'removes prefix' do
85
+ _(subject).must_equal('National Theatre: Frankenstein (starring Benedict Cumberbatch)')
86
+ end
87
+ end
88
+
89
+ describe 'National Theatre screening' do
90
+ let(:title) { 'NT Live: Treasure Island' }
91
+
92
+ it 'removes prefix' do
93
+ _(subject).must_equal('National Theatre: Treasure Island')
94
+ end
95
+ end
96
+
97
+ describe 'National Theatre Encore screening' do
98
+ let(:title) { 'NT Live: A Streetcar Named Desire (Young Vic) - Encore' }
99
+
100
+ it 'removes prefix' do
101
+ _(subject).must_equal('National Theatre: A Streetcar Named Desire (Young Vic)')
102
+ end
103
+ end
104
+
105
+ describe 'ROH screening' do
106
+ let(:title) { 'ROH - I Due Foscari' }
107
+
108
+ it 'removes prefix' do
109
+ _(subject).must_equal('Royal Opera House: I Due Foscari')
110
+ end
111
+ end
112
+
113
+ describe 'RSC screening' do
114
+ let(:title) { "RSC Live: Love's Labour's Lost" }
115
+
116
+ it 'removes prefix' do
117
+ _(subject).must_equal("Royal Shakespeare Company: Love's Labour's Lost")
118
+ end
119
+ end
120
+
121
+ describe 'SciScreen screening' do
122
+ let(:title) { 'SciScreen: Dallas Buyers Club' }
123
+
124
+ it 'removes prefix' do
125
+ _(subject).must_equal('Dallas Buyers Club')
126
+ end
127
+ end
128
+
129
+ describe 'special screening' do
130
+ let(:title) { 'Billy Elliot The Musical Live - Special Performance' }
131
+
132
+ it 'removes prefix' do
133
+ _(subject).must_equal('Billy Elliot The Musical Live')
134
+ end
135
+ end
136
+
137
+ describe 'Take 2 screening' do
138
+ let(:title) { 'Take 2 - Boyhood' }
139
+
140
+ it 'removes prefix' do
141
+ _(subject).must_equal('Boyhood')
142
+ end
143
+ end
144
+
145
+ describe 'singalong and movies for juniors' do
146
+ let(:title) { 'Frozen Sing-Along - Movies For Juniors' }
147
+
148
+ it 'removes prefix' do
149
+ _(subject).must_equal('Frozen')
150
+ end
151
+ end
152
+ end
153
+ end