From 5f4b1f74e5eb4680a6bf304ea733521f32e7b7f1 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Fri, 6 Jun 2025 21:12:46 +0200 Subject: [PATCH] gemini-suggestions, tests-fail --- .rubocop.yml | 73 ++++++++++++++++++++++++++++++ Gemfile | 18 +++++++- Gemfile.lock | 64 ++++++++++++++++++++++++++ app.rb | 26 +++++------ spec/app_spec.rb | 106 ++++++++++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 31 +++++++++++++ test/app_test.rb | 96 +++++++++++++++++++++++++++++++++++++++ test/test_helper.rb | 30 +++++++++++++ 8 files changed, 429 insertions(+), 15 deletions(-) create mode 100644 .rubocop.yml create mode 100644 spec/app_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 test/app_test.rb create mode 100644 test/test_helper.rb diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..49c47df --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,73 @@ +# .rubocop.yml +require: + - rubocop-performance # For performance-related cops + - rubocop-minitest + - rubocop-rspec + +AllCops: + TargetRubyVersion: 3.4.4 + Exclude: + - 'vendor/**/*' + - 'tmp/**/*' + - 'log/**/*' + - 'Gemfile.lock' + - 'Containerfile' # Not a Ruby file + - 'responses.yml' # Not a Ruby file + - 'README.md' # Not a Ruby file + +# Style and Layout Cops +Layout/LineLength: + Max: 120 # A bit more lenient than default 80, but enforce a limit + Exclude: + - 'Rakefile' # Rake tasks can sometimes have long lines + +Metrics/BlockLength: + # Allows longer blocks for RSpec and Minitest spec files + # Adjust as needed for your test framework + Exclude: + - '**/*_spec.rb' + - '**/*_test.rb' + # For non-test files, a reasonable limit + Max: 50 + +Metrics/AbcSize: + Max: 20 # Adjust if methods are consistently complex + +Metrics/MethodLength: + Max: 15 # Keep methods concise + +Style/Documentation: + Enabled: false # Disable documentation comments for simple apps + +Style/FrozenStringLiteralComment: + Enabled: true # Good practice for Ruby 2.3+ + +Style/ClassAndModuleChildren: + Enabled: false # Allows MyApp::MyClass instead of MyApp::MyClass + +Style/StringLiterals: + EnforcedStyle: single_quotes # Prefer single quotes unless interpolation is needed + +Style/StringLiteralsInInterpolation: + EnforcedStyle: single_quotes # Prefer single quotes even within interpolation + +Style/WordArray: + MinSize: 3 # Use %w[] for arrays of strings longer than this + +Style/HashEachMethods: + Enabled: true # Prefer #each_key, #each_value, #each_pair over #each + +Style/CaseEquality: + Enabled: false # Often used in Sinatra routes, e.g. when matched against paths + +# Naming Cops +Naming/FileName: + Exclude: + - 'app.rb' # Allow for `app.rb` which is common for Sinatra entry points + +# Performance Cops (from rubocop-performance) +Performance/CompareWithBlock: + Enabled: true + +Performance/StringIdentifierArgument: + Enabled: true diff --git a/Gemfile b/Gemfile index b2cfbb1..849886c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,24 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gem 'sinatra', '~> 3.0' -gem 'puma', '~> 6.0' gem 'json', '~> 2.6' +gem 'puma', '~> 6.0' +gem 'sinatra', '~> 3.0' gem 'yaml', '~> 0.2' group :development do gem 'rerun' end + +group :development, :test do + gem 'rack-test' # For HTTP integration tests + # Choose one testing framework: + gem 'minitest' # Uncomment this line if you prefer Minitest + gem 'rspec' # Uncomment this line if you prefer RSpec + + gem 'rubocop' + gem 'rubocop-minitest' + gem 'rubocop-performance' + gem 'rubocop-rspec' +end diff --git a/Gemfile.lock b/Gemfile.lock index ad0e1ce..addbc08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,9 @@ GEM remote: https://rubygems.org/ specs: + ast (2.4.3) base64 (0.3.0) + diff-lcs (1.6.2) ffi (1.17.2) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) @@ -14,23 +16,75 @@ GEM ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-musl) json (2.12.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + minitest (5.25.5) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) nio4r (2.7.4) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc + prism (1.4.0) puma (6.6.0) nio4r (~> 2.0) + racc (1.8.1) rack (2.2.17) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) + rack-test (2.2.0) + rack (>= 1.3) + rainbow (3.1.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) + regexp_parser (2.10.0) rerun (0.14.0) listen (~> 3.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.4) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + rubocop (1.76.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.45.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-minitest (0.38.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.6.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) sinatra (3.2.0) mustermann (~> 3.0) @@ -38,6 +92,9 @@ GEM rack-protection (= 3.2.0) tilt (~> 2.0) tilt (2.6.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) yaml (0.4.0) PLATFORMS @@ -55,8 +112,15 @@ PLATFORMS DEPENDENCIES json (~> 2.6) + minitest puma (~> 6.0) + rack-test rerun + rspec + rubocop + rubocop-minitest + rubocop-performance + rubocop-rspec sinatra (~> 3.0) yaml (~> 0.2) diff --git a/app.rb b/app.rb index a73725e..3c5d610 100644 --- a/app.rb +++ b/app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'sinatra' require 'json' require 'yaml' @@ -35,16 +37,14 @@ def determine_language(params, headers) # Parse Accept-Language header if headers['HTTP_ACCEPT_LANGUAGE'] accepted_langs = headers['HTTP_ACCEPT_LANGUAGE'] - .split(',') - .map { |lang| lang.split(';').first.downcase.strip } + .split(',') + .map { |lang| lang.split(';').first.downcase.strip } # Check for exact matches first accepted_langs.each do |lang| return lang if RESPONSES[lang] - end - # Check for language family matches (e.g., 'nb' or 'nn' -> 'no') - accepted_langs.each do |lang| + # Check for language family matches (e.g., 'nb' or 'nn' -> 'no') case lang when /^zh/ # Chinese variants return 'zh' @@ -111,7 +111,7 @@ get '/' do no_response = get_no_response(lang) case request.accept.first&.to_s - when /application\/json/ + when %r{application/json} content_type :json { answer: no_response, @@ -150,8 +150,8 @@ get '/api/no' do language: lang, language_name: get_language_name(lang), timestamp: Time.now.iso8601, - service: "No as a Service", - version: "2.0.0" + service: 'No as a Service', + version: '2.0.0' }.to_json end @@ -163,7 +163,7 @@ get '/api/no/:lang' do content_type :json status 404 return { - error: "Language not supported", + error: 'Language not supported', requested: lang, available: get_available_languages.map { |l| l[:code] }, timestamp: Time.now.iso8601 @@ -176,8 +176,8 @@ get '/api/no/:lang' do language: lang, language_name: get_language_name(lang), timestamp: Time.now.iso8601, - service: "No as a Service", - version: "2.0.0" + service: 'No as a Service', + version: '2.0.0' }.to_json end @@ -185,7 +185,7 @@ end get '/health' do content_type :json { - status: "healthy", + status: 'healthy', languages_loaded: RESPONSES.keys.size, timestamp: Time.now.iso8601 }.to_json @@ -197,7 +197,7 @@ get '/*' do no_response = get_no_response(lang) case request.accept.first&.to_s - when /application\/json/ + when %r{application/json} content_type :json { answer: no_response, diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 0000000..f22ddf5 --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# spec/app_spec.rb +require_relative 'spec_helper' # Load common setup + +RSpec.describe 'No as a Service' do + include Rack::Test::Methods # Rack::Test methods included via spec_helper.rb + + # The `app` method is defined in spec_helper.rb, required by Rack::Test + + context 'GET /' do + it 'returns a default English "no" in HTML' do + get '/' + # Debugging: puts last_response.status, last_response.body + expect(last_response.status).to eq(200), "Expected status 200, got #{last_response.status}" + expect(last_response.body).to include('No'), "Expected body to include 'No', got:\n#{last_response.body}" + expect(last_response.content_type).to include('text/html'), + "Expected HTML content type, got #{last_response.content_type}" + end + + it 'returns JSON when Accept header is application/json' do + header 'Accept', 'application/json' + get '/' + expect(last_response.status).to eq(200), "Expected status 200 with Accept: JSON, got #{last_response.status}" + expect(last_response.content_type).to include('application/json'), + "Expected JSON content type with Accept: JSON, got #{last_response.content_type}" + + json_response = JSON.parse(last_response.body) + expect(json_response).to be_a(Hash), "Expected JSON response to be a Hash, got #{json_response.class}" + expect(json_response['response']).to be_a(String), + "Expected 'response' key with string value, got #{json_response['response'].class}" + expect(json_response['response']).not_to be_empty, "Expected 'response' value to be non-empty" + end + + it 'returns JSON when format parameter is json' do + get '/?format=json' + expect(last_response.status).to eq(200), "Expected status 200 with ?format=json, got #{last_response.status}" + expect(last_response.content_type).to include('application/json'), + "Expected JSON content type with ?format=json, got #{last_response.content_type}" + + json_response = JSON.parse(last_response.body) + expect(json_response).to be_a(Hash), "Expected JSON response to be a Hash, got #{json_response.class}" + expect(json_response['response']).to be_a(String), + "Expected 'response' key with string value, got #{json_response['response'].class}" + expect(json_response['response']).not_to be_empty, "Expected 'response' value to be non-empty" + end + end + + context 'GET /:lang_code' do + it 'returns "no" in Norwegian (no)' do + get '/no' + expect(last_response.status).to eq(200), "Expected status 200 for /no, got #{last_response.status}" + expect(last_response.body).to include('Nei'), + "Expected body to include 'Nei' for /no, got:\n#{last_response.body}" + expect(last_response.content_type).to include('text/html'), + "Expected HTML content type for /no, got #{last_response.content_type}" + end + + it 'returns "no" in Swedish (sv)' do + get '/sv' + expect(last_response.status).to eq(200), "Expected status 200 for /sv, got #{last_response.status}" + expect(last_response.body).to include('Nej'), + "Expected body to include 'Nej' for /sv, got:\n#{last_response.body}" + expect(last_response.content_type).to include('text/html'), + "Expected HTML content type for /sv, got #{last_response.content_type}" + end + + it 'returns 404 for an unsupported language code' do + get '/xyz' + expect(last_response.status).to eq(404), + "Expected status 404 for unsupported language, got #{last_response.status}" + expect(last_response.body).to include('Not Found'), + "Expected body to include 'Not Found', got:\n#{last_response.body}" + end + end + + context 'GET /languages' do + it 'lists all supported languages in JSON' do + get '/languages' + expect(last_response.status).to eq(200), "Expected status 200 for /languages, got #{last_response.status}" + expect(last_response.content_type).to include('application/json'), + "Expected JSON content type for /languages, got #{last_response.content_type}" + + json_response = JSON.parse(last_response.body) + expect(json_response).to be_an(Array), "Expected JSON response to be an Array, got #{json_response.class}" + + # Check for a subset of expected languages. Add all languages from your responses.yml. + expected_languages = %w[en no sv fi is da fo].sort + actual_languages = json_response.sort + + expect(actual_languages.size).to eq(expected_languages.size), + "Expected #{expected_languages.size} languages, got #{actual_languages.size}" + expect(actual_languages).to eq(expected_languages), 'Language list mismatch.' + end + end + + context 'GET /health' do + it 'returns OK' do + get '/health' + expect(last_response.status).to eq(200), "Expected status 200 for /health, got #{last_response.status}" + expect(last_response.body).to eq('OK'), "Expected body 'OK' for /health, got '#{last_response.body}'" + expect(last_response.content_type).to include('text/plain'), + "Expected text/plain content type for /health, got #{last_response.content_type}" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..16ea0d1 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# spec/spec_helper.rb +ENV['RACK_ENV'] = 'test' # Set environment to test, important for Sinatra config + +require 'rack/test' +require 'rspec' # For RSpec +require 'json' # Required if you're testing JSON responses + +# --- CRITICAL APP LOADING --- +# Adjust this path if your app.rb is NOT directly in the parent directory of 'spec/' +# Example: If app.rb is in `src/app.rb`, use `require_relative '../src/app.rb'` +require_relative '../app' + +# Configure Rack::Test +RSpec.configure do |config| + config.include Rack::Test::Methods # Include Rack::Test methods + + # This is *THE* most critical part for Sinatra tests. + # It must return the instance of your Sinatra application. + def app + # Option 1: For classic style Sinatra apps (most common for small apps) + # where you just have `get '/' do ... end` directly in app.rb + Sinatra::Application + + # Option 2: For modular Sinatra apps (if your app.rb defines a class) + # class MyApp < Sinatra::Base; ... end + # If this is your case, uncomment the line below and replace 'MyApp' + # MyApp.new + end +end diff --git a/test/app_test.rb b/test/app_test.rb new file mode 100644 index 0000000..ea9e3b6 --- /dev/null +++ b/test/app_test.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# test/app_test.rb +require_relative 'test_helper' # Load common setup + +class AppTest < Minitest::Test + include Rack::Test::Methods # Include Rack::Test methods + + # The `app` method is defined in test_helper.rb, required by Rack::Test + + def test_root_path_returns_default_english_no + get '/' + # Debugging: puts last_response.status, last_response.body + assert last_response.ok?, "Expected status 200, got #{last_response.status}" + assert_includes last_response.body, 'No', "Expected body to include 'No', got:\n#{last_response.body}" + assert_includes last_response.content_type, 'text/html', + "Expected HTML content type, got #{last_response.content_type}" + end + + def test_language_path_returns_specific_language_no + # Test Norwegian + get '/no' + assert last_response.ok?, "Expected status 200 for /no, got #{last_response.status}" + assert_includes last_response.body, 'Nei', "Expected body to include 'Nei' for /no, got:\n#{last_response.body}" + assert_includes last_response.content_type, 'text/html', + "Expected HTML content type for /no, got #{last_response.content_type}" + + # Test Swedish + get '/sv' + assert last_response.ok?, "Expected status 200 for /sv, got #{last_response.status}" + assert_includes last_response.body, 'Nej', "Expected body to include 'Nej' for /sv, got:\n#{last_response.body}" + assert_includes last_response.content_type, 'text/html', + "Expected HTML content type for /sv, got #{last_response.content_type}" + end + + def test_unsupported_language_returns_not_found + get '/xyz' # Non-existent language + assert_equal 404, last_response.status, "Expected status 404 for unsupported language, got #{last_response.status}" + # Asserting a general 'Not Found' message. Adjust if your 404 page has a specific string. + assert_includes last_response.body, 'Not Found', "Expected body to include 'Not Found', got:\n#{last_response.body}" + end + + def test_lists_all_supported_languages_in_json + get '/languages' + assert last_response.ok?, "Expected status 200 for /languages, got #{last_response.status}" + assert_includes last_response.content_type, 'application/json', + "Expected JSON content type for /languages, got #{last_response.content_type}" + + # Ensure JSON parsing is successful + json_response = JSON.parse(last_response.body) + assert_kind_of Array, json_response, "Expected JSON response to be an Array, got #{json_response.class}" + + # Check for a subset of expected languages. Add all languages from your responses.yml. + expected_languages = %w[en no sv fi is da fo].sort # Sort for consistent comparison if order isn't guaranteed + actual_languages = json_response.sort + + assert_equal expected_languages.size, actual_languages.size, + "Expected #{expected_languages.size} languages, got #{actual_languages.size}" + assert_equal expected_languages, actual_languages, 'Language list mismatch.' + end + + def test_returns_json_when_accept_header_is_json + header 'Accept', 'application/json' # Set the Accept header + get '/' + assert last_response.ok?, "Expected status 200 with Accept: JSON, got #{last_response.status}" + assert_includes last_response.content_type, 'application/json', + "Expected JSON content type with Accept: JSON, got #{last_response.content_type}" + + json_response = JSON.parse(last_response.body) + assert_kind_of Hash, json_response, "Expected JSON response to be a Hash, got #{json_response.class}" + assert json_response['response'].is_a?(String), + "Expected 'response' key with string value, got #{json_response['response'].class}" + assert json_response['response'].length.positive?, "Expected 'response' value to be non-empty" + end + + def test_returns_json_when_format_param_is_json + get '/?format=json' # Use the format parameter + assert last_response.ok?, "Expected status 200 with ?format=json, got #{last_response.status}" + assert_includes last_response.content_type, 'application/json', + "Expected JSON content type with ?format=json, got #{last_response.content_type}" + + json_response = JSON.parse(last_response.body) + assert_kind_of Hash, json_response, "Expected JSON response to be a Hash, got #{json_response.class}" + assert json_response['response'].is_a?(String), + "Expected 'response' key with string value, got #{json_response['response'].class}" + assert json_response['response'].length.positive?, "Expected 'response' value to be non-empty" + end + + def test_health_check_returns_ok + get '/health' + assert last_response.ok?, "Expected status 200 for /health, got #{last_response.status}" + assert_equal 'OK', last_response.body, "Expected body 'OK' for /health, got '#{last_response.body}'" + assert_includes last_response.content_type, 'text/plain', + "Expected text/plain content type for /health, got #{last_response.content_type}" + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..f997212 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# test/test_helper.rb +ENV['RACK_ENV'] = 'test' # Set environment to test, important for Sinatra config + +require 'rack/test' +require 'minitest/autorun' # For Minitest +require 'json' # Required if you're testing JSON responses + +# --- CRITICAL APP LOADING --- +# Adjust this path if your app.rb is NOT directly in the parent directory of 'test/' +# Example: If app.rb is in `src/app.rb`, use `require_relative '../src/app.rb'` +require_relative '../app' + +# Configure Rack::Test +module Rack::Test::Methods + def app + # This is *THE* most critical part for Sinatra tests. + # It must return the instance of your Sinatra application. + + # Option 1: For classic style Sinatra apps (most common for small apps) + # where you just have `get '/' do ... end` directly in app.rb + Sinatra::Application + + # Option 2: For modular Sinatra apps (if your app.rb defines a class) + # class MyApp < Sinatra::Base; ... end + # If this is your case, uncomment the line below and replace 'MyApp' + # MyApp.new + end +end -- 2.52.0