mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
Restrict instance API to recent updates (#374)
This commit is contained in:
@@ -158,7 +158,8 @@ module PotatoMesh
|
||||
end
|
||||
|
||||
# Fetch all instance rows ready to be served by the API while handling
|
||||
# malformed rows gracefully.
|
||||
# malformed rows gracefully. The dataset is restricted to records updated
|
||||
# within the rolling window defined by PotatoMesh::Config.week_seconds.
|
||||
#
|
||||
# @return [Array<Hash>] list of cleaned instance payloads.
|
||||
def load_instances_for_api
|
||||
@@ -166,22 +167,30 @@ module PotatoMesh
|
||||
|
||||
db = open_database(readonly: true)
|
||||
db.results_as_hash = true
|
||||
now = Time.now.to_i
|
||||
min_last_update_time = now - PotatoMesh::Config.week_seconds
|
||||
sql = <<~SQL
|
||||
SELECT id, domain, pubkey, name, version, channel, frequency,
|
||||
latitude, longitude, last_update_time, is_private, signature
|
||||
FROM instances
|
||||
WHERE domain IS NOT NULL AND TRIM(domain) != ''
|
||||
AND pubkey IS NOT NULL AND TRIM(pubkey) != ''
|
||||
AND last_update_time IS NOT NULL AND last_update_time >= ?
|
||||
ORDER BY LOWER(domain)
|
||||
SQL
|
||||
|
||||
rows = with_busy_retry do
|
||||
db.execute(
|
||||
<<~SQL
|
||||
SELECT id, domain, pubkey, name, version, channel, frequency,
|
||||
latitude, longitude, last_update_time, is_private, signature
|
||||
FROM instances
|
||||
WHERE domain IS NOT NULL AND TRIM(domain) != ''
|
||||
AND pubkey IS NOT NULL AND TRIM(pubkey) != ''
|
||||
ORDER BY LOWER(domain)
|
||||
SQL
|
||||
)
|
||||
db.execute(sql, min_last_update_time)
|
||||
end
|
||||
|
||||
rows.each_with_object([]) do |row, memo|
|
||||
normalized = normalize_instance_row(row)
|
||||
memo << normalized if normalized
|
||||
next unless normalized
|
||||
|
||||
last_update_time = normalized["lastUpdateTime"]
|
||||
next unless last_update_time.is_a?(Integer) && last_update_time >= min_last_update_time
|
||||
|
||||
memo << normalized
|
||||
end
|
||||
rescue SQLite3::Exception => e
|
||||
warn_log(
|
||||
|
||||
99
web/spec/instances_spec.rb
Normal file
99
web/spec/instances_spec.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) 2025 l5yth
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "sqlite3"
|
||||
|
||||
RSpec.describe PotatoMesh::App::Instances do
|
||||
let(:application_class) { PotatoMesh::Application }
|
||||
let(:week_seconds) { PotatoMesh::Config.week_seconds }
|
||||
|
||||
# Execute the provided block with a configured SQLite connection.
|
||||
#
|
||||
# @param readonly [Boolean] whether the connection should be read-only.
|
||||
# @yieldparam db [SQLite3::Database] configured database handle.
|
||||
# @return [void]
|
||||
def with_db(readonly: false)
|
||||
db = SQLite3::Database.new(PotatoMesh::Config.db_path, readonly: readonly)
|
||||
db.busy_timeout = PotatoMesh::Config.db_busy_timeout_ms
|
||||
db.execute("PRAGMA foreign_keys = ON")
|
||||
yield db
|
||||
ensure
|
||||
db&.close
|
||||
end
|
||||
|
||||
before do
|
||||
FileUtils.mkdir_p(File.dirname(PotatoMesh::Config.db_path))
|
||||
application_class.init_db unless application_class.db_schema_present?
|
||||
with_db do |db|
|
||||
db.execute("DELETE FROM instances")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".load_instances_for_api" do
|
||||
it "only returns instances updated within the configured rolling window" do
|
||||
fixed_time = Time.utc(2025, 1, 15, 12, 0, 0)
|
||||
allow(Time).to receive(:now).and_return(fixed_time)
|
||||
|
||||
application_class.ensure_self_instance_record!
|
||||
|
||||
recent_timestamp = fixed_time.to_i - (week_seconds / 2)
|
||||
stale_timestamp = fixed_time.to_i - week_seconds - 60
|
||||
|
||||
with_db do |db|
|
||||
db.execute(
|
||||
"INSERT INTO instances (id, domain, pubkey, last_update_time, is_private) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
"recent-instance",
|
||||
"recent.mesh.test",
|
||||
PotatoMesh::Application::INSTANCE_PUBLIC_KEY_PEM,
|
||||
recent_timestamp,
|
||||
0,
|
||||
],
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO instances (id, domain, pubkey, last_update_time, is_private) VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
"stale-instance",
|
||||
"stale.mesh.test",
|
||||
PotatoMesh::Application::INSTANCE_PUBLIC_KEY_PEM,
|
||||
stale_timestamp,
|
||||
0,
|
||||
],
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO instances (id, domain, pubkey, is_private) VALUES (?, ?, ?, ?)",
|
||||
[
|
||||
"missing-instance",
|
||||
"missing.mesh.test",
|
||||
PotatoMesh::Application::INSTANCE_PUBLIC_KEY_PEM,
|
||||
0,
|
||||
],
|
||||
)
|
||||
end
|
||||
|
||||
payload = application_class.load_instances_for_api
|
||||
domains = payload.map { |row| row["domain"] }
|
||||
lower_bound = fixed_time.to_i - week_seconds
|
||||
|
||||
expect(domains).to include("recent.mesh.test")
|
||||
expect(domains).to include(application_class.app_constant(:INSTANCE_DOMAIN))
|
||||
expect(domains).not_to include("stale.mesh.test")
|
||||
expect(domains).not_to include("missing.mesh.test")
|
||||
expect(payload.all? { |row| row["lastUpdateTime"] >= lower_bound }).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user