Compare commits

..
Author SHA1 Message Date
Jeremy Daer ae7a34113f Block IPv6 encapsulation and CGNAT ranges in SSRF guard
Add NAT64 (64:ff9b::/96), 6to4 (2002::/16), and CGNAT shared address
space (100.64.0.0/10) to the private-network guard. These ranges can
smuggle requests toward internal networks and were not covered by the
existing predicates, leaving an SSRF bypass. Introduce DISALLOWED_RANGES
and OR it into private_ip?, preserving LOCAL_IP behavior.

Ref: weekend run #3873122 (IPv6 encapsulation SSRF ranges)
2026-07-20 02:17:12 -07:00
22 changed files with 43 additions and 352 deletions

No files matched your search

-13
View File
@@ -67,17 +67,6 @@ jobs:
CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}" CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}"
echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT" echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT"
- name: Compute app version
id: version
shell: bash
run: |
set -eu
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
else
echo "version=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
fi
- name: Extract Docker metadata (tags, labels) with arch suffix - name: Extract Docker metadata (tags, labels) with arch suffix
id: meta id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
@@ -105,8 +94,6 @@ jobs:
build-args: | build-args: |
OCI_SOURCE=${{ env.SOURCE_URL }} OCI_SOURCE=${{ env.SOURCE_URL }}
OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }} OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }}
APP_VERSION=${{ steps.version.outputs.version }}
GIT_REVISION=${{ github.sha }}
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
-3
View File
@@ -66,9 +66,6 @@ ENV HTTP_WRITE_TIMEOUT=300
COPY --from=build --chown=rails:rails /usr/local/bundle /usr/local/bundle COPY --from=build --chown=rails:rails /usr/local/bundle /usr/local/bundle
COPY --from=build --chown=rails:rails /rails /rails COPY --from=build --chown=rails:rails /rails /rails
# Install ONCE backup/restore hooks
COPY --chmod=755 hooks /hooks
# Set version and revision # Set version and revision
ARG APP_VERSION ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION ENV APP_VERSION=$APP_VERSION
-4
View File
@@ -53,7 +53,3 @@ If you'd rather run the Docker image yourself, you can read more about that in t
You are welcome - and encouraged - to modify Campfire to your liking. You are welcome - and encouraged - to modify Campfire to your liking.
Please see our [development guide](docs/development.md) for how to get Campfire set up for local development. Please see our [development guide](docs/development.md) for how to get Campfire set up for local development.
## Security
See [SECURITY.md](SECURITY.md) for how to report a vulnerability and a description of our trust model.
-22
View File
@@ -1,22 +0,0 @@
# Security
Report vulnerabilities through our [HackerOne program](https://hackerone.com/basecamp) or at
https://github.com/basecamp/once-campfire/security. Note that Campfire is in scope for our security
program, but is not bounty eligible.
## Trust model
Campfire is self-hosted and single-tenant, so the administrator is the server operator,
with shell, network, and database access already. Anything requiring the administrator
role grants nothing they do not already have, and is not a vulnerability.
We do want reports of anything a **non-administrator** can reach, and of any credential or
network path held by the Campfire process but not by the operator's own shell.
## Intentional behavior
Bot webhook URLs are unrestricted: an administrator can point one at any address,
including internal ones, because operators legitimately wire bots to their own services.
Link unfurling is different because any member can trigger it by pasting a URL, so it
validates destinations through `RestrictedHTTP::PrivateNetworkGuard`. The difference is who
picks the destination.
+8 -4
View File
@@ -8,8 +8,9 @@ class Accounts::LogosController < ApplicationController
if stale?(etag: Current.account) if stale?(etag: Current.account)
expires_in 5.minutes, public: true, stale_while_revalidate: 1.week expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
if (logo_variant = Current.account&.logo_variant(logo_size)) if Current.account&.logo&.attached?
send_png_file ActiveStorage::Blob.service.path_for(logo_variant.key) logo = Current.account.logo.variant(logo_variant).processed
send_png_file ActiveStorage::Blob.service.path_for(logo.key)
else else
send_stock_icon send_stock_icon
end end
@@ -22,6 +23,9 @@ class Accounts::LogosController < ApplicationController
end end
private private
LARGE_SQUARE_PNG_VARIANT = { resize_to_limit: [ 512, 512 ], format: :png }
SMALL_SQUARE_PNG_VARIANT = { resize_to_limit: [ 192, 192 ], format: :png }
def send_png_file(path) def send_png_file(path)
send_file path, content_type: "image/png", disposition: :inline send_file path, content_type: "image/png", disposition: :inline
end end
@@ -34,8 +38,8 @@ class Accounts::LogosController < ApplicationController
end end
end end
def logo_size def logo_variant
small_logo? ? :small : :large small_logo? ? SMALL_SQUARE_PNG_VARIANT : LARGE_SQUARE_PNG_VARIANT
end end
def small_logo? def small_logo?
+4 -1
View File
@@ -9,7 +9,8 @@ class Users::AvatarsController < ApplicationController
if stale?(etag: @user) if stale?(etag: @user)
expires_in 30.minutes, public: true, stale_while_revalidate: 1.week expires_in 30.minutes, public: true, stale_while_revalidate: 1.week
if (avatar_variant = @user.avatar_variant) if @user.avatar.attached?
avatar_variant = @user.avatar.variant(SQUARE_WEBP_VARIANT).processed
send_webp_blob_file avatar_variant.key send_webp_blob_file avatar_variant.key
elsif @user.bot? elsif @user.bot?
render_default_bot render_default_bot
@@ -25,6 +26,8 @@ class Users::AvatarsController < ApplicationController
end end
private private
SQUARE_WEBP_VARIANT = { resize_to_limit: [ 512, 512 ], format: :webp }
def send_webp_blob_file(key) def send_webp_blob_file(key)
send_file ActiveStorage::Blob.service.path_for(key), content_type: "image/webp", disposition: :inline send_file ActiveStorage::Blob.service.path_for(key), content_type: "image/webp", disposition: :inline
end end
+1 -9
View File
@@ -1,14 +1,6 @@
class Account < ApplicationRecord class Account < ApplicationRecord
include Joinable include Joinable
has_one_attached :logo do |attachable| has_one_attached :logo
attachable.variant :large, resize_to_limit: [ 512, 512 ], format: :png
attachable.variant :small, resize_to_limit: [ 192, 192 ], format: :png
end
has_json :settings, restrict_room_creation_to_administrators: false has_json :settings, restrict_room_creation_to_administrators: false
def logo_variant(size)
logo.variant(size).processed if logo.variable?
end
end end
+1 -7
View File
@@ -2,9 +2,7 @@ module User::Avatar
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
has_one_attached :avatar do |attachable| has_one_attached :avatar
attachable.variant :square, resize_to_limit: [ 512, 512 ], format: :webp
end
end end
class_methods do class_methods do
@@ -16,8 +14,4 @@ module User::Avatar
def avatar_token def avatar_token
signed_id(purpose: :avatar) signed_id(purpose: :avatar)
end end
def avatar_variant
avatar.variant(:square).processed if avatar.variable?
end
end end
-2
View File
@@ -24,8 +24,6 @@ class Webhook < ApplicationRecord
Net::HTTP::Post.new(uri, "Content-Type" => "application/json").tap { |request| request.body = payload } Net::HTTP::Post.new(uri, "Content-Type" => "application/json").tap { |request| request.body = payload }
end end
# No PrivateNetworkGuard, unlike Opengraph::Fetch: only an administrator sets this URL,
# and operators legitimately point bots at their own internal services.
def http def http
Net::HTTP.new(uri.host, uri.port).tap do |http| Net::HTTP.new(uri.host, uri.port).tap do |http|
http.use_ssl = (uri.scheme == "https") http.use_ssl = (uri.scheme == "https")
+1 -1
View File
@@ -1,2 +1,2 @@
Rails.application.config.app_version = ENV["APP_VERSION"].presence || ENV["GIT_REVISION"].presence || "0" Rails.application.config.app_version = ENV.fetch("APP_VERSION", "0")
Rails.application.config.git_revision = ENV["GIT_REVISION"] Rails.application.config.git_revision = ENV["GIT_REVISION"]
-9
View File
@@ -1,9 +0,0 @@
# Disable unfuzzed libvips operations.
#
# To block loaders we need to call `Vips.block` after Rails and image_processing set their
# defaults. Force the order of operations by autoloading the file now.
ActiveStorage::Transformers::Vips
Vips.block_untrusted(true)
Vips.block("VipsForeignLoadOpenslide", true) # prevent sqlite segfault in forked parallel workers
Rails.application.config.active_storage.variable_content_types -=
%w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ]
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -euo pipefail
db_name="${RAILS_ENV:-production}.sqlite3"
snapshot="/rails/storage/backups/$db_name"
db="/rails/storage/db/$db_name"
if [ -f "$snapshot" ]; then
mkdir -p "$(dirname "$db")"
cp "$snapshot" "$db"
rm -f "$db-wal" "$db-shm"
fi
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
exec /rails/script/admin/prepare-backup
+10 -53
View File
@@ -1,4 +1,3 @@
require "ipaddr"
require "resolv" require "resolv"
module RestrictedHTTP module RestrictedHTTP
@@ -7,32 +6,15 @@ module RestrictedHTTP
module PrivateNetworkGuard module PrivateNetworkGuard
extend self extend self
# IPv4 special-use ranges (RFC 5735/6890) not already covered by the LOCAL_IP = IPAddr.new("0.0.0.0/8") # "This" network
# private?/loopback?/link_local? predicates in #disallowed_ipv4?.
DISALLOWED_IPV4 = %w[
0.0.0.0/8 100.64.0.0/10 192.0.0.0/24 192.0.2.0/24 192.88.99.0/24
198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
].map { |cidr| IPAddr.new(cidr) }.freeze
# IPv6 special-use ranges not caught by the predicates. 6to4 (2002::/16) and # IPv6 transition/encapsulation and shared-address ranges that can smuggle
# Teredo (2001::/32) are deprecated transition mechanisms with no legitimate # requests toward internal networks and must never be reachable.
# fetch target, so they are blocked outright. ULA (fc00::/7, incl. the AWS DISALLOWED_RANGES = [
# IMDSv6 address fd00:ec2::254), link-local, and loopback are covered by the IPAddr.new("64:ff9b::/96"), # NAT64 (RFC 6052) — embeds IPv4 in IPv6
# predicates in #disallowed_ipv6?. IPAddr.new("2002::/16"), # 6to4 (RFC 3056) — embeds IPv4 in IPv6
DISALLOWED_IPV6 = %w[ IPAddr.new("100.64.0.0/10") # CGNAT / shared address space (RFC 6598)
::/128 100::/64 2001::/32 2001:2::/48 2001:db8::/32 2002::/16 ]
fec0::/10 ff00::/8
].map { |cidr| IPAddr.new(cidr) }.freeze
# NAT64 prefixes: the well-known prefix (RFC 6052/6146) and the local-use
# prefix (RFC 8215). An address here embeds an IPv4 target in its low 32
# bits; we extract it and re-check against the IPv4 rules so NAT64 to a
# public address still resolves while NAT64 to an internal address is
# blocked.
NAT64_PREFIXES = [
IPAddr.new("64:ff9b::/96"),
IPAddr.new("64:ff9b:1::/48")
].freeze
def resolve(hostname) def resolve(hostname)
Resolv.getaddress(hostname).tap do |ip| Resolv.getaddress(hostname).tap do |ip|
@@ -41,36 +23,11 @@ module RestrictedHTTP
end end
def private_ip?(ip) def private_ip?(ip)
ipaddr = IPAddr.new(ip) IPAddr.new(ip).then do |ipaddr|
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? || ipaddr.ipv4_mapped? || ipaddr.ipv4_compat? || LOCAL_IP.include?(ipaddr) || DISALLOWED_RANGES.any? { |range| range.include?(ipaddr) }
# DNS never legitimately returns these embedded forms, so block them all
# regardless of the address they wrap.
if ipaddr.ipv4_mapped? || ipaddr.ipv4_compat?
true
elsif ipaddr.ipv4?
disallowed_ipv4?(ipaddr)
elsif NAT64_PREFIXES.any? { |prefix| prefix.include?(ipaddr) }
disallowed_ipv4?(embedded_ipv4(ipaddr))
else
disallowed_ipv6?(ipaddr)
end end
rescue IPAddr::InvalidAddressError rescue IPAddr::InvalidAddressError
true true
end end
private
def disallowed_ipv4?(ipaddr)
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
DISALLOWED_IPV4.any? { |range| range.include?(ipaddr) }
end
def disallowed_ipv6?(ipaddr)
ipaddr.private? || ipaddr.loopback? || ipaddr.link_local? ||
DISALLOWED_IPV6.any? { |range| range.include?(ipaddr) }
end
def embedded_ipv4(ipaddr)
IPAddr.new([ ipaddr.to_i & 0xffffffff ].pack("N").unpack("C4").join("."))
end
end end
end end
@@ -30,13 +30,6 @@ class Accounts::LogosControllerTest < ActionDispatch::IntegrationTest
assert_valid_png_response size: 192 assert_valid_png_response size: 192
end end
test "show stock when custom logo cannot be resized" do
accounts(:signal).update! logo: fixture_file_upload("pixel.bmp", "image/bmp")
get account_logo_url
assert_valid_png_response size: 512
end
test "destroy" do test "destroy" do
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg") accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
@@ -18,14 +18,6 @@ class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
assert_equal "image/webp", @response.content_type assert_equal "image/webp", @response.content_type
end end
test "show initials when image cannot be resized" do
users(:kevin).update! avatar: fixture_file_upload("pixel.bmp", "image/bmp")
get user_avatar_url(users(:kevin).avatar_token)
assert_response :success
assert_select "text", text: "K"
end
test "show image with invalid token responds 404" do test "show image with invalid token responds 404" do
get user_avatar_url("not-a-valid-token") get user_avatar_url("not-a-valid-token")
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 B

@@ -64,48 +64,24 @@ class RestrictedHTTP::PrivateNetworkGuardTest < ActiveSupport::TestCase
assert_private_ip "::93.184.216.34" assert_private_ip "::93.184.216.34"
end end
test "private_ip? returns true for carrier-grade NAT addresses (RFC6598)" do # IPv6/encapsulation and shared-address ranges (SSRF bypass prevention)
test "private_ip? returns true for NAT64 addresses" do
assert_private_ip "64:ff9b::a00:1" # NAT64-embedded 10.0.0.1
end
test "private_ip? returns true for 6to4 addresses" do
assert_private_ip "2002::1"
assert_private_ip "2002:0a00:0001::" # 6to4-embedded 10.0.0.1
end
test "private_ip? returns true for CGNAT / shared address space" do
assert_private_ip "100.64.0.1" assert_private_ip "100.64.0.1"
assert_private_ip "100.127.255.255" assert_private_ip "100.127.255.255"
end end
test "private_ip? returns true for NAT64 addresses embedding a private IPv4" do test "private_ip? returns false for public addresses outside the new ranges" do
assert_private_ip "64:ff9b::a9fe:a9fe" # NAT64 -> 169.254.169.254 (AWS metadata) assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("93.184.216.34")
assert_private_ip "64:ff9b::a00:5" # NAT64 -> 10.0.0.5
end
test "private_ip? returns true for local-use NAT64 addresses embedding a private IPv4 (RFC8215)" do
assert_private_ip "64:ff9b:1::a00:1" # local-use NAT64 -> 10.0.0.1
end
test "private_ip? returns false for local-use NAT64 addresses embedding a public IPv4 (RFC8215)" do
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("64:ff9b:1::808:808") # -> 8.8.8.8
end
test "private_ip? returns false for NAT64 addresses embedding a public IPv4" do
# DNS64 legitimately synthesizes these for public sites on IPv6-only hosts.
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("64:ff9b::808:808") # -> 8.8.8.8
end
test "private_ip? returns true for 6to4 and Teredo transition addresses" do
assert_private_ip "2002:a9fe:a9fe::" # 6to4 embedding 169.254.169.254
assert_private_ip "2001::1" # Teredo
end
test "private_ip? returns true for IPv6 loopback, ULA, and link-local" do
assert_private_ip "::1"
assert_private_ip "fd00:ec2::254" # AWS IMDSv6 (ULA)
assert_private_ip "fe80::1"
end
test "private_ip? returns true for IPv6 multicast, documentation, and benchmarking ranges" do
assert_private_ip "ff02::1"
assert_private_ip "2001:db8::1"
assert_private_ip "2001:2::1" # benchmarking (RFC5180), matches 198.18.0.0/15
end
test "private_ip? returns false for public IPv6 addresses" do
assert_not RestrictedHTTP::PrivateNetworkGuard.private_ip?("2606:4700:4700::1111")
end end
test "private_ip? returns true for invalid addresses" do test "private_ip? returns true for invalid addresses" do
-118
View File
@@ -1,118 +0,0 @@
require "test_helper"
require "vips"
require "tempfile"
# libvips selects a loader from a file's actual bytes, not from its declared content type. These
# tests pin which loader is selected for each file type under the app's configured loader policy
# (config/initializers/vips.rb).
class VipsLoaderPolicyTest < ActiveSupport::TestCase
# Header bytes are enough for libvips to identify a format; native types are encoded live, exotic
# ones are represented by their magic bytes.
FTYP_AVIF = "\x00\x00\x00\x1cftypavif\x00\x00\x00\x00avifmif1miaf".b
FTYP_HEIC = "\x00\x00\x00\x1cftypheic\x00\x00\x00\x00heicmif1miaf".b
BMP = "BM" + [ 0, 0, 54 ].pack("V3") + "\x00" * 40
PSD = "8BPS" + [ 1 ].pack("n") + "\x00" * 26
ICO = "\x00\x00\x01\x00\x01\x00" + "\x00" * 16
SVG = %q(<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>)
test "loads PNG" do
assert_equal "VipsForeignLoadPngFile", loader_for(encode("png"))
end
test "loads GIF" do
assert_equal "VipsForeignLoadNsgifFile", loader_for(encode("gif"))
end
test "loads JPEG" do
assert_equal "VipsForeignLoadJpegFile", loader_for(encode("jpg"))
end
test "loads TIFF" do
assert_equal "VipsForeignLoadTiffFile", loader_for(encode("tif"))
end
test "loads WebP" do
assert_equal "VipsForeignLoadWebpFile", loader_for(encode("webp"))
end
test "loads AVIF" do
assert_equal "VipsForeignLoadHeifFile", loader_for(FTYP_AVIF)
end
test "loads HEIC" do
assert_equal "VipsForeignLoadHeifFile", loader_for(FTYP_HEIC)
end
test "denies BMP through magickload" do
assert_nil loader_for(BMP)
end
test "denies PSD through magickload" do
assert_nil loader_for(PSD)
end
test "denies ICO through magickload" do
assert_nil loader_for(ICO)
end
test "denies SVG through svgload" do
assert_nil loader_for(SVG)
end
test "denies OpenSlide files through openslideload" do
# OpenSlide files can segfault the embedded sqlite in forked parallel workers
assert_loader_blocked :openslideload, ".svs"
end
test "denies FITS files through fitsload" do
assert_loader_blocked :fitsload, ".fits"
end
test "denies MATLAB files through matload" do
assert_loader_blocked :matload, ".mat"
end
test "denies NIFTI files through niftiload" do
assert_loader_blocked :niftiload, ".nii"
end
test "denies RAW files through dcrawload" do
assert_loader_blocked :dcrawload, ".raw"
end
test "denies VIPS files through vipsload" do
assert_loader_blocked :vipsload, ".vips"
end
private
# Invoke a specific libvips loader directly and assert it is refused because the
# operation is blocked (rather than because the bytes are not a valid image).
def assert_loader_blocked(operation, extension)
Tempfile.create([ "blocked_loader", extension ], binmode: true) do |file|
file.write "not an image"
file.flush
error = assert_raises(Vips::Error) { Vips::Image.public_send(operation, file.path) }
actual = error.message.chomp
# note that exception message may include multiple errors on separate lines,
# so `^` and `$` anchors are used instead of `\A` and `\z`.
if actual =~ /^VipsOperation: class \"#{operation}\" not found$/
skip "libvips does not support #{operation} on this system"
end
assert_match(/^#{operation}: operation is blocked$/, actual)
end
end
def encode(ext)
Vips::Image.black(8, 8).add(128).cast("uchar").write_to_buffer(".#{ext}")
end
def loader_for(bytes)
Tempfile.create(%w[loader_probe .img], binmode: true) do |file|
file.write bytes
file.flush
Vips.vips_foreign_find_load(file.path)
end
end
end
-16
View File
@@ -15,20 +15,4 @@ class AccountTest < ActiveSupport::TestCase
accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "false" }) accounts(:signal).update!(settings: { "restrict_room_creation_to_administrators" => "false" })
assert_not accounts(:signal).reload.settings.restrict_room_creation_to_administrators? assert_not accounts(:signal).reload.settings.restrict_room_creation_to_administrators?
end end
test "logo_variant is a resized variant of a variable logo" do
accounts(:signal).logo.attach io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg"
assert_kind_of ActiveStorage::VariantWithRecord, accounts(:signal).logo_variant(:large)
end
test "logo_variant is nil when the logo cannot be resized" do
accounts(:signal).logo.attach io: file_fixture("pixel.bmp").open, filename: "pixel.bmp", content_type: "image/bmp"
assert_nil accounts(:signal).logo_variant(:large)
end
test "logo_variant is nil without a logo" do
assert_nil accounts(:signal).logo_variant(:large)
end
end end
+4 -4
View File
@@ -49,8 +49,8 @@ class Opengraph::FetchTest < ActiveSupport::TestCase
# to a resolved IP, not a hostname to re-resolve. # to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ] WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1") Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
TCPSocket.expects(:open).with { |*args, **| args.first == @url.host }.never TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
TCPSocket.expects(:open).with { |*args, **| args.first == "1.2.3.4" && args[1] == 443 }.throws(:dns_not_rebound) TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
assert_throws :dns_not_rebound do assert_throws :dns_not_rebound do
@fetch.fetch_document(@url) @fetch.fetch_document(@url)
@@ -66,8 +66,8 @@ class Opengraph::FetchTest < ActiveSupport::TestCase
# to a resolved IP, not a hostname to re-resolve. # to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ] WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1") Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
TCPSocket.expects(:open).with { |*args, **| args.first == @url.host }.never TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
TCPSocket.expects(:open).with { |*args, **| args.first == "1.2.3.4" && args[1] == 443 }.throws(:dns_not_rebound) TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
assert_throws :dns_not_rebound do assert_throws :dns_not_rebound do
@fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4") @fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4")
-19
View File
@@ -1,19 +0,0 @@
require "test_helper"
class User::AvatarTest < ActiveSupport::TestCase
test "avatar_variant is a resized variant of a variable avatar" do
users(:kevin).avatar.attach io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg"
assert_kind_of ActiveStorage::VariantWithRecord, users(:kevin).avatar_variant
end
test "avatar_variant is nil when the avatar cannot be resized" do
users(:kevin).avatar.attach io: file_fixture("pixel.bmp").open, filename: "pixel.bmp", content_type: "image/bmp"
assert_nil users(:kevin).avatar_variant
end
test "avatar_variant is nil without an avatar" do
assert_nil users(:kevin).avatar_variant
end
end