Skip to content
Open
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
145 changes: 145 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"encoding/json"
"fmt"
"strings"
"testing"

coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"github.com/jfrog/jfrog-cli/utils/tests"
clientUtils "github.com/jfrog/jfrog-client-go/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var apiCli *coreTests.JfrogCli

func InitApiTests() {
initApiCli()
}

func initApiTest(t *testing.T) {
if !*tests.TestApi {
t.Skip("Skipping api command test. To run add the '-test.api=true' option.")
}
}

func initApiCli() {
if apiCli != nil {
return
}
*tests.JfrogUrl = clientUtils.AddTrailingSlashIfNeeded(*tests.JfrogUrl)
apiCli = coreTests.NewJfrogCli(execMain, "jfrog", authenticateApiCmd())
}

func authenticateApiCmd() string {
cred := fmt.Sprintf("--url=%s", *tests.JfrogUrl)
if *tests.JfrogAccessToken != "" {
cred += fmt.Sprintf(" --access-token=%s", *tests.JfrogAccessToken)
} else {
cred += fmt.Sprintf(" --user=%s --password=%s", *tests.JfrogUser, *tests.JfrogPassword)
}
return cred
}

// TestApiGetArtifactoryPing verifies a GET request to the Artifactory ping endpoint returns 200 with "OK".
func TestApiGetArtifactoryPing(t *testing.T) {
initApiTest(t)
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "artifactory/api/system/ping")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
assert.Contains(t, strings.TrimSpace(string(stdout)), "OK")
}

// TestApiGetAccessPing verifies a GET request to the Access ping endpoint returns 200 with JSON {"status":"UP"}.
func TestApiGetAccessPing(t *testing.T) {
initApiTest(t)
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "access/api/v1/system/ping")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
var result map[string]string
require.NoError(t, json.Unmarshal(stdout, &result))
assert.Equal(t, "UP", result["status"])
}

// TestApiVerbose verifies that --verbose writes request/response diagnostic lines to stderr.
func TestApiVerbose(t *testing.T) {
initApiTest(t)
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--verbose", "artifactory/api/system/ping")
require.NoError(t, err)
stderrStr := string(stderr)
assert.Contains(t, stderrStr, "* Request to")
assert.Contains(t, stderrStr, "> GET")
assert.Contains(t, stderrStr, "* Response")
// Status code is still written as the last stderr line.
lines := strings.Split(strings.TrimSpace(stderrStr), "\n")
assert.Equal(t, "200", lines[len(lines)-1])
assert.Contains(t, strings.TrimSpace(string(stdout)), "OK")
}

// TestApiNotFound verifies that a 404 response causes a non-zero exit.
func TestApiNotFound(t *testing.T) {
initApiTest(t)
err := apiCli.Exec("api", "artifactory/api/nosuchendpointxxx")
assert.Error(t, err)
}

// TestApiMethodPost verifies that --method=POST is forwarded to the server.
// Uses the AQL search endpoint, which requires POST.
func TestApiMethodPost(t *testing.T) {
initApiTest(t)
_, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--method=POST", `--data=items.find({})`, "artifactory/api/search/aql")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
}

// TestApiPostWithData verifies that --data sends a request body with a POST.
// Uses the AQL search endpoint, which requires a POST body.
func TestApiPostWithData(t *testing.T) {
initApiTest(t)
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--method=POST", `--data=items.find({})`, "artifactory/api/search/aql")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
var result map[string]interface{}
require.NoError(t, json.Unmarshal(stdout, &result))
_, hasResults := result["results"]
assert.True(t, hasResults)
}

// TestApiCustomHeader verifies that a custom --header value is accepted and the request succeeds.
func TestApiCustomHeader(t *testing.T) {
initApiTest(t)
_, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--header=X-Jfrog-Test: integration-test", "access/api/v1/system/ping")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
}

// TestApiMethodCaseInsensitive verifies that method names are normalized to uppercase.
func TestApiMethodCaseInsensitive(t *testing.T) {
initApiTest(t)
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--method=get", "artifactory/api/system/ping")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
assert.Contains(t, strings.TrimSpace(string(stdout)), "OK")
}

// TestApiLeadingSlashInPath verifies that a path without a leading slash is handled correctly.
func TestApiLeadingSlashInPath(t *testing.T) {
initApiTest(t)
// Without leading slash — the command should normalise it internally.
stdout, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "artifactory/api/system/ping")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
assert.Contains(t, strings.TrimSpace(string(stdout)), "OK")
}

// TestApiWithInputFile verifies that --input reads a request body from a file.
func TestApiWithInputFile(t *testing.T) {
initApiTest(t)
// Write an AQL query to a temp file and POST it to the AQL search endpoint.
payloadFile := tests.CreateTempFile(t, "items.find({})")
_, stderr, err := tests.GetCmdOutput(t, apiCli, "api", "--method=POST", "--input="+payloadFile, "artifactory/api/search/aql")
require.NoError(t, err)
assert.Equal(t, "200", strings.TrimSpace(string(stderr)))
}
4 changes: 2 additions & 2 deletions buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import (
huggingfaceCommands "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/huggingface"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/mvn"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/npm"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/pnpm"
containerutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/pnpm"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/terraform"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/yarn"
commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
Expand Down Expand Up @@ -68,13 +68,13 @@ import (
"github.com/jfrog/jfrog-cli/docs/buildtools/mvnconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/npmcommand"
"github.com/jfrog/jfrog-cli/docs/buildtools/npmconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/pnpmcommand"
nugetdocs "github.com/jfrog/jfrog-cli/docs/buildtools/nuget"
"github.com/jfrog/jfrog-cli/docs/buildtools/nugetconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/pipconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/pipenvconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/pipenvinstall"
"github.com/jfrog/jfrog-cli/docs/buildtools/pipinstall"
"github.com/jfrog/jfrog-cli/docs/buildtools/pnpmcommand"
"github.com/jfrog/jfrog-cli/docs/buildtools/pnpmconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/poetry"
"github.com/jfrog/jfrog-cli/docs/buildtools/poetryconfig"
Expand Down
2 changes: 1 addition & 1 deletion docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
"testing"
"time"

urfavecli "github.com/urfave/cli"
"github.com/jfrog/jfrog-client-go/utils/log"
urfavecli "github.com/urfave/cli"

tests2 "github.com/jfrog/jfrog-cli-artifactory/utils/tests"

Expand Down
62 changes: 62 additions & 0 deletions docs/general/api/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package api

import "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"

var Usage = []string{"api <endpoint-path>"}

func GetDescription() string {
return "Invoke a JFrog Platform HTTP API using the configured server URL and credentials (hostname and token are not passed manually; use 'jf config' or --url / --access-token / --server-id as usual). REST API reference: " + coreutils.JFrogHelpUrl + "jfrog-platform-documentation/rest-apis (OpenAPI bundles are not shipped with the CLI)."
}

func GetArguments() string {
return ` endpoint-path
API path on the platform host (for example: /access/api/v1/users or /artifactory/api/repositories). The configured platform base URL is prepended automatically.

EXAMPLES
# List users (Access API)
$ jf api /access/api/v2/users

# Get a user by name
$ jf api /access/api/v2/users/admin

# Create a user (POST JSON body from a file, or use -d/--data for an inline body — not both)
$ jf api /access/api/v2/users -X POST --input ./user.json -H "Content-Type: application/json"
$ jf api /access/api/v2/users -X POST -d '{"name":"admin"}' -H "Content-Type: application/json"

# Replace a user
$ jf api /access/api/v2/users/newuser -X PUT --input ./user.json -H "Content-Type: application/json"

# Delete a user
$ jf api -X DELETE /access/api/v2/users/tempuser

# List local repositories (Artifactory REST)
$ jf api /artifactory/api/repositories

# Create a local Maven repository
$ jf api /artifactory/api/repositories/my-maven-local -X PUT -H "Content-Type: application/json" --input ./repo-maven-local.json

# Update repository configuration
$ jf api /artifactory/api/repositories/libs-release -X POST -H "Content-Type: application/json" --input ./repo-config.json

# Delete a repository
$ jf api /artifactory/api/repositories/old-repo -X DELETE

# One Model GraphQL
$ jf api /onemodel/api/v1/graphql -X POST -H "Content-Type: application/json" --input ./graphql-query.json

# Set a request timeout (seconds)
$ jf api /artifactory/api/repositories --timeout 10

OUTPUT
The response body is written to standard output. The HTTP status code is written to standard error as a single line. Non-2xx responses still print the body and exit with status 1.

REFERENCES
Binary Management (Artifactory): https://docs.jfrog.com/artifactory/reference/
JFrog Security : https://docs.jfrog.com/security/reference/
Governance (AppTrust) : https://docs.jfrog.com/governance/reference/
Integrations : https://docs.jfrog.com/integrations/reference/
Project Management : https://docs.jfrog.com/projects/reference/
Platform Administration : https://docs.jfrog.com/administration/reference/
SEE ALSO
Use 'jf config' to add or select a server. Use --server-id to target a specific configuration.`
}
Loading
Loading