mirror of
https://github.com/we-promise/sure.git
synced 2026-05-09 05:35:00 +00:00
* feat(api): expose family exports * fix(api): harden family export review paths * fix(api): tighten family export review paths * fix(api): reject invalid family export params * fix(api): address family export review * fix(api): share uuid guard for exports
202 lines
6.6 KiB
Ruby
202 lines
6.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
class Api::V1::FamilyExportsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
@admin = users(:family_admin)
|
|
@member = users(:family_member)
|
|
@family = @admin.family
|
|
|
|
@admin.api_keys.active.destroy_all
|
|
@member.api_keys.active.destroy_all
|
|
|
|
@api_key = ApiKey.create!(
|
|
user: @admin,
|
|
name: "Test Read-Write Key",
|
|
scopes: [ "read_write" ],
|
|
display_key: "test_rw_#{SecureRandom.hex(8)}",
|
|
source: "web"
|
|
)
|
|
|
|
@read_only_api_key = ApiKey.create!(
|
|
user: @admin,
|
|
name: "Test Read Key",
|
|
scopes: [ "read" ],
|
|
display_key: "test_ro_#{SecureRandom.hex(8)}",
|
|
source: "mobile"
|
|
)
|
|
|
|
@member_api_key = ApiKey.create!(
|
|
user: @member,
|
|
name: "Member Read-Write Key",
|
|
scopes: [ "read_write" ],
|
|
display_key: "test_member_#{SecureRandom.hex(8)}",
|
|
source: "web"
|
|
)
|
|
|
|
redis = Redis.new
|
|
redis.del("api_rate_limit:#{@api_key.id}")
|
|
redis.del("api_rate_limit:#{@read_only_api_key.id}")
|
|
redis.del("api_rate_limit:#{@member_api_key.id}")
|
|
redis.close
|
|
end
|
|
|
|
test "lists family exports" do
|
|
completed_export = @family.family_exports.create!(status: "completed")
|
|
processing_export = @family.family_exports.create!(status: "processing")
|
|
|
|
get api_v1_family_exports_url, headers: api_headers(@read_only_api_key)
|
|
assert_response :success
|
|
|
|
json_response = JSON.parse(response.body)
|
|
export_ids = json_response["data"].map { |export| export["id"] }
|
|
|
|
assert_includes export_ids, completed_export.id
|
|
assert_includes export_ids, processing_export.id
|
|
assert_equal @family.family_exports.count, json_response["meta"]["total_count"]
|
|
end
|
|
|
|
test "shows a family export" do
|
|
export = @family.family_exports.create!(status: "completed")
|
|
export.export_file.attach(
|
|
io: StringIO.new("test zip content"),
|
|
filename: "test.zip",
|
|
content_type: "application/zip"
|
|
)
|
|
|
|
get api_v1_family_export_url(export), headers: api_headers(@read_only_api_key)
|
|
assert_response :success
|
|
|
|
json_response = JSON.parse(response.body)
|
|
assert_equal export.id, json_response["data"]["id"]
|
|
assert_equal "completed", json_response["data"]["status"]
|
|
assert_equal true, json_response["data"]["downloadable"]
|
|
assert_equal download_api_v1_family_export_path(export), json_response["data"]["download_path"]
|
|
assert_equal true, json_response["data"]["file"]["attached"]
|
|
assert_equal "application/zip", json_response["data"]["file"]["content_type"]
|
|
end
|
|
|
|
test "creates a family export job" do
|
|
assert_enqueued_with(job: FamilyDataExportJob) do
|
|
assert_difference("@family.family_exports.count") do
|
|
post api_v1_family_exports_url, headers: api_headers(@api_key)
|
|
end
|
|
end
|
|
|
|
assert_response :accepted
|
|
json_response = JSON.parse(response.body)
|
|
export = FamilyExport.find(json_response["data"]["id"])
|
|
|
|
assert_equal "pending", export.status
|
|
assert_equal @family.id, export.family_id
|
|
end
|
|
|
|
test "read-only key cannot create a family export" do
|
|
assert_no_difference("@family.family_exports.count") do
|
|
post api_v1_family_exports_url, headers: api_headers(@read_only_api_key)
|
|
end
|
|
|
|
assert_response :forbidden
|
|
assert_equal "insufficient_scope", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "create rejects unsupported params" do
|
|
assert_no_difference("@family.family_exports.count") do
|
|
post api_v1_family_exports_url,
|
|
params: { family_export: { status: "completed" } },
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
assert_equal "invalid_params", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "non-admin cannot access family exports" do
|
|
get api_v1_family_exports_url, headers: api_headers(@member_api_key)
|
|
assert_response :forbidden
|
|
|
|
assert_equal "forbidden", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "returns not found for another family's export" do
|
|
other_family = families(:empty)
|
|
other_export = other_family.family_exports.create!(status: "completed")
|
|
|
|
get api_v1_family_export_url(other_export), headers: api_headers(@read_only_api_key)
|
|
assert_response :not_found
|
|
|
|
assert_equal "record_not_found", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "returns not found for malformed export id" do
|
|
get api_v1_family_export_url("not-a-uuid"), headers: api_headers(@read_only_api_key)
|
|
assert_response :not_found
|
|
|
|
assert_equal "record_not_found", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "download returns not found for malformed export id" do
|
|
get download_api_v1_family_export_url("not-a-uuid"), headers: api_headers(@read_only_api_key)
|
|
assert_response :not_found
|
|
|
|
assert_equal "record_not_found", JSON.parse(response.body)["error"]
|
|
end
|
|
|
|
test "redirects completed export downloads to the attached file" do
|
|
export = @family.family_exports.create!(status: "completed")
|
|
export.export_file.attach(
|
|
io: StringIO.new("test zip content"),
|
|
filename: "test.zip",
|
|
content_type: "application/zip"
|
|
)
|
|
|
|
get download_api_v1_family_export_url(export), headers: api_headers(@read_only_api_key)
|
|
assert_response :redirect
|
|
assert_includes response.location, "/rails/active_storage/blobs/redirect/"
|
|
assert_includes response.location, "test.zip"
|
|
end
|
|
|
|
test "download returns conflict when export is not ready" do
|
|
export = @family.family_exports.create!(status: "processing")
|
|
|
|
get download_api_v1_family_export_url(export), headers: api_headers(@read_only_api_key)
|
|
assert_response :conflict
|
|
|
|
json_response = JSON.parse(response.body)
|
|
assert_equal "export_not_ready", json_response["error"]
|
|
end
|
|
|
|
test "download handles storage URL failures without leaking details" do
|
|
export = @family.family_exports.create!(status: "completed")
|
|
export.export_file.attach(
|
|
io: StringIO.new("test zip content"),
|
|
filename: "test.zip",
|
|
content_type: "application/zip"
|
|
)
|
|
|
|
Api::V1::FamilyExportsController.any_instance
|
|
.stubs(:rails_blob_url)
|
|
.raises(StandardError, "storage down")
|
|
|
|
get download_api_v1_family_export_url(export), headers: api_headers(@read_only_api_key)
|
|
assert_response :internal_server_error
|
|
|
|
json_response = JSON.parse(response.body)
|
|
assert_equal "internal_server_error", json_response["error"]
|
|
assert_equal "An unexpected error occurred", json_response["message"]
|
|
assert_not_includes response.body, "storage down"
|
|
end
|
|
|
|
test "requires authentication" do
|
|
get api_v1_family_exports_url
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
private
|
|
|
|
def api_headers(api_key)
|
|
{ "X-Api-Key" => api_key.plain_key }
|
|
end
|
|
end
|