Skip to content

Commit

Permalink
Merge branch 'develop' into abc
Browse files Browse the repository at this point in the history
  • Loading branch information
ducdm49 committed Aug 2, 2024
2 parents 2e2543e + e3487e1 commit 2ca1705
Show file tree
Hide file tree
Showing 86 changed files with 5,432 additions and 111 deletions.
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@
# Go workspace file
go.work

# User-specific stuff
.idea/*
.idea
.idea/

# .tfstate files
*.tfstate
*.tfstate.*

# .log files
*.log
*.log.*
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,16 @@

- [Terraform](https://developer.hashicorp.com/terraform/downloads) >= 1.0
- [Go](https://golang.org/doc/install) >= 1.21

## Testing
```sh
export FPTCLOUD_API_URL=local_api_url  2 ↵  11155  17:25:00
export FPTCLOUD_REGION=your_region
export FPTCLOUD_TENANT_NAME=your_tenant_anme
export FPTCLOUD_TOKEN=your_token
export TF_ACC=1
export VPC_ID=your_vpc_id

# Now run test command
make testacc TESTARGS='-run=test_name'
```
Binary file added bin/tfplugindocs
Binary file not shown.
49 changes: 49 additions & 0 deletions commons/api_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package commons

import "fmt"

var ApiPath = struct {
SSH string
Storage func(vpcId string) string
StorageUpdateAttached func(vpcId string, storageId string) string
StoragePolicy func(vpcId string) string
Flavor func(vpcId string) string
Image func(vpcId string) string
SecurityGroup func(vpcId string) string
RenameSecurityGroup func(vpcId string, securityGroupId string) string
UpdateApplyToSecurityGroup func(vpcId string, securityGroupId string) string
SecurityGroupRule func(vpcId string, securityGroupRuleId string) string
CreateSecurityGroupRule func(vpcId string) string
}{
SSH: "/v1/user/sshs",
Storage: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/storage", vpcId)
},
StorageUpdateAttached: func(vpcId string, storageId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/storage/%s/update-attached", vpcId, storageId)
},
StoragePolicy: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/storage-policies", vpcId)
},
Flavor: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/flavors", vpcId)
},
Image: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/images", vpcId)
},
SecurityGroup: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/security-group", vpcId)
},
RenameSecurityGroup: func(vpcId string, securityGroupId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/security-group/%s/rename", vpcId, securityGroupId)
},
UpdateApplyToSecurityGroup: func(vpcId string, securityGroupId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/security-group/%s/apply-to", vpcId, securityGroupId)
},
SecurityGroupRule: func(vpcId string, securityGroupRuleId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/security-group-rule/%s", vpcId, securityGroupRuleId)
},
CreateSecurityGroupRule: func(vpcId string) string {
return fmt.Sprintf("/v1/terraform/vpc/%s/security-group-rule", vpcId)
},
}
100 changes: 77 additions & 23 deletions commons/client.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package fptcloud
package commons

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"time"
)

// Client is the means of connecting to the Fpt cloud API service
type Client struct {
BaseURL *url.URL
UserAgent string
APIKey string
TenantName string
Region string
LastJSONResponse string

Expand All @@ -32,16 +36,10 @@ type HTTPError struct {
Reason string
}

// Result is the result of a SimpleResponse
type Result string

// SimpleResponse is a structure that returns success and/or any error
type SimpleResponse struct {
ID string `json:"id"`
Result Result `json:"result"`
ErrorCode string `json:"code"`
ErrorReason string `json:"reason"`
ErrorDetails string `json:"details"`
Data string
Status string
}

// ConfigAdvanceClientForTesting initializes a Client connecting to a local test server and allows for specifying methods
Expand All @@ -57,15 +55,12 @@ type ValueAdvanceClientForTesting struct {
ResponseBody string
}

// ResultSuccess represents a successful SimpleResponse
const ResultSuccess = "success"

func (e HTTPError) Error() string {
return fmt.Sprintf("%d: %s, %s", e.Code, e.Status, e.Reason)
}

// NewClientWithURL initializes a Client with a specific API URL
func NewClientWithURL(apiKey, apiUrl, region string) (*Client, error) {
func NewClientWithURL(apiKey, apiUrl, region string, tenantName string) (*Client, error) {
parsedURL, err := url.Parse(apiUrl)
if err != nil {
return nil, err
Expand All @@ -76,11 +71,13 @@ func NewClientWithURL(apiKey, apiUrl, region string) (*Client, error) {
}

client := &Client{
BaseURL: parsedURL,
APIKey: apiKey,
Region: region,
BaseURL: parsedURL,
APIKey: apiKey,
Region: region,
TenantName: tenantName,
httpClient: &http.Client{
Transport: httpTransport,
Timeout: 5 * time.Minute,
},
}
return client, nil
Expand All @@ -91,17 +88,12 @@ func (c *Client) prepareClientURL(requestURL string) *url.URL {
return u
}

// InitClient initializes a Client connecting to the production API
func InitClient(apiKey, region string) (*Client, error) {
return NewClientWithURL(apiKey, "https://console-api.fptcloud.com/api", region)
}

func (c *Client) sendRequest(req *http.Request) ([]byte, error) {
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Encoding", "gzip")
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", c.APIKey))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))

c.httpClient.Transport = &http.Transport{
DisableCompression: false,
Expand All @@ -110,7 +102,6 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, error) {
if req.Method == "GET" || req.Method == "DELETE" {
// add the region param
param := req.URL.Query()
param.Add("region", c.Region)
req.URL.RawQuery = param.Encode()
}

Expand Down Expand Up @@ -180,6 +171,21 @@ func (c *Client) SendDeleteRequest(requestURL string) ([]byte, error) {
return c.sendRequest(req)
}

// SendDeleteRequestWithBody sends a correctly authenticated delete request to the API server
func (c *Client) SendDeleteRequestWithBody(requestURL string, params interface{}) ([]byte, error) {
u := c.prepareClientURL(requestURL)

// we create a new buffer and encode everything to json to send it in the request
jsonValue, _ := json.Marshal(params)

req, err := http.NewRequest("DELETE", u.String(), bytes.NewBuffer(jsonValue))
if err != nil {
return nil, err
}

return c.sendRequest(req)
}

// SetUserAgent sets the user agent for the client
func (c *Client) SetUserAgent(component *Component) {
if component.ID == "" {
Expand All @@ -188,3 +194,51 @@ func (c *Client) SetUserAgent(component *Component) {
c.UserAgent = fmt.Sprintf("%s/%s-%s %s", component.Name, component.Version, component.ID, c.UserAgent)
}
}

// DecodeSimpleResponse parses a response body in to a SimpleResponse object
func (c *Client) DecodeSimpleResponse(resp []byte) (*SimpleResponse, error) {
response := SimpleResponse{}
err := json.NewDecoder(bytes.NewReader(resp)).Decode(&response)
return &response, err
}

// NewClientForTestingWithServer initializes a Client connecting to a passed-in local test server
func NewClientForTestingWithServer(server *httptest.Server) (*Client, error) {
client, err := NewClientWithURL("TEST-API-KEY", server.URL, "TEST", "TEST")
if err != nil {
return nil, err
}
client.httpClient = server.Client()
return client, err
}

// NewClientForTesting initializes a Client connecting to a local test server
func NewClientForTesting(responses map[string]string) (*Client, *httptest.Server, error) {
var responseSent bool

server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
for reqUrl, response := range responses {
if strings.Contains(req.URL.String(), reqUrl) {
responseSent = true
_, err := rw.Write([]byte(response))
if err != nil {
return
}
}
}

if !responseSent {
fmt.Println("Failed to find a matching request!")
fmt.Println("URL:", req.URL.String())

_, err := rw.Write([]byte(`{"result": "failed to find a matching request"}`))
if err != nil {
return
}
}
}))

client, err := NewClientForTestingWithServer(server)

return client, server, err
}
105 changes: 105 additions & 0 deletions commons/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package commons

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestNewClientWithURL_ValidURL(t *testing.T) {
client, err := NewClientWithURL("apiKey", "https://api.example.com", "region", "tenant")
assert.NoError(t, err)
assert.NotNil(t, client)
assert.Equal(t, "https://api.example.com", client.BaseURL.String())
}

func TestNewClientWithURL_InvalidURL(t *testing.T) {
client, err := NewClientWithURL("apiKey", ":", "region", "tenant")
assert.Error(t, err)
assert.Nil(t, client)
}

func TestSendGetRequest_ValidResponse(t *testing.T) {
client, server, err := NewClientForTesting(map[string]string{
"/test": `{"data": "success"}`,
})
assert.NoError(t, err)
defer server.Close()

resp, err := client.SendGetRequest("/test")
assert.NoError(t, err)
assert.Contains(t, string(resp), "success")
}

func TestSendGetRequest_InvalidURL(t *testing.T) {
client, err := NewClientWithURL("apiKey", "https://api.example.com", "region", "tenant")
assert.NoError(t, err)

resp, err := client.SendGetRequest("://invalid-url")
assert.Error(t, err)
assert.Nil(t, resp)
}

func TestSendPostRequest_ValidResponse(t *testing.T) {
client, server, err := NewClientForTesting(map[string]string{
"/test": `{"data": "success"}`,
})
assert.NoError(t, err)
defer server.Close()

resp, err := client.SendPostRequest("/test", map[string]string{"key": "value"})
assert.NoError(t, err)
assert.Contains(t, string(resp), "success")
}

func TestSendPostRequest_InvalidJSON(t *testing.T) {
client, err := NewClientWithURL("apiKey", "https://api.example.com", "region", "tenant")
assert.NoError(t, err)

resp, err := client.SendPostRequest("/test", make(chan int))
assert.Error(t, err)
assert.Nil(t, resp)
}

func TestSendDeleteRequest_ValidResponse(t *testing.T) {
client, server, err := NewClientForTesting(map[string]string{
"/test": `{"data": "deleted"}`,
})
assert.NoError(t, err)
defer server.Close()

resp, err := client.SendDeleteRequest("/test")
assert.NoError(t, err)
assert.Contains(t, string(resp), "deleted")
}

func TestSendDeleteRequestWithBody_ValidResponse(t *testing.T) {
client, server, err := NewClientForTesting(map[string]string{
"/test": `{"data": "deleted"}`,
})
assert.NoError(t, err)
defer server.Close()

resp, err := client.SendDeleteRequestWithBody("/test", map[string]string{"key": "value"})
assert.NoError(t, err)
assert.Contains(t, string(resp), "deleted")
}

func TestSetUserAgent_SetsCorrectly(t *testing.T) {
client, err := NewClientWithURL("apiKey", "https://api.example.com", "region", "tenant")
assert.NoError(t, err)

component := &Component{ID: "123", Name: "TestComponent", Version: "1.0"}
client.SetUserAgent(component)
assert.Contains(t, client.UserAgent, "TestComponent/1.0-123")
}

func TestDecodeSimpleResponse_ValidResponse(t *testing.T) {
client, err := NewClientWithURL("apiKey", "https://api.example.com", "region", "tenant")
assert.NoError(t, err)

resp := []byte(`{"Data": "success", "Status": "ok"}`)
simpleResp, err := client.DecodeSimpleResponse(resp)
assert.NoError(t, err)
assert.Equal(t, "success", simpleResp.Data)
assert.Equal(t, "ok", simpleResp.Status)
}
5 changes: 5 additions & 0 deletions commons/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package commons

const (
DefaultApiUrl = "https://console-api.fptcloud.com/api"
)
Loading

0 comments on commit 2ca1705

Please sign in to comment.