Skip to content
Merged
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ fm pages request get <path>
fm workspace auth login [--token-stdin]
fm workspace auth status
fm workspace auth logout
fm workspace openapi
fm workspace request get <api-v1-path>
fm workspace openapi [--raw]
fm workspace request get <api-v1-path> [--raw]
```

## Installation
Expand Down Expand Up @@ -138,12 +138,15 @@ have an approved SSO account. A `401 Unauthorized` response can mean that the
token is invalid, expired, or revoked, or that the account is not approved.
Workspace requests are GET-only and must stay under `/api/v1/`. Use
`fm workspace openapi` (or `fm workspace schema`) to fetch the machine-readable
OpenAPI specification for discovery and self-description.
OpenAPI specification for discovery and self-description. Both `openapi` and
`request get` accept `--raw` to output the verbatim response body without JSON
envelopes, suitable for redirection or piping.

```sh
fm workspace auth login
fm workspace request get /api/v1/me
fm workspace openapi
fm workspace openapi --raw > openapi.json
```

`publish` requires `--owner` and `--html-file`. `update` accepts an HTML
Expand Down
17 changes: 13 additions & 4 deletions lib/feedmob/cli/commands/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,26 @@ def service_name = 'pages'

class WorkspaceRequestGet < RequestGet
desc 'Perform an authenticated GET request against the FeedMob Workspace API'
option :raw, type: :boolean, default: false, desc: 'Write the response body verbatim (incompatible with --json)'

def call(path:, **)
validate_workspace_path!(path)
super
def call(path:, raw: false, **)
validate_workspace_path!(path, raw)
return super(path:) unless raw

credential = credential!(service)
response = runtime.client(service).request(method: :get, path:, token: credential.value, raw: true)
(@out || $stdout).write(response.data)
end

def service_name = 'workspace'

private

def validate_workspace_path!(path)
def validate_workspace_path!(path, raw)
if raw && FeedMob::CLI.json?
raise Error.new(code: 'invalid_input', message: '--raw cannot be combined with --json.')
end

return if path.to_s.start_with?('/api/v1/')

raise Error.new(
Expand Down
22 changes: 19 additions & 3 deletions lib/feedmob/cli/commands/workspace.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'json'
require_relative 'base'

module FeedMob
module CLI
module Commands
Expand All @@ -13,9 +13,25 @@ def service_name = 'workspace'

class WorkspaceOpenapi < WorkspaceBase
desc 'Fetch the OpenAPI schema for FeedMob Workspace'
option :raw, type: :boolean, default: false, desc: 'Write the response body verbatim (incompatible with --json)'

def call(raw: false, **)
if raw && FeedMob::CLI.json?
raise Error.new(code: 'invalid_input', message: '--raw cannot be combined with --json.')
end

def call(**)
credential = credential!(service)
if raw
response = runtime.client(service).request(
method: :get,
path: '/api/v1/openapi',
token: credential.value,
raw: true
)
(@out || $stdout).write(response.data)
return
end

response = runtime.client(service).request(
method: :get,
path: '/api/v1/openapi',
Expand All @@ -28,7 +44,7 @@ def call(**)
status: response.status,
response: response.data
},
message: "GET /api/v1/openapi returned HTTP #{response.status}."
message: JSON.pretty_generate(response.data)
)
end
end
Expand Down
100 changes: 100 additions & 0 deletions test/cli_commands_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,46 @@ def test_workspace_request_get_rejects_paths_outside_the_versioned_workspace_api
assert_empty workspace_client.requests
end

def test_workspace_request_get_raw_writes_verbatim_response
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
)
workspace_client = FakeClient.new(
[FeedMob::CLI::HTTP::Response.new(status: 200, headers: {}, data: '{"user":{"id":42}}')]
)
use_runtime(
credentials:,
clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client }
)

stdout, stderr, status = run_cli('workspace', 'request', 'get', '/api/v1/me', '--raw')

assert_equal 0, status
assert_empty stderr
assert_equal '{"user":{"id":42}}', stdout
assert_equal(
{ method: :get, path: '/api/v1/me', token: 'fmapat_workspace', raw: true },
workspace_client.requests.fetch(0)
)
end

def test_workspace_request_get_rejects_raw_combined_with_json
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
)
workspace_client = FakeClient.new
use_runtime(
credentials:,
clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client }
)

stdout, _stderr, status = run_cli('workspace', 'request', 'get', '/api/v1/me', '--raw', '--json')

assert_equal 1, status
assert_equal 'invalid_input', JSON.parse(stdout).dig('error', 'code')
assert_empty workspace_client.requests
end

def test_workspace_openapi_fetches_the_versioned_openapi_schema
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
Expand Down Expand Up @@ -275,6 +315,66 @@ def test_workspace_openapi_requires_workspace_credential
assert_empty workspace_client.requests
end

def test_workspace_openapi_raw_writes_verbatim_schema
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
)
workspace_client = FakeClient.new(
[FeedMob::CLI::HTTP::Response.new(status: 200, headers: {}, data: '{"openapi":"3.0.0"}')]
)
use_runtime(
credentials:,
clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client }
)

stdout, stderr, status = run_cli('workspace', 'openapi', '--raw')

assert_equal 0, status
assert_empty stderr
assert_equal '{"openapi":"3.0.0"}', stdout
assert_equal(
{ method: :get, path: '/api/v1/openapi', token: 'fmapat_workspace', raw: true },
workspace_client.requests.fetch(0)
)
end

def test_workspace_openapi_rejects_raw_combined_with_json
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
)
workspace_client = FakeClient.new
use_runtime(
credentials:,
clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client }
)

stdout, _stderr, status = run_cli('workspace', 'openapi', '--raw', '--json')

assert_equal 1, status
assert_equal 'invalid_input', JSON.parse(stdout).dig('error', 'code')
assert_empty workspace_client.requests
end

def test_workspace_openapi_pretty_prints_schema_in_human_mode
credentials = FakeCredentials.new(
credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain')
)
schema = { 'openapi' => '3.0.0', 'info' => { 'title' => 'FeedMob Admin API' } }
workspace_client = FakeClient.new(
[FeedMob::CLI::HTTP::Response.new(status: 200, headers: {}, data: schema)]
)
use_runtime(
credentials:,
clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client }
)

stdout, stderr, status = run_cli('workspace', 'openapi')

assert_equal 0, status
assert_empty stderr
assert_equal "#{JSON.pretty_generate(schema)}\n", stdout
end

def test_pixel_logout_revokes_the_remote_token_then_deletes_local_keychain_value
credentials = FakeCredentials.new
pixel_client = FakeClient.new(
Expand Down
Loading