gemini-suggestions, tests-fail #1

Merged
olemd merged 2 commits from feature/gemini-fixes into main 2025-06-06 23:18:20 +02:00
8 changed files with 429 additions and 15 deletions

73
.rubocop.yml Normal file
View file

@ -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

18
Gemfile
View file

@ -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

View file

@ -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)

20
app.rb
View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra'
require 'json'
require 'yaml'
@ -43,10 +45,8 @@ def determine_language(params, headers)
# 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|
case lang
when /^zh/ # Chinese variants
return 'zh'
@ -113,7 +113,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,
@ -152,8 +152,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
@ -165,7 +165,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
@ -178,8 +178,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
@ -187,7 +187,7 @@ end
get '/health' do
content_type :json
{
status: "healthy",
status: 'healthy',
languages_loaded: RESPONSES.keys.size,
timestamp: Time.now.iso8601
}.to_json
@ -199,7 +199,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,

106
spec/app_spec.rb Normal file
View file

@ -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

31
spec/spec_helper.rb Normal file
View file

@ -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

96
test/app_test.rb Normal file
View file

@ -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

30
test/test_helper.rb Normal file
View file

@ -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