cineworld_uk 4.1.0-aarch64-linux-gnu → 4.2.0-aarch64-linux-gnu

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +23 -1
  4. data/Rakefile +3 -24
  5. data/cineworld_uk.gemspec +2 -2
  6. data/lib/cineworld_uk/internal/api_response.rb +7 -30
  7. data/lib/cineworld_uk/internal/guest_token.rb +9 -52
  8. data/lib/cineworld_uk/internal/parser/api/film.rb +6 -0
  9. data/lib/cineworld_uk/internal/parser/api/performance.rb +5 -0
  10. data/lib/cineworld_uk/internal/title_sanitizer.rb +2 -10
  11. data/lib/cineworld_uk/performance.rb +4 -1
  12. data/lib/cineworld_uk/version.rb +2 -2
  13. data/lib/cineworld_uk.rb +2 -1
  14. data/test/fixtures/api/showtimes-014.json +67 -0
  15. data/test/fixtures/api/sites.json +52 -0
  16. data/test/lib/cineworld_uk/cinema_test.rb +63 -223
  17. data/test/lib/cineworld_uk/internal/api_response_test.rb +27 -182
  18. data/test/lib/cineworld_uk/internal/fixture_shape_test.rb +14 -0
  19. data/test/lib/cineworld_uk/internal/guest_token_test.rb +87 -0
  20. data/test/lib/cineworld_uk/internal/parser/api/cinema_address_test.rb +44 -85
  21. data/test/lib/cineworld_uk/internal/parser/api/film_test.rb +53 -131
  22. data/test/lib/cineworld_uk/internal/parser/api/performance_test.rb +27 -45
  23. data/test/lib/cineworld_uk/internal/title_sanitizer_test.rb +75 -126
  24. data/test/lib/cineworld_uk/performance_test.rb +153 -180
  25. data/test/lib/cineworld_uk/version_test.rb +3 -3
  26. data/test/live/api_shape_test.rb +22 -0
  27. data/test/live/integration_test.rb +13 -43
  28. data/test/live/live_helper.rb +11 -0
  29. data/test/package_test.rb +14 -7
  30. data/test/support/api_shapes.rb +48 -0
  31. data/test/support/vista_fixtures.rb +58 -0
  32. data/test/test_helper.rb +3 -0
  33. metadata +33 -28
  34. data/tasks/package.rake +0 -84
  35. data/test/fixtures/api/cinema-list.json +0 -1
  36. data/test/fixtures/api/dates-14.json +0 -1
  37. data/test/fixtures/api/performances-tomorrow-14.json +0 -1
  38. data/test/fixtures/api/vista-showtimes-brighton.json +0 -1
  39. data/test/support/fixture_reader.rb +0 -32
@@ -1,249 +1,89 @@
1
1
  require_relative '../../test_helper'
2
- require_relative '../../support/fixture_reader'
3
-
4
- describe CineworldUk::Cinema do
5
- include Support::FixtureReader
6
2
 
7
- let(:described_class) { CineworldUk::Cinema }
8
- let(:api_response) { Minitest::Mock.new }
3
+ class CineworldUk::CinemaTest < Minitest::Test
4
+ include Support::VistaFixtures
9
5
 
10
- before { WebMock.disable_net_connect! }
11
- after { WebMock.allow_net_connect! }
12
-
13
- describe '.all' do
14
- subject { described_class.all }
15
-
16
- before { api_response.expect(:cinema_list, cinema_list_json) }
17
-
18
- it 'returns an Array of CineworldUK::Cinemas' do
19
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
20
- _(subject).must_be_instance_of(Array)
21
- subject.each do |value|
22
- _(value).must_be_instance_of(CineworldUk::Cinema)
23
- end
24
- end
25
- end
26
-
27
- it 'returns the correctly sized array' do
28
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
29
- _(subject.size).must_equal(87)
30
- end
31
- end
32
- end
33
-
34
- describe '#adr' do
35
- subject { described_class.new(id).adr }
36
-
37
- before do
38
- api_response.expect(:cinema_list, cinema_list_json)
39
- end
40
-
41
- describe 'Brighton (14)' do
42
- let(:id) { 14 }
43
-
44
- it 'returns the address hash' do
45
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
46
- _(subject).must_equal(
47
- street_address: 'Brighton Marina Village',
48
- extended_address: nil,
49
- locality: 'Brighton',
50
- region: nil,
51
- postal_code: 'BN2 5UF',
52
- country: 'United Kingdom'
53
- )
54
- end
55
- end
56
- end
57
- end
58
-
59
- describe '#address' do
60
- subject { described_class.new(id).address }
61
-
62
- before do
63
- api_response.expect(:cinema_list, cinema_list_json)
64
- end
65
-
66
- describe 'Brighton (14)' do
67
- let(:id) { 14 }
68
-
69
- it 'returns the address hash' do
70
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
71
- _(subject).must_equal(
72
- street_address: 'Brighton Marina Village',
73
- extended_address: nil,
74
- locality: 'Brighton',
75
- region: nil,
76
- postal_code: 'BN2 5UF',
77
- country: 'United Kingdom'
78
- )
79
- end
80
- end
81
- end
82
- end
83
-
84
- describe '#extended_address' do
85
- subject { described_class.new(id).extended_address }
86
-
87
- before do
88
- api_response.expect(:cinema_list, cinema_list_json)
89
- end
90
-
91
- describe 'Brighton (14)' do
92
- let(:id) { 14 }
93
-
94
- it 'returns the address hash' do
95
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
96
- _(subject).must_equal('')
97
- end
98
- end
99
- end
6
+ def setup
7
+ setup_vista
8
+ stub_sites
100
9
  end
101
10
 
102
- describe '#full_name' do
103
- subject { described_class.new(id).full_name }
104
-
105
- before { api_response.expect(:cinema_list, cinema_list_json) }
106
-
107
- describe 'simple name (Brighton)' do
108
- let(:id) { 14 }
109
-
110
- it 'returns the brand in the name' do
111
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
112
- _(subject).must_equal 'Cineworld Brighton'
113
- end
114
- end
115
- end
116
-
117
- describe 'simple name (Brighton) called with String' do
118
- let(:id) { '14' }
119
-
120
- it 'returns the brand in the name' do
121
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
122
- _(subject).must_equal 'Cineworld Brighton'
123
- end
124
- end
125
- end
126
-
127
- describe 'complex name (Glasgow Silverburn)' do
128
- let(:id) { 88 }
129
-
130
- it 'returns the brand in the name' do
131
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
132
- _(subject).must_equal 'Cineworld Glasgow: Silverburn'
133
- end
134
- end
135
- end
11
+ def teardown
12
+ teardown_vista
13
+ super
136
14
  end
137
15
 
138
- describe '#locality' do
139
- subject { described_class.new(id).locality }
140
-
141
- before do
142
- api_response.expect(:cinema_list, cinema_list_json)
143
- end
144
-
145
- describe 'Brighton (14)' do
146
- let(:id) { 14 }
147
-
148
- it 'returns the town/city' do
149
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
150
- _(subject).must_equal 'Brighton'
151
- end
152
- end
153
- end
154
-
155
- describe 'London - Leicester Square (103)' do
156
- let(:id) { 103 }
16
+ def test_all_returns_cinemas
17
+ cinemas = CineworldUk::Cinema.all
157
18
 
158
- it 'returns borough of London' do
159
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
160
- _(subject).must_equal 'London'
161
- end
162
- end
163
- end
164
- end
19
+ assert(cinemas.all? { |cinema| cinema.instance_of?(CineworldUk::Cinema) })
20
+ assert_equal [14, 88, 103, 999], cinemas.map(&:id)
21
+ end
165
22
 
166
- describe '#postal_code' do
167
- subject { described_class.new(id).postal_code }
168
-
169
- before do
170
- api_response.expect(:cinema_list, cinema_list_json)
171
- end
172
-
173
- describe 'Brighton (14)' do
174
- let(:id) { 14 }
23
+ def test_all_fetches_sites_once
24
+ CineworldUk::Cinema.all
25
+ CineworldUk::Cinema.new(14).name
175
26
 
176
- it 'returns the post code' do
177
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
178
- _(subject).must_equal 'BN2 5UF'
179
- end
180
- end
181
- end
27
+ assert_requested :get, "#{BASE_URL}sites", times: 1
182
28
  end
183
29
 
184
- describe '#region' do
185
- subject { described_class.new(id).region }
186
-
187
- before do
188
- api_response.expect(:cinema_list, cinema_list_json)
189
- end
30
+ def test_adr
31
+ expected = {
32
+ street_address: '1 Example Marina',
33
+ extended_address: nil,
34
+ locality: 'Brighton',
35
+ region: 'East Sussex',
36
+ postal_code: 'BN2 0ZZ',
37
+ country: 'United Kingdom'
38
+ }
39
+
40
+ assert_equal expected, CineworldUk::Cinema.new(14).adr
41
+ assert_equal expected, CineworldUk::Cinema.new(14).address
42
+ end
190
43
 
191
- describe 'no region - Brighton (14)' do
192
- let(:id) { 14 }
44
+ def test_extended_address_is_empty
45
+ assert_equal '', CineworldUk::Cinema.new(14).extended_address
46
+ end
193
47
 
194
- it 'returns the empty string is none exists' do
195
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
196
- _(subject).must_equal ''
197
- end
198
- end
199
- end
48
+ def test_full_name
49
+ assert_equal 'Cineworld Brighton', CineworldUk::Cinema.new(14).full_name
50
+ end
200
51
 
201
- describe 'London - Leicester Square (103)' do
202
- let(:id) { 103 }
52
+ def test_full_name_accepts_string_id
53
+ assert_equal 'Cineworld Brighton', CineworldUk::Cinema.new('14').full_name
54
+ end
203
55
 
204
- it 'returns "London"' do
205
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
206
- _(subject).must_equal 'London'
207
- end
208
- end
209
- end
210
- end
211
-
212
- describe '#street_address' do
213
- subject { described_class.new(id).street_address }
56
+ def test_full_name_with_multiple_words
57
+ assert_equal 'Cineworld Glasgow Silverburn', CineworldUk::Cinema.new(88).full_name
58
+ end
214
59
 
215
- before do
216
- api_response.expect(:cinema_list, cinema_list_json)
217
- end
60
+ def test_name_replaces_dash_separator
61
+ assert_equal 'Example: Riverside', CineworldUk::Cinema.new(999).name
62
+ end
218
63
 
219
- describe 'Brighton (14)' do
220
- let(:id) { 14 }
64
+ def test_locality
65
+ assert_equal 'Brighton', CineworldUk::Cinema.new(14).locality
66
+ assert_equal 'London', CineworldUk::Cinema.new(103).locality
67
+ end
221
68
 
222
- it 'returns the first line of the Address' do
223
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
224
- _(subject).must_equal 'Brighton Marina Village'
225
- end
226
- end
227
- end
69
+ def test_postal_code
70
+ assert_equal 'BN2 0ZZ', CineworldUk::Cinema.new(14).postal_code
228
71
  end
229
72
 
230
- describe '#url' do
231
- subject { described_class.new(id).url }
73
+ def test_region
74
+ assert_equal 'East Sussex', CineworldUk::Cinema.new(14).region
75
+ end
232
76
 
233
- before do
234
- api_response.expect(:cinema_list, cinema_list_json)
235
- end
77
+ def test_region_defaults_to_london_for_london_cinemas
78
+ assert_equal 'London', CineworldUk::Cinema.new(103).region
79
+ end
236
80
 
237
- describe 'Brighton (14)' do
238
- let(:id) { 14 }
81
+ def test_street_address
82
+ assert_equal '1 Example Marina', CineworldUk::Cinema.new(14).street_address
83
+ end
239
84
 
240
- it 'returns the url' do
241
- CineworldUk::Internal::ApiResponse.stub :new, api_response do
242
- _(subject).must_equal(
243
- "http://www.cineworld.co.uk/cinemas/#{id}/information"
244
- )
245
- end
246
- end
247
- end
85
+ def test_url
86
+ assert_equal 'http://www.cineworld.co.uk/cinemas/14/information',
87
+ CineworldUk::Cinema.new(14).url
248
88
  end
249
89
  end
@@ -1,207 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../../test_helper"
4
- require "tmpdir"
5
4
 
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
5
+ class CineworldUk::Internal::ApiResponseTest < Minitest::Test
6
+ include Support::VistaFixtures
43
7
 
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
8
+ def setup
9
+ setup_vista
67
10
  end
68
11
 
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"
12
+ def teardown
13
+ teardown_vista
14
+ super
88
15
  end
89
16
 
90
- it "rejects missing and rejected tokens without exposing credentials" do
17
+ def test_rejects_missing_token
91
18
  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
19
 
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"
20
+ assert_raises(CineworldUk::AuthenticationError) { CineworldUk::Cinema.all }
121
21
  end
122
22
 
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
23
+ def test_rejects_token_without_exposing_it
24
+ CineworldUk.token = "expired-secret"
25
+ stub_request(:get, "#{BASE_URL}sites").to_return(status: 401, body: "expired-secret")
133
26
 
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
27
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk::Cinema.all }
140
28
 
141
- after do
142
- FileUtils.remove_entry(@directory)
143
- CineworldUk.token = nil
29
+ refute_includes error.message, "expired-secret"
144
30
  end
145
31
 
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
32
+ def test_uses_replacement_token
33
+ CineworldUk.token = "replacement"
34
+ stub_request(:get, "#{BASE_URL}sites")
35
+ .with(headers: { "Authorization" => "Bearer replacement" })
36
+ .to_return(body: '{"sites":[]}')
151
37
 
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]
38
+ assert_equal [], CineworldUk::Cinema.all
157
39
  end
158
40
 
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
41
+ def test_does_not_follow_redirects_or_hide_forbidden_responses
42
+ [302, 403].each do |status|
43
+ reset_caches
44
+ stub_request(:get, "#{BASE_URL}sites")
45
+ .to_return(status: status, headers: { "Location" => "https://example.com/" })
167
46
 
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
47
+ error = assert_raises(RuntimeError) { CineworldUk::Cinema.all }
183
48
 
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"
49
+ assert_includes error.message, status.to_s
197
50
  end
198
51
  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
52
  end
@@ -0,0 +1,14 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ class FixtureShapeTest < Minitest::Test
4
+ include Support::VistaFixtures
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))
13
+ end
14
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "base64"
5
+ require "tmpdir"
6
+
7
+ class CineworldUk::Internal::GuestTokenTest < Minitest::Test
8
+ def setup
9
+ @directory = Dir.mktmpdir("cineworld token ")
10
+ @curl = File.join(@directory, "curl-impersonate")
11
+ CineworldUk.token = "existing-token"
12
+ end
13
+
14
+ def teardown
15
+ FileUtils.remove_entry(@directory)
16
+ CineworldUk.token = nil
17
+ end
18
+
19
+ def test_sets_and_returns_token_from_200_or_404_page
20
+ html, token = token_page(Time.now.to_i + 3600)
21
+
22
+ [200, 404].each do |status|
23
+ serve_token_page(html, status: status)
24
+
25
+ assert_equal token, CineworldUk.acquire_token!(curl: @curl)
26
+ assert_equal token, CineworldUk.token
27
+ end
28
+ end
29
+
30
+ def test_rejects_expired_tokens_including_boundary_and_keeps_current_token
31
+ now = Time.at(1_800_000_000)
32
+ Time.stub(:now, now) do
33
+ [-1, 0, 1].each do |offset|
34
+ html, token = token_page(now.to_i + offset)
35
+ serve_token_page(html)
36
+ if offset.positive?
37
+ assert_equal token, CineworldUk.acquire_token!(curl: @curl)
38
+ else
39
+ assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
40
+ assert_equal "existing-token", CineworldUk.token
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_reports_bad_pages_without_exposing_the_response
47
+ html, token = token_page(Time.now.to_i + 3600)
48
+ invalid_payload = Base64.urlsafe_encode64("broken-secret", padding: false)
49
+ [[html, 403], [html, 302], ["Cloudflare block", 200],
50
+ ["<script id='__NEXT_DATA__'>{</script>", 200],
51
+ ["<script id='__NEXT_DATA__'>{}</script>", 200],
52
+ [html.sub(token, "broken-secret"), 200],
53
+ [html.sub(token, "header.#{invalid_payload}.signature"), 200]].each do |body, status|
54
+ serve_token_page(body, status: status)
55
+
56
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
57
+
58
+ refute_includes error.full_message, token
59
+ refute_includes error.full_message, "broken-secret"
60
+ assert_equal "existing-token", CineworldUk.token
61
+ end
62
+ end
63
+
64
+ def test_reports_missing_executable_or_curl_failure
65
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
66
+ assert_includes error.message, "Install curl-impersonate"
67
+
68
+ serve_token_page("", status: 0, exit_code: 28)
69
+ error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
70
+ assert_includes error.message, "curl exit 28"
71
+ end
72
+
73
+ private
74
+
75
+ def serve_token_page(html, status: 200, exit_code: 0)
76
+ output = "#{html}\n#{status}"
77
+ File.write(@curl, "#!#{RbConfig.ruby}\nSTDOUT.write(#{output.dump})\nexit #{exit_code}\n")
78
+ File.chmod(0o700, @curl)
79
+ end
80
+
81
+ def token_page(expires)
82
+ payload = Base64.urlsafe_encode64(JSON.generate("exp" => expires), padding: false)
83
+ token = "header.#{payload}.signature"
84
+ data = { "props" => { "pageProps" => { "environment" => { "gasToken" => token } } } }
85
+ ["<script type='application/json' id='__NEXT_DATA__'>#{JSON.generate(data)}</script>", token]
86
+ end
87
+ end