Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Api
module V1
module Beacons
module Beacon
class BaseController < Api::V1::BaseController
include Api::BeaconAuthentication
end
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/api/v1/beacon/files_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Api
module V1
module Beacon
class FilesController < Beacon::BaseController
include ActiveStorage::Streaming

def show
blob = Current.beacon.accessible_blobs.find(params[:id])

if request.headers["Range"].present?
send_blob_byte_range_data(blob, request.headers["Range"])
else
send_blob_stream(blob, disposition: :attachment)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Api
module V1
module Beacons
class StatusesController < Beacons::BaseController
module Beacon
class StatusesController < Beacon::BaseController
def show
render json: {
status: "ok",
Expand Down
10 changes: 10 additions & 0 deletions app/models/beacon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,14 @@
def revoked?
revoked_at.present?
end

def accessible_blobs
ActiveStorage::Blob
.joins(:attachments)
.where(active_storage_attachments: { record_type: "Topic", name: "documents" })
.joins("INNER JOIN topics ON topics.id = active_storage_attachments.record_id")
.joins("INNER JOIN beacon_topics ON beacon_topics.topic_id = topics.id")
.where(beacon_topics: { beacon_id: id })
end
end

Check failure on line 61 in app/models/beacon.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingEmptyLines: 1 trailing blank lines detected.
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@
namespace :v1 do
resources :tags, only: %i[index show]

namespace :beacons do
namespace :beacon do
resource :status, only: :show
resources :files, only: :show
end
end
end
Expand Down
52 changes: 52 additions & 0 deletions spec/models/beacon_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,56 @@
expect(beacon).to be_revoked
end
end

describe "#accessible_blobs" do
let(:beacon) { create(:beacon) }
let(:other_beacon) { create(:beacon) }
let(:topic_with_documents) { create(:topic, :with_documents) }
let(:other_topic_with_documents) { create(:topic, :with_documents) }

before do
create(:beacon_topic, beacon: beacon, topic: topic_with_documents)
create(:beacon_topic, beacon: other_beacon, topic: other_topic_with_documents)
end

it "returns blobs from topics associated with the beacon" do
document_blob = topic_with_documents.documents.first.blob

expect(beacon.accessible_blobs).to include(document_blob)
end

it "does not return blobs from topics associated with other beacons" do
other_document_blob = other_topic_with_documents.documents.first.blob

expect(beacon.accessible_blobs).not_to include(other_document_blob)
end

it "only returns document attachments" do
topic = create(:topic)
create(:beacon_topic, beacon: beacon, topic: topic)

topic.documents.attach(
io: StringIO.new("test"),
filename: "document.pdf",
content_type: "application/pdf"
)

topic.files.attach(
io: StringIO.new("test"),
filename: "file.txt",
content_type: "text/plain"
)

document_blob = topic.documents.first.blob
file_blob = topic.files.first.blob

expect(beacon.accessible_blobs).to include(document_blob)
expect(beacon.accessible_blobs).not_to include(file_blob)
end

it "returns an ActiveRecord relation" do
expect(beacon.accessible_blobs).to be_a(ActiveRecord::Relation)
end
end
end

Check failure on line 142 in spec/models/beacon_spec.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingEmptyLines: 1 trailing blank lines detected.
12 changes: 6 additions & 6 deletions spec/requests/api/v1/beacon_authentication_spec.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
require "rails_helper"

RSpec.describe "Beacon Authentication", type: :request do
describe "GET /api/v1/beacons/status" do
describe "GET /api/v1/beacon/status" do
it "returns beacon info with a valid API key" do
beacon, raw_key = create_beacon_with_key(name: "Test Beacon")

get "/api/v1/beacons/status", headers: beacon_auth_headers(raw_key)
get "/api/v1/beacon/status", headers: beacon_auth_headers(raw_key)

expect(response).to have_http_status(:ok)
body = response.parsed_body
Expand All @@ -15,14 +15,14 @@
end

it "returns unauthorized when no authorization header is present" do
get "/api/v1/beacons/status"
get "/api/v1/beacon/status"

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body["error"]).to eq("Missing authorization header")
end

it "returns unauthorized with an invalid API key" do
get "/api/v1/beacons/status", headers: beacon_auth_headers("sk_live_invalid_key_000000000")
get "/api/v1/beacon/status", headers: beacon_auth_headers("sk_live_invalid_key_000000000")

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body["error"]).to eq("Invalid API key")
Expand All @@ -32,14 +32,14 @@
beacon, raw_key = create_beacon_with_key
beacon.revoke!

get "/api/v1/beacons/status", headers: beacon_auth_headers(raw_key)
get "/api/v1/beacon/status", headers: beacon_auth_headers(raw_key)

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body["error"]).to eq("API key has been revoked")
end

it "returns unauthorized with a malformed authorization header" do
get "/api/v1/beacons/status", headers: { "Authorization" => "Token sk_live_something" }
get "/api/v1/beacon/status", headers: { "Authorization" => "Token sk_live_something" }

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body["error"]).to eq("Missing authorization header")
Expand Down
69 changes: 69 additions & 0 deletions spec/requests/api/v1/beacon_files_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require "rails_helper"

RSpec.describe "Beacon Files", type: :request do
describe "GET /api/v1/beacon/files/:id" do
let(:beacon_with_key) { create_beacon_with_key(name: "Test Beacon") }
let(:beacon) { beacon_with_key.first }
let(:raw_key) { beacon_with_key.second }
let(:topic) { create(:topic, :with_documents) }
let(:document) { topic.documents.first }
let(:blob_id) { document.blob_id }

before do
create(:beacon_topic, beacon: beacon, topic: topic)
end

context "when downloading without Range header" do
it "returns the full file with attachment disposition" do
get "/api/v1/beacon/files/#{blob_id}", headers: beacon_auth_headers(raw_key)

expect(response).to have_http_status(:success)
expect(response.headers["Content-Disposition"]).to include("attachment")
end
end

context "when downloading with Range header" do
it "returns partial content with byte range" do
get "/api/v1/beacon/files/#{blob_id}",
headers: beacon_auth_headers(raw_key).merge("Range" => "bytes=0-99")

expect(response).to have_http_status(:partial_content)
end

it "handles multiple byte ranges" do
get "/api/v1/beacon/files/#{blob_id}",
headers: beacon_auth_headers(raw_key).merge("Range" => "bytes=0-99,200-299")

expect(response).to have_http_status(:partial_content)
end
end

context "when file does not exist" do
it "returns not found" do
get "/api/v1/beacon/files/99999", headers: beacon_auth_headers(raw_key)

expect(response).to have_http_status(:not_found)
end
end

context "when file belongs to a different beacon" do
let(:other_beacon_with_key) { create_beacon_with_key(name: "Other Beacon") }
let(:other_beacon) { other_beacon_with_key.first }
let(:other_raw_key) { other_beacon_with_key.second }

it "returns not found" do
get "/api/v1/beacon/files/#{blob_id}", headers: beacon_auth_headers(other_raw_key)

expect(response).to have_http_status(:not_found)
end
end

context "when beacon is not authenticated" do
it "returns unauthorized" do
get "/api/v1/beacon/files/#{blob_id}"

expect(response).to have_http_status(:unauthorized)
end
end
end
end
Loading