diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..42a5b4e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,86 @@ +name: Go Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + unit-tests: + name: Run Tests + runs-on: ubuntu-latest + + steps: + # Step 1: Check out the repository + - name: Checkout code + uses: actions/checkout@v3 + + # Step 2: Set up Go + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + # Step 3: Install dependencies + - name: Install dependencies + run: go mod tidy + + # Step 4: Run tests + - name: Run tests + run: go test ./... --short -v + + # Step 5: Upload test results (optional) + - name: Upload test results + if: ${{ always() }} + uses: actions/upload-artifact@v3 + with: + name: test-results + path: '**/test-report.xml' + + integration-tests: + name: Run Integration Tests + runs-on: ubuntu-latest + + services: + apicurio: + image: apicurio/apicurio-registry:3.0.5 + ports: + - 9080:8080 + options: >- + --env APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED=true + --env APICURIO_REST_DELETION_ARTIFACT_ENABLED=true + --env APICURIO_REST_DELETION_ARTIFACT_VERSION_ENABLED=true + --env APICURIO_REST_DELETION_GROUP_ENABLED=true + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v3 + + # Set up Go environment + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + # Install dependencies + - name: Install dependencies + run: go mod tidy + + # Wait for Apicurio Registry to start + - name: Wait for Apicurio Registry + run: | + for i in {1..30}; do + if curl -s http://localhost:9080/health || [ $i -eq 30 ]; then + break + fi + echo "Waiting for Apicurio Registry to be ready..." + sleep 2 + done + + # Run integration tests + - name: Run integration tests + run: go test ./... -run Integration -v \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cd879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Output of the build process +bin/ +build/ + +# Test binaries +*.test + +# Output of `go run` +*.out + +# Directories for dependency management +vendor/ + +# IDE and editor configuration files +.idea/ +*.iml +.vscode/ +*.swp + +# OS-specific files +.DS_Store +Thumbs.db + +# Logs and debugging files +*.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..22f7f4d --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Go-APICURIO-SDK + +[![Go Reference](https://pkg.go.dev/badge/github.com/mollie/go-apicurio-registry.svg)](https://pkg.go.dev/github.com/mollie/go-apicurio-registry) +[![Integration Tests](https://github.com/mollie/go-apicurio-registry/actions/workflows/tests.yml/badge.svg)](https://github.com/mollie/go-apicurio-registry/actions) + +**Go-APICURIO-SDK** is an open-source Go SDK for interacting with [Apicurio Schema Registry v3.0 APIs](https://www.apicur.io/). This library provides an idiomatic Go interface to manage schemas, validate data, and handle schema evolution in Go applications. It is designed to make it easy for developers to integrate the Apicurio Schema Registry into their Go-based microservices or event-driven systems. + +## 🚧 Work In Progress + +This SDK is currently under active development. Key features and improvements are being added incrementally. While contributions and feedback are welcome, please note that some APIs and features may change as the project evolves. + +## Features + +- **Schema Management**: Create, update, delete, and retrieve schemas. +- **Validation**: Validate payloads against registered schemas. +- **Schema Evolution**: Tools for managing schema compatibility and evolution. +- **Integration**: Works seamlessly with the Apicurio Schema Registry. + +## Getting Started + +### Prerequisites + +- Go 1.18+ installed on your system. +- Access to an Apicurio Schema Registry instance. + +### Installation + +To use the SDK, add it to your project using `go get`: + +```bash +go get github.com/mollie/go-apicurio-registry +``` + +### Development +Running Locally with Docker +This project includes a docker-compose.yml file for setting up a local Apicurio Schema Registry instance. Run the following command to start the registry: + +```bash +docker-compose up +``` + +### Running Tests + +The repository includes unit and integration tests. Use the following command to run all tests: + +```bash +go test ./... +``` + +## Contribution Guidelines +Contributions are welcome! Please see the CONTRIBUTING.md (to be added) for details on the process. + + +## License +This project is licensed under the Apache License 2.0. diff --git a/apis/admin.go b/apis/admin.go new file mode 100644 index 0000000..7d43c76 --- /dev/null +++ b/apis/admin.go @@ -0,0 +1,168 @@ +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "net/http" +) + +type AdminAPI struct { + Client *client.Client +} + +func NewAdminAPI(client *client.Client) *AdminAPI { + return &AdminAPI{ + Client: client, + } +} + +// ListGlobalRules Gets a list of all the currently configured global rules (if any). +// GET /admin/rules +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/listGlobalRules +func (api *AdminAPI) ListGlobalRules(ctx context.Context) ([]models.Rule, error) { + url := fmt.Sprintf("%s/admin/rules", api.Client.BaseURL) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var rules []models.Rule + if err := handleResponse(resp, http.StatusOK, &rules); err != nil { + return nil, err + } + + return rules, nil +} + +// CreateGlobalRule Creates a new global rule. +// POST /admin/rules +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/createGlobalRule +func (api *AdminAPI) CreateGlobalRule(ctx context.Context, rule models.Rule, level models.RuleLevel) error { + url := fmt.Sprintf("%s/admin/rules", api.Client.BaseURL) + + // Prepare the request body + body := models.CreateUpdateGlobalRuleRequest{ + RuleType: rule, + Config: level, + } + resp, err := api.executeRequest(ctx, http.MethodPost, url, body) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// DeleteAllGlobalRule Adds a rule to the list of globally configured rules. +// DELETE /admin/rules +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/deleteAllGlobalRules +func (api *AdminAPI) DeleteAllGlobalRule(ctx context.Context) error { + url := fmt.Sprintf("%s/admin/rules", api.Client.BaseURL) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// GetGlobalRule Returns information about the named globally configured rule. +// GET /admin/rules/{rule} +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/getGlobalRuleConfig +func (api *AdminAPI) GetGlobalRule(ctx context.Context, rule models.Rule) (models.RuleLevel, error) { + url := fmt.Sprintf("%s/admin/rules/%s", api.Client.BaseURL, rule) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + + var globalRule models.GlobalRuleResponse + if err := handleResponse(resp, http.StatusOK, &globalRule); err != nil { + return "", err + } + + return globalRule.Config, nil +} + +// UpdateGlobalRule Updates the configuration of the named globally configured rule. +// PUT /admin/rules/{rule} +// See https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/updateGlobalRuleConfig +func (api *AdminAPI) UpdateGlobalRule(ctx context.Context, rule models.Rule, level models.RuleLevel) error { + url := fmt.Sprintf("%s/admin/rules/%s", api.Client.BaseURL, rule) + + // Prepare the request body + body := models.CreateUpdateGlobalRuleRequest{ + RuleType: rule, + Config: level, + } + resp, err := api.executeRequest(ctx, http.MethodPut, url, body) + if err != nil { + return err + } + + var globalRule models.GlobalRuleResponse + if err := handleResponse(resp, http.StatusOK, &globalRule); err != nil { + return err + } + + return nil +} + +// DeleteGlobalRule Deletes the named globally configured rule. +// DELETE /admin/rules/{rule} +// See https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Global-rules/operation/deleteGlobalRule +func (api *AdminAPI) DeleteGlobalRule(ctx context.Context, rule models.Rule) error { + url := fmt.Sprintf("%s/admin/rules/%s", api.Client.BaseURL, rule) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// executeRequest handles the creation and execution of an HTTP request. +func (api *AdminAPI) executeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { + var reqBody []byte + var err error + contentType := "*/*" + + switch v := body.(type) { + case string: + reqBody = []byte(v) + contentType = "*/*" + case []byte: + reqBody = v + contentType = "*/*" + default: + contentType = "application/json" + reqBody, err = json.Marshal(body) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request body as JSON") + } + } + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(reqBody)) + if err != nil { + return nil, errors.Wrap(err, "failed to create HTTP request") + } + + // Set appropriate Content-Type header + if body != nil { + req.Header.Set("Content-Type", contentType) + } + + // Execute the request + resp, err := api.Client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to execute HTTP request") + } + + return resp, nil +} diff --git a/apis/admin_test.go b/apis/admin_test.go new file mode 100644 index 0000000..42f7671 --- /dev/null +++ b/apis/admin_test.go @@ -0,0 +1,416 @@ +package apis_test + +import ( + "context" + "encoding/json" + "github.com/mollie/go-apicurio-registry/apis" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +const ( + TitleBadRequest = "Bad request" + TitleInternalServerError = "Internal server error" + TitleNotFound = "Not found" + TitleConflict = "Conflict" +) + +func TestRulesAPI_ListGlobalRules(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockReferences := []models.Rule{models.RuleValidity, models.RuleCompatibility} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockReferences) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + result, err := api.ListGlobalRules(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result, 2) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + res, err := api.ListGlobalRules(context.Background()) + assert.Error(t, err) + assert.Nil(t, res) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestRulesAPI_CreateGlobalRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.CreateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusBadRequest, Title: TitleBadRequest}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + err := api.CreateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, apiErr.Status) + assert.Equal(t, TitleBadRequest, apiErr.Title) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusConflict, Title: TitleConflict}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + err := api.CreateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusConflict, apiErr.Status) + assert.Equal(t, TitleConflict, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + err := api.CreateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestRulesAPI_DeleteAllGlobalRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.DeleteAllGlobalRule(context.Background()) + assert.NoError(t, err) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.DeleteAllGlobalRule(context.Background()) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestRulesAPI_GetGlobalRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.GlobalRuleResponse{ + RuleType: models.RuleValidity, + Config: models.ValidityLevelFull, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + result, err := api.GetGlobalRule(context.Background(), models.RuleValidity) + assert.NoError(t, err) + assert.Equal(t, models.ValidityLevelFull, result) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + result, err := api.GetGlobalRule(context.Background(), models.RuleValidity) + assert.Error(t, err) + assert.Empty(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + result, err := api.GetGlobalRule(context.Background(), models.RuleValidity) + assert.Error(t, err) + assert.Empty(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) +} + +func TestRulesAPI_UpdateGlobalRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.GlobalRuleResponse{ + RuleType: models.RuleValidity, + Config: models.ValidityLevelFull, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.UpdateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + assert.NoError(t, err) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.UpdateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.UpdateGlobalRule(context.Background(), models.RuleValidity, models.ValidityLevelFull) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) + +} + +func TestRulesAPI_DeleteGlobalRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.DeleteGlobalRule(context.Background(), models.RuleValidity) + assert.NoError(t, err) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.DeleteGlobalRule(context.Background(), models.RuleValidity) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/admin/rules/") + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewAdminAPI(mockClient) + + err := api.DeleteGlobalRule(context.Background(), models.RuleValidity) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} diff --git a/apis/artifacts.go b/apis/artifacts.go new file mode 100644 index 0000000..d79378f --- /dev/null +++ b/apis/artifacts.go @@ -0,0 +1,445 @@ +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "io" + "net/http" +) + +type ArtifactsAPI struct { + Client *client.Client +} + +func NewArtifactsAPI(client *client.Client) *ArtifactsAPI { + return &ArtifactsAPI{ + Client: client, + } +} + +var ( + ErrArtifactNotFound = errors.New("artifact not found") + ErrMethodNotAllowed = errors.New("method not allowed or disabled on the server") + ErrInvalidInput = errors.New("input must be between 1 and 512 characters") +) + +// SearchArtifacts - Search for artifacts using the given filter parameters. +// Search for artifacts using the given filter parameters. +// See: +func (api *ArtifactsAPI) SearchArtifacts(ctx context.Context, params *models.SearchArtifactsParams) (*[]models.SearchedArtifact, error) { + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/search/artifacts%s", api.Client.BaseURL, query) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var result models.SearchArtifactsAPIResponse + if err := handleResponse(resp, http.StatusOK, &result); err != nil { + return nil, err + } + + return &result.Artifacts, nil +} + +// SearchArtifactsByContent searches for artifacts that match the provided content. +// Returns a paginated list of all artifacts with at least one version that matches the posted content. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/searchArtifactsByContent +func (api *ArtifactsAPI) SearchArtifactsByContent(ctx context.Context, content []byte, params *models.SearchArtifactsByContentParams) (*[]models.SearchedArtifact, error) { + // Convert params to query string + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/search/artifacts%s", api.Client.BaseURL, query) + resp, err := api.executeRequest(ctx, http.MethodPost, url, content) + if err != nil { + return nil, err + } + + var result models.SearchArtifactsAPIResponse + if err := handleResponse(resp, http.StatusOK, &result); err != nil { + return nil, err + } + + return &result.Artifacts, nil +} + +// ListArtifactReferences Returns a list containing all the artifact references using the artifact content ID. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/referencesByContentId +func (api *ArtifactsAPI) ListArtifactReferences(ctx context.Context, contentID int64) (*[]models.ArtifactReference, error) { + url := fmt.Sprintf("%s/ids/contentId/%d/references", api.Client.BaseURL, contentID) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var references []models.ArtifactReference + if err := handleResponse(resp, http.StatusOK, &references); err != nil { + return nil, err + } + + return &references, nil +} + +// ListArtifactReferencesByGlobalID Returns a list containing all the artifact references using the artifact global ID. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/referencesByContentHash +func (api *ArtifactsAPI) ListArtifactReferencesByGlobalID(ctx context.Context, globalID int64, params *models.ListArtifactReferencesByGlobalIDParams) (*[]models.ArtifactReference, error) { + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/ids/globalIds/%d/references%s", api.Client.BaseURL, globalID, query) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var references []models.ArtifactReference + if err := handleResponse(resp, http.StatusOK, &references); err != nil { + return nil, err + } + + return &references, nil +} + +// ListArtifactReferencesByHash Returns a list containing all the artifact references using the artifact content hash. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/referencesByContentHash +func (api *ArtifactsAPI) ListArtifactReferencesByHash(ctx context.Context, contentHash string) (*[]models.ArtifactReference, error) { + url := fmt.Sprintf("%s/ids/contentHashes/%s/references", api.Client.BaseURL, contentHash) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var references []models.ArtifactReference + if err := handleResponse(resp, http.StatusOK, &references); err != nil { + return nil, err + } + + return &references, nil +} + +// ListArtifactsInGroup lists all artifacts in a specified group. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/referencesByContentHash +func (api *ArtifactsAPI) ListArtifactsInGroup(ctx context.Context, groupID string, params *models.ListArtifactsInGroupParams) (*models.ListArtifactsResponse, error) { + if err := validateInput(groupID, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/groups/%s/artifacts%s", api.Client.BaseURL, groupID, query) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var result models.ListArtifactsResponse + if err := handleResponse(resp, http.StatusOK, &result); err != nil { + return nil, err + } + + return &result, nil +} + +// GetArtifactContentByHash Gets the content for an artifact version in the registry using the SHA-256 hash of the content +// This content hash may be shared by multiple artifact versions in the case where the artifact versions have identical content. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/getContentByHash +func (api *ArtifactsAPI) GetArtifactContentByHash(ctx context.Context, contentHash string) (*models.ArtifactContent, error) { + url := fmt.Sprintf("%s/ids/contentHashes/%s", api.Client.BaseURL, contentHash) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, errors.Wrapf(ErrArtifactNotFound, "content hash: %s", contentHash) + } + + if resp.StatusCode != http.StatusOK { + apiError, parseErr := parseAPIError(resp) + if parseErr != nil { + return nil, errors.Wrap(parseErr, "unexpected error") + } + return nil, apiError + } + + // Parse artifact type header + artifactType, err := parseArtifactTypeHeader(resp) + if err != nil { + return nil, err + } + + // Parse the response body + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + + return &models.ArtifactContent{ + Content: string(content), + ArtifactType: artifactType, + }, nil +} + +// GetArtifactContentByID Gets the content for an artifact version in the registry using the unique content identifier for that content +// This content ID may be shared by multiple artifact versions in the case where the artifact versions are identical. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/getContentById +func (api *ArtifactsAPI) GetArtifactContentByID(ctx context.Context, contentID int64) (*models.ArtifactContent, error) { + url := fmt.Sprintf("%s/ids/contentIds/%d", api.Client.BaseURL, contentID) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, errors.Wrapf(ErrArtifactNotFound, "content ID: %d", contentID) + } + + if resp.StatusCode != http.StatusOK { + apiError, parseErr := parseAPIError(resp) + if parseErr != nil { + return nil, errors.Wrap(parseErr, "unexpected error") + } + return nil, apiError + } + + // Parse artifact type header + artifactType, err := parseArtifactTypeHeader(resp) + if err != nil { + return nil, err + } + + // Parse the response body + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + + return &models.ArtifactContent{ + Content: string(content), + ArtifactType: artifactType, + }, nil +} + +// DeleteArtifactsInGroup deletes all artifacts in a given group. +// Deletes all the artifacts that exist in a given group. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/deleteArtifactsInGroup +func (api *ArtifactsAPI) DeleteArtifactsInGroup(ctx context.Context, groupID string) error { + if err := validateInput(groupID, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts", api.Client.BaseURL, groupID) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// DeleteArtifact deletes a specific artifact identified by groupId and artifactId. +// Deletes an artifact completely, resulting in all versions of the artifact also being deleted. This may fail for one of the following reasons: +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/deleteArtifact +func (api *ArtifactsAPI) DeleteArtifact(ctx context.Context, groupID, artifactId string) error { + if err := validateInput(groupID, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s", api.Client.BaseURL, groupID, artifactId) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusMethodNotAllowed { + return ErrMethodNotAllowed + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// CreateArtifact Creates a new artifact. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifacts/operation/createArtifact +func (api *ArtifactsAPI) CreateArtifact(ctx context.Context, groupId string, artifact models.CreateArtifactRequest, params *models.CreateArtifactParams) (*models.ArtifactDetail, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + url := fmt.Sprintf("%s/groups/%s/artifacts%s", api.Client.BaseURL, groupId, query) + + resp, err := api.executeRequest(ctx, http.MethodPost, url, artifact) + if err != nil { + return nil, err + } + + var response models.CreateArtifactResponse + if err := handleResponse(resp, http.StatusOK, &response); err != nil { + return nil, err + } + + return &response.Artifact, nil +} + +// ListArtifactRules lists all artifact rules for a given artifact. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/createArtifactRule +func (api *ArtifactsAPI) ListArtifactRules(ctx context.Context, groupID, artifactId string) ([]models.Rule, error) { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules", api.Client.BaseURL, groupID, artifactId) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var rules []models.Rule + if err := handleResponse(resp, http.StatusOK, &rules); err != nil { + return nil, err + } + + return rules, nil +} + +// CreateArtifactRule creates a new artifact rule for a given artifact. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/createArtifactRule +func (api *ArtifactsAPI) CreateArtifactRule(ctx context.Context, groupID, artifactId string, rule models.Rule, level models.RuleLevel) error { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules", api.Client.BaseURL, groupID, artifactId) + + // Prepare the request body + body := models.CreateUpdateGlobalRuleRequest{ + RuleType: rule, + Config: level, + } + resp, err := api.executeRequest(ctx, http.MethodPost, url, body) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// DeleteAllArtifactRule deletes all artifact rules for a given artifact. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/deleteArtifactRules +func (api *ArtifactsAPI) DeleteAllArtifactRule(ctx context.Context, groupID, artifactId string) error { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules", api.Client.BaseURL, groupID, artifactId) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// GetArtifactRule gets the rule level for a given artifact rule. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/getArtifactRuleConfig +func (api *ArtifactsAPI) GetArtifactRule(ctx context.Context, groupID, artifactId string, rule models.Rule) (models.RuleLevel, error) { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules/%s", api.Client.BaseURL, groupID, artifactId, rule) + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + + var globalRule models.GlobalRuleResponse + if err := handleResponse(resp, http.StatusOK, &globalRule); err != nil { + return "", err + } + + return globalRule.Config, nil +} + +// UpdateArtifactRule updates the rule level for a given artifact rule. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/updateArtifactRuleConfig +func (api *ArtifactsAPI) UpdateArtifactRule(ctx context.Context, groupID, artifactId string, rule models.Rule, level models.RuleLevel) error { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules/%s", api.Client.BaseURL, groupID, artifactId, rule) + + // Prepare the request body + body := models.CreateUpdateGlobalRuleRequest{ + RuleType: rule, + Config: level, + } + resp, err := api.executeRequest(ctx, http.MethodPut, url, body) + if err != nil { + return err + } + + var globalRule models.GlobalRuleResponse + if err := handleResponse(resp, http.StatusOK, &globalRule); err != nil { + return err + } + + return nil +} + +// DeleteArtifactRule deletes a specific artifact rule for a given artifact. +// See: https://www.apicur.io/registry/docs/apicurio-registry/3.0.x/assets-attachments/registry-rest-api.htm#tag/Artifact-rules/operation/deleteArtifactRule +func (api *ArtifactsAPI) DeleteArtifactRule(ctx context.Context, groupID, artifactId string, rule models.Rule) error { + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/rules/%s", api.Client.BaseURL, groupID, artifactId, rule) + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// executeRequest handles the creation and execution of an HTTP request. +func (api *ArtifactsAPI) executeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { + var reqBody []byte + var err error + contentType := "*/*" + + switch v := body.(type) { + case string: + reqBody = []byte(v) + contentType = "*/*" + case []byte: + reqBody = v + contentType = "*/*" + default: + contentType = "application/json" + reqBody, err = json.Marshal(body) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request body as JSON") + } + } + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(reqBody)) + if err != nil { + return nil, errors.Wrap(err, "failed to create HTTP request") + } + + // Set appropriate Content-Type header + if body != nil { + req.Header.Set("Content-Type", contentType) + } + + // Execute the request + resp, err := api.Client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to execute HTTP request") + } + + return resp, nil +} diff --git a/apis/artifacts_test.go b/apis/artifacts_test.go new file mode 100644 index 0000000..7f1c1c9 --- /dev/null +++ b/apis/artifacts_test.go @@ -0,0 +1,1101 @@ +package apis_test + +import ( + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/apis" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +const ( + DefaultBaseURL = "http://localhost:9080/apis/registry/v3" + groupID = "test-group" + artifactID = "test-artifact" +) + +var ( + stubArtifactContent = `{"type": "record", "name": "Test", "fields": [{"name": "field1", "type": "string"}]}` + stubArtifactId = "test-artifact" + stubGroupId = "test-group" +) + +func setupHTTPClient() *client.Client { + baseURL := os.Getenv("APICURIO_BASE_URL") + if baseURL == "" { + baseURL = DefaultBaseURL + } + httpClient := &http.Client{Timeout: 10 * time.Second} + apiClient := client.NewClient(baseURL, client.WithHTTPClient(httpClient)) + return apiClient +} + +func setupArtifactAPIClient() *apis.ArtifactsAPI { + apiClient := setupHTTPClient() + return apis.NewArtifactsAPI(apiClient) +} + +func cleanup(t *testing.T, artifactsAPI *apis.ArtifactsAPI) { + ctx := context.Background() + err := artifactsAPI.DeleteArtifactsInGroup(ctx, groupID) + if err != nil { + var APIError *models.APIError + if errors.As(err, &APIError) && APIError.Status == 404 { + return + } + t.Fatalf("Failed to clean up artifacts: %v", err) + } +} + +func TestSearchArtifacts(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.SearchArtifactsAPIResponse{ + Artifacts: []models.SearchedArtifact{ + {GroupId: "test-group", ArtifactId: "artifact-1", Name: "Test Artifact"}, + }, + Count: 1, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/search/artifacts", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.SearchArtifactsParams{Name: "Test Artifact"} + result, err := api.SearchArtifacts(context.Background(), params) + assert.NoError(t, err) + assert.NotNil(t, result) + }) + + t.Run("Server Error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.SearchArtifactsParams{} + result, err := api.SearchArtifacts(context.Background(), params) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestSearchArtifactsByContent(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.SearchArtifactsAPIResponse{ + Artifacts: []models.SearchedArtifact{ + {GroupId: "test-group", ArtifactId: "artifact-1", Name: "Test Artifact"}, + }, + Count: 1, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/search/artifacts", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.SearchArtifactsByContentParams{Canonical: true} + result, err := api.SearchArtifactsByContent(context.Background(), []byte("{\"key\":\"value\"}"), params) + assert.NoError(t, err) + assert.NotNil(t, result) + }) + + t.Run("Invalid Content", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.SearchArtifactsByContentParams{} + result, err := api.SearchArtifactsByContent(context.Background(), []byte(""), params) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestListArtifactReferences(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockReferences := []models.ArtifactReference{ + {GroupID: "group-1", ArtifactID: "artifact-1", Version: "v1"}, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/references") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockReferences) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactReferences(context.Background(), 123) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, *result, 1) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactReferences(context.Background(), 123) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestListArtifactReferencesByGlobalID(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockReferences := []models.ArtifactReference{ + {GroupID: "group-1", ArtifactID: "artifact-1", Version: "v1"}, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/globalIds") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockReferences) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.ListArtifactReferencesByGlobalIDParams{RefType: models.OutBound} + result, err := api.ListArtifactReferencesByGlobalID(context.Background(), 123, params) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, *result, 1) + }) + + t.Run("Error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.ListArtifactReferencesByGlobalIDParams{} + result, err := api.ListArtifactReferencesByGlobalID(context.Background(), 123, params) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestListArtifactReferencesByHash(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockReferences := []models.ArtifactReference{ + {GroupID: "group-1", ArtifactID: "artifact-1", Version: "v1"}, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/contentHashes") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockReferences) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactReferencesByHash(context.Background(), "hash-123") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, *result, 1) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactReferencesByHash(context.Background(), "hash-123") + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestListArtifactsInGroup(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.ListArtifactsResponse{ + Artifacts: []models.SearchedArtifact{ + {GroupId: "group-1", ArtifactId: "artifact-1", Name: "Test Artifact"}, + }, + Count: 1, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/groups/group-1/artifacts") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.ListArtifactsInGroupParams{Limit: 10, Offset: 0, Order: "asc"} + result, err := api.ListArtifactsInGroup(context.Background(), "group-1", params) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Artifacts, 1) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + params := &models.ListArtifactsInGroupParams{} + result, err := api.ListArtifactsInGroup(context.Background(), "group-1", params) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestGetArtifactContentByHash(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockContent := models.ArtifactContent{ + Content: "{\"key\":\"value\"}", + ArtifactType: models.Json, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/contentHashes/hash-123") + assert.Equal(t, http.MethodGet, r.Method) + + w.Header().Set("X-Registry-ArtifactType", "JSON") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(mockContent.Content)) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.GetArtifactContentByHash(context.Background(), "hash-123") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "{\"key\":\"value\"}", result.Content) + assert.Equal(t, models.Json, result.ArtifactType) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.GetArtifactContentByHash(context.Background(), "hash-123") + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestGetArtifactContentByID(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockContent := models.ArtifactContent{ + Content: "{\"key\":\"value\"}", + ArtifactType: models.Json, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/contentIds/123") + assert.Equal(t, http.MethodGet, r.Method) + + w.Header().Set("X-Registry-ArtifactType", "JSON") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(mockContent.Content)) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.GetArtifactContentByID(context.Background(), 123) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "{\"key\":\"value\"}", result.Content) + assert.Equal(t, models.Json, result.ArtifactType) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.GetArtifactContentByID(context.Background(), 123) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestDeleteArtifactsInGroup(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + err := api.DeleteArtifactsInGroup(context.Background(), "test-group") + assert.NoError(t, err) + }) + + t.Run("Forbidden", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + err := api.DeleteArtifactsInGroup(context.Background(), "test-group") + assert.Error(t, err) + }) +} + +func TestDeleteArtifact(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + err := api.DeleteArtifact(context.Background(), "test-group", "artifact-1") + assert.NoError(t, err) + }) + + t.Run("Not Allowed", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + err := api.DeleteArtifact(context.Background(), "test-group", "artifact-1") + assert.Error(t, err) + assert.Equal(t, apis.ErrMethodNotAllowed, err) + }) +} + +func TestCreateArtifact(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.CreateArtifactResponse{ + Artifact: models.ArtifactDetail{ + GroupID: "test-group", + ArtifactID: "artifact-1", + Name: "New Artifact", + Description: "Test Description", + }, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/artifacts") + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + artifact := models.CreateArtifactRequest{ + ArtifactType: models.Json, + FirstVersion: models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: "{\"key\":\"value\"}", + }, + }, + } + params := &models.CreateArtifactParams{ + IfExists: models.IfExistsCreate, + } + result, err := api.CreateArtifact(context.Background(), "test-group", artifact, params) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "artifact-1", result.ArtifactID) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusConflict) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + artifact := models.CreateArtifactRequest{ + ArtifactType: models.Json, + FirstVersion: models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: "{\"key\":\"value\"}", + }, + }, + } + + params := &models.CreateArtifactParams{} + result, err := api.CreateArtifact(context.Background(), "test-group", artifact, params) + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestArtifactsAPI_ListArtifactRules(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockReferences := []models.Rule{models.RuleValidity, models.RuleCompatibility} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockReferences) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactRules(context.Background(), stubGroupId, stubArtifactId) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result, 2) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactRules(context.Background(), stubGroupId, stubArtifactId) + assert.Error(t, err) + assert.Nil(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("Internal Server Error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: TitleInternalServerError}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + result, err := api.ListArtifactRules(context.Background(), stubGroupId, stubArtifactId) + assert.Error(t, err) + assert.Nil(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestArtifactsAPI_CreateArtifactRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + + err := api.CreateArtifactRule(context.Background(), stubGroupId, stubArtifactId, models.RuleValidity, models.ValidityLevelFull) + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusBadRequest, Title: TitleBadRequest}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.CreateArtifactRule(context.Background(), stubGroupId, stubArtifactId, models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, apiErr.Status) + assert.Equal(t, TitleBadRequest, apiErr.Title) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusConflict, Title: TitleConflict}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.CreateArtifactRule(context.Background(), stubGroupId, stubArtifactId, models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusConflict, apiErr.Status) + assert.Equal(t, TitleConflict, apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.CreateArtifactRule(context.Background(), stubGroupId, stubArtifactId, models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodPost, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.CreateArtifactRule(context.Background(), stubGroupId, stubArtifactId, models.RuleValidity, models.ValidityLevelFull) + + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestArtifactsAPI_DeleteAllArtifactRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteAllArtifactRule(context.Background(), stubGroupId, stubArtifactId) + assert.NoError(t, err) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteAllArtifactRule(context.Background(), stubGroupId, stubArtifactId) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules", stubGroupId, stubArtifactId)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteAllArtifactRule(context.Background(), stubGroupId, stubArtifactId) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestArtifactsAPI_GetArtifactRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.GlobalRuleResponse{ + RuleType: models.RuleValidity, + Config: models.ValidityLevelFull, + } + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + result, err := api.GetArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, models.ValidityLevelFull, result) + }) + + t.Run("NotFound", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + result, err := api.GetArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.Error(t, err) + assert.Empty(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + result, err := api.GetArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.Error(t, err) + assert.Empty(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestArtifactsAPI_UpdateArtifactRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockRule := models.RuleValidity + mockResponse := models.GlobalRuleResponse{ + RuleType: mockRule, + Config: models.ValidityLevelFull, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.UpdateArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule, models.ValidityLevelFull) + assert.NoError(t, err) + }) + + t.Run("NotFound", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.UpdateArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule, models.ValidityLevelFull) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.UpdateArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule, models.ValidityLevelFull) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +func TestArtifactsAPI_DeleteArtifactRule(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.NoError(t, err) + }) + + t.Run("NotFound", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusNotFound, Title: TitleNotFound}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusNotFound, apiErr.Status) + assert.Equal(t, TitleNotFound, apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + mockRule := models.RuleValidity + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, fmt.Sprintf("/groups/%s/artifacts/%s/rules/%s", stubGroupId, stubArtifactId, mockRule)) + assert.Equal(t, http.MethodDelete, r.Method) + + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: http.StatusInternalServerError, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewArtifactsAPI(mockClient) + err := api.DeleteArtifactRule(context.Background(), stubGroupId, stubArtifactId, mockRule) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, http.StatusInternalServerError, apiErr.Status) + assert.Equal(t, TitleInternalServerError, apiErr.Title) + }) +} + +/***********************/ +/***** Integration *****/ +/***********************/ +func TestArtifactsAPIIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + artifactsAPI := setupArtifactAPIClient() + + // Clean up before and after tests + t.Cleanup(func() { cleanup(t, artifactsAPI) }) + cleanup(t, artifactsAPI) + + ctx := context.Background() + + // Test CreateArtifact + t.Run("CreateArtifact", func(t *testing.T) { + artifact := models.CreateArtifactRequest{ + ArtifactType: models.Json, + ArtifactID: artifactID, + Name: artifactID, + FirstVersion: models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: stubArtifactContent, + }, + }, + } + + params := &models.CreateArtifactParams{ + IfExists: models.IfExistsFail, + } + + resp, err := artifactsAPI.CreateArtifact(ctx, groupID, artifact, params) + assert.NoError(t, err) + assert.Equal(t, groupID, resp.GroupID) + assert.Equal(t, artifactID, resp.Name) + }) + + // Test SearchArtifacts + t.Run("SearchArtifacts", func(t *testing.T) { + params := &models.SearchArtifactsParams{ + Name: artifactID, + } + resp, err := artifactsAPI.SearchArtifacts(ctx, params) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(*resp), 1) + }) + + // Test ListArtifactReferences + t.Run("ListArtifactReferences", func(t *testing.T) { + contentID := int64(12345) // Replace with a valid content ID for your tests + _, err := artifactsAPI.ListArtifactReferences(ctx, contentID) + assert.Error(t, err) // Expect an error since no content ID exists + }) + + // Test ListArtifactReferencesByGlobalID + t.Run("ListArtifactReferencesByGlobalID", func(t *testing.T) { + globalID := int64(12345) // Replace with a valid global ID for your tests + params := &models.ListArtifactReferencesByGlobalIDParams{} + _, err := artifactsAPI.ListArtifactReferencesByGlobalID(ctx, globalID, params) + assert.Error(t, err) // Expect an error since no global ID exists + }) + + // Test ListArtifactReferencesByHash + t.Run("ListArtifactReferencesByHash", func(t *testing.T) { + contentHash := "invalidhash" // Replace with a valid content hash for your tests + _, err := artifactsAPI.ListArtifactReferencesByHash(ctx, contentHash) + assert.Error(t, err) // Expect an error since no hash exists + }) + + // Test ListArtifactsInGroup + t.Run("ListArtifactsInGroup", func(t *testing.T) { + params := &models.ListArtifactsInGroupParams{} + resp, err := artifactsAPI.ListArtifactsInGroup(ctx, groupID, params) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(resp.Artifacts), 1) + }) + + // Test GetArtifactContentByHash + t.Run("GetArtifactContentByHash", func(t *testing.T) { + contentHash := "invalidhash" // Replace with a valid content hash for your tests + _, err := artifactsAPI.GetArtifactContentByHash(ctx, contentHash) + assert.Error(t, err) // Expect an error since no hash exists + }) + + // Test GetArtifactContentByID + t.Run("GetArtifactContentByID", func(t *testing.T) { + contentID := int64(12345) // Replace with a valid content ID for your tests + _, err := artifactsAPI.GetArtifactContentByID(ctx, contentID) + assert.Error(t, err) // Expect an error since no content ID exists + }) + + // Test DeleteArtifactsInGroup + t.Run("DeleteArtifactsInGroup", func(t *testing.T) { + err := artifactsAPI.DeleteArtifactsInGroup(ctx, groupID) + assert.NoError(t, err) + }) + + // Test DeleteArtifact + t.Run("DeleteArtifact", func(t *testing.T) { + + // Re-create the artifact + artifact := models.CreateArtifactRequest{ + ArtifactType: models.Json, + ArtifactID: artifactID, + Name: artifactID, + FirstVersion: models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: stubArtifactContent, + }, + }, + } + params := &models.CreateArtifactParams{ + IfExists: models.IfExistsFail, + } + + resp, err := artifactsAPI.CreateArtifact(ctx, groupID, artifact, params) + assert.NoError(t, err) + assert.Equal(t, groupID, resp.GroupID) + assert.Equal(t, artifactID, resp.Name) + + // Delete the artifact + err = artifactsAPI.DeleteArtifact(ctx, groupID, artifactID) + assert.NoError(t, err) + }) +} diff --git a/apis/helpers.go b/apis/helpers.go new file mode 100644 index 0000000..dad1e93 --- /dev/null +++ b/apis/helpers.go @@ -0,0 +1,93 @@ +package apis + +import ( + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "io" + "net/http" + "regexp" +) + +const ( + ContentTypeJSON = "application/json" + ContentTypeAll = "*/*" +) + +var ( + regexGroupIDArtifactID = regexp.MustCompile(`^.{1,512}$`) + regexVersion = regexp.MustCompile(`[a-zA-Z0-9._\-+]{1,256}`) +) + +// ErrInvalidInput is returned when an input validation fails. +func validateInput(input string, regex *regexp.Regexp, name string) error { + if match := regex.MatchString(input); !match { + return errors.Wrapf(ErrInvalidInput, "%s: %s", name, input) + } + return nil +} + +// parseAPIError parses an API error response and returns an APIError struct. +func parseAPIError(resp *http.Response) (*models.APIError, error) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read error response body: %w", err) + } + + var apiError models.APIError + if err := json.Unmarshal(body, &apiError); err != nil { + return nil, fmt.Errorf("failed to parse error response: %w", err) + } + + return &apiError, nil +} + +func parseArtifactTypeHeader(resp *http.Response) (models.ArtifactType, error) { + artifactTypeHeader := resp.Header.Get("X-Registry-ArtifactType") + artifactType, err := models.ParseArtifactType(artifactTypeHeader) + if err != nil { + return "", errors.Wrapf(err, "invalid artifact type in response header: %s", artifactTypeHeader) + } + return artifactType, nil +} + +// handleResponse reads the response body and checks the status code. +func handleResponse(resp *http.Response, expectedStatus int, result interface{}) error { + defer resp.Body.Close() + + if resp.StatusCode != expectedStatus { + apiError, parseErr := parseAPIError(resp) + if parseErr != nil { + return errors.Wrap(parseErr, "unexpected server error") + } + return apiError + } + + if result != nil && resp.StatusCode == expectedStatus { + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return errors.Wrap(err, "failed to parse response body") + } + } + + return nil +} + +// handleRawResponse reads the response body and checks the status code. +func handleRawResponse(resp *http.Response, expectedStatus int) (string, error) { + defer resp.Body.Close() + if resp.StatusCode != expectedStatus { + apiError, parseErr := parseAPIError(resp) + if parseErr != nil { + return "", errors.Wrap(parseErr, "unexpected server error") + } + return "", apiError + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrap(err, "failed to read response body") + } + + return string(content), nil +} diff --git a/apis/metadata.go b/apis/metadata.go new file mode 100644 index 0000000..21a7874 --- /dev/null +++ b/apis/metadata.go @@ -0,0 +1,158 @@ +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "net/http" +) + +// MetadataAPI handles metadata-related operations for artifacts. +type MetadataAPI struct { + Client *client.Client +} + +// NewMetadataAPI creates a new MetadataAPI instance. +func NewMetadataAPI(client *client.Client) *MetadataAPI { + return &MetadataAPI{ + Client: client, + } +} + +// GetArtifactVersionMetadata retrieves metadata for a single artifact version. +func (api *MetadataAPI) GetArtifactVersionMetadata(ctx context.Context, groupId, artifactId, versionExpression string) (*models.ArtifactVersionMetadata, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s", api.Client.BaseURL, groupId, artifactId, versionExpression) + + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var metadata models.ArtifactVersionMetadata + if err := handleResponse(resp, http.StatusOK, &metadata); err != nil { + return nil, err + } + + return &metadata, nil +} + +// UpdateArtifactVersionMetadata updates the user-editable metadata of an artifact version. +func (api *MetadataAPI) UpdateArtifactVersionMetadata(ctx context.Context, groupId, artifactId, versionExpression string, metadata models.UpdateArtifactMetadataRequest) error { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s", api.Client.BaseURL, groupId, artifactId, versionExpression) + + resp, err := api.executeRequest(ctx, http.MethodPut, url, metadata) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// GetArtifactMetadata retrieves metadata for an artifact based on the latest version or the next available non-disabled version. +func (api *MetadataAPI) GetArtifactMetadata(ctx context.Context, groupId, artifactId string) (*models.ArtifactMetadata, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s", api.Client.BaseURL, groupId, artifactId) + + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var metadata models.ArtifactMetadata + if err := handleResponse(resp, http.StatusOK, &metadata); err != nil { + return nil, err + } + + return &metadata, nil +} + +// UpdateArtifactMetadata updates the editable parts of an artifact's metadata. +func (api *MetadataAPI) UpdateArtifactMetadata(ctx context.Context, groupId, artifactId string, metadata models.UpdateArtifactMetadataRequest) error { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + + // Construct the URL + url := fmt.Sprintf("%s/groups/%s/artifacts/%s", api.Client.BaseURL, groupId, artifactId) + + resp, err := api.executeRequest(ctx, http.MethodPut, url, metadata) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// executeRequest executes an HTTP request with the given method, URL, and body. +func (api *MetadataAPI) executeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { + var reqBody []byte + var err error + contentType := "*/*" + + switch v := body.(type) { + case string: + reqBody = []byte(v) + contentType = "*/*" + case []byte: + reqBody = v + contentType = "*/*" + default: + contentType = "application/json" + reqBody, err = json.Marshal(body) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request body as JSON") + } + } + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(reqBody)) + if err != nil { + return nil, errors.Wrap(err, "failed to create HTTP request") + } + + // Set appropriate Content-Type header + if body != nil { + req.Header.Set("Content-Type", contentType) + } + + // Execute the request + resp, err := api.Client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to execute HTTP request") + } + + return resp, nil +} diff --git a/apis/metadata_test.go b/apis/metadata_test.go new file mode 100644 index 0000000..a17f3d1 --- /dev/null +++ b/apis/metadata_test.go @@ -0,0 +1,295 @@ +package apis_test + +import ( + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/apis" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +const ( + versionExpression = "1.0.0" +) + +func TestGetArtifactVersionMetadata(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockMetadata := models.ArtifactVersionMetadata{ + BaseMetadata: models.BaseMetadata{ + GroupID: "test-group", + ArtifactID: "artifact-1", + Name: "Test Artifact", + Description: "Test Description", + ArtifactType: "JSON", + }, + Version: "1.0", + GlobalID: 12345, + ContentID: 67890, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/groups/test-group/artifacts/artifact-1/versions/1.0") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockMetadata) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + result, err := api.GetArtifactVersionMetadata(context.Background(), "test-group", "artifact-1", "1.0") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "Test Artifact", result.Name) + assert.Equal(t, "1.0", result.Version) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + result, err := api.GetArtifactVersionMetadata(context.Background(), "test-group", "artifact-1", "1.0") + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestUpdateArtifactVersionMetadata(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + assert.Contains(t, r.URL.Path, "/groups/test-group/artifacts/artifact-1") + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + metadata := models.UpdateArtifactMetadataRequest{ + Name: "Updated Artifact", + Description: "Updated Description", + } + + err := api.UpdateArtifactVersionMetadata(context.Background(), "test-group", "artifact-1", "1.0.0", metadata) + assert.NoError(t, err) + }) + + t.Run("Invalid Input", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + metadata := models.UpdateArtifactMetadataRequest{ + Name: "", + } + + err := api.UpdateArtifactVersionMetadata(context.Background(), "test-group", "artifact-1", "1.0", metadata) + assert.Error(t, err) + }) +} + +func TestGetArtifactMetadata(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockMetadata := models.ArtifactMetadata{ + BaseMetadata: models.BaseMetadata{ + GroupID: "test-group", + ArtifactID: "artifact-1", + Name: "Test Artifact", + Description: "Test Description", + ArtifactType: "JSON", + }, + ModifiedBy: "user-1", + ModifiedOn: "2024-12-09", + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.Path, "/groups/test-group/artifacts/artifact-1") + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockMetadata) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + result, err := api.GetArtifactMetadata(context.Background(), "test-group", "artifact-1") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "Test Artifact", result.Name) + assert.Equal(t, "user-1", result.ModifiedBy) + }) + + t.Run("Artifact Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + result, err := api.GetArtifactMetadata(context.Background(), "test-group", "artifact-1") + assert.Error(t, err) + assert.Nil(t, result) + }) +} + +func TestUpdateArtifactMetadata(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + assert.Contains(t, r.URL.Path, "/groups/test-group/artifacts/artifact-1") + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + metadata := models.UpdateArtifactMetadataRequest{ + Name: "Updated Artifact", + Description: "Updated Description", + Labels: map[string]string{"env": "prod"}, + } + + err := api.UpdateArtifactMetadata(context.Background(), "test-group", "artifact-1", metadata) + assert.NoError(t, err) + }) + + t.Run("Invalid Input", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewMetadataAPI(mockClient) + + metadata := models.UpdateArtifactMetadataRequest{} + + err := api.UpdateArtifactMetadata(context.Background(), "test-group", "artifact-1", metadata) + assert.Error(t, err) + }) +} + +/***********************/ +/***** Integration *****/ +/***********************/ + +func setupMetadataAPIClient() *apis.MetadataAPI { + apiClient := setupHTTPClient() + return apis.NewMetadataAPI(apiClient) +} + +func TestMetadataAPIIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + metadataAPI := setupMetadataAPIClient() + + ctx := context.Background() + + // Prepare test data + artifactsAPI := apis.NewArtifactsAPI(metadataAPI.Client) + + // Clean up before and after tests + t.Cleanup(func() { cleanup(t, artifactsAPI) }) + cleanup(t, artifactsAPI) + + artifact := models.CreateArtifactRequest{ + ArtifactType: models.Json, + ArtifactID: artifactID, + Name: artifactID, + FirstVersion: models.CreateVersionRequest{ + Version: versionExpression, + Content: models.CreateContentRequest{ + Content: stubArtifactContent, + }, + }, + } + createParams := &models.CreateArtifactParams{ + IfExists: models.IfExistsFail, + } + _, err := artifactsAPI.CreateArtifact(ctx, groupID, artifact, createParams) + if err != nil { + t.Fatalf("Failed to create artifact: %v", err) + } + + // Test GetArtifactVersionMetadata + t.Run("GetArtifactVersionMetadata", func(t *testing.T) { + result, err := metadataAPI.GetArtifactVersionMetadata(ctx, groupID, artifactID, versionExpression) + assert.NoError(t, err) + assert.NotNil(t, result) + fmt.Println(result) + assert.Equal(t, artifactID, result.ArtifactID) + assert.Equal(t, versionExpression, result.Version) + }) + + // Test UpdateArtifactVersionMetadata + t.Run("UpdateArtifactVersionMetadata", func(t *testing.T) { + updateRequest := models.UpdateArtifactMetadataRequest{ + Name: "Updated Artifact Version Name", + Description: "Updated Artifact Version Description", + } + + err := metadataAPI.UpdateArtifactVersionMetadata(ctx, groupID, artifactID, versionExpression, updateRequest) + assert.NoError(t, err) + + // Verify the update + updatedMetadata, err := metadataAPI.GetArtifactVersionMetadata(ctx, groupID, artifactID, versionExpression) + assert.NoError(t, err) + assert.Equal(t, "Updated Artifact Version Name", updatedMetadata.Name) + assert.Equal(t, "Updated Artifact Version Description", updatedMetadata.Description) + }) + + // Test GetArtifactMetadata + t.Run("GetArtifactMetadata", func(t *testing.T) { + result, err := metadataAPI.GetArtifactMetadata(ctx, groupID, artifactID) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, artifactID, result.ArtifactID) + }) + + // Test UpdateArtifactMetadata + t.Run("UpdateArtifactMetadata", func(t *testing.T) { + updateRequest := models.UpdateArtifactMetadataRequest{ + Name: "Updated Artifact Name", + Description: "Updated Artifact Description", + Labels: map[string]string{ + "env": "production", + }, + } + + err := metadataAPI.UpdateArtifactMetadata(ctx, groupID, artifactID, updateRequest) + assert.NoError(t, err) + + // Verify the update + updatedMetadata, err := metadataAPI.GetArtifactMetadata(ctx, groupID, artifactID) + assert.NoError(t, err) + assert.Equal(t, "Updated Artifact Name", updatedMetadata.Name) + assert.Equal(t, "Updated Artifact Description", updatedMetadata.Description) + assert.Equal(t, "production", updatedMetadata.Labels["env"]) + }) +} diff --git a/apis/rules.go b/apis/rules.go new file mode 100644 index 0000000..7b272ed --- /dev/null +++ b/apis/rules.go @@ -0,0 +1 @@ +package apis diff --git a/apis/versions.go b/apis/versions.go new file mode 100644 index 0000000..4b812f7 --- /dev/null +++ b/apis/versions.go @@ -0,0 +1,556 @@ +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/pkg/errors" + "net/http" +) + +type VersionsAPI struct { + Client *client.Client +} + +func NewVersionsAPI(client *client.Client) *VersionsAPI { + return &VersionsAPI{ + Client: client, + } +} + +// DeleteArtifactVersion deletes a single version of the artifact. +// Parameters `groupId`, `artifactId`, and the unique `versionExpression` are needed. +// This feature must be enabled using the `registry.rest.artifact.deletion.enabled` property. +func (api *VersionsAPI) DeleteArtifactVersion( + ctx context.Context, + groupID, artifactID, versionExpression string, +) error { + // Validate inputs + if err := validateInput(groupID, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactID, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + + // Construct the URL + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s", api.Client.BaseURL, groupID, artifactID, versionExpression) + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// GetArtifactVersionReferences retrieves all references for a single artifact version. +func (api *VersionsAPI) GetArtifactVersionReferences(ctx context.Context, + groupId, artifactId, versionExpression string, + params *models.ArtifactVersionReferencesParams, +) (*[]models.ArtifactReference, error) { + // Validate inputs + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + + // Start building the URL + url := fmt.Sprintf( + "%s/groups/%s/artifacts/%s/versions/%s/references%s", + api.Client.BaseURL, + groupId, + artifactId, + versionExpression, + query, + ) + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var references []models.ArtifactReference + if err = handleResponse(resp, http.StatusOK, &references); err != nil { + return nil, err + } + + return &references, nil +} + +// GetArtifactVersionComments retrieves all comments for a version of an artifact. +func (api *VersionsAPI) GetArtifactVersionComments( + ctx context.Context, + groupId, artifactId, versionExpression string, +) (*[]models.ArtifactComment, error) { + // Validate inputs + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + // Construct the URL + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s/comments", api.Client.BaseURL, groupId, artifactId, versionExpression) + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + // Parse the response + var comments []models.ArtifactComment + if err = handleResponse(resp, http.StatusOK, &comments); err != nil { + return nil, err + } + + return &comments, nil +} + +// AddArtifactVersionComment adds a new comment to a specific artifact version. +func (api *VersionsAPI) AddArtifactVersionComment( + ctx context.Context, + groupId, artifactId, versionExpression string, + commentValue string, +) (*models.ArtifactComment, error) { + // Validate inputs + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + // Construct the URL + url := fmt.Sprintf( + "%s/groups/%s/artifacts/%s/versions/%s/comments", + api.Client.BaseURL, + groupId, + artifactId, + versionExpression, + ) + + // Create the request body + requestBody := map[string]string{ + "value": commentValue, + } + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodPost, url, requestBody) + if err != nil { + return nil, err + } + + // Handle the response + var comment models.ArtifactComment + if err := handleResponse(resp, http.StatusOK, &comment); err != nil { + return nil, err + } + + return &comment, nil +} + +// UpdateArtifactVersionComment updates the value of a single comment in an artifact version. +func (api *VersionsAPI) UpdateArtifactVersionComment( + ctx context.Context, + groupId, artifactId, versionExpression, commentId string, + updatedComment string, +) error { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + // Build the URL + url := fmt.Sprintf( + "%s/groups/%s/artifacts/%s/versions/%s/comments/%s", + api.Client.BaseURL, + groupId, + artifactId, + versionExpression, + commentId, + ) + + // Create the request body + requestBody := map[string]string{ + "value": updatedComment, + } + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodPut, url, requestBody) + if err != nil { + return err + } + + // Handle the response + if err := handleResponse(resp, http.StatusNoContent, nil); err != nil { + return err + } + + return nil +} + +// DeleteArtifactVersionComment deletes a single comment from an artifact version. +func (api *VersionsAPI) DeleteArtifactVersionComment( + ctx context.Context, + groupId, artifactId, versionExpression, commentId string, +) error { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + + url := fmt.Sprintf( + "%s/groups/%s/artifacts/%s/versions/%s/comments/%s", + api.Client.BaseURL, + groupId, + artifactId, + versionExpression, + commentId, + ) + + resp, err := api.executeRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) + +} + +// ListArtifactVersions retrieves all versions of an artifact. +func (api *VersionsAPI) ListArtifactVersions( + ctx context.Context, + groupId, artifactId string, + params *models.ListArtifactsInGroupParams, +) (*[]models.ArtifactVersion, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions%s", api.Client.BaseURL, groupId, artifactId, query) + + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var versionsResponse = models.ArtifactVersionListResponse{} + if err = handleResponse(resp, http.StatusOK, &versionsResponse); err != nil { + return nil, err + } + + return &versionsResponse.Versions, nil + +} + +// CreateArtifactVersion creates a new version of the artifact. +func (api *VersionsAPI) CreateArtifactVersion( + ctx context.Context, + groupId, artifactId string, + request *models.CreateVersionRequest, + dryRun bool, +) (*models.ArtifactVersionDetailed, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions", api.Client.BaseURL, groupId, artifactId) + if dryRun { + url = fmt.Sprintf("%s?dryRun=true", url) + } + + resp, err := api.executeRequest(ctx, http.MethodPost, url, request) + if err != nil { + return nil, err + } + + var version models.ArtifactVersionDetailed + if err = handleResponse(resp, http.StatusOK, &version); err != nil { + return nil, err + } + + return &version, nil + +} + +// GetArtifactVersionContent retrieves a single version of the artifact. +func (api *VersionsAPI) GetArtifactVersionContent( + ctx context.Context, + groupId, artifactId, versionExpression string, + params *models.ArtifactReferenceParams, +) (*models.ArtifactContent, error) { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + query := "" + if params != nil { + query = "?" + params.ToQuery().Encode() + } + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s/content%s", api.Client.BaseURL, groupId, artifactId, versionExpression, query) + + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + content, err := handleRawResponse(resp, http.StatusOK) + if err != nil { + return nil, err + } + + return &models.ArtifactContent{ + Content: content, + }, nil +} + +// UpdateArtifactVersionContent updates the content of a single version of the artifact. +func (api *VersionsAPI) UpdateArtifactVersionContent( + ctx context.Context, + groupId, artifactId, versionExpression string, + content *models.CreateContentRequest, +) error { + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s/content", api.Client.BaseURL, groupId, artifactId, versionExpression) + + resp, err := api.executeRequest(ctx, http.MethodPut, url, content) + if err != nil { + return err + } + + return handleResponse(resp, http.StatusNoContent, nil) +} + +// SearchForArtifactVersions searches for versions of an artifact. +func (api *VersionsAPI) SearchForArtifactVersions( + ctx context.Context, + params *models.SearchVersionParams, +) (*[]models.ArtifactVersion, error) { + + query := "" + if params != nil { + query = params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/search/versions?%s", api.Client.BaseURL, query) + + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + var searchVersionsResponse = models.ArtifactVersionListResponse{} + if err = handleResponse(resp, http.StatusOK, &searchVersionsResponse); err != nil { + return nil, err + } + + return &searchVersionsResponse.Versions, nil +} + +// SearchForArtifactVersionByContent searches for a version of an artifact by content. +func (api *VersionsAPI) SearchForArtifactVersionByContent( + ctx context.Context, + content string, + params *models.SearchVersionByContentParams, +) (*[]models.ArtifactVersion, error) { + query := "" + if params != nil { + query = params.ToQuery().Encode() + } + + url := fmt.Sprintf("%s/search/versions?%s", api.Client.BaseURL, query) + + resp, err := api.executeRequest(ctx, http.MethodPost, url, content) + if err != nil { + return nil, err + } + + var searchVersionsResponse = models.ArtifactVersionListResponse{} + if err = handleResponse(resp, http.StatusOK, &searchVersionsResponse); err != nil { + return nil, err + } + + return &searchVersionsResponse.Versions, nil +} + +// GetArtifactVersionState retrieves the current state of an artifact version. +func (api *VersionsAPI) GetArtifactVersionState( + ctx context.Context, + groupId, artifactId, versionExpression string, +) (*models.State, error) { + // Validate inputs + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return nil, err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return nil, err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return nil, err + } + + // Build the URL + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s/state", api.Client.BaseURL, groupId, artifactId, versionExpression) + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + // Parse response + var stateResponse models.StateResponse + if err = handleResponse(resp, http.StatusOK, &stateResponse); err != nil { + return nil, err + } + + return &stateResponse.State, nil +} + +// UpdateArtifactVersionState updates the state of an artifact version. +func (api *VersionsAPI) UpdateArtifactVersionState( + ctx context.Context, + groupId, artifactId, versionExpression string, + state models.State, + dryRun bool, +) error { + // Validate inputs + if err := validateInput(groupId, regexGroupIDArtifactID, "Group ID"); err != nil { + return err + } + if err := validateInput(artifactId, regexGroupIDArtifactID, "Artifact ID"); err != nil { + return err + } + if err := validateInput(versionExpression, regexVersion, "Version Expression"); err != nil { + return err + } + + // Construct the URL with optional dryRun parameter + url := fmt.Sprintf("%s/groups/%s/artifacts/%s/versions/%s/state", api.Client.BaseURL, groupId, artifactId, versionExpression) + if dryRun { + url += "?dryRun=true" + } + + // Create request body + requestBody := models.StateRequest{ + State: state, + } + + // Execute the request + resp, err := api.executeRequest(ctx, http.MethodPut, url, requestBody) + if err != nil { + return err + } + + // Handle response + if err = handleResponse(resp, http.StatusNoContent, nil); err != nil { + return err + } + + return nil +} + +// executeRequest handles the creation and execution of an HTTP request. +func (api *VersionsAPI) executeRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { + var reqBody []byte + var err error + contentType := "*/*" + + switch v := body.(type) { + case string: + reqBody = []byte(v) + contentType = "*/*" + case []byte: + reqBody = v + contentType = "*/*" + default: + contentType = "application/json" + reqBody, err = json.Marshal(body) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request body as JSON") + } + } + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(reqBody)) + if err != nil { + return nil, errors.Wrap(err, "failed to create HTTP request") + } + + // Set appropriate Content-Type header + if body != nil { + req.Header.Set("Content-Type", contentType) + } + + // Execute the request + resp, err := api.Client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to execute HTTP request") + } + + return resp, nil +} diff --git a/apis/versions_test.go b/apis/versions_test.go new file mode 100644 index 0000000..6dfb8ec --- /dev/null +++ b/apis/versions_test.go @@ -0,0 +1,1767 @@ +package apis_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/mollie/go-apicurio-registry/apis" + "github.com/mollie/go-apicurio-registry/client" + "github.com/mollie/go-apicurio-registry/models" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +const ( + version = "1.0.0" + newVersion = "1.1.0" + commentID = "test-comment" + stubContent = `{"type":"record","name":"TestRecord","fields":[{"name":"field1","type":"string"}]}` + stubNewContent = `{"type":"record","name":"TestRecord","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}` + stubReference = `{"groupId":"test-group","artifactId":"ref-artifact","version":"1.0.0"}` +) + +func setupVersionAPIClient() *apis.ArtifactsAPI { + apiClient := setupHTTPClient() + return apis.NewArtifactsAPI(apiClient) +} + +func TestVersionsAPI_DeleteArtifactVersion(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Validate the request + assert.Equal(t, "/groups/test-group/artifacts/test-artifact/versions/1.0.0", r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + + // Respond with a successful status code + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + // Create a mock client and API instance + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Call the method + err := api.DeleteArtifactVersion(context.Background(), "test-group", "test-artifact", "1.0.0") + + // Assertions + assert.NoError(t, err) + }) + + t.Run("Not Found", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/test-artifact/versions/1.0.0", r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + + // Simulate a 404 Not Found response + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{ + Detail: "Artifact version not found", + Status: http.StatusNotFound, + }) + if err != nil { + t.Error(err) + } + })) + defer server.Close() + + // Create a mock client and API instance + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Call the method + err := api.DeleteArtifactVersion(context.Background(), "test-group", "test-artifact", "1.0.0") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) + + t.Run("Method Not Allowed", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/test-artifact/versions/1.0.0", r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + + // Simulate a 405 Method Not Allowed response + w.WriteHeader(http.StatusMethodNotAllowed) + err := json.NewEncoder(w).Encode(models.APIError{ + Detail: "Method not allowed", + Status: http.StatusMethodNotAllowed, + }) + if err != nil { + t.Error(err) + } + })) + defer server.Close() + + // Create a mock client and API instance + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Call the method + err := api.DeleteArtifactVersion(context.Background(), "test-group", "test-artifact", "1.0.0") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "Method not allowed") + }) + + t.Run("Internal Server Error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/test-artifact/versions/1.0.0", r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + + // Simulate a 500 Internal Server Error response + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{ + Detail: "Internal Server Error", + Status: http.StatusInternalServerError, + }) + if err != nil { + t.Error(err) + } + })) + defer server.Close() + + // Create a mock client and API instance + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Call the method + err := api.DeleteArtifactVersion(context.Background(), "test-group", "test-artifact", "1.0.0") + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "Internal Server Error") + }) +} + +func TestVersionsAPI_GetArtifactVersionReferences(t *testing.T) { + t.Run("Success with Parameters", func(t *testing.T) { + mockResponse := []models.ArtifactReference{ + {GroupID: "test-group", ArtifactID: "artifact-1", Version: "1", Name: "Reference 1"}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/artifact-1/versions/1/references?refType=INBOUND", r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.ArtifactVersionReferencesParams{RefType: "INBOUND"} + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", params) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, 1, len(*result)) + assert.Equal(t, "Reference 1", (*result)[0].Name) + }) + + t.Run("Success without Parameters", func(t *testing.T) { + mockResponse := []models.ArtifactReference{ + {GroupID: "test-group", ArtifactID: "artifact-1", Version: "1", Name: "Reference 1"}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/artifact-1/versions/1/references", r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", nil) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, 1, len(*result)) + assert.Equal(t, "Reference 1", (*result)[0].Name) + }) + + t.Run("Bad Request (400)", func(t *testing.T) { + mockError := models.APIError{Title: "Bad Request", Detail: "Invalid refType parameter"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.ArtifactVersionReferencesParams{RefType: "INVALID"} + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", params) + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Bad Request", apiErr.Title) + assert.Equal(t, "Invalid refType parameter", apiErr.Detail) + }) + + t.Run("Not Found (404)", func(t *testing.T) { + mockError := models.APIError{Title: "Not Found", Detail: "Artifact not found"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.ArtifactVersionReferencesParams{} + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", params) + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Not Found", apiErr.Title) + assert.Equal(t, "Artifact not found", apiErr.Detail) + }) + + t.Run("Method Not Allowed (405)", func(t *testing.T) { + mockError := models.APIError{Title: "Method Not Allowed", Detail: "This method is not allowed"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.ArtifactVersionReferencesParams{} + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", params) + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Method Not Allowed", apiErr.Title) + assert.Equal(t, "This method is not allowed", apiErr.Detail) + }) + + t.Run("Internal Server Error (500)", func(t *testing.T) { + mockError := models.APIError{Title: "Internal Server Error", Detail: "An unexpected error occurred"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.ArtifactVersionReferencesParams{} + result, err := api.GetArtifactVersionReferences(context.Background(), "test-group", "artifact-1", "1", params) + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Internal Server Error", apiErr.Title) + assert.Equal(t, "An unexpected error occurred", apiErr.Detail) + }) +} + +func TestVersionsAPI_GetArtifactVersionComments(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := []models.ArtifactComment{ + {CommentID: "12345", Value: "This is a comment on an artifact version.", Owner: "dwayne", CreatedOn: "2023-07-01T15:22:01Z"}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/test-group/artifacts/artifact-1/versions/1/comments", r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + result, err := api.GetArtifactVersionComments(context.Background(), "test-group", "artifact-1", "1") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, 1, len(*result)) + assert.Equal(t, "This is a comment on an artifact version.", (*result)[0].Value) + }) + + t.Run("Bad Request (400)", func(t *testing.T) { + mockError := models.APIError{Title: "Bad Request", Detail: "Invalid version expression"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + result, err := api.GetArtifactVersionComments(context.Background(), "test-group", "artifact-1", "invalid-version") + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Bad Request", apiErr.Title) + assert.Equal(t, "Invalid version expression", apiErr.Detail) + }) + + t.Run("Not Found (404)", func(t *testing.T) { + mockError := models.APIError{Title: "Not Found", Detail: "Artifact not found"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + result, err := api.GetArtifactVersionComments(context.Background(), "non-existent-group", "non-existent-artifact", "1") + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Not Found", apiErr.Title) + assert.Equal(t, "Artifact not found", apiErr.Detail) + }) + + t.Run("Internal Server Error (500)", func(t *testing.T) { + mockError := models.APIError{Title: "Internal Server Error", Detail: "An unexpected error occurred"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(mockError) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + result, err := api.GetArtifactVersionComments(context.Background(), "test-group", "artifact-1", "1") + assert.Error(t, err) + assert.Nil(t, result) + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, "Internal Server Error", apiErr.Title) + assert.Equal(t, "An unexpected error occurred", apiErr.Detail) + }) +} + +func TestVersionsAPI_AddArtifactVersionComment(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.ArtifactComment{ + CommentID: "12345", + Value: "This is a new comment on an artifact version.", + Owner: "dwayne", + CreatedOn: "2023-07-01T15:22:01Z", + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/v1/comments", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + // Check if request body matches + var requestBody models.ArtifactComment + err := json.NewDecoder(r.Body).Decode(&requestBody) + assert.NoError(t, err) + assert.Equal(t, "This is a new comment on an artifact version.", requestBody.Value) + + // Write the response + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "This is a new comment on an artifact version." + result, err := api.AddArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", comment) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, mockResponse, *result) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid input"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "" + result, err := api.AddArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", comment) + + assert.Error(t, err) + assert.Nil(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid input", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "Artifact not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "This is a new comment" + result, err := api.AddArtifactVersionComment(context.Background(), "invalid-group", "example-artifact", "v1", comment) + + assert.Error(t, err) + assert.Nil(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "Artifact not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "This is a new comment" + result, err := api.AddArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", comment) + + assert.Error(t, err) + assert.Nil(t, result) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_UpdateArtifactVersionComment(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/v1/comments/12345", r.URL.Path) + assert.Equal(t, http.MethodPut, r.Method) + // Return success response + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "Updated comment value" + err := api.UpdateArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345", comment) + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid input"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "" + err := api.UpdateArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345", comment) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid input", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "Comment not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "" + err := api.UpdateArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "invalid-comment-id", comment) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "Comment not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + comment := "" + err := api.UpdateArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345", comment) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_DeleteArtifactVersionComment(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/v1/comments/12345", r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + // Return success response + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.DeleteArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345") + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid input"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.DeleteArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345") + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid input", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "Comment not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.DeleteArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "invalid-comment-id") + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "Comment not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.DeleteArtifactVersionComment(context.Background(), "my-group", "example-artifact", "v1", "12345") + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_ListArtifactVersions(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.ArtifactVersionListResponse{ + Count: 2, + Versions: []models.ArtifactVersion{ + { + CreatedOn: "2024-12-10T08:56:40Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 47, + Version: "2.0.0", + ContentID: 47, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:40Z", + }, + { + CreatedOn: "2024-12-10T08:56:17Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 46, + Version: "1.0.0", + ContentID: 46, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:17Z", + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + // Write the response + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + versions, err := api.ListArtifactVersions(context.Background(), "my-group", "example-artifact", nil) + assert.NoError(t, err) + assert.NotNil(t, versions) + assert.Equal(t, 2, len(*versions)) + assert.Equal(t, "2.0.0", (*versions)[0].Version) + assert.Equal(t, "1.0.0", (*versions)[1].Version) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + versions, err := api.ListArtifactVersions(context.Background(), "my-group", "example-artifact", nil) + assert.Error(t, err) + assert.Nil(t, versions) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + versions, err := api.ListArtifactVersions(context.Background(), "my-group", "example-artifact", nil) + assert.Error(t, err) + assert.Nil(t, versions) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_CreateArtifactVersion(t *testing.T) { + + t.Run("Success", func(t *testing.T) { + + mockResponse := models.ArtifactVersionDetailed{ + ArtifactVersion: models.ArtifactVersion{ + Version: "1.0.0", + CreatedOn: "2024-12-10T08:56:40Z", + ArtifactType: models.Json, + GlobalID: 40, + State: models.StateEnabled, + ContentID: 10, + ArtifactID: "my-artifact-id", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:40Z", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + // Return success response + // Write the response + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + IsDraft: false, + } + res, err := api.CreateArtifactVersion(context.Background(), "my-group", "example-artifact", createVersion, false) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, "1.0.0", res.Version) + assert.Equal(t, "Artifact Name", res.Name) + assert.Equal(t, "Artifact Description", res.Description) + assert.Equal(t, 2, len(res.Labels)) + assert.Equal(t, models.Json, res.ArtifactType) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid input"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + IsDraft: false, + } + res, err := api.CreateArtifactVersion(context.Background(), "my-group", "example-artifact", createVersion, false) + assert.Error(t, err) + assert.Nil(t, res) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid input", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "Comment not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + IsDraft: false, + } + res, err := api.CreateArtifactVersion(context.Background(), "my-group", "example-artifact", createVersion, false) + assert.Error(t, err) + assert.Nil(t, res) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "Comment not found", apiErr.Title) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 409, Title: "Conflict"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + IsDraft: false, + } + res, err := api.CreateArtifactVersion(context.Background(), "my-group", "example-artifact", createVersion, false) + assert.Error(t, err) + assert.Nil(t, res) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 409, apiErr.Status) + assert.Equal(t, "Conflict", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + }, + Name: "Artifact Name", + Description: "Artifact Description", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + IsDraft: false, + } + res, err := api.CreateArtifactVersion(context.Background(), "my-group", "example-artifact", createVersion, false) + assert.Error(t, err) + assert.Nil(t, res) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) + +} + +func TestVersionsAPI_GetArtifactVersionContent(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := `{"a": "1"}` + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/1.0.0/content", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + // Write the response + w.Header().Set("X-Registry-ArtifactType", string(models.Json)) + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(mockResponse)) + assert.NoError(t, err) + + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + content, err := api.GetArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", nil) + assert.NoError(t, err) + assert.NotEmpty(t, content) + assert.Equal(t, `{"a": "1"}`, content.Content) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "bad request"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + version, err := api.GetArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", nil) + assert.Error(t, err) + assert.Nil(t, version) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "bad request", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + version, err := api.GetArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", nil) + assert.Error(t, err) + assert.Nil(t, version) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + version, err := api.GetArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", nil) + assert.Error(t, err) + assert.Nil(t, version) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_UpdateArtifactVersionContent(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/1.0.0/content", r.URL.Path) + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + } + + err := api.UpdateArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", createVersion) + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/1.0.0/content", r.URL.Path) + assert.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid input"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + } + + err := api.UpdateArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", createVersion) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid input", apiErr.Title) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "Comment not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + } + + err := api.UpdateArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", createVersion) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "Comment not found", apiErr.Title) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 409, Title: "Conflict"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + } + + err := api.UpdateArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", createVersion) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 409, apiErr.Status) + assert.Equal(t, "Conflict", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + createVersion := &models.CreateContentRequest{ + Content: `{"a": "1"}`, + ContentType: "application/json", + } + + err := api.UpdateArtifactVersionContent(context.Background(), "my-group", "example-artifact", "1.0.0", createVersion) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_SearchForArtifactVersions(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.ArtifactVersionListResponse{ + Count: 2, + Versions: []models.ArtifactVersion{ + { + CreatedOn: "2024-12-10T08:56:40Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 47, + Version: "2.0.0", + ContentID: 47, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:40Z", + }, + { + CreatedOn: "2024-12-10T08:56:17Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 46, + Version: "1.0.0", + ContentID: 46, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:17Z", + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/search/versions", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + + // Write the response + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Search for json artifact and enabled state + params := &models.SearchVersionParams{ + ArtifactType: models.Json, + State: models.StateEnabled, + } + versions, err := api.SearchForArtifactVersions(context.Background(), params) + assert.NoError(t, err) + assert.NotNil(t, versions) + assert.Equal(t, 2, len(*versions)) + assert.Equal(t, "2.0.0", (*versions)[0].Version) + assert.Equal(t, "1.0.0", (*versions)[1].Version) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + // Search for json artifact and enabled state + params := &models.SearchVersionParams{ + ArtifactType: models.Json, + State: models.StateEnabled, + } + versions, err := api.SearchForArtifactVersions(context.Background(), params) + assert.Error(t, err) + assert.Nil(t, versions) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_SearchForArtifactVersionByContent(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.ArtifactVersionListResponse{ + Count: 2, + Versions: []models.ArtifactVersion{ + { + CreatedOn: "2024-12-10T08:56:40Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 47, + Version: "2.0.0", + ContentID: 47, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:40Z", + }, + { + CreatedOn: "2024-12-10T08:56:17Z", + ArtifactType: models.Json, + State: models.StateEnabled, + GlobalID: 46, + Version: "1.0.0", + ContentID: 46, + ArtifactID: "example-artifact", + GroupID: "my-group", + ModifiedOn: "2024-12-10T08:56:17Z", + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/search/versions", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, "test-content", string(body)) + + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.SearchVersionByContentParams{Limit: 10, Offset: 0} + versions, err := api.SearchForArtifactVersionByContent(context.Background(), "test-content", params) + assert.NoError(t, err) + assert.NotNil(t, versions) + assert.Equal(t, 2, len(*versions)) + assert.Equal(t, "2.0.0", (*versions)[0].Version) + assert.Equal(t, "1.0.0", (*versions)[1].Version) + }) + + t.Run("BadRequest - Empty Content", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "content cannot be empty"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.SearchVersionByContentParams{Limit: 10, Offset: 0} + versions, err := api.SearchForArtifactVersionByContent(context.Background(), "", params) + assert.Error(t, err) + assert.Nil(t, versions) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "content cannot be empty", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + params := &models.SearchVersionByContentParams{Limit: 10, Offset: 0} + versions, err := api.SearchForArtifactVersionByContent(context.Background(), "test-content", params) + assert.Error(t, err) + assert.Nil(t, versions) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_GetArtifactVersionState(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockResponse := models.StateResponse{ + State: models.StateEnabled, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/1.0/state", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + + // Write the response + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(mockResponse) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + state, err := api.GetArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0") + assert.NoError(t, err) + assert.NotNil(t, state) + assert.Equal(t, models.StateEnabled, *state) + }) + + t.Run("NotFound", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(models.APIError{Status: 404, Title: "not found"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + state, err := api.GetArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0") + assert.Error(t, err) + assert.Nil(t, state) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 404, apiErr.Status) + assert.Equal(t, "not found", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + state, err := api.GetArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0") + assert.Error(t, err) + assert.Nil(t, state) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +func TestVersionsAPI_UpdateArtifactVersionState(t *testing.T) { + t.Run("Success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/groups/my-group/artifacts/example-artifact/versions/1.0/state", r.URL.Path) + assert.Equal(t, http.MethodPut, r.Method) + + // Validate request body + var requestBody map[string]string + err := json.NewDecoder(r.Body).Decode(&requestBody) + assert.NoError(t, err) + assert.Equal(t, "ENABLED", requestBody["state"]) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.UpdateArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0", models.StateEnabled, false) + assert.NoError(t, err) + }) + + t.Run("BadRequest", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(models.APIError{Status: 400, Title: "Invalid state"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.UpdateArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0", "INVALID_STATE", false) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 400, apiErr.Status) + assert.Equal(t, "Invalid state", apiErr.Title) + }) + + t.Run("Conflict", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusConflict) + err := json.NewEncoder(w).Encode(models.APIError{Status: 409, Title: "Conflict"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.UpdateArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0", models.StateDraft, false) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 409, apiErr.Status) + assert.Equal(t, "Conflict", apiErr.Title) + }) + + t.Run("InternalServerError", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + err := json.NewEncoder(w).Encode(models.APIError{Status: 500, Title: "Internal server error"}) + assert.NoError(t, err) + })) + defer server.Close() + + mockClient := &client.Client{BaseURL: server.URL, HTTPClient: server.Client()} + api := apis.NewVersionsAPI(mockClient) + + err := api.UpdateArtifactVersionState(context.Background(), "my-group", "example-artifact", "1.0", models.StateEnabled, false) + assert.Error(t, err) + + var apiErr *models.APIError + ok := errors.As(err, &apiErr) + assert.True(t, ok) + assert.Equal(t, 500, apiErr.Status) + assert.Equal(t, "Internal server error", apiErr.Title) + }) +} + +/***********************/ +/***** Integration *****/ +/***********************/ + +func setupVersionsAPIClient() *apis.VersionsAPI { + apiClient := setupHTTPClient() + return apis.NewVersionsAPI(apiClient) +} + +func TestVersionsAPIIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + + versionsAPI := setupVersionsAPIClient() + + // Prepare test data + artifactsAPI := apis.NewArtifactsAPI(versionsAPI.Client) + + // Clean up before and after tests + t.Cleanup(func() { cleanup(t, artifactsAPI) }) + cleanup(t, artifactsAPI) + + // Test CreateArtifactVersion + t.Run("CreateArtifactVersion", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + request := &models.CreateVersionRequest{ + Version: newVersion, + Content: models.CreateContentRequest{ + Content: stubNewContent, + }, + } + + resp, err := versionsAPI.CreateArtifactVersion(ctx, groupID, generatedArtifactID, request, false) + assert.NoError(t, err) + assert.Equal(t, newVersion, resp.Version) + }) + + // Test ListArtifactVersions + t.Run("ListArtifactVersions", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + params := &models.ListArtifactsInGroupParams{} + resp, err := versionsAPI.ListArtifactVersions(ctx, groupID, generatedArtifactID, params) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(*resp), 1) + }) + + // Test GetArtifactVersionReferences + t.Run("GetArtifactVersionReferences", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + params := &models.ArtifactVersionReferencesParams{} + references, err := versionsAPI.GetArtifactVersionReferences(ctx, groupID, generatedArtifactID, version, params) + assert.NoError(t, err) + assert.NotNil(t, references) + }) + + // Test AddArtifactVersionComment + t.Run("AddArtifactVersionComment", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + comment, err := versionsAPI.AddArtifactVersionComment(ctx, groupID, generatedArtifactID, version, "Test comment") + assert.NoError(t, err) + assert.Equal(t, "Test comment", comment.Value) + }) + + // Test GetArtifactVersionComments + t.Run("GetArtifactVersionComments", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + // Add a comment first + comment, err := versionsAPI.AddArtifactVersionComment(ctx, groupID, generatedArtifactID, version, "Test comment") + assert.NoError(t, err) + assert.Equal(t, "Test comment", comment.Value) + + // Get comments + comments, err := versionsAPI.GetArtifactVersionComments(ctx, groupID, generatedArtifactID, version) + assert.NoError(t, err) + assert.NotNil(t, comments) + }) + + // Test UpdateArtifactVersionComment + t.Run("UpdateArtifactVersionComment", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + // Add a comment first + comment, err := versionsAPI.AddArtifactVersionComment(ctx, groupID, generatedArtifactID, version, "Initial comment") + assert.NoError(t, err) + + // Update the comment + err = versionsAPI.UpdateArtifactVersionComment(ctx, groupID, generatedArtifactID, version, comment.CommentID, "Updated comment") + assert.NoError(t, err) + }) + + // Test DeleteArtifactVersionComment + t.Run("DeleteArtifactVersionComment", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + // Add a comment first + comment, err := versionsAPI.AddArtifactVersionComment(ctx, groupID, generatedArtifactID, version, "Temporary comment") + assert.NoError(t, err) + + // Delete the comment + err = versionsAPI.DeleteArtifactVersionComment(ctx, groupID, generatedArtifactID, version, comment.CommentID) + assert.NoError(t, err) + }) + + // Test DeleteArtifactVersion + t.Run("DeleteArtifactVersion", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + err = versionsAPI.DeleteArtifactVersion(ctx, groupID, generatedArtifactID, version) + assert.NoError(t, err) + }) + + // Test GetArtifactVersionContent + t.Run("GetArtifactVersionContent", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + params := &models.ArtifactReferenceParams{} + content, err := versionsAPI.GetArtifactVersionContent(ctx, groupID, generatedArtifactID, version, params) + assert.NoError(t, err) + assert.NotNil(t, content) + }) + + // Test UpdateArtifactVersionContent + t.Run("UpdateArtifactVersionContent", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + content := &models.CreateContentRequest{ + Content: stubContent, + } + err = versionsAPI.UpdateArtifactVersionContent(ctx, groupID, generatedArtifactID, version, content) + assert.NoError(t, err) + }) + + // Test SearchForArtifactVersions + t.Run("SearchForArtifactVersions", func(t *testing.T) { + _, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + params := &models.SearchVersionParams{ + Version: version, + } + versions, err := versionsAPI.SearchForArtifactVersions(ctx, params) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(*versions), 1) + }) + + // Test GetArtifactVersionState + t.Run("GetArtifactVersionState", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + state, err := versionsAPI.GetArtifactVersionState(ctx, groupID, generatedArtifactID, version) + assert.NoError(t, err) + assert.Equal(t, models.StateDraft, *state) + }) + + // Test UpdateArtifactVersionState + t.Run("UpdateArtifactVersionState", func(t *testing.T) { + generatedArtifactID, err := generateArtifactForTest(ctx, artifactsAPI) + if err != nil { + t.Fatal(err) + } + + err = versionsAPI.UpdateArtifactVersionState(ctx, groupID, generatedArtifactID, version, models.StateDeprecated, false) + assert.NoError(t, err) + }) +} + +func generateArtifactForTest(ctx context.Context, artifactsAPI *apis.ArtifactsAPI) (string, error) { + // Helper to generate unique artifact IDs + generateArtifactID := func(prefix string) string { + return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano()) + } + + newArtifactID := generateArtifactID("test-artifact") + + artifact := models.CreateArtifactRequest{ + ArtifactID: newArtifactID, + ArtifactType: models.Json, + Name: newArtifactID, + FirstVersion: models.CreateVersionRequest{ + Version: "1.0.0", + Content: models.CreateContentRequest{ + Content: stubArtifactContent, + ContentType: "application/json", + }, + IsDraft: true, + }, + } + createParams := &models.CreateArtifactParams{ + IfExists: models.IfExistsFail, + } + _, err := artifactsAPI.CreateArtifact(ctx, groupID, artifact, createParams) + if err != nil { + return "", err + } + return newArtifactID, nil +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..8f5ddd2 --- /dev/null +++ b/client/client.go @@ -0,0 +1,70 @@ +package client + +import ( + "net" + "net/http" + "time" +) + +// Client is a reusable HTTP client for the SDK. +type Client struct { + BaseURL string + HTTPClient *http.Client + AuthHeader string +} + +// Option is a functional option for configuring the Client. +type Option func(*Client) + +// WithHTTPClient is an option for setting a custom http.Client. +func WithHTTPClient(httpClient *http.Client) Option { + return func(c *Client) { + c.HTTPClient = httpClient + } +} + +// WithAuthHeader is an option for setting an authentication header. +func WithAuthHeader(authHeader string) Option { + return func(c *Client) { + c.AuthHeader = authHeader + } +} + +// defaultHTTPClient provides a preconfigured HTTP client for the SDK. +func defaultHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + }, + } +} + +func NewClient(baseURL string, options ...Option) *Client { + client := &Client{ + BaseURL: baseURL, + HTTPClient: defaultHTTPClient(), + } + + // Apply functional options + for _, opt := range options { + opt(client) + } + + return client +} + +// Do perform an HTTP request with optional authentication. +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.AuthHeader != "" { + req.Header.Set("Authorization", c.AuthHeader) + } + req.Header.Set("Content-Type", "application/json") + return c.HTTPClient.Do(req) +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..98187a9 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,76 @@ +package client_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/mollie/go-apicurio-registry/client" + "github.com/stretchr/testify/assert" +) + +func TestNewClient_Defaults(t *testing.T) { + c := client.NewClient("https://example.com") + + assert.Equal(t, "https://example.com", c.BaseURL) + assert.NotNil(t, c.HTTPClient) + assert.Equal(t, 30*time.Second, c.HTTPClient.Timeout) +} + +func TestNewClient_WithCustomHTTPClient(t *testing.T) { + customHTTPClient := &http.Client{Timeout: 10 * time.Second} + + c := client.NewClient("https://example.com", client.WithHTTPClient(customHTTPClient)) + + assert.Equal(t, "https://example.com", c.BaseURL) + assert.Equal(t, customHTTPClient, c.HTTPClient) +} + +func TestNewClient_WithAuthHeader(t *testing.T) { + authHeader := "Bearer test-token" + + c := client.NewClient("https://example.com", client.WithAuthHeader(authHeader)) + + assert.Equal(t, "Bearer test-token", c.AuthHeader) +} + +func TestClient_Do_WithAuthHeader(t *testing.T) { + // Create a test HTTP server + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + w.WriteHeader(http.StatusOK) + }) + server := httptest.NewServer(handler) + defer server.Close() + + c := client.NewClient(server.URL, client.WithAuthHeader("Bearer test-token")) + + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + assert.NoError(t, err) + + resp, err := c.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestClient_Do_WithoutAuthHeader(t *testing.T) { + // Create a test HTTP server + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Empty(t, r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + w.WriteHeader(http.StatusOK) + }) + server := httptest.NewServer(handler) + defer server.Close() + + c := client.NewClient(server.URL) + + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + assert.NoError(t, err) + + resp, err := c.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aebc3f7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + apicurio-registry: + image: apicurio/apicurio-registry:3.0.5 + ports: + - "9080:8080" + environment: + LOG_LEVEL: DEBUG + QUARKUS_HTTP_CORS_ORIGINS: '*' # Allow CORS from all origins + APICURIO_REST_MUTABILITY_ARTIFACT_VERSION_CONTENT_ENABLED: "true" + APICURIO_REST_DELETION_ARTIFACT_ENABLED: "true" + APICURIO_REST_DELETION_ARTIFACT_VERSION_ENABLED: "true" + APICURIO_REST_DELETION_GROUP_ENABLED: "true" + + apicurio-ui: + image: apicurio/apicurio-registry-ui:3.0.5 + ports: + - "9090:8080" + environment: + REGISTRY_API_URL: "http://localhost:9080/apis/registry/v3" + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5d7ec6f --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/mollie/go-apicurio-registry + +go 1.23 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33dd589 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/models/errors.go b/models/errors.go new file mode 100644 index 0000000..881783d --- /dev/null +++ b/models/errors.go @@ -0,0 +1,23 @@ +package models + +import "fmt" + +var ( + ErrUnknownArtifactType = fmt.Errorf("unknown artifact type") +) + +// APIError represents the structure of an error response from the API. +type APIError struct { + Detail string `json:"detail"` // A human-readable explanation specific to the problem + Type string `json:"type"` // A URI reference identifying the problem type + Title string `json:"title"` // A short, human-readable summary of the problem type + Status int `json:"status"` // The HTTP status code + Instance string `json:"instance"` // A URI reference identifying the specific occurrence + Name string `json:"name"` // The name of the error (e.g., server exception class name) +} + +// Error satisfies the error interface and formats the APIError as a string. +func (e *APIError) Error() string { + return fmt.Sprintf("[%d] %s: %s (detail: %s, instance: %s, type: %s)", + e.Status, e.Title, e.Name, e.Detail, e.Instance, e.Type) +} diff --git a/models/general.go b/models/general.go new file mode 100644 index 0000000..f6638d4 --- /dev/null +++ b/models/general.go @@ -0,0 +1,130 @@ +package models + +// IfExistsType represents the IfExists types for creating an artifact. +type IfExistsType string + +const ( + IfExistsFail IfExistsType = "FAIL" // (default) - server rejects the content with a 409 error + IfExistsCreate IfExistsType = "CREATE_VERSION" // server creates a new version of the existing artifact and returns it + IfExistsFindOrCreateVersion IfExistsType = "FIND_OR_CREATE_VERSION" // server returns an existing version that matches the provided content if such a version exists, otherwise a new version is created +) + +// State represents the state of an artifact. +type State string + +const ( + StateEnabled State = "ENABLED" + StateDisabled State = "DISABLED" + StateDeprecated State = "DEPRECATED" + StateDraft State = "DRAFT" +) + +// Order represents the order of the results. +type Order string + +const ( + OrderAsc Order = "asc" + OrderDesc Order = "desc" +) + +// OrderBy represents the field to sort by. +type OrderBy string + +const ( + OrderByGroupId OrderBy = "groupId" + OrderByArtifactId OrderBy = "artifactId" + OrderByVersion OrderBy = "version" + OrderByName OrderBy = "name" + OrderByCreatedOn OrderBy = "createdOn" + OrderByModifiedOn OrderBy = "modifiedOn" + OrderByGlobalId OrderBy = "globalId" +) + +// HandleReferencesType represents the type of handling references. +type HandleReferencesType string + +const ( + HandleReferencesTypePreserve HandleReferencesType = "PRESERVE" + HandleReferencesTypeDereference HandleReferencesType = "DEREFERENCE" + HandleReferencesTypeRewrite HandleReferencesType = "REWRITE" +) + +// RefType represents the type of reference. +type RefType string + +const ( + OutBound RefType = "OUTBOUND" + InBound RefType = "INBOUND" +) + +// ArtifactType represents the type of artifact. +type ArtifactType string + +const ( + Avro ArtifactType = "AVRO" // Avro artifact type + Protobuf ArtifactType = "PROTOBUF" // Protobuf artifact type + Json ArtifactType = "JSON" // JSON artifact type + KConnect ArtifactType = "KCONNECT" // Kafka Connect artifact type + OpenAPI ArtifactType = "OPENAPI" // OpenAPI artifact type + AsyncAPI ArtifactType = "ASYNCAPI" // AsyncAPI artifact type + GraphQL ArtifactType = "GRAPHQL" // GraphQL artifact type + WSDL ArtifactType = "WSDL" // WSDL artifact type + XSD ArtifactType = "XSD" // XSD artifact type +) + +// ParseArtifactType parses a string and returns the corresponding ArtifactType. +func ParseArtifactType(artifactType string) (ArtifactType, error) { + switch artifactType { + case string(Avro): + return Avro, nil + case string(Protobuf): + return Protobuf, nil + case string(Json): + return Json, nil + case string(KConnect): + return KConnect, nil + case string(OpenAPI): + return OpenAPI, nil + case string(AsyncAPI): + return AsyncAPI, nil + case string(GraphQL): + return GraphQL, nil + case string(WSDL): + return WSDL, nil + case string(XSD): + return XSD, nil + default: + return "", ErrUnknownArtifactType + } +} + +type Rule string + +const ( + RuleValidity Rule = "VALIDITY" + RuleCompatibility Rule = "COMPATIBILITY" + RuleIntegrity Rule = "INTEGRITY" +) + +// RuleLevel represents the level of different rules for VALIDITY, COMPATIBILITY, and INTEGRITY. +type RuleLevel string + +const ( + IntegrityLevelNone RuleLevel = "NONE" + IntegrityLevelRefsExist RuleLevel = "REFS_EXIST" + IntegrityLevelAllRefsMapped RuleLevel = "ALL_REFS_MAPPED" + IntegrityLevelNoDuplicates RuleLevel = "NO_DUPLICATES" + IntegrityLevelFull RuleLevel = "FULL" + + CompatibilityLevelBackward RuleLevel = "BACKWARD" + CompatibilityLevelBackwardTransitive RuleLevel = "BACKWARD_TRANSITIVE" + CompatibilityLevelForward RuleLevel = "FORWARD" + CompatibilityLevelForwardTransitive RuleLevel = "FORWARD_TRANSITIVE" + CompatibilityLevelFull RuleLevel = "FULL" + CompatibilityLevelFullTransitive RuleLevel = "FULL_TRANSITIVE" + CompatibilityLevelNone RuleLevel = "NONE" + + ValidityLevelNone RuleLevel = "NONE" + ValidityLevelSyntaxOnly RuleLevel = "SYNTAX_ONLY" + ValidityLevelFull RuleLevel = "FULL" +) diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..2c59d89 --- /dev/null +++ b/models/models.go @@ -0,0 +1,105 @@ +package models + +// ======================================== +// SECTION: Models +// ======================================== + +// ArtifactReference represents a reference to an artifact. +type ArtifactReference struct { + GroupID string `json:"groupId"` + ArtifactID string `json:"artifactId"` + Version string `json:"version"` + Name string `json:"name"` +} + +// SearchedArtifact represents the search result of an artifact. +type SearchedArtifact struct { + GroupId string `json:"groupId"` + ArtifactId string `json:"artifactId"` + Name string `json:"name"` + Description string `json:"description"` + ArtifactType ArtifactType `json:"artifactType"` + Owner string `json:"owner"` + CreatedOn string `json:"createdOn"` + ModifiedBy string `json:"modifiedBy"` + ModifiedOn string `json:"modifiedOn"` +} + +// ArtifactContent represents the content of an artifact + the type of the artifact. +type ArtifactContent struct { + Content string `json:"content"` + ArtifactType ArtifactType `json:"artifactType"` +} + +// ArtifactDetail represents the detailed information about an artifact. +type ArtifactDetail struct { + GroupID string `json:"groupId"` + ArtifactID string `json:"artifactId"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + CreatedOn string `json:"createdOn"` + ModifiedOn string `json:"modifiedOn"` + ContentID int64 `json:"contentId"` + Labels map[string]string `json:"labels"` +} + +// BaseMetadata contains common fields shared by both artifact and artifact version metadata. +type BaseMetadata struct { + GroupID string `json:"groupId"` + ArtifactID string `json:"artifactId"` + Name string `json:"name"` + Description string `json:"description"` + ArtifactType string `json:"artifactType"` + Owner string `json:"owner"` + CreatedOn string `json:"createdOn"` + Labels map[string]string `json:"labels"` +} + +// ArtifactVersionMetadata represents metadata for a single artifact version. +type ArtifactVersionMetadata struct { + BaseMetadata + Version string `json:"version"` + GlobalID int64 `json:"globalId"` + ContentID int64 `json:"contentId"` +} + +// ArtifactMetadata represents metadata for an artifact. +type ArtifactMetadata struct { + BaseMetadata + ModifiedBy string `json:"modifiedBy"` + ModifiedOn string `json:"modifiedOn"` +} + +// ArtifactComment represents a comment on a specific artifact version. +// It's used in the response of GetArtifactVersionComments +type ArtifactComment struct { + CommentID string `json:"commentId"` // Unique identifier for the comment. + Value string `json:"value"` // The content of the comment. + Owner string `json:"owner"` // The user who created the comment. + CreatedOn string `json:"createdOn"` // The timestamp when the comment was created. +} + +// ArtifactVersion represents a single version of an artifact. it has the minimum information +// required to identify an artifact version. while ArtifactVersionDetailed has more information +type ArtifactVersion struct { + Version string `json:"version" validate:"required"` // A single version of the artifact + Owner string `json:"owner" validate:"required"` // Owner of the artifact version + CreatedOn string `json:"createdOn" validate:"required"` // Creation timestamp + ArtifactType ArtifactType `json:"artifactType" validate:"required"` // Type of the artifact + GlobalID int64 `json:"globalId" validate:"required"` // Global identifier for the artifact version + State State `json:"state,omitempty" validate:"omitempty,oneof=ENABLED DISABLED DEPRECATED DRAFT"` // State of the artifact version + ContentID int64 `json:"contentId" validate:"required"` // Content ID of the artifact version + ArtifactID string `json:"artifactId" validate:"required,max=512"` // Artifact ID + GroupID string `json:"groupId,omitempty" validate:"omitempty,max=512"` // Artifact group ID + ModifiedBy string `json:"modifiedBy,omitempty"` // User who last modified the artifact version + ModifiedOn string `json:"modifiedOn,omitempty"` // Last modification timestamp +} + +// ArtifactVersionDetailed represents a single version of an artifact with additional information. +type ArtifactVersionDetailed struct { + ArtifactVersion // Embedding ArtifactVersion + Name string `json:"name,omitempty"` // Name of the artifact version + Description string `json:"description,omitempty"` // Description of the artifact version + Labels map[string]string `json:"labels,omitempty"` // User-defined name-value pairs +} diff --git a/models/params.go b/models/params.go new file mode 100644 index 0000000..07411a1 --- /dev/null +++ b/models/params.go @@ -0,0 +1,309 @@ +package models + +import ( + "net/url" + "strconv" + "strings" +) + +// ======================================== +// SECTION: Params +// ======================================== + +// SearchArtifactsParams represents the optional parameters for searching artifacts. +type SearchArtifactsParams struct { + Name string // Filter by artifact name + Offset int // Default: 0 + Limit int // Default: 20 + Order Order // Default: "asc", Enum: "asc", "desc" + OrderBy OrderBy // Field to sort by, e.g., "name", "createdOn" + Labels []string // Filter by one or more name/value labels + Description string // Filter by description + GroupID string // Filter by artifact group + GlobalID int64 // Filter by globalId + ContentID int64 // Filter by contentId + ArtifactID string // Filter by artifactId + ArtifactType ArtifactType // Filter by artifact type (e.g., AVRO, JSON) +} + +// ToQuery converts the SearchArtifactsParams struct to URL query parameters. +func (p *SearchArtifactsParams) ToQuery() url.Values { + query := url.Values{} + + if p.Name != "" { + query.Set("name", p.Name) + } + if p.Offset != 0 { + query.Set("offset", strconv.Itoa(p.Offset)) + } + if p.Limit != 0 { + query.Set("limit", strconv.Itoa(p.Limit)) + } + if p.Order != "" { + query.Set("order", string(p.Order)) + } + if p.OrderBy != "" { + query.Set("orderby", string(p.OrderBy)) + } + if len(p.Labels) > 0 { + query.Set("labels", strings.Join(p.Labels, ",")) + } + if p.Description != "" { + query.Set("description", p.Description) + } + if p.GroupID != "" { + query.Set("groupId", p.GroupID) + } + if p.GlobalID != 0 { + query.Set("globalId", strconv.FormatInt(p.GlobalID, 10)) + } + if p.ContentID != 0 { + query.Set("contentId", strconv.FormatInt(p.ContentID, 10)) + } + if p.ArtifactID != "" { + query.Set("artifactId", p.ArtifactID) + } + if p.ArtifactType != "" { + query.Set("artifactType", string(p.ArtifactType)) + } + + return query +} + +// SearchArtifactsByContentParams represents the query parameters for the search by content API. +type SearchArtifactsByContentParams struct { + Canonical bool // Canonicalize the content + ArtifactType string // Artifact type (e.g., AVRO, JSON) + GroupID string // Filter by group ID + Offset int // Number of artifacts to skip + Limit int // Number of artifacts to return + Order Order // Sort order (asc, desc) + OrderBy OrderBy // Field to sort by +} + +// ToQuery converts the SearchArtifactsByContentParams struct to query parameters. +func (p *SearchArtifactsByContentParams) ToQuery() url.Values { + query := url.Values{} + + if p.Canonical { + query.Set("canonical", "true") + } + if p.ArtifactType != "" { + query.Set("artifactType", p.ArtifactType) + } + if p.GroupID != "" { + query.Set("groupId", p.GroupID) + } + if p.Offset != 0 { + query.Set("offset", strconv.Itoa(p.Offset)) + } + if p.Limit != 0 { + query.Set("limit", strconv.Itoa(p.Limit)) + } + if p.Order != "" { + query.Set("order", string(p.Order)) + } + if p.OrderBy != "" { + query.Set("orderby", string(p.OrderBy)) + } + + return query +} + +// CreateArtifactParams represents the parameters for creating an artifact. +type CreateArtifactParams struct { + IfExists IfExistsType // IfExists behavior @See IfExistsType + Canonical bool // Indicates whether to canonicalize the artifact content. + DryRun bool // If true, no changes are made, only checks are performed. +} + +// ToQuery converts the parameters into a query string. +func (p *CreateArtifactParams) ToQuery() url.Values { + query := url.Values{} + if p.IfExists != "" { + query.Set("ifExists", string(p.IfExists)) + } + if p.Canonical { + query.Set("canonical", "true") + } + if p.DryRun { + query.Set("dryRun", "true") + } + return query +} + +// ListArtifactReferencesByGlobalIDParams represents the optional parameters for listing references by global ID. +type ListArtifactReferencesByGlobalIDParams struct { + RefType RefType +} + +// ToQuery converts the params struct to URL query parameters. +func (p *ListArtifactReferencesByGlobalIDParams) ToQuery() url.Values { + query := url.Values{} + if p != nil && p.RefType != "" { + query.Set("refType", string(p.RefType)) + } + return query +} + +// ListArtifactsInGroupParams represents the query parameters for listing artifacts in a group. +type ListArtifactsInGroupParams struct { + Limit int // Number of artifacts to return (default: 20) + Offset int // Number of artifacts to skip (default: 0) + Order string // Enum: "asc", "desc" + OrderBy string // Enum: "groupId", "artifactId", "createdOn", "modifiedOn", "artifactType", "name" +} + +// ToQuery converts the ListArtifactsInGroupParams struct to query parameters. +func (p *ListArtifactsInGroupParams) ToQuery() url.Values { + query := url.Values{} + if p.Limit != 0 { + query.Set("limit", strconv.Itoa(p.Limit)) + } + if p.Offset != 0 { + query.Set("offset", strconv.Itoa(p.Offset)) + } + if p.Order != "" { + query.Set("order", p.Order) + } + if p.OrderBy != "" { + query.Set("orderby", p.OrderBy) + } + return query +} + +// ArtifactVersionReferencesParams represents the query parameters for GetArtifactVersionReferences. +type ArtifactVersionReferencesParams struct { + RefType RefType // "INBOUND" or "OUTBOUND" +} + +// ToQuery converts the ArtifactVersionReferencesParams struct to URL query parameters. +func (p *ArtifactVersionReferencesParams) ToQuery() url.Values { + query := url.Values{} + if p != nil && p.RefType != "" { + query.Set("refType", string(p.RefType)) + } + return query +} + +// ArtifactReferenceParams represents the query parameters for artifact references. +type ArtifactReferenceParams struct { + HandleReferencesType HandleReferencesType +} + +// ToQuery converts the ArtifactReferenceParams into URL query parameters. +func (p ArtifactReferenceParams) ToQuery() url.Values { + query := url.Values{} + if p.HandleReferencesType != "" { + query.Set("references", string(p.HandleReferencesType)) + } + return query +} + +// SearchVersionParams represents the query parameters for searching artifact versions. +type SearchVersionParams struct { + Version string + Offset int + Limit int + Order Order + OrderBy OrderBy + Labels []string + Description string + GroupID string + GlobalID int64 + ContentID int64 + ArtifactID string + Name string + State State + ArtifactType ArtifactType +} + +// ToQuery converts the SearchVersionParams into URL query parameters. +func (p *SearchVersionParams) ToQuery() url.Values { + query := url.Values{} + if p.Version != "" { + query.Set("version", p.Version) + } + if p.Offset > 0 { + query.Set("offset", strconv.Itoa(p.Offset)) + } + if p.Limit > 0 { + query.Set("limit", strconv.Itoa(p.Limit)) + } + if p.Order != "" { + query.Set("order", string(p.Order)) + } + if p.OrderBy != "" { + query.Set("orderby", string(p.OrderBy)) + } + if len(p.Labels) > 0 { + query.Set("labels", strings.Join(p.Labels, ",")) + } + if p.Description != "" { + query.Set("description", p.Description) + } + if p.GroupID != "" { + query.Set("groupId", p.GroupID) + } + if p.GlobalID > 0 { + query.Set("globalId", strconv.FormatInt(p.GlobalID, 10)) + } + if p.ContentID > 0 { + query.Set("contentId", strconv.FormatInt(p.ContentID, 10)) + } + if p.ArtifactID != "" { + query.Set("artifactId", p.ArtifactID) + } + if p.Name != "" { + query.Set("name", p.Name) + } + if p.State != "" { + query.Set("state", string(p.State)) + } + if p.ArtifactType != "" { + query.Set("artifactType", string(p.ArtifactType)) + } + return query +} + +// SearchVersionByContentParams defines the query parameters for searching artifact versions by content. +type SearchVersionByContentParams struct { + Canonical *bool + ArtifactType ArtifactType + Offset int + Limit int + Order Order + OrderBy OrderBy + GroupID string + ArtifactID string +} + +// ToQuery converts the SearchVersionByContentParams into URL query parameters. +func (p *SearchVersionByContentParams) ToQuery() url.Values { + query := url.Values{} + if p.Canonical != nil { + query.Set("canonical", strconv.FormatBool(*p.Canonical)) + } + if p.ArtifactType != "" { + query.Set("artifactType", string(p.ArtifactType)) + } + if p.Offset > 0 { + query.Set("offset", strconv.Itoa(p.Offset)) + } + if p.Limit > 0 { + query.Set("limit", strconv.Itoa(p.Limit)) + } + if p.Order != "" { + query.Set("order", string(p.Order)) + } + if p.OrderBy != "" { + query.Set("orderby", string(p.OrderBy)) + } + if p.GroupID != "" { + query.Set("groupId", p.GroupID) + } + if p.ArtifactID != "" { + query.Set("artifactId", p.ArtifactID) + } + return query +} diff --git a/models/requests.go b/models/requests.go new file mode 100644 index 0000000..cf3d6d2 --- /dev/null +++ b/models/requests.go @@ -0,0 +1,50 @@ +package models + +// ======================================== +// SECTION: Requests +// ======================================== + +// CreateArtifactRequest represents the request to create an artifact. +type CreateArtifactRequest struct { + ArtifactID string `json:"artifactId,omitempty"` + ArtifactType ArtifactType `json:"artifactType"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + FirstVersion CreateVersionRequest `json:"firstVersion,omitempty"` +} + +// CreateVersionRequest represents the request to create a version for an artifact. +type CreateVersionRequest struct { + Version string `json:"version"` + Content CreateContentRequest `json:"content" validate:"required"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Branches []string `json:"branches,omitempty"` + IsDraft bool `json:"isDraft"` +} + +// CreateContentRequest represents the content of an artifact. +type CreateContentRequest struct { + Content string `json:"content"` + References []ArtifactReference `json:"references,omitempty"` + ContentType string `json:"contentType"` +} + +// UpdateArtifactMetadataRequest represents the metadata update request. +type UpdateArtifactMetadataRequest struct { + Name string `json:"name,omitempty"` // Editable name + Description string `json:"description,omitempty"` // Editable description + Labels map[string]string `json:"labels,omitempty"` // Editable labels + Owner string `json:"owner,omitempty"` // Editable owner +} + +type StateRequest struct { + State State `json:"state"` +} + +type CreateUpdateGlobalRuleRequest struct { + RuleType Rule `json:"ruleType"` + Config RuleLevel `json:"config"` +} diff --git a/models/responses.go b/models/responses.go new file mode 100644 index 0000000..114d4cd --- /dev/null +++ b/models/responses.go @@ -0,0 +1,37 @@ +package models + +// ======================================== +// SECTION: Responses +// ======================================== + +// SearchArtifactsAPIResponse represents the response from the search artifacts API. +type SearchArtifactsAPIResponse struct { + Artifacts []SearchedArtifact `json:"artifacts"` + Count int `json:"count"` +} + +// ListArtifactsResponse represents the response from the list artifacts API. +type ListArtifactsResponse struct { + Artifacts []SearchedArtifact `json:"artifacts"` + Count int `json:"count"` +} + +// CreateArtifactResponse represents the response from the create artifact API. +type CreateArtifactResponse struct { + Artifact ArtifactDetail `json:"artifact"` +} + +// ArtifactVersionListResponse represents the response of GetArtifactVersions. +type ArtifactVersionListResponse struct { + Count int `json:"count"` + Versions []ArtifactVersion `json:"versions"` +} + +type StateResponse struct { + State State `json:"state"` +} + +type GlobalRuleResponse struct { + RuleType Rule `json:"ruleType"` + Config RuleLevel `json:"config"` +}