Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds snippet support #3364

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/3364.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
snippets: Add support to upload snippets
```
207 changes: 207 additions & 0 deletions snippets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package cloudflare

import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"

"github.com/goccy/go-json"
)

type Snippet struct {
CreatedOn string `json:"created_on"`
ModifiedOn string `json:"modified_on"`
SnippetName string `json:"snippet_name"`
}

type ListSnippetsParams struct{}

type ListSnippetsResponse struct {
Response
ResultInfo
Snippets []Snippet `json:"result"`
}

// ListSnippets returns a list of Snippets for a given zone.
//
// API reference: https://developers.cloudflare.com/api/operations/zone-snippets
func (api *API) ListSnippets(ctx context.Context, rc *ResourceContainer, params ListSnippetsParams) (ListSnippetsResponse, *ResultInfo, error) {
if rc.Level != ZoneRouteLevel {
return ListSnippetsResponse{}, &ResultInfo{}, ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return ListSnippetsResponse{}, &ResultInfo{}, ErrMissingZoneID
}

uri := fmt.Sprintf("/zones/%s/snippets", rc.Identifier)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return ListSnippetsResponse{}, &ResultInfo{}, err
}

var r ListSnippetsResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ListSnippetsResponse{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

return r, &r.ResultInfo, nil
}

type GetSnippetParams struct {
SnippetName string
}

type GetSnippetResponse struct {
Response
Snippet Snippet `json:"result"`
}

// GetSnippet returns a single Snippet for a given zone.
//
// API reference: https://developers.cloudflare.com/api/operations/zone-snippets-snippet
func (api *API) GetSnippet(ctx context.Context, rc *ResourceContainer, params GetSnippetParams) (GetSnippetResponse, error) {
if rc.Level != ZoneRouteLevel {
return GetSnippetResponse{}, ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return GetSnippetResponse{}, ErrMissingZoneID
}

uri := fmt.Sprintf("/zones/%s/snippets/%s", rc.Identifier, params.SnippetName)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return GetSnippetResponse{}, err
}

var r GetSnippetResponse
err = json.Unmarshal(res, &r)
if err != nil {
return GetSnippetResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

return r, nil
}

type DeleteSnippetParams struct {
SnippetName string
}

type DeleteSnippetResponse struct {
Response
}

// DeleteSnippet deletes a single Snippet for a given zone.
//
// API reference: https://developers.cloudflare.com/api/operations/zone-snippets-snippet
func (api *API) DeleteSnippet(ctx context.Context, rc *ResourceContainer, params DeleteSnippetParams) (DeleteSnippetResponse, error) {
if rc.Level != ZoneRouteLevel {
return DeleteSnippetResponse{}, ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return DeleteSnippetResponse{}, ErrMissingZoneID
}

uri := fmt.Sprintf("/zones/%s/snippets/%s", rc.Identifier, params.SnippetName)
res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
if err != nil {
return DeleteSnippetResponse{}, err
}

var r DeleteSnippetResponse
err = json.Unmarshal(res, &r)
if err != nil {
return DeleteSnippetResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

return r, nil
}

type PutSnippetParams struct {
SnippetName string
Files map[string]string
MainModule string
}

type PutSnippetParamsMetadata struct {
MainModule string `json:"main_module"`
}

type PutSnippetResponse struct {
Response
Snippet Snippet `json:"result"`
}

// PutSnippet updates a Snippet for a given zone.
//
// API reference: https://developers.cloudflare.com/api/operations/zone-snippets-snippet
func (api *API) PutSnippet(ctx context.Context, rc *ResourceContainer, params PutSnippetParams) (PutSnippetResponse, error) {
if rc.Level != ZoneRouteLevel {
return PutSnippetResponse{}, ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return PutSnippetResponse{}, ErrMissingZoneID
}

uri := fmt.Sprintf("/zones/%s/snippets/%s", rc.Identifier, params.SnippetName)

header, body, err := formatSnippetMultipartBody(params)
if err != nil {
return PutSnippetResponse{}, err
}

headers := make(http.Header)
headers.Set("Content-Type", header)

res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)
if err != nil {
return PutSnippetResponse{}, err
}

var r PutSnippetResponse
err = json.Unmarshal(res, &r)
if err != nil {
return PutSnippetResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}

return r, nil
}

func formatSnippetMultipartBody(params PutSnippetParams) (string, []byte, error) {
var buf = &bytes.Buffer{}
var mpw = multipart.NewWriter(buf)
defer mpw.Close()

metadataParam := PutSnippetParamsMetadata{
MainModule: params.MainModule,
}

metadata, err := json.Marshal(metadataParam)
if err != nil {
return "", nil, err
}

mpw.WriteField("metadata", string(metadata))

for k, v := range params.Files {
fw, err := mpw.CreateFormFile(k, k)
if err != nil {
return "", nil, err
}

_, err = fw.Write([]byte(v))
if err != nil {
return "", nil, err
}
}

mpw.Close()

return mpw.FormDataContentType(), buf.Bytes(), nil
}
168 changes: 168 additions & 0 deletions snippets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package cloudflare

import (
"context"
"fmt"
"io"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

func TestListSnippets(t *testing.T) {
setup()
defer teardown()
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"success": true,
"result": [
{
"created_on": "2024-01-12T17:40:39Z",
"modified_on": "2024-01-12T17:40:39Z",
"snippet_name": "hello_world"
}
],
"result_info": {
"page": 1,
"per_page": 25,
"total_pages": 1,
"count": 1,
"total_count": 1
}
}`)
}

mux.HandleFunc("/zones/"+testZoneID+"/snippets", handler)

t.Run("Lists snippets found in the response", func(t *testing.T) {
params := ListSnippetsParams{}
response, _, err := client.ListSnippets(context.Background(), ZoneIdentifier(testZoneID), params)
assert.NoError(t, err)

assert.Equal(t, "hello_world", response.Snippets[0].SnippetName)
})
}

func TestDeleteSnippets(t *testing.T) {
setup()
defer teardown()
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"errors":[],
"messages":[],
"success": true
}`)
}

mux.HandleFunc("/zones/"+testZoneID+"/snippets/hello_world", handler)

t.Run("Deletes a snippet", func(t *testing.T) {
params := DeleteSnippetParams{
SnippetName: "hello_world",
}
response, err := client.DeleteSnippet(context.Background(), ZoneIdentifier(testZoneID), params)
assert.NoError(t, err)

assert.Equal(t, true, response.Success)
})
}

func TestGetSnippet(t *testing.T) {
setup()
defer teardown()
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"success": true,
"result": {
"created_on": "2024-01-12T17:40:39Z",
"modified_on": "2024-01-12T17:40:39Z",
"snippet_name": "hello_world"
}
}`)
}

mux.HandleFunc("/zones/"+testZoneID+"/snippets/hello_world", handler)

t.Run("Gets the hello_world snippet", func(t *testing.T) {
params := GetSnippetParams{
SnippetName: "hello_world",
}
response, err := client.GetSnippet(context.Background(), ZoneIdentifier(testZoneID), params)
assert.NoError(t, err)

assert.Equal(t, true, response.Success)
assert.Equal(t, "hello_world", response.Snippet.SnippetName)
})
}

func TestUploadSnippet(t *testing.T) {
setup()
defer teardown()

handler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)

err := r.ParseMultipartForm(1024 * 1024 * 10)
assert.NoError(t, err)

assert.Equal(t, "{\"main_module\":\"main.js\"}", r.MultipartForm.Value["metadata"][0])

fileHeader, ok := r.MultipartForm.File["main.js"]
assert.True(t, ok)

file, err := fileHeader[0].Open()
assert.NoError(t, err)
defer file.Close()

content, err := io.ReadAll(file)
assert.NoError(t, err)

assert.Equal(t, "main.js", fileHeader[0].Filename)
assert.Equal(t, "console.log('main.js')", string(content))

fileHeader, ok = r.MultipartForm.File["worker.js"]
assert.True(t, ok)

file, err = fileHeader[0].Open()
assert.NoError(t, err)
defer file.Close()

content, err = io.ReadAll(file)
assert.NoError(t, err)

assert.Equal(t, "worker.js", fileHeader[0].Filename)
assert.Equal(t, "console.log('worker.js')", string(content))

w.Header().Set("content-type", "application/json")
fmt.Fprint(w, `{
"errors": [],
"messages": [],
"success": true,
"result": {
"created_on": "2023-07-24-00:00:00",
"modified_on": "2023-07-24-00:00:00",
"snippet_name": "test_snippet"
}
}`)
}
mux.HandleFunc("/zones/"+testZoneID+"/snippets/test_snippet", handler)

t.Run("Test multiples files are encoded in the request body", func(t *testing.T) {
params := PutSnippetParams{
SnippetName: "test_snippet",
MainModule: "main.js",
Files: map[string]string{
"main.js": "console.log('main.js')",
"worker.js": "console.log('worker.js')",
},
}
response, err := client.PutSnippet(context.Background(), ZoneIdentifier(testZoneID), params)
assert.NoError(t, err)

assert.Equal(t, "test_snippet", response.Snippet.SnippetName)
})
}
Loading