vue_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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +21 -0
  3. data/.github/workflows/release.yml +19 -0
  4. data/.gitignore +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +10 -0
  7. data/COMM-LICENSE +22 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE +617 -0
  10. data/README.md +141 -0
  11. data/Rakefile +35 -0
  12. data/lib/vue_uk/cinema.rb +115 -0
  13. data/lib/vue_uk/internal/api_response.rb +51 -0
  14. data/lib/vue_uk/internal/http.rb +42 -0
  15. data/lib/vue_uk/internal/parser/api/showtime.rb +73 -0
  16. data/lib/vue_uk/internal/title_sanitizer.rb +31 -0
  17. data/lib/vue_uk/internal/website.rb +22 -0
  18. data/lib/vue_uk/performance.rb +70 -0
  19. data/lib/vue_uk/version.rb +6 -0
  20. data/lib/vue_uk.rb +19 -0
  21. data/test/fixtures/api/cinemas.json +42 -0
  22. data/test/fixtures/api/films-10051.json +103 -0
  23. data/test/fixtures/website/cinema-10051.html +8 -0
  24. data/test/lib/vue_uk/cinema_test.rb +110 -0
  25. data/test/lib/vue_uk/internal/api_response_test.rb +68 -0
  26. data/test/lib/vue_uk/internal/fixture_shape_test.rb +18 -0
  27. data/test/lib/vue_uk/internal/parser/api/showtime_test.rb +44 -0
  28. data/test/lib/vue_uk/internal/title_sanitizer_test.rb +30 -0
  29. data/test/lib/vue_uk/internal/website_test.rb +22 -0
  30. data/test/lib/vue_uk/performance_test.rb +108 -0
  31. data/test/lib/vue_uk/version_test.rb +7 -0
  32. data/test/live/api_shape_test.rb +17 -0
  33. data/test/live/integration_test.rb +25 -0
  34. data/test/live/live_helper.rb +9 -0
  35. data/test/support/api_shapes.rb +38 -0
  36. data/test/support/fakes.rb +31 -0
  37. data/test/support/fixtures_helper.rb +21 -0
  38. data/test/test_helper.rb +20 -0
  39. data/vue_uk.gemspec +30 -0
  40. metadata +195 -0
@@ -0,0 +1,103 @@
1
+ {
2
+ "result": [
3
+ {
4
+ "filmId": "HO1",
5
+ "filmTitle": "The Example Film",
6
+ "showingGroups": [
7
+ {
8
+ "date": "2026-10-24T00:00:00",
9
+ "sessions": [
10
+ {
11
+ "sessionId": "1001",
12
+ "bookingUrl": "/book-tickets/summary/10051/HO1/1001",
13
+ "showTimeWithTimeZone": "2026-10-24T10:30:00+01:00",
14
+ "attributes": [
15
+ { "name": "English", "shortName": "", "value": "english" },
16
+ { "name": "Audio Description (AD)", "shortName": "AD", "value": "audio" }
17
+ ]
18
+ },
19
+ {
20
+ "sessionId": "1002",
21
+ "bookingUrl": "/book-tickets/summary/10051/HO1/1002",
22
+ "showTimeWithTimeZone": "2026-10-24T14:00:00+01:00",
23
+ "attributes": [
24
+ { "name": "3D", "shortName": "3D", "value": "3d" }
25
+ ]
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "date": "2026-10-25T00:00:00",
31
+ "sessions": [
32
+ {
33
+ "sessionId": "1009",
34
+ "bookingUrl": "/book-tickets/summary/10051/HO1/1009",
35
+ "showTimeWithTimeZone": "2026-10-25T10:30:00+00:00",
36
+ "attributes": []
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "filmId": "HO2",
44
+ "filmTitle": "Imaginary Picture (2026) (Tamil)",
45
+ "showingGroups": [
46
+ {
47
+ "date": "2026-10-24T00:00:00",
48
+ "sessions": [
49
+ {
50
+ "sessionId": "1003",
51
+ "bookingUrl": "/book-tickets/summary/10051/HO2/1003",
52
+ "showTimeWithTimeZone": "2026-10-24T18:00:00+01:00",
53
+ "attributes": [
54
+ { "name": "Tamil", "shortName": "Tamil", "value": "tamil" },
55
+ { "name": "Open Captioned", "shortName": "Open Captioned", "value": "open-captioned" }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ ]
61
+ },
62
+ {
63
+ "filmId": "HO3",
64
+ "filmTitle": "Mystery Screening (24 October 2026)",
65
+ "showingGroups": [
66
+ {
67
+ "date": "2026-10-24T00:00:00",
68
+ "sessions": [
69
+ {
70
+ "sessionId": "1004",
71
+ "bookingUrl": "/book-tickets/summary/10051/HO3/1004",
72
+ "showTimeWithTimeZone": "2026-10-24T11:00:00+01:00",
73
+ "attributes": [
74
+ { "name": "Mighty Mornings", "shortName": "Mighty Mornings", "value": "mightymornings" }
75
+ ]
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "filmId": "HO4",
83
+ "filmTitle": "CFF: Made Up Matinee",
84
+ "showingGroups": [
85
+ {
86
+ "date": "2026-10-24T00:00:00",
87
+ "sessions": [
88
+ {
89
+ "sessionId": "1005",
90
+ "bookingUrl": "/book-tickets/summary/10051/HO4/1005",
91
+ "showTimeWithTimeZone": "2026-10-25T00:15:00+01:00",
92
+ "attributes": [
93
+ { "name": "Big Screen Events", "shortName": "", "value": "big-screen-events" }
94
+ ]
95
+ }
96
+ ]
97
+ }
98
+ ]
99
+ }
100
+ ],
101
+ "responseCode": 0,
102
+ "errorMessage": null
103
+ }
@@ -0,0 +1,8 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head><title>Vue Accrington</title></head>
4
+ <body>
5
+ <div id="__next"></div>
6
+ <script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"layoutData":{"sitecore":{"context":{"cinema":{"cinemaId":{"value":"10051"},"cinemaName":{"value":"Accrington"},"cinemaAddress":{"value":"1 Example Viaduct\r\nExample Road\r\nAccrington\r\nBB5 0ZZ "}}}}}}}}</script>
7
+ </body>
8
+ </html>
@@ -0,0 +1,110 @@
1
+ require_relative '../../test_helper'
2
+
3
+ class VueUk::CinemaTest < Minitest::Test
4
+ def setup
5
+ @api_response = FakeApiResponse.new
6
+ VueUk::Cinema.instance_variable_set(:@cinemas_hash, nil)
7
+ end
8
+
9
+ def test_new_stores_string_id
10
+ assert_equal '10051', VueUk::Cinema.new(10051).id
11
+ end
12
+
13
+ def test_all_returns_uk_cinemas
14
+ with_fakes do
15
+ cinemas = VueUk::Cinema.all
16
+
17
+ assert(cinemas.all? { |cinema| cinema.instance_of?(VueUk::Cinema) })
18
+ assert_equal %w(10051 10015), cinemas.map(&:id)
19
+ end
20
+ end
21
+
22
+ def test_name_and_full_name
23
+ with_fakes do
24
+ assert_equal 'Accrington', VueUk::Cinema.new(10051).name
25
+ assert_equal 'Vue Birmingham (Star City)', VueUk::Cinema.new(10015).full_name
26
+ end
27
+ end
28
+
29
+ def test_brand
30
+ assert_equal 'Vue', VueUk::Cinema.new(10051).brand
31
+ end
32
+
33
+ def test_slug
34
+ with_fakes do
35
+ assert_equal 'vue-birmingham-star-city', VueUk::Cinema.new(10015).slug
36
+ end
37
+ end
38
+
39
+ def test_url
40
+ with_fakes do
41
+ assert_equal 'https://www.myvue.com/cinema/accrington', VueUk::Cinema.new(10051).url
42
+ end
43
+ end
44
+
45
+ def test_adr_from_cinema_page
46
+ expected = {
47
+ street_address: '1 Example Viaduct',
48
+ extended_address: 'Example Road',
49
+ locality: 'Accrington',
50
+ region: nil,
51
+ postal_code: 'BB5 0ZZ',
52
+ country_name: 'United Kingdom'
53
+ }
54
+
55
+ with_fakes do
56
+ assert_equal expected, VueUk::Cinema.new(10051).adr
57
+ assert_equal 'BB5 0ZZ', VueUk::Cinema.new(10051).postal_code
58
+ end
59
+ end
60
+
61
+ def test_adr_with_country_after_postcode
62
+ website = Object.new
63
+ website.define_singleton_method(:cinema) do |_url|
64
+ { 'cinemaAddress' => { 'value' => "2 Example Park\r\nExample Rd, Barrow-in-Furness\r\nLA14 0ZZ, UK" } }
65
+ end
66
+
67
+ VueUk::Internal::ApiResponse.stub(:new, @api_response) do
68
+ VueUk::Internal::Website.stub(:new, website) do
69
+ adr = VueUk::Cinema.new(10051).adr
70
+
71
+ assert_equal 'LA14 0ZZ', adr[:postal_code]
72
+ assert_equal 'Example Rd, Barrow-in-Furness', adr[:locality]
73
+ assert_equal '2 Example Park', adr[:street_address]
74
+ end
75
+ end
76
+ end
77
+
78
+ def test_adr_with_county_before_postcode
79
+ adr = adr_for("1 Example Park\r\nExample Way\r\nColchester\r\nEssex CO4 0ZZ")
80
+
81
+ assert_equal 'CO4 0ZZ', adr[:postal_code]
82
+ assert_equal 'Essex', adr[:region]
83
+ assert_equal 'Colchester', adr[:locality]
84
+ end
85
+
86
+ def test_adr_ignores_map_coordinates
87
+ adr = adr_for("O2 Example Centre\r\n255 Example Road\r\nLondon\r\n51.548, -0.181")
88
+
89
+ assert_nil adr[:postal_code]
90
+ assert_equal 'London', adr[:locality]
91
+ assert_equal '255 Example Road', adr[:extended_address]
92
+ end
93
+
94
+ private
95
+
96
+ def adr_for(address)
97
+ website = Object.new
98
+ website.define_singleton_method(:cinema) { |_url| { 'cinemaAddress' => { 'value' => address } } }
99
+
100
+ VueUk::Internal::ApiResponse.stub(:new, @api_response) do
101
+ VueUk::Internal::Website.stub(:new, website) { VueUk::Cinema.new(10051).adr }
102
+ end
103
+ end
104
+
105
+ def with_fakes(&block)
106
+ VueUk::Internal::ApiResponse.stub(:new, @api_response) do
107
+ VueUk::Internal::Website.stub(:new, FakeWebsite.new, &block)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,68 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ class VueUk::Internal::ApiResponseTest < Minitest::Test
4
+ include FixturesHelper
5
+
6
+ BASE_URL = 'https://www.myvue.com/api/microservice'.freeze
7
+ FILMS_QUERY = { 'minEmbargoLevel' => '3', 'includesSession' => 'true', 'includeSessionAttributes' => 'true' }.freeze
8
+
9
+ def setup
10
+ @session = stub_request(:post, "#{BASE_URL}/auth/token")
11
+ .to_return(status: 200, body: '{}', headers: { 'Set-Cookie' => ['accessToken=abc; HttpOnly; Path=/', 'refreshToken=def; HttpOnly; Path=/'] })
12
+ end
13
+
14
+ def test_cinemas_sends_guest_session_cookies
15
+ stub_request(:get, "#{BASE_URL}/showings/cinemas")
16
+ .with(headers: { 'Accept' => 'application/json', 'Cookie' => 'accessToken=abc; refreshToken=def' })
17
+ .to_return(status: 200, body: cinemas_json)
18
+
19
+ assert_equal %w(10051 10015 90001), api.cinemas.map { |cinema| cinema['cinemaId'] }
20
+ end
21
+
22
+ def test_films_for_all_dates
23
+ stub_request(:get, "#{BASE_URL}/showings/cinemas/10051/films")
24
+ .with(query: FILMS_QUERY)
25
+ .to_return(status: 200, body: films_json('10051'))
26
+
27
+ assert_equal 'The Example Film', api.films('10051').first['filmTitle']
28
+ end
29
+
30
+ def test_reuses_one_session
31
+ stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 200, body: cinemas_json)
32
+ client = api
33
+
34
+ 2.times { client.cinemas }
35
+
36
+ assert_requested @session, times: 1
37
+ end
38
+
39
+ def test_starts_a_fresh_session_once_when_rejected
40
+ stub_request(:get, "#{BASE_URL}/showings/cinemas")
41
+ .to_return({ status: 401 }, { status: 200, body: cinemas_json })
42
+
43
+ assert_equal 3, api.cinemas.size
44
+ assert_requested @session, times: 2
45
+ end
46
+
47
+ def test_raises_when_a_fresh_session_is_rejected_too
48
+ stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 401, body: 'secret')
49
+
50
+ error = assert_raises(VueUk::Internal::Http::Unauthorized) { api.cinemas }
51
+
52
+ assert_equal 'Vue HTTP error 401', error.message
53
+ end
54
+
55
+ def test_other_errors_omit_body
56
+ stub_request(:get, "#{BASE_URL}/showings/cinemas").to_return(status: 500, body: 'secret')
57
+
58
+ error = assert_raises(RuntimeError) { api.cinemas }
59
+
60
+ assert_equal 'Vue HTTP error 500', error.message
61
+ end
62
+
63
+ private
64
+
65
+ def api
66
+ VueUk::Internal::ApiResponse.new
67
+ end
68
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ class FixtureShapeTest < Minitest::Test
4
+ include ApiShapes
5
+ include FixturesHelper
6
+
7
+ def test_cinemas_fixture_matches_shape
8
+ assert_cinemas_shape(JSON.parse(cinemas_json))
9
+ end
10
+
11
+ def test_films_fixture_matches_shape
12
+ assert_films_shape(JSON.parse(films_json('10051')))
13
+ end
14
+
15
+ def test_cinema_page_fixture_matches_shape
16
+ assert_cinema_page_shape(FakeWebsite.new.cinema('https://www.myvue.com/cinema/accrington/whats-on'))
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../../../../../test_helper'
2
+
3
+ class VueUk::Internal::Parser::Api::ShowtimeTest < Minitest::Test
4
+ def test_to_hash
5
+ expected = {
6
+ booking_url: 'https://www.myvue.com/book-tickets/summary/10051/HO1/1',
7
+ dimension: '2d',
8
+ festival: nil,
9
+ film_name: 'From Dusk Till Dawn',
10
+ film_year: 1996,
11
+ language: nil,
12
+ starting_at: Time.utc(2026, 10, 24, 17, 30),
13
+ variant: []
14
+ }
15
+
16
+ assert_equal expected, parse
17
+ end
18
+
19
+ def test_language_includes_english
20
+ assert_equal 'English', parse(attributes: ['English'])[:language]
21
+ end
22
+
23
+ def test_maps_attribute_names_to_variants
24
+ variant = parse(attributes: ['Autism Friendly', 'Big Shorts', 'Laser', 'Single Seat'])[:variant]
25
+
26
+ assert_equal %w(autism_friendly kids), variant
27
+ end
28
+
29
+ def test_missing_booking_url
30
+ assert_nil parse(booking_url: nil)[:booking_url]
31
+ end
32
+
33
+ private
34
+
35
+ def parse(attributes: [], booking_url: '/book-tickets/summary/10051/HO1/1')
36
+ session = {
37
+ 'bookingUrl' => booking_url,
38
+ 'showTimeWithTimeZone' => '2026-10-24T18:30:00+01:00',
39
+ 'attributes' => attributes.map { |name| { 'name' => name } }
40
+ }
41
+ film = { 'filmTitle' => 'From Dusk Till Dawn (1996)' }
42
+ VueUk::Internal::Parser::Api::Showtime.new(session, film: film).to_hash
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ class VueUk::Internal::TitleSanitizerTest < Minitest::Test
4
+ def test_removes_screening_date
5
+ assert_sanitized 'Mystery Screening', 'Mystery Screening (23 October 2026)'
6
+ end
7
+
8
+ def test_removes_language_version
9
+ assert_sanitized 'Jailer 2', 'Jailer 2 (Tamil)'
10
+ end
11
+
12
+ def test_removes_film_year
13
+ assert_sanitized 'Street Fighter', 'Street Fighter (2026)'
14
+ end
15
+
16
+ def test_keeps_titles_with_their_own_subtitles
17
+ [
18
+ 'Ghosts: The Possession of Button House',
19
+ 'Spider-Man: Brand New Day',
20
+ "We're Going on a Bear Hunt + The Tiger Who Came to Tea",
21
+ 'Blade Runner 2049'
22
+ ].each { |title| assert_sanitized title, title }
23
+ end
24
+
25
+ private
26
+
27
+ def assert_sanitized(expected, title)
28
+ assert_equal expected, VueUk::Internal::TitleSanitizer.new(title).sanitized
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../../../test_helper'
2
+
3
+ class VueUk::Internal::WebsiteTest < Minitest::Test
4
+ include FixturesHelper
5
+
6
+ URL = 'https://www.myvue.com/cinema/accrington/whats-on'.freeze
7
+
8
+ def test_cinema_reads_page_data
9
+ stub_request(:get, URL).to_return(status: 200, body: cinema_page_html('10051'))
10
+
11
+ cinema = VueUk::Internal::Website.new.cinema(URL)
12
+
13
+ assert_equal "1 Example Viaduct\r\nExample Road\r\nAccrington\r\nBB5 0ZZ ",
14
+ cinema.dig('cinemaAddress', 'value')
15
+ end
16
+
17
+ def test_cinema_without_page_data_is_empty
18
+ stub_request(:get, URL).to_return(status: 200, body: '<script id="__NEXT_DATA__">{}</script>')
19
+
20
+ assert_equal({}, VueUk::Internal::Website.new.cinema(URL))
21
+ end
22
+ end
@@ -0,0 +1,108 @@
1
+ require_relative '../../test_helper'
2
+
3
+ class VueUk::PerformanceTest < Minitest::Test
4
+ DATE = Date.new(2026, 10, 24)
5
+
6
+ def setup
7
+ @api_response = FakeApiResponse.new
8
+ VueUk::Cinema.instance_variable_set(:@cinemas_hash, nil)
9
+ end
10
+
11
+ def test_at_returns_performances_on_the_requested_date_only
12
+ performances = performances_at(10051)
13
+
14
+ assert(performances.all? { |p| p.instance_of?(VueUk::Performance) })
15
+ assert_equal 5, performances.count
16
+ end
17
+
18
+ def test_at_accepts_string_id
19
+ assert_equal 5, performances_at('10051').count
20
+ end
21
+
22
+ def test_at_sets_cinema_details
23
+ performances = performances_at(10051)
24
+
25
+ assert_equal ['10051'], performances.map(&:cinema_id).uniq
26
+ assert_equal ['Accrington'], performances.map(&:cinema_name).uniq
27
+ end
28
+
29
+ def test_at_parses_first_performance
30
+ performance = performances_at(10051).first
31
+
32
+ assert_equal 'The Example Film', performance.film_name
33
+ assert_equal Time.utc(2026, 10, 24, 9, 30), performance.starting_at
34
+ assert_equal '2d', performance.dimension
35
+ assert_equal 'English', performance.language
36
+ assert_equal %w(audio_described), performance.variant
37
+ assert_equal 'https://www.myvue.com/book-tickets/summary/10051/HO1/1001', performance.booking_url
38
+ end
39
+
40
+ def test_at_parses_3d
41
+ assert_equal '3d', performances_at(10051)[1].dimension
42
+ end
43
+
44
+ def test_at_moves_year_and_language_from_title
45
+ performance = performances_at(10051)[2]
46
+
47
+ assert_equal 'Imaginary Picture', performance.film_name
48
+ assert_equal 2026, performance.film_year
49
+ assert_equal 'Tamil', performance.language
50
+ assert performance.subtitled?
51
+ end
52
+
53
+ def test_at_strips_screening_date
54
+ performance = performances_at(10051)[3]
55
+
56
+ assert_equal 'Mystery Screening', performance.film_name
57
+ assert_equal %w(kids), performance.variant
58
+ end
59
+
60
+ def test_at_moves_festival_from_title
61
+ performance = performances_at(10051)[4]
62
+
63
+ assert_equal 'Made Up Matinee', performance.film_name
64
+ assert_equal Cinebase::Festival.new(name: 'CFF', year: 2026), performance.festival
65
+ assert_equal %w(arts), performance.variant
66
+ end
67
+
68
+ def test_at_keeps_after_midnight_on_next_utc_day
69
+ assert_equal Time.utc(2026, 10, 24, 23, 15), performances_at(10051).last.starting_at
70
+ end
71
+
72
+ def test_at_filters_one_request_to_the_date_range
73
+ performances = with_fake_api { VueUk::Performance.at(10051, from: DATE, to: DATE + 1) }
74
+
75
+ assert_equal 6, performances.count
76
+ assert_includes performances.map(&:starting_at), Time.utc(2026, 10, 25, 10, 30)
77
+ assert_equal 1, @api_response.requests
78
+ end
79
+
80
+ def test_at_rejects_range_ending_before_start
81
+ assert_raises(ArgumentError) { VueUk::Performance.at(10051, from: DATE, to: DATE - 1) }
82
+ end
83
+
84
+ def test_new_sets_defaults
85
+ performance = VueUk::Performance.new(
86
+ film_name: 'Carol', cinema_id: '10051', cinema_name: 'Accrington',
87
+ starting_at: Time.utc(2013, 9, 12, 11, 0)
88
+ )
89
+
90
+ assert_nil performance.booking_url
91
+ assert_equal '2d', performance.dimension
92
+ assert_nil performance.festival
93
+ assert_nil performance.film_year
94
+ assert_nil performance.language
95
+ refute performance.subtitled?
96
+ assert_equal [], performance.variant
97
+ end
98
+
99
+ private
100
+
101
+ def performances_at(cinema_id)
102
+ with_fake_api { VueUk::Performance.at(cinema_id, from: DATE, to: DATE) }
103
+ end
104
+
105
+ def with_fake_api(&block)
106
+ VueUk::Internal::ApiResponse.stub(:new, @api_response, &block)
107
+ end
108
+ end
@@ -0,0 +1,7 @@
1
+ require_relative '../../test_helper'
2
+
3
+ class VueUk::VersionTest < Minitest::Test
4
+ def test_version_is_defined
5
+ refute_nil VueUk::VERSION
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'live_helper'
2
+
3
+ class LiveApiShapeTest < Minitest::Test
4
+ include ApiShapes
5
+
6
+ def test_cinemas_are_readable
7
+ assert_cinemas_shape('result' => [{ 'cinemas' => VueUk::Internal::ApiResponse.new.cinemas }])
8
+ end
9
+
10
+ def test_films_are_readable
11
+ assert_films_shape('result' => VueUk::Internal::ApiResponse.new.films('10051'))
12
+ end
13
+
14
+ def test_cinema_page_is_readable
15
+ assert_cinema_page_shape(VueUk::Internal::Website.new.cinema('https://www.myvue.com/cinema/accrington/whats-on'))
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'live_helper'
2
+
3
+ class LiveCinemaTest < Minitest::Test
4
+ def test_all_returns_cinemas
5
+ cinemas = VueUk::Cinema.all
6
+
7
+ assert(cinemas.all? { |cinema| cinema.instance_of?(VueUk::Cinema) })
8
+ assert_operator cinemas.size, :>, 70
9
+ assert_equal 'Accrington', cinemas.detect { |cinema| cinema.id == '10051' }.name
10
+ end
11
+
12
+ def test_address_has_a_postcode
13
+ refute_empty VueUk::Cinema.new(10051).postal_code
14
+ end
15
+ end
16
+
17
+ class LivePerformanceTest < Minitest::Test
18
+ def test_at_returns_performances
19
+ tomorrow = Date.today + 1
20
+ performances = VueUk::Performance.at(10051, from: tomorrow, to: tomorrow)
21
+
22
+ assert(performances.all? { |p| p.instance_of?(VueUk::Performance) })
23
+ assert_operator performances.count, :>, 5
24
+ end
25
+ end
@@ -0,0 +1,9 @@
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/vue_uk.rb', __FILE__)
9
+ require_relative '../support/api_shapes'
@@ -0,0 +1,38 @@
1
+ require 'time'
2
+
3
+ # The parts of Vue 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_cinemas_shape(data)
9
+ cinemas = data.fetch('result').flat_map { |group| group.fetch('cinemas') }
10
+ refute_empty cinemas
11
+ cinemas.each do |cinema|
12
+ assert_match(/\A\d+\z/, cinema.fetch('cinemaId'))
13
+ assert_kind_of String, cinema.fetch('cinemaName')
14
+ assert_kind_of String, cinema.fetch('venueCurrency')
15
+ assert_match(%r{\Ahttps://www\.myvue\.com/cinema/[a-z0-9-]+/whats-on\z}, cinema.fetch('whatsOnUrl'))
16
+ end
17
+ end
18
+
19
+ def assert_films_shape(data)
20
+ films = data.fetch('result')
21
+ refute_empty films
22
+ films.each do |film|
23
+ assert_kind_of String, film.fetch('filmTitle')
24
+ film.fetch('showingGroups').each do |group|
25
+ assert_match(/\A\d{4}-\d{2}-\d{2}T/, group.fetch('date'))
26
+ group.fetch('sessions').each do |session|
27
+ assert_kind_of Time, Time.iso8601(session.fetch('showTimeWithTimeZone'))
28
+ assert_match(%r{\A/}, session.fetch('bookingUrl'))
29
+ session.fetch('attributes').each { |attribute| assert_kind_of String, attribute.fetch('name') }
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def assert_cinema_page_shape(cinema)
36
+ assert_kind_of String, cinema.fetch('cinemaAddress').fetch('value')
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'fixtures_helper'
2
+
3
+ class FakeApiResponse
4
+ include FixturesHelper
5
+
6
+ attr_reader :requests
7
+
8
+ def initialize
9
+ @requests = 0
10
+ end
11
+
12
+ def cinemas
13
+ JSON.parse(cinemas_json).fetch('result').flat_map { |group| group.fetch('cinemas') }
14
+ end
15
+
16
+ def films(cinema_id)
17
+ @requests += 1
18
+ JSON.parse(films_json(cinema_id)).fetch('result')
19
+ end
20
+ end
21
+
22
+ class FakeWebsite
23
+ include FixturesHelper
24
+
25
+ def cinema(url)
26
+ id = url[%r{/cinema/accrington/}] ? '10051' : (raise "No fixture for #{url}")
27
+ html = cinema_page_html(id)
28
+ JSON.parse(html[%r{<script id="__NEXT_DATA__"[^>]*>(.*?)</script>}m, 1])
29
+ .dig('props', 'pageProps', 'layoutData', 'sitecore', 'context', 'cinema')
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # Read hand-written fixtures from disk. They follow the shape in
2
+ # api_shapes.rb; they are never recorded output from myvue.com.
3
+ module FixturesHelper
4
+ private
5
+
6
+ def cinemas_json
7
+ read_fixture('api/cinemas.json')
8
+ end
9
+
10
+ def films_json(cinema_id)
11
+ read_fixture("api/films-#{cinema_id}.json")
12
+ end
13
+
14
+ def cinema_page_html(cinema_id)
15
+ read_fixture("website/cinema-#{cinema_id}.html")
16
+ end
17
+
18
+ def read_fixture(filepath)
19
+ File.read(File.expand_path("../../fixtures/#{filepath}", __FILE__))
20
+ end
21
+ end