From 406279f2ce2e1d735e77949db3ff8c62e944e755 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 13:08:49 +1200 Subject: [PATCH 1/5] Extract Utopia trace providers. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- config/sus.rb | 2 + lib/traces/provider/utopia.rb | 7 +++ .../provider/utopia/content/middleware.rb | 20 +++++++ .../provider/utopia/static/middleware.rb | 19 +++++++ lib/utopia/content/middleware.rb | 12 ---- lib/utopia/static/middleware.rb | 12 ---- .../provider/utopia/content/middleware.rb | 55 +++++++++++++++++++ .../provider/utopia/static/middleware.rb | 40 ++++++++++++++ 8 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 lib/traces/provider/utopia.rb create mode 100644 lib/traces/provider/utopia/content/middleware.rb create mode 100644 lib/traces/provider/utopia/static/middleware.rb create mode 100644 test/traces/provider/utopia/content/middleware.rb create mode 100644 test/traces/provider/utopia/static/middleware.rb diff --git a/config/sus.rb b/config/sus.rb index f517380d..354d59b0 100644 --- a/config/sus.rb +++ b/config/sus.rb @@ -5,3 +5,5 @@ require "covered/sus" include Covered::Sus + +ENV["TRACES_BACKEND"] ||= "traces/backend/test" diff --git a/lib/traces/provider/utopia.rb b/lib/traces/provider/utopia.rb new file mode 100644 index 00000000..ef21f7eb --- /dev/null +++ b/lib/traces/provider/utopia.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require_relative "utopia/content/middleware" +require_relative "utopia/static/middleware" diff --git a/lib/traces/provider/utopia/content/middleware.rb b/lib/traces/provider/utopia/content/middleware.rb new file mode 100644 index 00000000..e66b2e6d --- /dev/null +++ b/lib/traces/provider/utopia/content/middleware.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require_relative "../../../../utopia/content/middleware" + +require "traces/provider" + +Traces::Provider(Utopia::Content::Middleware) do + def respond(link, request, localization: request.localization) + attributes = { + "link.key" => link.key, + "link.href" => link.href, + "link.locale" => localization&.locale, + } + + Traces.trace("utopia.content.middleware.respond", attributes: attributes){super} + end +end diff --git a/lib/traces/provider/utopia/static/middleware.rb b/lib/traces/provider/utopia/static/middleware.rb new file mode 100644 index 00000000..a0b962cf --- /dev/null +++ b/lib/traces/provider/utopia/static/middleware.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require_relative "../../../../utopia/static/middleware" + +require "traces/provider" + +Traces::Provider(Utopia::Static::Middleware) do + def respond(request, path, extension, content_type, localization: request.localization) + attributes = { + path: path, + locale: localization&.locale, + } + + Traces.trace("utopia.static.respond", attributes: attributes){super} + end +end diff --git a/lib/utopia/content/middleware.rb b/lib/utopia/content/middleware.rb index 2fee3fb1..e12fb9dd 100644 --- a/lib/utopia/content/middleware.rb +++ b/lib/utopia/content/middleware.rb @@ -16,7 +16,6 @@ require "xrb/template" require "concurrent/map" -require "traces/provider" module Utopia module Content @@ -226,16 +225,5 @@ def relative_tag(name, node) end end - Traces::Provider(Middleware) do - def respond(link, request, localization: request.localization) - attributes = { - "link.key" => link.key, - "link.href" => link.href, - "link.locale" => localization&.locale, - } - - Traces.trace("utopia.content.middleware.respond", attributes: attributes){super} - end - end end end diff --git a/lib/utopia/static/middleware.rb b/lib/utopia/static/middleware.rb index 8eb0e5e6..e18e5470 100644 --- a/lib/utopia/static/middleware.rb +++ b/lib/utopia/static/middleware.rb @@ -11,8 +11,6 @@ require_relative "mime_types" require_relative "../localization/resolver" -require "traces/provider" - module Utopia module Static DEFAULT_CACHE_CONTROL = "public, max-age=3600".freeze @@ -138,15 +136,5 @@ def call(request) end end - Traces::Provider(Static) do - def respond(request, path, extension, content_type, localization: request.localization) - attributes = { - path: path, - locale: localization&.locale, - } - - Traces.trace("utopia.static.respond", attributes: attributes){super} - end - end end end diff --git a/test/traces/provider/utopia/content/middleware.rb b/test/traces/provider/utopia/content/middleware.rb new file mode 100644 index 00000000..8d52fced --- /dev/null +++ b/test/traces/provider/utopia/content/middleware.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "traces/provider/utopia" + +require "utopia/content/link" +require "utopia/localization/preferences" +require "utopia/request" + +describe Utopia::Content::Middleware do + let(:middleware) do + subject.new( + Protocol::HTTP::Middleware::NotFound, + root: File.expand_path(".", __dir__), + ) + end + + let(:request) {Utopia::Request["GET", "/example"]} + let(:localization) do + Utopia::Localization::Preferences.new( + all_locales: ["en"], + preferred_locales: ["en"], + default_locale: "en", + ) + end + + it "traces content responses" do + traces = [] + + mock(Traces) do |mock| + mock.wrap(:trace) do |original, name, attributes: nil, &block| + traces << [name, attributes] + original.call(name, attributes: attributes, &block) + end + end + + link = Utopia::Content::Link.new(:virtual, "example", "en", "/example", uri: "/target") + response = middleware.respond(link, request, localization: localization) + + expect(response.status).to be == 307 + expect(traces).to be == [[ + "utopia.content.middleware.respond", + { + "link.key" => "example.en", + "link.href" => "/target", + "link.locale" => "en", + }, + ]] + ensure + response&.close + middleware.close + end +end diff --git a/test/traces/provider/utopia/static/middleware.rb b/test/traces/provider/utopia/static/middleware.rb new file mode 100644 index 00000000..1b6d2226 --- /dev/null +++ b/test/traces/provider/utopia/static/middleware.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "traces/provider/utopia/static/middleware" + +require "utopia/request" + +describe Utopia::Static::Middleware do + let(:root) {File.expand_path("../../../../utopia/.static", __dir__)} + let(:middleware) {subject.new(Protocol::HTTP::Middleware::NotFound, root: root)} + let(:request) {Utopia::Request["GET", "/test.txt"]} + + it "traces static responses" do + traces = [] + + mock(Traces) do |mock| + mock.wrap(:trace) do |original, name, attributes: nil, &block| + traces << [name, attributes] + original.call(name, attributes: attributes, &block) + end + end + + path = request.url.path + response = middleware.respond(request, path, ".txt", "text/plain", localization: nil) + + expect(response.status).to be == 200 + expect(traces).to be == [[ + "utopia.static.respond", + { + path: path, + locale: nil, + }, + ]] + ensure + response&.close + middleware.close + end +end From 304ffae887834f8085d8c445ad0706848e53dffe Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 13:08:55 +1200 Subject: [PATCH 2/5] Cover static serving contracts. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- test/utopia/static.rb | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/utopia/static.rb b/test/utopia/static.rb index f81d5719..cffbe00f 100755 --- a/test/utopia/static.rb +++ b/test/utopia/static.rb @@ -29,6 +29,23 @@ expect(cache_control).to be(:frozen?) end + it "computes cache control from the served file" do + served_file = nil + application = Utopia::Application.build do + use Utopia::Static, + root: File.expand_path(".static", __dir__), + cache_control: proc{|file| served_file = file; "private, max-age=#{file.bytesize}"} + end + + response = application.call(Protocol::HTTP::Request["GET", "/test.txt"]) + + expect(response.headers["cache-control"]).to be == ["private", "max-age=12"] + expect(served_file).to be_a(Utopia::Static::LocalFile) + ensure + response&.close + application.close + end + let(:middleware) do root = File.expand_path(".static", __dir__) @@ -301,6 +318,19 @@ expect(first.etag).not.to be == second.etag end end + + it "matches strong entity tags for range requests" do + Dir.mktmpdir do |directory| + path = File.join(directory, "test.txt") + File.write(path, "Content") + + file = subject.new(path) + file.instance_variable_set(:@etag, '"strong"') + + expect(file.send(:if_range?, '"strong"')).to be == true + expect(file.send(:if_range?, '"different"')).to be == false + end + end end describe Utopia::Static::MIME_TYPES do @@ -328,4 +358,32 @@ ) end end + + describe Utopia::Static::MimeTypeLoader do + it "expands explicit extension mappings" do + extensions = subject.extensions_for([["example", "application/example"]]) + + expect(extensions).to have_keys( + ".example" => be == "application/example", + ) + end + + it "rejects unknown file extensions" do + expect do + subject.extensions_for(["not-a-real-extension"]) + end.to raise_exception(subject::ExpansionError, message: be =~ /Unknown file extension/) + end + + it "rejects unsupported definitions" do + expect do + subject.extensions_for([Object.new]) + end.to raise_exception(subject::ExpansionError, message: be =~ /Unsupported MIME type definition/) + end + + it "wraps errors while expanding named groups" do + expect do + subject.extensions_for([:missing], {}) + end.to raise_exception(subject::ExpansionError, message: be =~ /Error while processing :missing/) + end + end end From 2eb66c9ef1399cc250f51fb6423a8329b2b21686 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 13:18:10 +1200 Subject: [PATCH 3/5] Separate MIME type tests. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- test/utopia/static.rb | 53 ---------------------------- test/utopia/static/mime_types.rb | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 test/utopia/static/mime_types.rb diff --git a/test/utopia/static.rb b/test/utopia/static.rb index cffbe00f..871fdb05 100755 --- a/test/utopia/static.rb +++ b/test/utopia/static.rb @@ -333,57 +333,4 @@ end end - describe Utopia::Static::MIME_TYPES do - let(:extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:default])} - let(:script_extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:scripts])} - - it "groups script extensions" do - expect(script_extensions).to have_keys( - ".js" => be == "text/javascript", - ".mjs" => be == "text/javascript", - ".wasm" => be == "application/wasm", - ) - end - - it "should give the correct mime type" do - expect(extensions).to have_keys( - ".txt" => be == "text/plain", - ".mjs" => be == "text/javascript", - ".wasm" => be == "application/wasm", - ".webm" => be == "video/webm", - ".weba" => be == "audio/webm", - ".ogg" => be == "audio/vorbis", - ".spx" => be == "audio/speex", - ".html" => be == "text/html", - ) - end - end - - describe Utopia::Static::MimeTypeLoader do - it "expands explicit extension mappings" do - extensions = subject.extensions_for([["example", "application/example"]]) - - expect(extensions).to have_keys( - ".example" => be == "application/example", - ) - end - - it "rejects unknown file extensions" do - expect do - subject.extensions_for(["not-a-real-extension"]) - end.to raise_exception(subject::ExpansionError, message: be =~ /Unknown file extension/) - end - - it "rejects unsupported definitions" do - expect do - subject.extensions_for([Object.new]) - end.to raise_exception(subject::ExpansionError, message: be =~ /Unsupported MIME type definition/) - end - - it "wraps errors while expanding named groups" do - expect do - subject.extensions_for([:missing], {}) - end.to raise_exception(subject::ExpansionError, message: be =~ /Error while processing :missing/) - end - end end diff --git a/test/utopia/static/mime_types.rb b/test/utopia/static/mime_types.rb new file mode 100644 index 00000000..20b8e5aa --- /dev/null +++ b/test/utopia/static/mime_types.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "utopia/static/mime_types" + +describe Utopia::Static::MIME_TYPES do + let(:extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:default])} + let(:script_extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:scripts])} + + it "groups script extensions" do + expect(script_extensions).to have_keys( + ".js" => be == "text/javascript", + ".mjs" => be == "text/javascript", + ".wasm" => be == "application/wasm", + ) + end + + it "should give the correct mime type" do + expect(extensions).to have_keys( + ".txt" => be == "text/plain", + ".mjs" => be == "text/javascript", + ".wasm" => be == "application/wasm", + ".webm" => be == "video/webm", + ".weba" => be == "audio/webm", + ".ogg" => be == "audio/vorbis", + ".spx" => be == "audio/speex", + ".html" => be == "text/html", + ) + end +end + +describe Utopia::Static::MimeTypeLoader do + it "expands explicit extension mappings" do + extensions = subject.extensions_for([["example", "application/example"]]) + + expect(extensions).to have_keys( + ".example" => be == "application/example", + ) + end + + it "rejects unknown file extensions" do + expect do + subject.extensions_for(["not-a-real-extension"]) + end.to raise_exception(subject::ExpansionError, message: be =~ /Unknown file extension/) + end + + it "rejects unsupported definitions" do + expect do + subject.extensions_for([Object.new]) + end.to raise_exception(subject::ExpansionError, message: be =~ /Unsupported MIME type definition/) + end + + it "wraps errors while expanding named groups" do + expect do + subject.extensions_for([:missing], {}) + end.to raise_exception(subject::ExpansionError, message: be =~ /Error while processing :missing/) + end +end From dbfe208c67c1e9ae72279bccec777f8475816d5f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 13:19:27 +1200 Subject: [PATCH 4/5] Use concise MIME type test description. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- test/utopia/static/mime_types.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/utopia/static/mime_types.rb b/test/utopia/static/mime_types.rb index 20b8e5aa..db2a3381 100644 --- a/test/utopia/static/mime_types.rb +++ b/test/utopia/static/mime_types.rb @@ -5,9 +5,10 @@ require "utopia/static/mime_types" -describe Utopia::Static::MIME_TYPES do - let(:extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:default])} - let(:script_extensions) {Utopia::Static::MimeTypeLoader.extensions_for(subject[:scripts])} +describe "Utopia::Static::MIME_TYPES" do + let(:mime_types) {Utopia::Static::MIME_TYPES} + let(:extensions) {Utopia::Static::MimeTypeLoader.extensions_for(mime_types[:default])} + let(:script_extensions) {Utopia::Static::MimeTypeLoader.extensions_for(mime_types[:scripts])} it "groups script extensions" do expect(script_extensions).to have_keys( From d6ce790b09345e89d7b296f45037ca82d4f60be5 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 13:23:30 +1200 Subject: [PATCH 5/5] Separate local file tests. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- test/utopia/static.rb | 53 ---------------------------- test/utopia/static/local_file.rb | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 test/utopia/static/local_file.rb diff --git a/test/utopia/static.rb b/test/utopia/static.rb index 871fdb05..bd567de2 100755 --- a/test/utopia/static.rb +++ b/test/utopia/static.rb @@ -280,57 +280,4 @@ end end end - - describe Utopia::Static::LocalFile do - it "uses a consistent metadata snapshot" do - Dir.mktmpdir do |directory| - path = File.join(directory, "test.txt") - File.write(path, "Original") - - file = subject.new(path) - mtime_date = file.mtime_date - etag = file.etag - - File.write(path, "Updated content") - - expect(file.bytesize).to be == 8 - expect(file.mtime_date).to be == mtime_date - expect(file.etag).to be == etag - end - end - - it "includes subsecond modification time in entity tags" do - Dir.mktmpdir do |directory| - path = File.join(directory, "test.txt") - File.write(path, "Content") - - seconds = Time.now.to_i - 1 - first_mtime = Time.at(seconds, 100_000_000, :nanosecond) - second_mtime = Time.at(seconds, 200_000_000, :nanosecond) - - File.utime(first_mtime, first_mtime, path) - first = subject.new(path) - - File.utime(second_mtime, second_mtime, path) - second = subject.new(path) - - expect(first.mtime_date).to be == second.mtime_date - expect(first.etag).not.to be == second.etag - end - end - - it "matches strong entity tags for range requests" do - Dir.mktmpdir do |directory| - path = File.join(directory, "test.txt") - File.write(path, "Content") - - file = subject.new(path) - file.instance_variable_set(:@etag, '"strong"') - - expect(file.send(:if_range?, '"strong"')).to be == true - expect(file.send(:if_range?, '"different"')).to be == false - end - end - end - end diff --git a/test/utopia/static/local_file.rb b/test/utopia/static/local_file.rb new file mode 100644 index 00000000..76de1879 --- /dev/null +++ b/test/utopia/static/local_file.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "tmpdir" + +require "utopia/static/local_file" + +describe Utopia::Static::LocalFile do + it "uses a consistent metadata snapshot" do + Dir.mktmpdir do |directory| + path = File.join(directory, "test.txt") + File.write(path, "Original") + + file = subject.new(path) + mtime_date = file.mtime_date + etag = file.etag + + File.write(path, "Updated content") + + expect(file.bytesize).to be == 8 + expect(file.mtime_date).to be == mtime_date + expect(file.etag).to be == etag + end + end + + it "includes subsecond modification time in entity tags" do + Dir.mktmpdir do |directory| + path = File.join(directory, "test.txt") + File.write(path, "Content") + + seconds = Time.now.to_i - 1 + first_mtime = Time.at(seconds, 100_000_000, :nanosecond) + second_mtime = Time.at(seconds, 200_000_000, :nanosecond) + + File.utime(first_mtime, first_mtime, path) + first = subject.new(path) + + File.utime(second_mtime, second_mtime, path) + second = subject.new(path) + + expect(first.mtime_date).to be == second.mtime_date + expect(first.etag).not.to be == second.etag + end + end + + it "matches strong entity tags for range requests" do + Dir.mktmpdir do |directory| + path = File.join(directory, "test.txt") + File.write(path, "Content") + + file = subject.new(path) + file.instance_variable_set(:@etag, '"strong"') + + expect(file.send(:if_range?, '"strong"')).to be == true + expect(file.send(:if_range?, '"different"')).to be == false + end + end +end